初始化
This commit is contained in:
9
{{cookiecutter.project_slug}}/ManagerUI/.editorconfig
Normal file
9
{{cookiecutter.project_slug}}/ManagerUI/.editorconfig
Normal 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
|
2
{{cookiecutter.project_slug}}/ManagerUI/.env.development
Normal file
2
{{cookiecutter.project_slug}}/ManagerUI/.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_APP_TITLE=测试环境
|
||||
VITE_BASE_URL=api
|
2
{{cookiecutter.project_slug}}/ManagerUI/.env.production
Normal file
2
{{cookiecutter.project_slug}}/ManagerUI/.env.production
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_APP_TITLE={{ cookiecutter.project_hans }}
|
||||
VITE_BASE_URL=/
|
1
{{cookiecutter.project_slug}}/ManagerUI/.gitattributes
vendored
Normal file
1
{{cookiecutter.project_slug}}/ManagerUI/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
30
{{cookiecutter.project_slug}}/ManagerUI/.gitignore
vendored
Normal file
30
{{cookiecutter.project_slug}}/ManagerUI/.gitignore
vendored
Normal 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
|
6
{{cookiecutter.project_slug}}/ManagerUI/.prettierrc.json
Normal file
6
{{cookiecutter.project_slug}}/ManagerUI/.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
36
{{cookiecutter.project_slug}}/ManagerUI/README.md
Normal file
36
{{cookiecutter.project_slug}}/ManagerUI/README.md
Normal 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
|
||||
```
|
28
{{cookiecutter.project_slug}}/ManagerUI/eslint.config.js
Normal file
28
{{cookiecutter.project_slug}}/ManagerUI/eslint.config.js
Normal 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,
|
||||
])
|
13
{{cookiecutter.project_slug}}/ManagerUI/index.html
Normal file
13
{{cookiecutter.project_slug}}/ManagerUI/index.html
Normal 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>
|
13
{{cookiecutter.project_slug}}/ManagerUI/jsconfig.json
Normal file
13
{{cookiecutter.project_slug}}/ManagerUI/jsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
51
{{cookiecutter.project_slug}}/ManagerUI/package.json
Normal file
51
{{cookiecutter.project_slug}}/ManagerUI/package.json
Normal 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"
|
||||
}
|
||||
}
|
BIN
{{cookiecutter.project_slug}}/ManagerUI/public/favicon.ico
Normal file
BIN
{{cookiecutter.project_slug}}/ManagerUI/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
13
{{cookiecutter.project_slug}}/ManagerUI/src/App.vue
Normal file
13
{{cookiecutter.project_slug}}/ManagerUI/src/App.vue
Normal 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>
|
@ -0,0 +1,8 @@
|
||||
:root {
|
||||
}
|
||||
|
||||
.dark {
|
||||
}
|
||||
|
||||
.light {
|
||||
}
|
@ -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 |
@ -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>
|
@ -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>
|
@ -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('需要指定权限')
|
||||
}
|
||||
}
|
@ -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)
|
||||
},
|
||||
}
|
11
{{cookiecutter.project_slug}}/ManagerUI/src/directive/isa.js
Normal file
11
{{cookiecutter.project_slug}}/ManagerUI/src/directive/isa.js
Normal 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)
|
||||
}
|
||||
}
|
20
{{cookiecutter.project_slug}}/ManagerUI/src/http/api.js
Normal file
20
{{cookiecutter.project_slug}}/ManagerUI/src/http/api.js
Normal 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
|
26
{{cookiecutter.project_slug}}/ManagerUI/src/main.js
Normal file
26
{{cookiecutter.project_slug}}/ManagerUI/src/main.js
Normal 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')
|
93
{{cookiecutter.project_slug}}/ManagerUI/src/router/index.js
Normal file
93
{{cookiecutter.project_slug}}/ManagerUI/src/router/index.js
Normal 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
|
73
{{cookiecutter.project_slug}}/ManagerUI/src/stores/system.js
Normal file
73
{{cookiecutter.project_slug}}/ManagerUI/src/stores/system.js
Normal 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,
|
||||
})
|
11
{{cookiecutter.project_slug}}/ManagerUI/src/utils/crypto.js
Normal file
11
{{cookiecutter.project_slug}}/ManagerUI/src/utils/crypto.js
Normal 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) // 对数据进行加密
|
||||
}
|
@ -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>
|
@ -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>
|
@ -0,0 +1,9 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>首页</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
268
{{cookiecutter.project_slug}}/ManagerUI/src/views/LoggerView.vue
Normal file
268
{{cookiecutter.project_slug}}/ManagerUI/src/views/LoggerView.vue
Normal 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>
|
232
{{cookiecutter.project_slug}}/ManagerUI/src/views/LoginView.vue
Normal file
232
{{cookiecutter.project_slug}}/ManagerUI/src/views/LoginView.vue
Normal 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>
|
@ -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>
|
527
{{cookiecutter.project_slug}}/ManagerUI/src/views/RoleView.vue
Normal file
527
{{cookiecutter.project_slug}}/ManagerUI/src/views/RoleView.vue
Normal 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>
|
432
{{cookiecutter.project_slug}}/ManagerUI/src/views/TenantView.vue
Normal file
432
{{cookiecutter.project_slug}}/ManagerUI/src/views/TenantView.vue
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
89
{{cookiecutter.project_slug}}/ManagerUI/vite.config.js
Normal file
89
{{cookiecutter.project_slug}}/ManagerUI/vite.config.js
Normal 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)),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
Reference in New Issue
Block a user