初始化

This commit is contained in:
2025-04-23 11:03:01 +08:00
commit 89c2e6c5c4
148 changed files with 11513 additions and 0 deletions

View File

@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

View File

@ -0,0 +1,2 @@
VITE_APP_TITLE=测试环境
VITE_BASE_URL=api

View File

@ -0,0 +1,2 @@
VITE_APP_TITLE={{ cookiecutter.project_hans }}
VITE_BASE_URL=/

View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

View File

@ -0,0 +1,36 @@
# manager
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and
disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@ -0,0 +1,28 @@
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default defineConfig([
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
...pluginOxlint.configs['flat/recommended'],
skipFormatting,
])

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<link href="/favicon.ico" rel="icon">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title><%= VITE_APP_TITLE %></title>
</head>
<body>
<div id="app"></div>
<script src="/src/main.js" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"paths": {
"@/*": [
"./src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}

View File

@ -0,0 +1,51 @@
{
"name": "manager-ui",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
"lint:eslint": "eslint . --fix",
"lint": "run-s lint:*",
"format": "prettier --write src/"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.8.4",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"qs": "^6.14.0",
"tree-lodash": "^0.4.0",
"vue": "^3.5.13",
"vue-request": "^2.0.4",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vue/eslint-config-prettier": "^10.2.0",
"eslint": "^9.25.1",
"eslint-plugin-oxlint": "^0.16.7",
"eslint-plugin-vue": "~10.0.0",
"globals": "^16.0.0",
"npm-run-all2": "^7.0.2",
"oxlint": "^0.16.7",
"prettier": "3.5.3",
"sass-embedded": "^1.87.0",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.3.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,13 @@
<script setup>
import { onBeforeMount } from 'vue'
import { useSystemStore } from '@/stores/system.js'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
onBeforeMount(useSystemStore().init)
</script>
<template>
<a-config-provider :locale="zhCN" >
<RouterView />
</a-config-provider>
</template>

View File

@ -0,0 +1,8 @@
:root {
}
.dark {
}
.light {
}

View File

@ -0,0 +1,34 @@
@import 'base.css';
html,
body,
#app {
height: 100%;
width: 100%;
margin: 0;
}
/* 全局滚动条样式(内侧滚动条) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent; /* 滚动条轨道透明 */
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
.text-single {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,181 @@
<script setup>
import { reactive } from 'vue'
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
NotificationOutlined,
UserOutlined,
} from '@ant-design/icons-vue'
import LayoutMenu from '@/components/Layout/LayoutMenu.vue'
import { useSystemStore } from '@/stores/system.js'
import axios from '@/http/api.js'
import router from '@/router/index.js'
import { message } from 'ant-design-vue'
import { useRequest } from 'vue-request'
import qs from 'qs'
const systemStore = useSystemStore()
const state = reactive({
width: 256,
sider_width: 80,
sider: true,
drawer: false,
broken: false,
})
const breakpoint = (broken) => {
state.broken = broken
collapsed()
}
const collapsed = () => {
if (state.broken) {
state.sider_width = 0
state.sider = true
state.drawer = !state.drawer
} else {
state.sider_width = 80
state.drawer = false
state.sider = !state.sider
}
}
const logout = () => {
axios.post('/manager/logout').then(([err, res]) => {
if (err || res.code !== 0) {
message.error(err.msg || '注销失败')
} else {
message.success(res.msg || '注销成功')
useSystemStore().$reset()
}
})
router.push('/login')
}
const toMessage = () => router.push({ path: '/message' })
// 轮询消息
const fetchMessage = async (params) => {
let [err, res] = await axios.get('/message?' + qs.stringify(params, { allowDots: true }))
if (err || res.code !== 0) {
return message.warning('查询最新消息失败')
}
return res?.data?.records || []
}
const { data: messages } = useRequest(fetchMessage, {
pollingInterval: import.meta.env.DEV ? -1 : 1000,
defaultParams: [{ size: -1, status: 0 }],
})
</script>
<template>
<ALayout>
<ADrawer
v-model:open="state.drawer"
:bodyStyle="{ padding: '0px', backgroundColor: '#001529' }"
:closable="false"
:width="state.width"
placement="left"
>
<LayoutMenu />
</ADrawer>
<ALayoutSider
v-model:collapsed="state.sider"
:collapsed-width="state.sider_width"
:defaultCollapsed="true"
:trigger="null"
:width="state.width"
breakpoint="lg"
class="sider"
@breakpoint="breakpoint"
>
<LayoutMenu />
</ALayoutSider>
<ALayout>
<ALayoutHeader class="header">
<div class="btn-sider" @click="collapsed">
<MenuUnfoldOutlined v-if="state.sider || state.drawer" />
<MenuFoldOutlined v-else />
</div>
<ABadge :count="messages?.length || 0" class="btn-notify" @click="toMessage">
<NotificationOutlined />
</ABadge>
<ADropdown>
<div class="btn-avatar">
<AAvatar shape="square">
<template #icon>
<UserOutlined />
</template>
</AAvatar>
<span class="name">
<span v-if="systemStore.tenant">{{ systemStore.tenant }} / </span>
<span>{{ systemStore.realName || systemStore.name }}</span>
</span>
</div>
<template #overlay>
<AMenu>
<AMenuItem>系统设置</AMenuItem>
<AMenuItem @click="logout">注销登录</AMenuItem>
</AMenu>
</template>
</ADropdown>
</ALayoutHeader>
<ALayout>
<ALayoutContent class="content">
<RouterView />
</ALayoutContent>
</ALayout>
</ALayout>
</ALayout>
</template>
<style lang="scss" scoped>
.sider {
height: 100vh;
overflow-y: auto;
}
.header {
background-color: white;
padding: 0 10px;
display: flex;
.btn-sider {
height: 64px;
font-size: 20px;
padding: 0 10px;
cursor: pointer;
}
.btn-notify {
height: 40px;
font-size: 20px;
padding: 0 10px;
cursor: pointer;
line-height: 40px;
align-self: center;
margin-left: auto;
margin-right: 16px;
}
.btn-avatar {
cursor: pointer;
.name {
margin-left: 8px;
color: rgba(0, 0, 0, 0.65);
}
}
}
.content {
height: calc(100vh - 64px - 20px);
margin: 10px;
padding: 24px;
background-color: white;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,45 @@
<script setup>
import { AntDesignOutlined } from '@ant-design/icons-vue'
import router from '@/router/index.js'
import { useSystemStore } from '@/stores/system.js'
import { computed } from 'vue'
const title = computed(() => import.meta.env.VITE_APP_TITLE)
const systemStore = useSystemStore()
const selectMenu = ({ item, key }) => {
if (item.link) {
// 显示网页
router.push({
name: 'WebView',
query: { link: item.link },
})
} else {
router.push(key)
}
}
const toHome = () => router.push({ path: '/' })
</script>
<template>
<div>
<div class="title" @click="toHome">
<AntDesignOutlined />
<span style="margin-left: 12px">{{ title }}</span>
</div>
<AMenu :items="systemStore.fmtMenus()" mode="inline" theme="dark" @select="selectMenu" />
</div>
</template>
<style lang="scss" scoped>
.title {
color: white;
height: 64px;
line-height: 64px;
padding-left: 26px;
font-size: 1.8rem;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,20 @@
import { useSystemStore } from '@/stores/system.js'
export default function (el, binding) {
const systemStore = useSystemStore()
const { value } = binding
if (systemStore.isSa) return true
if (value && value instanceof Array && value.length > 0) {
let has = systemStore.auths.some((item) => {
return value.includes(item)
})
if (!has) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error('需要指定权限')
}
}

View File

@ -0,0 +1,9 @@
import hasAuth from '@/directive/hasAuth.js'
import isa from '@/directive/isa.js'
export default {
install(app) {
app.directive('hasAuth', hasAuth)
app.directive('isa', isa)
},
}

View File

@ -0,0 +1,11 @@
import { useSystemStore } from '@/stores/system.js'
export default function (el) {
const systemStore = useSystemStore()
if (systemStore.isSa) {
return true
} else {
el.parentNode && el.parentNode.removeChild(el)
}
}

View File

@ -0,0 +1,20 @@
import axios from 'axios'
import router from '@/router/index.js'
const http = axios.create({ baseURL: import.meta.env.VITE_BASE_URL })
http.interceptors.request.use(
(config) => config,
(error) => Promise.reject(error),
)
http.interceptors.response.use(
(response) => {
return [null, response.data]
},
(error) => {
let response = error.response
if (response.status === 401) return router.push('/login')
return [response.data, null]
},
)
export default http

View File

@ -0,0 +1,26 @@
import './assets/css/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
import directive from '@/directive/index.js'
const app = createApp(App)
dayjs.extend(localizedFormat)
dayjs.locale('zh-cn')
dayjs.extend(relativeTime)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.use(directive)
app.mount('#app')

View File

@ -0,0 +1,93 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import LayoutIndex from '@/components/Layout/LayoutIndex.vue'
import { useSystemStore } from '@/stores/system.js'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
component: () => import('@/views/LoginView.vue'),
},
{
path: '/',
component: LayoutIndex,
redirect: '/home',
children: [
{
path: '/home',
meta: { needLogin: true },
component: () => import('@/views/HomeView.vue'),
},
{
path: '/system',
children: [
{
path: '/system/account',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/AccountView.vue'),
},
{
path: '/system/role',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/RoleView.vue'),
},
{
path: '/system/authority',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/AuthorityView.vue'),
},
{
path: '/system/tenant',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/TenantView.vue'),
},
{
path: '/system/logger',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/LoggerView.vue'),
},
],
},
{
path: '/message',
name: 'Message',
component: () => import('@/views/MessageView.vue'),
},
{
path: '/webview',
name: 'WebView',
props: (route) => ({ link: route.query.link || '#' }),
component: () => import('@/views/WebView.vue'),
},
{
path: '403',
component: () => import('@/views/exception/403View.vue'),
},
{
path: '500',
component: () => import('@/views/exception/500View.vue'),
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/exception/404View.vue'),
},
],
},
],
})
router.beforeEach((to, from, next) => {
const systemStore = useSystemStore()
// 判断是否需要登录
if (to.meta.needLogin && !systemStore.isLogin) {
return next('/login')
}
// 判断是否拥有菜单权限
if (to.meta.needMenu && !systemStore.menus.some((item) => to.path.startsWith(item.path))) {
return next('/403')
}
return next()
})
export default router

View File

@ -0,0 +1,73 @@
import { defineStore } from 'pinia'
import { jwtDecode } from 'jwt-decode'
import Cookies from 'js-cookie'
import axios from '@/http/api.js'
import { fromArray, map } from 'tree-lodash'
import { toRaw } from 'vue'
const TokenKey = 'Sa-Token'
export const useSystemStore = defineStore('system', {
state: () => {
return {
token: '', // 令牌
name: '', // 用户名
realName: '', // 显示姓名
tenant: '', // 租户
auths: [], // 角色和权限
menus: [], // 菜单
}
},
getters: {
isSa(state) {
return !state.tenant
},
isLogin(state) {
return !!state.token
},
},
actions: {
// 解析令牌
parseToken() {
let token = Cookies.get(TokenKey)
if (token) {
this.token = token
var payload = jwtDecode(token)
this.name = payload['name']
this.realName = payload['real_name']
this.tenant = payload['tenant']
} else {
useSystemStore().$reset()
}
},
// 获得角色和权限
ownAuths() {
let token = Cookies.get(TokenKey)
if (token) {
axios.get('/manager/own/auths').then(([_, res]) => (this.auths = res.data || []))
}
},
// 获得菜单
ownMenus() {
let token = Cookies.get(TokenKey)
if (token) {
axios.get('/manager/own/menus').then(([_, res]) => (this.menus = res.data || []))
}
},
// 格式化菜单
fmtMenus() {
return map(fromArray(toRaw(this.menus), { parentKey: 'parentId' }), (item) => ({
key: item.path || item.value,
label: item.name,
link: item.link || '',
}))
},
// 初始化
init() {
this.parseToken()
this.ownAuths()
this.ownMenus()
},
},
persist: true,
})

View File

@ -0,0 +1,11 @@
import JSEncrypt from 'jsencrypt'
const publicKey = `
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDORIceR8iDNIdH366iHZ9LCrkq VF84SRgO0JsZO79vc/1hcsECcs7kQCtFD9kj5Bz4P4iMJQ+hZeaPBKmrfHl91DDr hjuACgA3Pk0Pr5TBdN3eemA0Ri50NyjhoGpJvE8dZe1sbn4lfQwtOsx+kmP+Ixb3 oa6wdPQb3gfnQJqxDQIDAQAB
`
export function encrypt(text) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(text) // 对数据进行加密
}

View File

@ -0,0 +1,578 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
const searchRef = ref()
const editRef = ref()
const pwdRef = ref()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
pwd: {
title: '设置密码',
show: false,
},
})
const columns = [
{
title: '账户',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '名称',
dataIndex: 'realName',
width: 120,
ellipsis: true,
},
{
title: '手机号',
dataIndex: 'mobile',
maxWidth: 150,
ellipsis: true,
},
{
title: '邮箱',
dataIndex: 'email',
maxWidth: 200,
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '可用' : '禁用'
},
filters: [
{ text: '可用', value: 0 },
{ text: '禁用', value: 1 },
],
filterMultiple: false,
},
{
title: '激活',
dataIndex: 'isActive',
width: 80,
customRender: ({ text: isActive }) => {
return isActive === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 180,
fixed: 'right',
},
]
const rules = {
name: [
{ required: true, message: '账户必填', trigger: 'blur' },
{ min: 2, max: 50, message: '最少2个字符最多50个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '密码必填', trigger: 'blur' },
{ min: 6, max: 100, message: '最少6个字符最多100个字符', trigger: 'blur' },
],
repeat: [
{ required: true, message: '重复密码必填', trigger: 'blur' },
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value !== state.edit.password) {
return Promise.reject('两次输入密码不同')
} else {
return Promise.resolve()
}
},
},
],
mobile: [
{
pattern: /^1[3-9]\d{9}$/,
message: '手机格式不正确',
trigger: 'blur',
},
],
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
tenant: [
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value || value === '') {
return Promise.resolve()
} else {
return Promise.reject('租户必选')
}
},
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const onPwdChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.pwd.show = true
}
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({ label: item.name, value: item.name }))
opts.unshift({ label: '公共数据', value: '' })
return opts
}
}
const fetchRoleOpt = async (params) => {
let [err, res] = await axios.get('/db/tRole', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
return res.data.records.map((item) => ({ label: item.name, value: item.id }))
}
}
const fetchData = (params) => {
return axios
.get('/db/tAccount?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/db/tAccount', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tAccount', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tAccount', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const submitPwd = async () => {
pwdRef.value.validate().then(() => {
if (state.edit.id) {
let params = { id: state.edit.id, password: state.edit.password }
axios.put('/manager/password', params).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改密码失败')
} else {
state.pwd.show = false
return message.success('修改密码成功')
}
})
}
})
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data: roleOpts, run: runRoleOpt } = useRequest(fetchRoleOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tAccount/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
// 文件导出
var blob = res
var filename = '凭据.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const roleOpt = computed({
get() {
if (state.edit?.roleIds) {
return state.edit.roleIds.split(',')
} else {
return []
}
},
set(val) {
state.edit.roleIds = val.join(',')
},
})
</script>
<template>
<div class="account">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="账户" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入账户" />
</a-form-item>
<a-form-item label="名称" name="realName">
<a-input v-model:value="state.search.realName" placeholder="请输入显示名称" />
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input v-model:value="state.search.mobile" placeholder="请输入手机号" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="state.search.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:account:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tAccount/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:account:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-button v-hasAuth="['system:account:put']" type="link" @click="onPwdChange(record)"
>密码修改
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:account:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-row>
<a-col :span="12">
<a-form-item label="账户" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入账户" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="名称" name="realName">
<a-input v-model:value="state.edit.realName" placeholder="请输入显示名称" />
</a-form-item>
</a-col>
</a-row>
<template v-if="!state.edit.id">
<a-form-item label="密码" name="password">
<a-input-password v-model:value="state.edit.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="重复密码" name="repeat">
<a-input-password v-model:value="state.edit.repeat" placeholder="请输入确认密码" />
</a-form-item>
</template>
<a-row>
<a-col :span="12">
<a-form-item label="手机号" name="mobile">
<a-input v-model:value="state.edit.mobile" placeholder="请输入手机号" />
</a-form-item>
</a-col>
<a-col :push="2" :span="12">
<a-form-item label="是否启用" name="status">
<a-switch v-model:checked="state.edit.status" :checkedValue="0" :unCheckedValue="1" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="邮箱" name="email">
<a-input v-model:value="state.edit.email" placeholder="请输入邮箱" />
</a-form-item>
</a-col>
<a-col :push="2" :span="12">
<a-form-item label="是否激活" name="isActive">
<a-switch
v-model:checked="state.edit.isActive"
:checkedValue="0"
:unCheckedValue="1"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="关联角色" name="roleIds">
<a-select
v-model:value="roleOpt"
:filterOption="false"
:options="roleOpts"
mode="multiple"
placeholder="请多选用户角色"
@search="(value) => runRoleOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-row>
<a-col :span="12">
<a-form-item label="关联员工" name="staffId">
<a-input v-model:value="state.edit.staffId" placeholder="请选择员工" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="关联会员" name="userId">
<a-input v-model:value="state.edit.userId" placeholder="请选择会员" />
</a-form-item>
</a-col>
</a-row>
<a-form-item v-isa label="租户" name="tenant">
<a-select
v-model:value="state.edit.tenant"
:filterOption="false"
:options="tenantOpts"
placeholder="请选择隶属租户"
show-search
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:open="state.pwd.show" :title="state.pwd.title" @ok="submitPwd">
<a-form ref="pwdRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="密码" name="password">
<a-input-password v-model:value="state.edit.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="重复密码" name="repeat">
<a-input-password v-model:value="state.edit.repeat" placeholder="请输入确认密码" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.account {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,548 @@
<script setup>
import _ from 'lodash'
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
const searchRef = ref()
const editRef = ref()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
})
const columns = [
{
title: '权限名称',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '英文值',
dataIndex: 'value',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '路径',
dataIndex: 'path',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '外部链接',
dataIndex: 'link',
maxWidth: 200,
ellipsis: true,
},
{
title: '类型',
dataIndex: 'type',
sorter: true,
width: 100,
customRender: ({ text: type }) => {
switch (type) {
case 0:
return '接口'
case 1:
return '菜单'
case 2:
return '按钮'
default:
return '其他'
}
},
filters: [
{ text: '接口', value: 0 },
{ text: '菜单', value: 1 },
{ text: '按钮', value: 2 },
],
filterMultiple: false,
},
{
title: '状态',
dataIndex: 'status',
width: 100,
customRender: ({ text: status }) => {
return status === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 120,
fixed: 'right',
},
]
const rules = {
name: [
{
required: true,
message: '权限名称必填',
trigger: 'blur',
},
{
min: 2,
max: 100,
message: '最少2个字符最多100个字符',
trigger: 'blur',
},
],
value: [
{
required: true,
message: '英文值必填',
trigger: 'blur',
},
{
min: 4,
max: 100,
message: '最少4个字符最多100个字符',
trigger: 'blur',
},
],
parentId: [
{
required: true,
message: '父节点必选',
trigger: 'blur',
},
],
type: [
{
required: true,
message: '类型必选',
trigger: 'blur',
},
],
tenant: [
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value || value === '') {
return Promise.resolve()
} else {
return Promise.reject('租户必选')
}
},
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant?' + qs.stringify(params, { skipNulls: true }))
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({ label: item.name, value: item.name }))
opts.unshift({ label: '公共数据', value: '' })
return opts
}
}
const fetchAuthorityOpt = async (params) => {
let [err, res] = await axios.get('/db/tAuthority?' + qs.stringify(params, { skipNulls: true }))
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({
label: item.name + ' (' + (item.description || '无描述') + ')',
value: item.id,
}))
opts.unshift({ label: '根节点', value: '0' })
return opts
}
}
const fetchData = (params) => {
return axios
.get('/db/tAuthority?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
if (state.edit.id === state.edit.parentId) {
return message.warning('父节点不能是自身')
}
axios.put('/db/tAuthority', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tAuthority', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tAuthority', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data: authorityOpts, run: runAuthorityOpt } = useRequest(fetchAuthorityOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const onChangePath = (path) => {
if (path) {
state.edit.value = _(path).trim('/').replaceAll('/', ':')
} else {
state.edit.value = ''
}
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tAuthority/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
// 文件导出
var blob = res
var filename = '权限.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
</script>
<template>
<div class="authority">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="权限名称" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="权限类型" name="type">
<a-select v-model:value="state.search.type" allowClear placeholder="请选择类型">
<a-select-option value="0">接口</a-select-option>
<a-select-option value="1">菜单</a-select-option>
<a-select-option value="2">按钮</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:authority:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tAuthority/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:authority:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:authority:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-row>
<a-col :span="12">
<a-form-item label="名称" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入权限名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="英文值" name="value">
<a-input v-model:value="state.edit.value" placeholder="请输入英文简写" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="父节点" name="parentId">
<a-select
v-model:value="state.edit.parentId"
:filterOption="false"
:options="authorityOpts"
placeholder="请选择父节点"
show-search
@search="(value) => runAuthorityOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-row>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="state.edit.type" placeholder="请选择类型">
<a-select-option :value="0">接口</a-select-option>
<a-select-option :value="1">菜单</a-select-option>
<a-select-option :value="2">按钮</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :push="2" :span="12">
<a-form-item label="是否启用" name="status">
<a-switch
v-model:checked="state.edit.status"
:checkedValue="0"
:unCheckedValue="1"
placeholder="请输入状态"
/>
</a-form-item>
</a-col>
</a-row>
<template v-if="state.edit.type === 1">
<a-form-item label="路径" name="path">
<a-input
v-model:value="state.edit.path"
placeholder="请输入以 / 开头的路径,将修改英文值"
@change="onChangePath(state.edit.path)"
/>
</a-form-item>
<a-form-item label="外部链接" name="link">
<a-input v-model:value="state.edit.link" placeholder="请输入外部链接" />
</a-form-item>
</template>
<a-form-item v-isa label="租户" name="tenant">
<a-select
v-model:value="state.edit.tenant"
:filterOption="false"
:options="tenantOpts"
placeholder="请选择隶属租户"
show-search
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.authority {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,9 @@
<script setup></script>
<template>
<div>
<h1>首页</h1>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,268 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { message } from 'ant-design-vue'
import { usePagination } from 'vue-request'
const searchRef = ref()
const state = reactive({
search: {},
edit: {},
})
const columns = [
{
title: '类型',
dataIndex: 'type',
sorter: true,
width: 200,
ellipsis: true,
customRender: ({ text }) => {
switch (text) {
case 0:
return '数据变更自动日志'
}
},
},
{
title: '操作',
dataIndex: 'operation',
sorter: true,
width: 80,
customRender: ({ text }) => {
switch (text) {
case 'insert':
return '新增'
case 'update':
return '修改'
case 'delete':
return '删除'
default:
return '其他'
}
},
},
{
title: '表名称',
dataIndex: 'tableName',
width: 100,
ellipsis: true,
},
{
title: '日志',
dataIndex: 'recordStaus',
width: 80,
align: 'center',
customRender: ({ text }) => (text ? '是' : '否'),
},
{
title: '变动内容',
dataIndex: 'changed',
maxWidth: 250,
ellipsis: true,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 80,
fixed: 'right',
},
]
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const fetchData = (params) => {
return axios
.get('/db/tLogger?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tLogger', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tLogger/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
// 文件导出
var blob = res
var filename = '日志.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
</script>
<template>
<div class="tenant">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="表名称" name="tableName">
<a-input v-model:value="state.search.tableName" placeholder="请输入表名称" />
</a-form-item>
<a-form-item label="操作类型" name="operation">
<a-select v-model:value="state.search.operation" allowClear placeholder="请选择类型">
<a-select-option value="insert">增加</a-select-option>
<a-select-option value="update">修改</a-select-option>
<a-select-option value="delete">删除</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['export:excel']" type="primary" @click="excelExport()">导出</a-button>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:logger:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</div>
</template>
<style lang="scss" scoped>
.tenant {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,232 @@
<script setup>
import api from '@/http/api.js'
import { computed, onBeforeMount, ref } from 'vue'
import { LockOutlined, SafetyOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { encrypt } from '@/utils/crypto.js'
import { useSystemStore } from '@/stores/system.js'
import router from '@/router/index.js'
const title = computed(() => import.meta.env.VITE_APP_TITLE)
const formRef = ref()
const systemStore = useSystemStore()
const userForm = ref({
name: '',
password: '',
captcha: '',
captchaId: '',
captchaImg: '',
})
const login = async () => {
await formRef.value.validate()
let [err, res] = await api.post('/manager/login', {
name: encrypt(userForm.value.name),
password: encrypt(userForm.value.password),
captcha: userForm.value.captcha,
captchaId: userForm.value.captchaId,
})
if (err || res.code !== 0) {
formRef.value.resetFields()
getCaptcha()
return message.error(err.msg || '登录失败')
} else {
systemStore.init()
router.push('/')
return message.success(res.data || '登录成功')
}
}
const getCaptcha = async () => {
let [err, res] = await api.get('/pub/captcha/img')
if (err || res.code !== 0) {
return message.error(err.msg || '获取验证码失败')
}
userForm.value.captchaImg = res.data.captcha
userForm.value.captchaId = res.data.objectId
}
onBeforeMount(getCaptcha)
</script>
<template>
<div class="login">
<div class="login-form">
<h3 class="title">{{ title }}</h3>
<a-form ref="formRef" :model="userForm" :wrapper-col="{ span: 18 }" name="basic">
<a-form-item
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
name="name"
>
<a-input v-model:value="userForm.name" placeholder="请输入用户名">
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
name="password"
>
<a-input-password v-model:value="userForm.password" placeholder="请输入密码">
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item
:rules="[{ required: true, message: '验证码不能为空', trigger: 'blur' }]"
name="captcha"
>
<a-input v-model:value="userForm.captcha" placeholder="请输入验证码">
<template #prefix>
<SafetyOutlined />
</template>
</a-input>
<a-image
:preview="false"
:src="userForm.captchaImg"
class="captcha"
@click="getCaptcha"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="login">登陆</a-button>
</a-form-item>
</a-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.login {
width: 100%;
height: 100%;
background-image: url(@/assets/images/login.webp);
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
@media screen and (min-width: 1025px) {
align-items: flex-start;
padding-left: 15%;
:deep(.login-form) {
width: 450px;
}
}
@media screen and (min-width: 769px) and (max-width: 1024px) {
align-items: center;
:deep(.login-form) {
width: 450px;
}
}
@media screen and (max-width: 768px) {
align-items: center;
:deep(.login-form) {
width: calc(100% - 40px);
}
}
.login-form {
background-color: rgb(104 121 165 / 30%);
border-radius: 20px;
border: 1px solid #fff;
.title {
text-align: center;
font-size: 20px;
color: #b3dbfc;
}
:deep(.ant-form) {
.ant-form-item {
margin-bottom: 30px;
.ant-form-item-row {
display: flex;
justify-content: center;
}
}
.ant-form-item-control-input-content {
display: flex;
}
.ant-input-affix-wrapper {
position: relative;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 20px;
padding: 0;
height: 40px;
.ant-input-prefix {
position: absolute;
left: 15px;
top: 50%;
z-index: 10;
transform: translateY(-50%);
margin-inline-end: 0;
font-size: 18px;
color: #b3dbfc;
}
.ant-input-suffix {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
.anticon {
color: #b3dbfc;
}
}
.ant-input {
padding-left: 40px;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0);
color: #b3dbfc;
&::placeholder {
color: #b3dbfc;
}
&:-webkit-autofill {
background-color: rgba(0, 0, 0, 0);
}
}
}
.ant-form-item-explain-error {
margin-top: 5px;
}
.captcha {
width: auto;
height: 100%;
margin-left: 8px;
cursor: pointer;
}
.ant-btn {
border-radius: 20px;
width: 100%;
height: 40px;
background-image: linear-gradient(to right, #2b72ff, #0055fb);
}
}
}
}
</style>

View File

@ -0,0 +1,371 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
import { useSystemStore } from '@/stores/system.js'
const searchRef = ref()
const editRef = ref()
const systemStore = useSystemStore()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
})
const columns = [
{
title: '发送人',
dataIndex: 'sender',
sorter: true,
maxWidth: 120,
ellipsis: true,
},
{
title: '接收人',
dataIndex: 'recipient',
maxWidth: 180,
ellipsis: true,
},
{
title: '过期时间',
dataIndex: 'expiry',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '未读' : '已读'
},
filters: [
{ text: '未读', value: 0 },
{ text: '已读', value: 1 },
],
filterMultiple: false,
},
{
title: '内容',
dataIndex: 'description',
width: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 160,
fixed: 'right',
},
]
const rules = {
recipient: [
{
required: true,
message: '接收人必填',
trigger: 'blur',
},
{
min: 2,
max: 100,
message: '最少2个字符最多100个字符',
trigger: 'blur',
},
],
description: [
{
required: true,
message: '内容必填',
trigger: 'blur',
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const onReadChange = async (record) => {
let [err, res] = await axios.put('/message/read/' + record.id)
if (err || res.code !== 0) {
return message.error(err.msg || '已读失败')
} else {
onTableChange()
}
}
const fetchRecipientOpt = async (params) => {
let res = await Promise.all([
axios.get('/db/tTenant', { params }),
axios.get('/db/tAccount', { params }),
])
if (res[0][0] || res[0][1].code !== 0 || res[1][0] || res[1][1].code !== 0) {
return message.error('加载数据失败')
}
res[0][1].data.records.forEach((record) => {
record.isExpiry = true
})
return [...res[0][1].data.records, ...res[1][1].data.records]
}
const fetchData = (params) => {
return axios
.get('/message?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
state.edit.sender = systemStore.realName || systemStore.name
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/message', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/message', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/message', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data: recipient, run: runRecipient } = useRequest(fetchRecipientOpt, {
manual: true,
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const recipientOpts = computed(() => {
if (recipient.value) {
let items = recipient.value.map((item) => ({
value: item.realName || item.name,
label: item.realName || item.name,
isExpiry: item.isExpiry || false,
}))
items.unshift({ value: '所有人', label: '所有人', isExpiry: true })
return items
} else {
return [{ value: '所有人', label: '所有人', isExpiry: true }]
}
})
const onSearchRecipient = (value) => {
if (value) runRecipient({ size: -1, mention: value })
}
const onSelectRecipient = (option) => {
if (option.isExpiry) state.edit.isExpiry = true
}
</script>
<template>
<div class="tenant">
<a-form ref="searchRef" :labelCol="{ flex: '40px' }" :model="state.search" layout="inline">
<a-form-item label="内容" name="description">
<a-input v-model:value="state.search.description" placeholder="请输入内容" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="state.search.status" placeholder="请选择类型">
<a-select-option value="">全部</a-select-option>
<a-select-option value="0">未读</a-select-option>
<a-select-option value="1">已读</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['message:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['message:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-button type="link" @click="onReadChange(record)">已读</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['message:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="接收人" name="recipient">
<a-mentions
v-model:value="state.edit.recipient"
:filterOption="false"
:options="recipientOpts"
placeholder="@接收人,可选所有人、租户、账户"
@search="onSearchRecipient"
@select="onSelectRecipient"
>
</a-mentions>
</a-form-item>
<a-form-item label="内容" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.tenant {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,527 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import _ from 'lodash'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
import { fromArray } from 'tree-lodash'
const searchRef = ref()
const editRef = ref()
const state = reactive({
search: {},
edit: {},
compare: {
id: 0,
tenant: '',
list: [],
before: [],
beforeHalf: [],
later: [],
laterHalf: [],
},
checked: [],
halfChecked: [],
modal: {
title: '',
show: false,
},
auth: {
title: '设置权限',
show: false,
},
})
const columns = [
{
title: '角色名称',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '英文值',
dataIndex: 'value',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 180,
fixed: 'right',
},
]
const rules = {
name: [
{ required: true, message: '角色名称必填', trigger: 'blur' },
{ min: 2, max: 100, message: '最少2个字符最多100个字符', trigger: 'blur' },
],
value: [
{ required: true, message: '英文值必填', trigger: 'blur' },
{ min: 4, max: 100, message: '最少4个字符最多100个字符', trigger: 'blur' },
],
tenant: [
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value || value === '') {
return Promise.resolve()
} else {
return Promise.reject('租户必选')
}
},
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const onAuthChange = (record) => {
state.compare.id = record.id
state.compare.tenant = record.tenant
fetchRoleAuthorityOpt({
size: -1,
roleId: state.compare.id,
})
state.auth.show = true
}
const onAuthCheck = (checkedKeys, { halfCheckedKeys }) => {
state.halfChecked = halfCheckedKeys
//
state.compare.later = checkedKeys
state.compare.laterHalf = halfCheckedKeys
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({ label: item.name, value: item.name }))
opts.unshift({ label: '公共数据', value: '' })
return opts
}
}
const fetchAuthorityOpt = async (params) => {
let [err, res] = await axios.get('/db/tAuthority?' + qs.stringify(params, { skipNulls: true }))
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
return res.data.records || []
}
}
const fetchRoleAuthorityOpt = async (params) => {
let [err, res] = await axios.get(
'/db/tRoleAuthority?' + qs.stringify(params, { skipNulls: true }),
)
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
} else {
const data = res.data.records || []
state.compare.list = data
//
state.checked = data.filter((item) => !item.half).map((item) => item.authorityId)
state.halfChecked = data.filter((item) => item.half).map((item) => item.authorityId)
state.compare.before = data.filter((item) => !item.half).map((item) => item.authorityId)
state.compare.beforeHalf = data.filter((item) => item.half).map((item) => item.authorityId)
}
}
const fetchData = (params) => {
return axios
.get('/db/tRole?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/db/tRole', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tRole', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tRole', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const submitCheck = () => {
// 比较前后选择
let deletes = _.difference(state.compare.before, state.compare.later)
let deletesHalf = _.difference(state.compare.beforeHalf, state.compare.laterHalf)
let creates = _.difference(state.compare.later, state.compare.before)
let createsHalf = _.difference(state.compare.laterHalf, state.compare.beforeHalf)
console.log(deletes, deletesHalf, creates, createsHalf)
// 删除
let deleteIds = state.compare.list
.filter(
(item) => _.includes(deletes, item.authorityId) || _.includes(deletesHalf, item.authorityId),
)
.map((item) => item.id)
if (deleteIds && deleteIds.length > 0) {
let params = { idList: deleteIds.join(',') }
axios.delete('/db/tRoleAuthority', { params })
}
// 新增
if (createsHalf && createsHalf.length > 0) {
axios.post(
'/db/tRoleAuthority/s',
createsHalf.map((item) => ({
tenant: state.compare.tenant,
roleId: state.compare.id,
authorityId: item,
half: true,
})),
)
}
if (creates && creates.length > 0) {
axios.post(
'/db/tRoleAuthority/s',
creates.map((item) => ({
tenant: state.compare.tenant,
roleId: state.compare.id,
authorityId: item,
half: false,
})),
)
}
state.auth.show = false
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data: authorityOpts } = useRequest(fetchAuthorityOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tRole/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
// 文件导出
var blob = res
var filename = '角色.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const treeAuth = computed(() => fromArray(authorityOpts.value, { parentKey: 'parentId' }))
</script>
<template>
<div class="role">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="角色名称" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:role:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tRole/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:role:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-button v-hasAuth="['system:role:put']" type="link" @click="onAuthChange(record)"
>分配权限
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:role:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="名称" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入角色名称" />
</a-form-item>
<a-form-item label="英文值" name="value">
<a-input v-model:value="state.edit.value" placeholder="请输入英文值" />
</a-form-item>
<a-form-item label="是否启用" name="status">
<a-switch
v-model:checked="state.edit.status"
:checkedValue="0"
:unCheckedValue="1"
placeholder="请输入状态"
/>
</a-form-item>
<a-form-item v-isa label="租户" name="tenant">
<a-select
v-model:value="state.edit.tenant"
:filterOption="false"
:options="tenantOpts"
placeholder="请选择隶属租户"
show-search
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:open="state.auth.show" :title="state.auth.title" @ok="submitCheck">
<a-tree
v-model:checked-keys="state.checked"
:field-names="{ key: 'id' }"
:tree-data="treeAuth"
checkable
@check="onAuthCheck"
>
<template #title="{ name, description }">
{{ name + '' }}
<span style="color: darkgray">{{ description }}</span>
</template>
</a-tree>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.role {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,432 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
const searchRef = ref()
const editRef = ref()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
})
const columns = [
{
title: '租户名称',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '共享给...',
dataIndex: 'shared',
width: 180,
ellipsis: true,
},
{
title: '租户类型',
dataIndex: 'type',
sorter: true,
width: 120,
customRender: ({ text: type }) => {
return type === 0 ? '企业' : '个人'
},
filters: [
{ text: '企业', value: 0 },
{ text: '个人', value: 1 },
],
filterMultiple: false,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 120,
fixed: 'right',
},
]
const rules = {
name: [
{
required: true,
message: '租户名称必填',
trigger: 'blur',
},
{
min: 2,
max: 100,
message: '最少2个字符最多100个字符',
trigger: 'blur',
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
return res.data.records.map((item) => ({ label: item.name, value: item.name }))
}
}
const fetchData = (params) => {
return axios
.get('/db/tTenant?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/db/tTenant', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tTenant', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tTenant', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tTenant/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
// 文件导出
var blob = res
var filename = '租户.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const tenantOpt = computed({
get() {
if (state.edit?.shared) {
return state.edit.shared.split(',')
} else {
return []
}
},
set(val) {
state.edit.shared = val.join(',')
},
})
</script>
<template>
<div class="tenant">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="租户名称" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="租户类型" name="type">
<a-select v-model:value="state.search.type" placeholder="请选择类型">
<a-select-option value="">全部</a-select-option>
<a-select-option value="0">企业</a-select-option>
<a-select-option value="1">个人</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:tenant:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tTenant/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:tenant:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:tenant:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="名称" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入名称" />
</a-form-item>
<a-row>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="state.edit.type" placeholder="请选择类型">
<a-select-option :value="0">企业</a-select-option>
<a-select-option :value="1">个人</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :push="2" :span="10">
<a-form-item label="是否启用" name="status">
<a-switch
v-model:checked="state.edit.status"
:checkedValue="0"
:unCheckedValue="1"
placeholder="请输入状态"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="共享数据给" name="status">
<a-select
v-model:value="tenantOpt"
:filterOption="false"
:options="tenantOpts"
mode="multiple"
placeholder="请多选共享数据的其他租户"
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.tenant {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,31 @@
<script setup>
import { LoadingOutlined } from '@ant-design/icons-vue'
import { ref } from 'vue'
defineProps({
link: String,
})
const loading = ref(true)
const iframeLoaded = () => {
loading.value = false
}
</script>
<template>
<a-result v-if="loading" subTitle="应用加载中,请耐心等待。" title="加载中...">
<template #icon>
<LoadingOutlined />
</template>
</a-result>
<iframe :onload="iframeLoaded" :src="link" allow="microphone" />
</template>
<style lang="scss" scoped>
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>

View File

@ -0,0 +1,16 @@
<script setup>
import router from '@/router/index.js'
const toHome = () => router.push({ path: '/' })
const toBack = () => router.back()
</script>
<template>
<a-result status="403" sub-title="抱歉您无权访问此页面" title="403">
<template #extra>
<a-button type="primary" @click="toHome">回到首页</a-button>
<a-button type="default" @click="toBack">上一页</a-button>
</template>
</a-result>
</template>

View File

@ -0,0 +1,16 @@
<script setup>
import router from '@/router/index.js'
const toHome = () => router.push({ path: '/' })
const toBack = () => router.back()
</script>
<template>
<a-result status="404" sub-title="抱歉您访问的页面不存在" title="404">
<template #extra>
<a-button type="primary" @click="toHome">回到首页</a-button>
<a-button type="default" @click="toBack">上一页</a-button>
</template>
</a-result>
</template>

View File

@ -0,0 +1,18 @@
<script setup>
import router from '@/router/index.js'
const toHome = () => router.push({ path: '/' })
const toBack = () => router.back()
</script>
<template>
<a-result status="500" sub-title="对不起服务器出错" title="500">
<template #extra>
<a-button type="primary" @click="toHome">回到首页</a-button>
<a-button type="default" @click="toBack">上一页</a-button>
</template>
</a-result>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,89 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import { createHtmlPlugin } from 'vite-plugin-html'
import viteCompression from 'vite-plugin-compression'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// eslint-disable-next-line no-undef
const env = loadEnv(mode, process.cwd())
return {
base: './',
build: {
emptyOutDir: true,
manifest: true,
outDir: '../src/main/resources/static/manager-ui',
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('ant-design-vue')) {
return 'ant-design-vue'
} else if (id.includes('dayjs')) {
return 'dayjs'
} else if (id.includes('lodash')) {
return 'lodash'
} else if (id.includes('tree-lodash')) {
return 'tree-lodash'
} else if (id.includes('vue-request')) {
return 'vue-request'
} else if (id.includes('js-cookie')) {
return 'js-cookie'
} else if (id.includes('jsencrypt')) {
return 'jsencrypt'
} else if (id.includes('jwt-decode')) {
return 'jwt-decode'
} else {
return 'vendor'
}
} else {
return undefined
}
},
},
},
},
plugins: [
vue(),
vueJsx(),
viteCompression({ threshold: 1024 * 300 }),
vueDevTools(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
createHtmlPlugin({
inject: {
data: {
VITE_APP_TITLE: env.VITE_APP_TITLE,
},
},
}),
],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
}
})