refactor: 重构数据库配置为SQLite开发环境并移除冗余文档
This commit is contained in:
159
admin-system/dashboard/package-lock.json
generated
159
admin-system/dashboard/package-lock.json
generated
@@ -9,11 +9,13 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "^2.11.3",
|
||||
"pinia": "^2.0.33",
|
||||
"three": "^0.179.1",
|
||||
"vue": "^3.2.45",
|
||||
@@ -118,6 +120,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@element-plus/icons-vue": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
|
||||
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
@@ -492,6 +503,31 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jiaminghi/bezier-curve": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmmirror.com/@jiaminghi/bezier-curve/-/bezier-curve-0.0.9.tgz",
|
||||
@@ -553,6 +589,17 @@
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"name": "@sxzz/popperjs-es",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
|
||||
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@simonwep/pickr": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
|
||||
@@ -3226,6 +3273,27 @@
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
||||
@@ -3336,6 +3404,42 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz",
|
||||
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA=="
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
|
||||
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.16",
|
||||
"@vueuse/metadata": "9.13.0",
|
||||
"@vueuse/shared": "9.13.0",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/ant-design-vue": {
|
||||
"version": "3.2.20",
|
||||
"resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-3.2.20.tgz",
|
||||
@@ -3552,6 +3656,32 @@
|
||||
"zrender": "5.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.11.3",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.3.tgz",
|
||||
"integrity": "sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"async-validator": "^4.2.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-html": "^1.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-unified": "^1.0.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-wheel-es": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -3646,6 +3776,12 @@
|
||||
"@esbuild/win32-x64": "0.18.20"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
@@ -3874,6 +4010,17 @@
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
|
||||
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/lodash-es": "*",
|
||||
"lodash": "*",
|
||||
"lodash-es": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -3909,6 +4056,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -3953,6 +4106,12 @@
|
||||
"integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "^2.11.3",
|
||||
"pinia": "^2.0.33",
|
||||
"three": "^0.179.1",
|
||||
"vue": "^3.2.45",
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
<div class="nav-left">
|
||||
<router-link to="/" class="nav-item">首页</router-link>
|
||||
<router-link to="/monitor" class="nav-item">监控中心</router-link>
|
||||
<router-link to="/monitor-center" class="nav-item">实时监控</router-link>
|
||||
<div class="nav-dropdown">
|
||||
<span class="nav-item">系统管理</span>
|
||||
<div class="dropdown-content">
|
||||
<router-link to="/system/users" v-permission="'user:view'">用户管理</router-link>
|
||||
<router-link to="/system/roles" v-permission="'role:view'">角色管理</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<router-link to="/government" class="nav-item">政府平台</router-link>
|
||||
<router-link to="/finance" class="nav-item">金融服务</router-link>
|
||||
<router-link to="/transport" class="nav-item">运输跟踪</router-link>
|
||||
@@ -130,6 +138,48 @@ export default {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-dropdown .nav-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
min-width: 120px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
padding: 8px 0;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: #333;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.nav-dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-dropdown:hover .nav-item {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
66
admin-system/dashboard/src/components/PermissionDirective.js
Normal file
66
admin-system/dashboard/src/components/PermissionDirective.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
// 权限指令
|
||||
const permission = {
|
||||
mounted(el, binding) {
|
||||
const { value } = binding
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (value) {
|
||||
const hasPermission = permissionStore.hasPermission.value(value)
|
||||
if (!hasPermission) {
|
||||
el.style.display = 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
const { value } = binding
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (value) {
|
||||
const hasPermission = permissionStore.hasPermission.value(value)
|
||||
if (!hasPermission) {
|
||||
el.style.display = 'none'
|
||||
} else {
|
||||
el.style.display = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 角色指令
|
||||
const role = {
|
||||
mounted(el, binding) {
|
||||
const { value } = binding
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (value) {
|
||||
const hasRole = permissionStore.hasRole.value(value)
|
||||
if (!hasRole) {
|
||||
el.style.display = 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
const { value } = binding
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (value) {
|
||||
const hasRole = permissionStore.hasRole.value(value)
|
||||
if (!hasRole) {
|
||||
el.style.display = 'none'
|
||||
} else {
|
||||
el.style.display = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
app.directive('permission', permission)
|
||||
app.directive('role', role)
|
||||
}
|
||||
}
|
||||
676
admin-system/dashboard/src/components/RealTimeMonitor.vue
Normal file
676
admin-system/dashboard/src/components/RealTimeMonitor.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<template>
|
||||
<div class="realtime-monitor">
|
||||
<a-card title="实时监控面板" :bordered="false">
|
||||
<!-- 系统状态概览 -->
|
||||
<div class="system-overview">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card size="small" :bordered="false" class="status-card">
|
||||
<div class="status-item">
|
||||
<div class="status-icon" :class="systemStatus.api.status">
|
||||
<a-badge :status="systemStatus.api.status === 'online' ? 'success' : 'error'" />
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<div class="status-title">API服务</div>
|
||||
<div class="status-value">{{ systemStatus.api.status === 'online' ? '正常' : '异常' }}</div>
|
||||
<div class="status-detail">响应时间: {{ systemStatus.api.responseTime }}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small" :bordered="false" class="status-card">
|
||||
<div class="status-item">
|
||||
<div class="status-icon" :class="systemStatus.database.status">
|
||||
<a-badge :status="systemStatus.database.status === 'online' ? 'success' : 'error'" />
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<div class="status-title">数据库</div>
|
||||
<div class="status-value">{{ systemStatus.database.status === 'online' ? '正常' : '异常' }}</div>
|
||||
<div class="status-detail">连接数: {{ systemStatus.database.connections }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small" :bordered="false" class="status-card">
|
||||
<div class="status-item">
|
||||
<div class="status-icon" :class="systemStatus.cache.status">
|
||||
<a-badge :status="systemStatus.cache.status === 'online' ? 'success' : 'error'" />
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<div class="status-title">缓存服务</div>
|
||||
<div class="status-value">{{ systemStatus.cache.status === 'online' ? '正常' : '异常' }}</div>
|
||||
<div class="status-detail">内存使用: {{ systemStatus.cache.memoryUsage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small" :bordered="false" class="status-card">
|
||||
<div class="status-item">
|
||||
<div class="status-icon" :class="systemStatus.server.status">
|
||||
<a-badge :status="systemStatus.server.status === 'online' ? 'success' : 'error'" />
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<div class="status-title">服务器</div>
|
||||
<div class="status-value">{{ systemStatus.server.status === 'online' ? '正常' : '异常' }}</div>
|
||||
<div class="status-detail">CPU: {{ systemStatus.server.cpuUsage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 实时数据流 -->
|
||||
<div class="realtime-data-stream">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-card title="实时交易监控" size="small" :bordered="false">
|
||||
<div ref="tradingChartContainer" class="mini-chart"></div>
|
||||
<div class="data-summary">
|
||||
<a-statistic-group>
|
||||
<a-statistic title="今日交易" :value="realtimeData.trading.todayCount" suffix="笔" />
|
||||
<a-statistic title="交易金额" :value="realtimeData.trading.todayAmount" :precision="2" prefix="¥" />
|
||||
</a-statistic-group>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="环境监测" size="small" :bordered="false">
|
||||
<div ref="environmentChartContainer" class="mini-chart"></div>
|
||||
<div class="data-summary">
|
||||
<a-statistic-group>
|
||||
<a-statistic title="温度" :value="realtimeData.environment.temperature" suffix="°C" />
|
||||
<a-statistic title="湿度" :value="realtimeData.environment.humidity" suffix="%" />
|
||||
</a-statistic-group>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 告警信息 -->
|
||||
<div class="alert-section">
|
||||
<a-card title="系统告警" size="small" :bordered="false">
|
||||
<a-list
|
||||
:data-source="alerts"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-badge :status="getAlertStatus(item.level)" />
|
||||
</template>
|
||||
<template #title>
|
||||
<span :class="`alert-${item.level}`">{{ item.title }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="alert-description">
|
||||
<div>{{ item.message }}</div>
|
||||
<div class="alert-time">{{ formatTime(item.timestamp) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-button size="small" @click="handleAlert(item)">处理</a-button>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 在线用户 -->
|
||||
<div class="online-users">
|
||||
<a-card title="在线用户" size="small" :bordered="false">
|
||||
<div class="user-stats">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-statistic title="总在线用户" :value="onlineUsers.total" />
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-statistic title="管理员" :value="onlineUsers.admin" />
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-statistic title="普通用户" :value="onlineUsers.user" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div class="user-activity">
|
||||
<a-timeline size="small">
|
||||
<a-timeline-item
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
:color="getActivityColor(activity.type)"
|
||||
>
|
||||
<div class="activity-content">
|
||||
<div class="activity-user">{{ activity.user }}</div>
|
||||
<div class="activity-action">{{ activity.action }}</div>
|
||||
<div class="activity-time">{{ formatTime(activity.timestamp) }}</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { monitorAPI } from '../services/api.js';
|
||||
|
||||
const tradingChartContainer = ref(null);
|
||||
const environmentChartContainer = ref(null);
|
||||
let tradingChart = null;
|
||||
let environmentChart = null;
|
||||
let updateTimer = null;
|
||||
|
||||
// 系统状态
|
||||
const systemStatus = ref({
|
||||
api: { status: 'online', responseTime: 45 },
|
||||
database: { status: 'online', connections: 12 },
|
||||
cache: { status: 'online', memoryUsage: 68 },
|
||||
server: { status: 'online', cpuUsage: 35 }
|
||||
});
|
||||
|
||||
// 实时数据
|
||||
const realtimeData = ref({
|
||||
trading: {
|
||||
todayCount: 156,
|
||||
todayAmount: 2456789.50
|
||||
},
|
||||
environment: {
|
||||
temperature: 22.5,
|
||||
humidity: 65
|
||||
}
|
||||
});
|
||||
|
||||
// 告警信息
|
||||
const alerts = ref([
|
||||
{
|
||||
id: 1,
|
||||
level: 'warning',
|
||||
title: '服务器CPU使用率过高',
|
||||
message: '服务器CPU使用率达到85%,建议检查系统负载',
|
||||
timestamp: new Date(Date.now() - 5 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
level: 'info',
|
||||
title: '数据备份完成',
|
||||
message: '今日数据备份已成功完成',
|
||||
timestamp: new Date(Date.now() - 30 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
level: 'error',
|
||||
title: '支付接口异常',
|
||||
message: '第三方支付接口响应超时,部分交易可能受影响',
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000)
|
||||
}
|
||||
]);
|
||||
|
||||
// 在线用户
|
||||
const onlineUsers = ref({
|
||||
total: 245,
|
||||
admin: 8,
|
||||
user: 237
|
||||
});
|
||||
|
||||
// 最近活动
|
||||
const recentActivities = ref([
|
||||
{
|
||||
id: 1,
|
||||
user: '张三',
|
||||
action: '创建了新的牛只档案',
|
||||
type: 'create',
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: '李四',
|
||||
action: '完成了交易订单 #12345',
|
||||
type: 'trade',
|
||||
timestamp: new Date(Date.now() - 5 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user: '王五',
|
||||
action: '登录系统',
|
||||
type: 'login',
|
||||
timestamp: new Date(Date.now() - 8 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
user: '赵六',
|
||||
action: '更新了环境监测数据',
|
||||
type: 'update',
|
||||
timestamp: new Date(Date.now() - 12 * 60 * 1000)
|
||||
}
|
||||
]);
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 交易监控图表
|
||||
if (tradingChartContainer.value) {
|
||||
tradingChart = echarts.init(tradingChartContainer.value);
|
||||
updateTradingChart();
|
||||
}
|
||||
|
||||
// 环境监测图表
|
||||
if (environmentChartContainer.value) {
|
||||
environmentChart = echarts.init(environmentChartContainer.value);
|
||||
updateEnvironmentChart();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新交易图表
|
||||
const updateTradingChart = () => {
|
||||
if (!tradingChart) return;
|
||||
|
||||
// 生成最近24小时的模拟数据
|
||||
const hours = [];
|
||||
const amounts = [];
|
||||
const counts = [];
|
||||
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const hour = new Date();
|
||||
hour.setHours(hour.getHours() - i);
|
||||
hours.push(hour.getHours() + ':00');
|
||||
|
||||
// 模拟交易数据,白天交易更活跃
|
||||
const hourOfDay = hour.getHours();
|
||||
const baseActivity = hourOfDay >= 9 && hourOfDay <= 18 ? 1 : 0.3;
|
||||
amounts.push(Math.floor(Math.random() * 50000 * baseActivity + 10000));
|
||||
counts.push(Math.floor(Math.random() * 20 * baseActivity + 5));
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: hours,
|
||||
axisLabel: { fontSize: 10 }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
axisLabel: { fontSize: 10 }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '笔数',
|
||||
axisLabel: { fontSize: 10 }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '交易金额',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: amounts,
|
||||
itemStyle: { color: '#1890ff' },
|
||||
lineStyle: { width: 2 },
|
||||
symbol: 'none'
|
||||
},
|
||||
{
|
||||
name: '交易笔数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: counts,
|
||||
itemStyle: { color: '#52c41a', opacity: 0.7 }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
tradingChart.setOption(option);
|
||||
};
|
||||
|
||||
// 更新环境图表
|
||||
const updateEnvironmentChart = () => {
|
||||
if (!environmentChart) return;
|
||||
|
||||
// 生成最近24小时的环境数据
|
||||
const hours = [];
|
||||
const temperatures = [];
|
||||
const humidities = [];
|
||||
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const hour = new Date();
|
||||
hour.setHours(hour.getHours() - i);
|
||||
hours.push(hour.getHours() + ':00');
|
||||
|
||||
// 模拟温度和湿度数据
|
||||
const hourOfDay = hour.getHours();
|
||||
const baseTemp = 20 + 8 * Math.sin((hourOfDay / 24) * 2 * Math.PI);
|
||||
temperatures.push(Math.round((baseTemp + (Math.random() - 0.5) * 4) * 10) / 10);
|
||||
|
||||
const baseHumidity = 60 + 15 * Math.sin(((hourOfDay + 12) / 24) * 2 * Math.PI);
|
||||
humidities.push(Math.round(baseHumidity + (Math.random() - 0.5) * 10));
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: hours,
|
||||
axisLabel: { fontSize: 10 }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '温度(°C)',
|
||||
axisLabel: { fontSize: 10 }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '湿度(%)',
|
||||
axisLabel: { fontSize: 10 }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '温度',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: temperatures,
|
||||
itemStyle: { color: '#ff4d4f' },
|
||||
lineStyle: { width: 2 },
|
||||
symbol: 'none',
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '湿度',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: humidities,
|
||||
itemStyle: { color: '#1890ff' },
|
||||
lineStyle: { width: 2 },
|
||||
symbol: 'none',
|
||||
smooth: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
environmentChart.setOption(option);
|
||||
};
|
||||
|
||||
// 获取告警状态
|
||||
const getAlertStatus = (level) => {
|
||||
const statusMap = {
|
||||
error: 'error',
|
||||
warning: 'warning',
|
||||
info: 'success'
|
||||
};
|
||||
return statusMap[level] || 'default';
|
||||
};
|
||||
|
||||
// 获取活动颜色
|
||||
const getActivityColor = (type) => {
|
||||
const colorMap = {
|
||||
create: 'green',
|
||||
trade: 'blue',
|
||||
login: 'gray',
|
||||
update: 'orange'
|
||||
};
|
||||
return colorMap[type] || 'gray';
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp) => {
|
||||
const now = new Date();
|
||||
const time = new Date(timestamp);
|
||||
const diff = now - time;
|
||||
|
||||
if (diff < 60 * 1000) {
|
||||
return '刚刚';
|
||||
} else if (diff < 60 * 60 * 1000) {
|
||||
return Math.floor(diff / (60 * 1000)) + '分钟前';
|
||||
} else if (diff < 24 * 60 * 60 * 1000) {
|
||||
return Math.floor(diff / (60 * 60 * 1000)) + '小时前';
|
||||
} else {
|
||||
return time.toLocaleDateString();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理告警
|
||||
const handleAlert = (alert) => {
|
||||
console.log('处理告警:', alert);
|
||||
// 这里可以添加处理告警的逻辑
|
||||
};
|
||||
|
||||
// 更新实时数据
|
||||
const updateRealtimeData = async () => {
|
||||
try {
|
||||
const response = await monitorAPI.getRealtimeData();
|
||||
if (response.success) {
|
||||
systemStatus.value = response.data.systemStatus;
|
||||
realtimeData.value = response.data.realtimeData;
|
||||
onlineUsers.value = response.data.onlineUsers;
|
||||
} else {
|
||||
// 模拟数据更新
|
||||
updateMockData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取实时数据失败:', error);
|
||||
updateMockData();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新模拟数据
|
||||
const updateMockData = () => {
|
||||
// 随机更新一些数据
|
||||
realtimeData.value.trading.todayCount += Math.floor(Math.random() * 3);
|
||||
realtimeData.value.trading.todayAmount += Math.random() * 10000;
|
||||
|
||||
realtimeData.value.environment.temperature += (Math.random() - 0.5) * 0.5;
|
||||
realtimeData.value.environment.humidity += Math.floor((Math.random() - 0.5) * 2);
|
||||
|
||||
systemStatus.value.server.cpuUsage = Math.max(20, Math.min(90,
|
||||
systemStatus.value.server.cpuUsage + (Math.random() - 0.5) * 10));
|
||||
|
||||
onlineUsers.value.total += Math.floor((Math.random() - 0.5) * 4);
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (tradingChart) tradingChart.resize();
|
||||
if (environmentChart) environmentChart.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initCharts();
|
||||
updateRealtimeData();
|
||||
|
||||
// 设置定时更新
|
||||
updateTimer = setInterval(() => {
|
||||
updateRealtimeData();
|
||||
updateTradingChart();
|
||||
updateEnvironmentChart();
|
||||
}, 30000); // 30秒更新一次
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (updateTimer) {
|
||||
clearInterval(updateTimer);
|
||||
}
|
||||
if (tradingChart) {
|
||||
tradingChart.dispose();
|
||||
}
|
||||
if (environmentChart) {
|
||||
environmentChart.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.realtime-monitor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.system-overview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.status-detail {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.realtime-data-stream {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mini-chart {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.data-summary {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #faad14;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-description {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.online-users .user-stats {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activity-user {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.activity-action {
|
||||
color: #666;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head-title) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-list-item) {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
:deep(.ant-timeline-item-content) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-title) {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-content) {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
310
admin-system/dashboard/src/components/charts/CattleChart.vue
Normal file
310
admin-system/dashboard/src/components/charts/CattleChart.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="cattle-chart">
|
||||
<a-card title="牛只统计分析" :bordered="false">
|
||||
<div class="chart-controls">
|
||||
<a-space>
|
||||
<a-select v-model:value="timeRange" @change="handleTimeRangeChange" style="width: 120px">
|
||||
<a-select-option value="7d">近7天</a-select-option>
|
||||
<a-select-option value="30d">近30天</a-select-option>
|
||||
<a-select-option value="90d">近3个月</a-select-option>
|
||||
<a-select-option value="1y">近1年</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="chartType" @change="handleChartTypeChange" style="width: 120px">
|
||||
<a-select-option value="line">折线图</a-select-option>
|
||||
<a-select-option value="bar">柱状图</a-select-option>
|
||||
<a-select-option value="area">面积图</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
<div ref="chartContainer" class="chart-container"></div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { cattleAPI } from '../../services/api.js';
|
||||
|
||||
const chartContainer = ref(null);
|
||||
const timeRange = ref('30d');
|
||||
const chartType = ref('line');
|
||||
let chartInstance = null;
|
||||
|
||||
const chartData = ref({
|
||||
dates: [],
|
||||
totalCattle: [],
|
||||
newCattle: [],
|
||||
soldCattle: [],
|
||||
healthyCattle: [],
|
||||
sickCattle: []
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance = echarts.init(chartContainer.value);
|
||||
updateChart();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance || !chartData.value.dates.length) return;
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '牛只数量趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = `<div style="font-weight: bold; margin-bottom: 5px;">${params[0].axisValue}</div>`;
|
||||
params.forEach(param => {
|
||||
result += `<div style="margin: 2px 0;">
|
||||
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${param.color}; border-radius: 50%; margin-right: 5px;"></span>
|
||||
${param.seriesName}: ${param.value}头
|
||||
</div>`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['总数量', '新增', '出售', '健康', '患病'],
|
||||
top: 30
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.value.dates,
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return echarts.format.formatTime('MM-dd', new Date(value));
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} 头'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总数量',
|
||||
type: chartType.value,
|
||||
stack: chartType.value === 'area' ? 'Total' : null,
|
||||
areaStyle: chartType.value === 'area' ? {} : null,
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: chartData.value.totalCattle,
|
||||
itemStyle: {
|
||||
color: '#1890ff'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '新增',
|
||||
type: chartType.value,
|
||||
stack: chartType.value === 'area' ? 'Total' : null,
|
||||
areaStyle: chartType.value === 'area' ? {} : null,
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: chartData.value.newCattle,
|
||||
itemStyle: {
|
||||
color: '#52c41a'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '出售',
|
||||
type: chartType.value,
|
||||
stack: chartType.value === 'area' ? 'Total' : null,
|
||||
areaStyle: chartType.value === 'area' ? {} : null,
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: chartData.value.soldCattle,
|
||||
itemStyle: {
|
||||
color: '#faad14'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '健康',
|
||||
type: chartType.value,
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: chartData.value.healthyCattle,
|
||||
itemStyle: {
|
||||
color: '#73d13d'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '患病',
|
||||
type: chartType.value,
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: chartData.value.sickCattle,
|
||||
itemStyle: {
|
||||
color: '#ff4d4f'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await cattleAPI.getChartData({
|
||||
timeRange: timeRange.value,
|
||||
type: 'statistics'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
chartData.value = response.data;
|
||||
await nextTick();
|
||||
updateChart();
|
||||
} else {
|
||||
// 使用模拟数据
|
||||
generateMockData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载牛只统计数据失败:', error);
|
||||
generateMockData();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = () => {
|
||||
const days = timeRange.value === '7d' ? 7 : timeRange.value === '30d' ? 30 : timeRange.value === '90d' ? 90 : 365;
|
||||
const dates = [];
|
||||
const totalCattle = [];
|
||||
const newCattle = [];
|
||||
const soldCattle = [];
|
||||
const healthyCattle = [];
|
||||
const sickCattle = [];
|
||||
|
||||
let baseTotal = 1200;
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dates.push(date.toISOString().split('T')[0]);
|
||||
|
||||
// 模拟数据变化
|
||||
const dailyNew = Math.floor(Math.random() * 20) + 5;
|
||||
const dailySold = Math.floor(Math.random() * 15) + 2;
|
||||
baseTotal = baseTotal + dailyNew - dailySold;
|
||||
|
||||
totalCattle.push(baseTotal);
|
||||
newCattle.push(dailyNew);
|
||||
soldCattle.push(dailySold);
|
||||
healthyCattle.push(Math.floor(baseTotal * 0.92) + Math.floor(Math.random() * 20));
|
||||
sickCattle.push(Math.floor(baseTotal * 0.08) + Math.floor(Math.random() * 10));
|
||||
}
|
||||
|
||||
chartData.value = {
|
||||
dates,
|
||||
totalCattle,
|
||||
newCattle,
|
||||
soldCattle,
|
||||
healthyCattle,
|
||||
sickCattle
|
||||
};
|
||||
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 处理时间范围变化
|
||||
const handleTimeRangeChange = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 处理图表类型变化
|
||||
const handleChartTypeChange = () => {
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => chartData.value, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cattle-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
margin-bottom: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head-title) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div class="environment-chart">
|
||||
<a-card title="环境监测数据" :bordered="false">
|
||||
<div class="chart-controls">
|
||||
<a-space>
|
||||
<a-select v-model:value="timeRange" @change="handleTimeRangeChange" style="width: 120px">
|
||||
<a-select-option value="24h">近24小时</a-select-option>
|
||||
<a-select-option value="7d">近7天</a-select-option>
|
||||
<a-select-option value="30d">近30天</a-select-option>
|
||||
</a-select>
|
||||
<a-checkbox-group v-model:value="selectedMetrics" @change="handleMetricsChange">
|
||||
<a-checkbox value="temperature">温度</a-checkbox>
|
||||
<a-checkbox value="humidity">湿度</a-checkbox>
|
||||
<a-checkbox value="airQuality">空气质量</a-checkbox>
|
||||
<a-checkbox value="soilMoisture">土壤湿度</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-space>
|
||||
</div>
|
||||
<div ref="chartContainer" class="chart-container"></div>
|
||||
|
||||
<!-- 实时数据卡片 -->
|
||||
<div class="realtime-data">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="当前温度"
|
||||
:value="realtimeData.temperature"
|
||||
suffix="°C"
|
||||
:value-style="{ color: getTemperatureColor(realtimeData.temperature) }"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="当前湿度"
|
||||
:value="realtimeData.humidity"
|
||||
suffix="%"
|
||||
:value-style="{ color: getHumidityColor(realtimeData.humidity) }"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="空气质量指数"
|
||||
:value="realtimeData.airQuality"
|
||||
:value-style="{ color: getAirQualityColor(realtimeData.airQuality) }"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="土壤湿度"
|
||||
:value="realtimeData.soilMoisture"
|
||||
suffix="%"
|
||||
:value-style="{ color: getSoilMoistureColor(realtimeData.soilMoisture) }"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { environmentAPI } from '../../services/api.js';
|
||||
|
||||
const chartContainer = ref(null);
|
||||
const timeRange = ref('24h');
|
||||
const selectedMetrics = ref(['temperature', 'humidity', 'airQuality', 'soilMoisture']);
|
||||
let chartInstance = null;
|
||||
|
||||
const chartData = ref({
|
||||
timestamps: [],
|
||||
temperature: [],
|
||||
humidity: [],
|
||||
airQuality: [],
|
||||
soilMoisture: []
|
||||
});
|
||||
|
||||
const realtimeData = ref({
|
||||
temperature: 22.5,
|
||||
humidity: 65,
|
||||
airQuality: 85,
|
||||
soilMoisture: 45
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance = echarts.init(chartContainer.value);
|
||||
updateChart();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance || !chartData.value.timestamps.length) return;
|
||||
|
||||
const series = [];
|
||||
const yAxes = [];
|
||||
let yAxisIndex = 0;
|
||||
|
||||
// 温度系列
|
||||
if (selectedMetrics.value.includes('temperature')) {
|
||||
series.push({
|
||||
name: '温度',
|
||||
type: 'line',
|
||||
yAxisIndex: yAxisIndex,
|
||||
data: chartData.value.temperature,
|
||||
itemStyle: { color: '#ff4d4f' },
|
||||
smooth: true
|
||||
});
|
||||
yAxes.push({
|
||||
type: 'value',
|
||||
name: '温度(°C)',
|
||||
position: yAxisIndex % 2 === 0 ? 'left' : 'right',
|
||||
axisLabel: {
|
||||
formatter: '{value}°C'
|
||||
},
|
||||
splitLine: { show: yAxisIndex === 0 }
|
||||
});
|
||||
yAxisIndex++;
|
||||
}
|
||||
|
||||
// 湿度系列
|
||||
if (selectedMetrics.value.includes('humidity')) {
|
||||
series.push({
|
||||
name: '湿度',
|
||||
type: 'line',
|
||||
yAxisIndex: yAxisIndex,
|
||||
data: chartData.value.humidity,
|
||||
itemStyle: { color: '#1890ff' },
|
||||
smooth: true
|
||||
});
|
||||
yAxes.push({
|
||||
type: 'value',
|
||||
name: '湿度(%)',
|
||||
position: yAxisIndex % 2 === 0 ? 'left' : 'right',
|
||||
axisLabel: {
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false }
|
||||
});
|
||||
yAxisIndex++;
|
||||
}
|
||||
|
||||
// 空气质量系列
|
||||
if (selectedMetrics.value.includes('airQuality')) {
|
||||
series.push({
|
||||
name: '空气质量',
|
||||
type: 'line',
|
||||
yAxisIndex: yAxisIndex,
|
||||
data: chartData.value.airQuality,
|
||||
itemStyle: { color: '#52c41a' },
|
||||
smooth: true
|
||||
});
|
||||
yAxes.push({
|
||||
type: 'value',
|
||||
name: '空气质量指数',
|
||||
position: yAxisIndex % 2 === 0 ? 'left' : 'right',
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { show: false }
|
||||
});
|
||||
yAxisIndex++;
|
||||
}
|
||||
|
||||
// 土壤湿度系列
|
||||
if (selectedMetrics.value.includes('soilMoisture')) {
|
||||
series.push({
|
||||
name: '土壤湿度',
|
||||
type: 'line',
|
||||
yAxisIndex: yAxisIndex,
|
||||
data: chartData.value.soilMoisture,
|
||||
itemStyle: { color: '#faad14' },
|
||||
smooth: true
|
||||
});
|
||||
yAxes.push({
|
||||
type: 'value',
|
||||
name: '土壤湿度(%)',
|
||||
position: yAxisIndex % 2 === 0 ? 'left' : 'right',
|
||||
axisLabel: {
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false }
|
||||
});
|
||||
}
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '环境监测趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = `<div style="font-weight: bold; margin-bottom: 5px;">${params[0].axisValue}</div>`;
|
||||
params.forEach(param => {
|
||||
let unit = '';
|
||||
if (param.seriesName === '温度') unit = '°C';
|
||||
else if (param.seriesName === '湿度' || param.seriesName === '土壤湿度') unit = '%';
|
||||
|
||||
result += `<div style="margin: 2px 0;">
|
||||
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${param.color}; border-radius: 50%; margin-right: 5px;"></span>
|
||||
${param.seriesName}: ${param.value}${unit}
|
||||
</div>`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: selectedMetrics.value.map(metric => {
|
||||
const names = {
|
||||
temperature: '温度',
|
||||
humidity: '湿度',
|
||||
airQuality: '空气质量',
|
||||
soilMoisture: '土壤湿度'
|
||||
};
|
||||
return names[metric];
|
||||
}),
|
||||
top: 30
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.value.timestamps,
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
const date = new Date(value);
|
||||
if (timeRange.value === '24h') {
|
||||
return echarts.format.formatTime('hh:mm', date);
|
||||
} else {
|
||||
return echarts.format.formatTime('MM-dd', date);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: yAxes,
|
||||
series: series
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await environmentAPI.getChartData({
|
||||
timeRange: timeRange.value,
|
||||
metrics: selectedMetrics.value
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
chartData.value = response.data.historical;
|
||||
realtimeData.value = response.data.realtime;
|
||||
await nextTick();
|
||||
updateChart();
|
||||
} else {
|
||||
// 使用模拟数据
|
||||
generateMockData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载环境监测数据失败:', error);
|
||||
generateMockData();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = () => {
|
||||
const points = timeRange.value === '24h' ? 24 : timeRange.value === '7d' ? 7 * 24 : 30 * 24;
|
||||
const interval = timeRange.value === '24h' ? 1 : timeRange.value === '7d' ? 1 : 1; // 小时间隔
|
||||
|
||||
const timestamps = [];
|
||||
const temperature = [];
|
||||
const humidity = [];
|
||||
const airQuality = [];
|
||||
const soilMoisture = [];
|
||||
|
||||
for (let i = points - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - i * interval);
|
||||
timestamps.push(date.toISOString());
|
||||
|
||||
// 模拟环境数据 - 添加一些周期性和随机性
|
||||
const hourOfDay = date.getHours();
|
||||
const dayOfYear = Math.floor((date - new Date(date.getFullYear(), 0, 0)) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// 温度:白天高,夜晚低,夏季高,冬季低
|
||||
const baseTemp = 20 + 10 * Math.sin((dayOfYear / 365) * 2 * Math.PI); // 季节变化
|
||||
const dailyTemp = baseTemp + 8 * Math.sin((hourOfDay / 24) * 2 * Math.PI); // 日变化
|
||||
temperature.push(Math.round((dailyTemp + (Math.random() - 0.5) * 4) * 10) / 10);
|
||||
|
||||
// 湿度:与温度相反的趋势
|
||||
const baseHumidity = 60 + 20 * Math.sin(((dayOfYear + 180) / 365) * 2 * Math.PI);
|
||||
const dailyHumidity = baseHumidity + 15 * Math.sin(((hourOfDay + 12) / 24) * 2 * Math.PI);
|
||||
humidity.push(Math.round(Math.max(30, Math.min(90, dailyHumidity + (Math.random() - 0.5) * 10))));
|
||||
|
||||
// 空气质量:随机波动,偶尔有污染
|
||||
const baseAQ = 80 + (Math.random() - 0.5) * 30;
|
||||
const pollution = Math.random() < 0.1 ? -30 : 0; // 10%概率有污染
|
||||
airQuality.push(Math.round(Math.max(20, Math.min(100, baseAQ + pollution))));
|
||||
|
||||
// 土壤湿度:相对稳定,偶尔有灌溉
|
||||
const baseSoil = 45 + (Math.random() - 0.5) * 10;
|
||||
const irrigation = Math.random() < 0.05 ? 20 : 0; // 5%概率有灌溉
|
||||
soilMoisture.push(Math.round(Math.max(20, Math.min(80, baseSoil + irrigation))));
|
||||
}
|
||||
|
||||
chartData.value = {
|
||||
timestamps,
|
||||
temperature,
|
||||
humidity,
|
||||
airQuality,
|
||||
soilMoisture
|
||||
};
|
||||
|
||||
// 更新实时数据为最新值
|
||||
realtimeData.value = {
|
||||
temperature: temperature[temperature.length - 1],
|
||||
humidity: humidity[humidity.length - 1],
|
||||
airQuality: airQuality[airQuality.length - 1],
|
||||
soilMoisture: soilMoisture[soilMoisture.length - 1]
|
||||
};
|
||||
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 处理时间范围变化
|
||||
const handleTimeRangeChange = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 处理指标选择变化
|
||||
const handleMetricsChange = () => {
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
// 获取温度颜色
|
||||
const getTemperatureColor = (temp) => {
|
||||
if (temp < 10) return '#1890ff';
|
||||
if (temp < 25) return '#52c41a';
|
||||
if (temp < 35) return '#faad14';
|
||||
return '#ff4d4f';
|
||||
};
|
||||
|
||||
// 获取湿度颜色
|
||||
const getHumidityColor = (humidity) => {
|
||||
if (humidity < 40) return '#ff4d4f';
|
||||
if (humidity < 70) return '#52c41a';
|
||||
return '#1890ff';
|
||||
};
|
||||
|
||||
// 获取空气质量颜色
|
||||
const getAirQualityColor = (aqi) => {
|
||||
if (aqi >= 80) return '#52c41a';
|
||||
if (aqi >= 60) return '#faad14';
|
||||
return '#ff4d4f';
|
||||
};
|
||||
|
||||
// 获取土壤湿度颜色
|
||||
const getSoilMoistureColor = (moisture) => {
|
||||
if (moisture < 30) return '#ff4d4f';
|
||||
if (moisture < 60) return '#faad14';
|
||||
return '#52c41a';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => chartData.value, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
|
||||
watch(() => selectedMetrics.value, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.environment-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.realtime-data {
|
||||
background: #fafafa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head-title) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-title) {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-content) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
440
admin-system/dashboard/src/components/charts/TradingChart.vue
Normal file
440
admin-system/dashboard/src/components/charts/TradingChart.vue
Normal file
@@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<div class="trading-chart">
|
||||
<a-card title="交易统计分析" :bordered="false">
|
||||
<div class="chart-controls">
|
||||
<a-space>
|
||||
<a-select v-model:value="timeRange" @change="handleTimeRangeChange" style="width: 120px">
|
||||
<a-select-option value="7d">近7天</a-select-option>
|
||||
<a-select-option value="30d">近30天</a-select-option>
|
||||
<a-select-option value="90d">近3个月</a-select-option>
|
||||
<a-select-option value="1y">近1年</a-select-option>
|
||||
</a-select>
|
||||
<a-radio-group v-model:value="viewType" @change="handleViewTypeChange" button-style="solid" size="small">
|
||||
<a-radio-button value="amount">交易金额</a-radio-button>
|
||||
<a-radio-button value="count">交易笔数</a-radio-button>
|
||||
<a-radio-button value="both">综合视图</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-space>
|
||||
</div>
|
||||
<div ref="chartContainer" class="chart-container"></div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { tradingAPI } from '../../services/api.js';
|
||||
|
||||
const chartContainer = ref(null);
|
||||
const timeRange = ref('30d');
|
||||
const viewType = ref('both');
|
||||
let chartInstance = null;
|
||||
|
||||
const chartData = ref({
|
||||
dates: [],
|
||||
totalAmount: [],
|
||||
totalCount: [],
|
||||
successAmount: [],
|
||||
successCount: [],
|
||||
pendingAmount: [],
|
||||
pendingCount: [],
|
||||
cancelledCount: []
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance = echarts.init(chartContainer.value);
|
||||
updateChart();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance || !chartData.value.dates.length) return;
|
||||
|
||||
let option = {};
|
||||
|
||||
if (viewType.value === 'amount') {
|
||||
// 交易金额视图
|
||||
option = {
|
||||
title: {
|
||||
text: '交易金额趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = `<div style="font-weight: bold; margin-bottom: 5px;">${params[0].axisValue}</div>`;
|
||||
params.forEach(param => {
|
||||
result += `<div style="margin: 2px 0;">
|
||||
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${param.color}; border-radius: 50%; margin-right: 5px;"></span>
|
||||
${param.seriesName}: ¥${(param.value / 10000).toFixed(2)}万
|
||||
</div>`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['总交易额', '成功交易额', '待处理交易额'],
|
||||
top: 30
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.value.dates,
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return echarts.format.formatTime('MM-dd', new Date(value));
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return '¥' + (value / 10000).toFixed(1) + '万';
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总交易额',
|
||||
type: 'line',
|
||||
data: chartData.value.totalAmount,
|
||||
itemStyle: { color: '#1890ff' },
|
||||
areaStyle: { opacity: 0.3 }
|
||||
},
|
||||
{
|
||||
name: '成功交易额',
|
||||
type: 'line',
|
||||
data: chartData.value.successAmount,
|
||||
itemStyle: { color: '#52c41a' }
|
||||
},
|
||||
{
|
||||
name: '待处理交易额',
|
||||
type: 'line',
|
||||
data: chartData.value.pendingAmount,
|
||||
itemStyle: { color: '#faad14' }
|
||||
}
|
||||
]
|
||||
};
|
||||
} else if (viewType.value === 'count') {
|
||||
// 交易笔数视图
|
||||
option = {
|
||||
title: {
|
||||
text: '交易笔数趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = `<div style="font-weight: bold; margin-bottom: 5px;">${params[0].axisValue}</div>`;
|
||||
params.forEach(param => {
|
||||
result += `<div style="margin: 2px 0;">
|
||||
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${param.color}; border-radius: 50%; margin-right: 5px;"></span>
|
||||
${param.seriesName}: ${param.value}笔
|
||||
</div>`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['总交易笔数', '成功交易', '待处理', '已取消'],
|
||||
top: 30
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.value.dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} 笔'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总交易笔数',
|
||||
type: 'bar',
|
||||
data: chartData.value.totalCount,
|
||||
itemStyle: { color: '#1890ff' }
|
||||
},
|
||||
{
|
||||
name: '成功交易',
|
||||
type: 'bar',
|
||||
data: chartData.value.successCount,
|
||||
itemStyle: { color: '#52c41a' }
|
||||
},
|
||||
{
|
||||
name: '待处理',
|
||||
type: 'bar',
|
||||
data: chartData.value.pendingCount,
|
||||
itemStyle: { color: '#faad14' }
|
||||
},
|
||||
{
|
||||
name: '已取消',
|
||||
type: 'bar',
|
||||
data: chartData.value.cancelledCount,
|
||||
itemStyle: { color: '#ff4d4f' }
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
// 综合视图 - 双Y轴
|
||||
option = {
|
||||
title: {
|
||||
text: '交易综合分析',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = `<div style="font-weight: bold; margin-bottom: 5px;">${params[0].axisValue}</div>`;
|
||||
params.forEach(param => {
|
||||
const unit = param.seriesName.includes('金额') ? '万元' : '笔';
|
||||
const value = param.seriesName.includes('金额') ? (param.value / 10000).toFixed(2) : param.value;
|
||||
result += `<div style="margin: 2px 0;">
|
||||
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${param.color}; border-radius: 50%; margin-right: 5px;"></span>
|
||||
${param.seriesName}: ${value}${unit}
|
||||
</div>`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['交易金额', '交易笔数'],
|
||||
top: 30
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.value.dates
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '交易金额',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return '¥' + (value / 10000).toFixed(1) + '万';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '交易笔数',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
formatter: '{value} 笔'
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '交易金额',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: chartData.value.totalAmount,
|
||||
itemStyle: { color: '#1890ff' },
|
||||
areaStyle: { opacity: 0.3 }
|
||||
},
|
||||
{
|
||||
name: '交易笔数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: chartData.value.totalCount,
|
||||
itemStyle: { color: '#52c41a', opacity: 0.8 }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await tradingAPI.getChartData({
|
||||
timeRange: timeRange.value,
|
||||
type: 'statistics'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
chartData.value = response.data;
|
||||
await nextTick();
|
||||
updateChart();
|
||||
} else {
|
||||
// 使用模拟数据
|
||||
generateMockData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载交易统计数据失败:', error);
|
||||
generateMockData();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = () => {
|
||||
const days = timeRange.value === '7d' ? 7 : timeRange.value === '30d' ? 30 : timeRange.value === '90d' ? 90 : 365;
|
||||
const dates = [];
|
||||
const totalAmount = [];
|
||||
const totalCount = [];
|
||||
const successAmount = [];
|
||||
const successCount = [];
|
||||
const pendingAmount = [];
|
||||
const pendingCount = [];
|
||||
const cancelledCount = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dates.push(date.toISOString().split('T')[0]);
|
||||
|
||||
// 模拟交易数据
|
||||
const dailyCount = Math.floor(Math.random() * 50) + 20;
|
||||
const dailyAmount = dailyCount * (Math.random() * 50000 + 10000); // 每笔1-6万
|
||||
|
||||
const successRate = 0.7 + Math.random() * 0.2; // 70-90%成功率
|
||||
const pendingRate = 0.1 + Math.random() * 0.1; // 10-20%待处理
|
||||
const cancelledRate = 1 - successRate - pendingRate;
|
||||
|
||||
totalCount.push(dailyCount);
|
||||
totalAmount.push(Math.floor(dailyAmount));
|
||||
successCount.push(Math.floor(dailyCount * successRate));
|
||||
successAmount.push(Math.floor(dailyAmount * successRate));
|
||||
pendingCount.push(Math.floor(dailyCount * pendingRate));
|
||||
pendingAmount.push(Math.floor(dailyAmount * pendingRate));
|
||||
cancelledCount.push(Math.floor(dailyCount * cancelledRate));
|
||||
}
|
||||
|
||||
chartData.value = {
|
||||
dates,
|
||||
totalAmount,
|
||||
totalCount,
|
||||
successAmount,
|
||||
successCount,
|
||||
pendingAmount,
|
||||
pendingCount,
|
||||
cancelledCount
|
||||
};
|
||||
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 处理时间范围变化
|
||||
const handleTimeRangeChange = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 处理视图类型变化
|
||||
const handleViewTypeChange = () => {
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => chartData.value, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trading-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
margin-bottom: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head-title) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { useAuthStore } from './store/auth.js'
|
||||
import PermissionDirective from './components/PermissionDirective.js'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
@@ -14,6 +15,7 @@ const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
app.use(PermissionDirective)
|
||||
|
||||
// 初始化认证状态
|
||||
const authStore = useAuthStore()
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '../store/auth.js'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 导入组件
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import Monitor from '@/views/Monitor.vue'
|
||||
import MonitorCenter from '@/views/MonitorCenter.vue'
|
||||
import UserManagement from '@/views/system/UserManagement.vue'
|
||||
import RoleManagement from '@/views/system/RoleManagement.vue'
|
||||
import Government from '@/views/Government.vue'
|
||||
import Finance from '@/views/Finance.vue'
|
||||
import Transport from '@/views/Transport.vue'
|
||||
@@ -10,7 +17,7 @@ import Eco from '@/views/Eco.vue'
|
||||
import Gov from '@/views/Gov.vue'
|
||||
import Trade from '@/views/Trade.vue'
|
||||
import Login from '@/views/Login.vue'
|
||||
import UserManagement from '@/views/UserManagement.vue'
|
||||
import UserManagementOld from '@/views/UserManagement.vue'
|
||||
import CattleManagement from '@/views/CattleManagement.vue'
|
||||
import MallManagement from '@/views/MallManagement.vue'
|
||||
|
||||
@@ -33,6 +40,30 @@ const routes = [
|
||||
component: Monitor,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/monitor-center',
|
||||
name: 'MonitorCenter',
|
||||
component: MonitorCenter,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/system/users',
|
||||
name: 'UserManagement',
|
||||
component: UserManagement,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permission: 'user:view'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/roles',
|
||||
name: 'RoleManagement',
|
||||
component: RoleManagement,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permission: 'role:view'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/government',
|
||||
name: 'Government',
|
||||
@@ -77,8 +108,8 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'UserManagement',
|
||||
component: UserManagement,
|
||||
name: 'UserManagementOld',
|
||||
component: UserManagementOld,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
@@ -101,25 +132,28 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 初始化认证状态
|
||||
if (!authStore.isAuthenticated) {
|
||||
authStore.initAuth()
|
||||
// 检查是否需要认证
|
||||
if (to.meta.requiresAuth) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (to.meta.permission) {
|
||||
const permissionStore = usePermissionStore()
|
||||
if (!permissionStore.hasPermission.value(to.meta.permission)) {
|
||||
ElMessage.error('您没有访问该页面的权限')
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要认证
|
||||
if (to.meta.requiresAuth !== false && !authStore.isAuthenticated) {
|
||||
// 需要认证但未登录,跳转到登录页
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && authStore.isAuthenticated) {
|
||||
// 已登录用户访问登录页,跳转到首页
|
||||
next('/')
|
||||
} else {
|
||||
// 正常访问
|
||||
next()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -47,6 +47,43 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
// ======================================
|
||||
// 监控API
|
||||
// ======================================
|
||||
export const monitorAPI = {
|
||||
// 获取系统状态
|
||||
getSystemStatus: () => apiClient.get('/monitor/system-status'),
|
||||
|
||||
// 获取实时数据流
|
||||
getRealTimeData: () => apiClient.get('/monitor/realtime-data'),
|
||||
|
||||
// 获取告警信息
|
||||
getAlerts: () => apiClient.get('/monitor/alerts'),
|
||||
|
||||
// 获取在线用户
|
||||
getOnlineUsers: () => apiClient.get('/monitor/online-users'),
|
||||
|
||||
// 获取性能指标
|
||||
getPerformanceMetrics: () => apiClient.get('/monitor/performance'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 环境监测API
|
||||
// ======================================
|
||||
export const environmentAPI = {
|
||||
// 获取环境数据
|
||||
getEnvironmentData: (params) => apiClient.get('/environment/data', { params }),
|
||||
|
||||
// 获取实时环境指标
|
||||
getRealTimeMetrics: () => apiClient.get('/environment/realtime'),
|
||||
|
||||
// 获取历史环境数据
|
||||
getHistoryData: (params) => apiClient.get('/environment/history', { params }),
|
||||
|
||||
// 获取环境告警
|
||||
getEnvironmentAlerts: () => apiClient.get('/environment/alerts'),
|
||||
};
|
||||
|
||||
// ======================================
|
||||
// 认证相关API
|
||||
// ======================================
|
||||
@@ -251,6 +288,51 @@ export const systemAPI = {
|
||||
getOperationLogs: (params) => apiClient.get('/logs/operations', { params }),
|
||||
};
|
||||
|
||||
// 用户管理API
|
||||
export const userManagementAPI = {
|
||||
// 获取用户列表
|
||||
getUsers: (params) => apiClient.get('/admin/users', { params }),
|
||||
|
||||
// 创建用户
|
||||
createUser: (data) => apiClient.post('/admin/users', data),
|
||||
|
||||
// 更新用户
|
||||
updateUser: (id, data) => apiClient.put(`/admin/users/${id}`, data),
|
||||
|
||||
// 删除用户
|
||||
deleteUser: (id) => apiClient.delete(`/admin/users/${id}`),
|
||||
|
||||
// 更新用户状态
|
||||
updateUserStatus: (id, status) => apiClient.patch(`/admin/users/${id}/status`, { status }),
|
||||
|
||||
// 重置用户密码
|
||||
resetPassword: (id, password) => apiClient.patch(`/admin/users/${id}/password`, { password })
|
||||
}
|
||||
|
||||
// 角色管理API
|
||||
export const roleManagementAPI = {
|
||||
// 获取角色列表
|
||||
getRoles: () => apiClient.get('/admin/roles'),
|
||||
|
||||
// 创建角色
|
||||
createRole: (data) => apiClient.post('/admin/roles', data),
|
||||
|
||||
// 更新角色
|
||||
updateRole: (id, data) => apiClient.put(`/admin/roles/${id}`, data),
|
||||
|
||||
// 删除角色
|
||||
deleteRole: (id) => apiClient.delete(`/admin/roles/${id}`),
|
||||
|
||||
// 获取角色权限
|
||||
getRolePermissions: (id) => apiClient.get(`/admin/roles/${id}/permissions`),
|
||||
|
||||
// 更新角色权限
|
||||
updateRolePermissions: (id, permissions) => apiClient.put(`/admin/roles/${id}/permissions`, { permissions }),
|
||||
|
||||
// 获取所有权限
|
||||
getAllPermissions: () => apiClient.get('/admin/permissions')
|
||||
}
|
||||
|
||||
// 导出所有API
|
||||
export default {
|
||||
auth: authAPI,
|
||||
@@ -262,6 +344,10 @@ export default {
|
||||
mall: mallAPI,
|
||||
dashboard: dashboardAPI,
|
||||
system: systemAPI,
|
||||
monitor: monitorAPI,
|
||||
environment: environmentAPI,
|
||||
userManagement: userManagementAPI,
|
||||
roleManagement: roleManagementAPI,
|
||||
};
|
||||
|
||||
// 导出axios实例供其他地方使用
|
||||
|
||||
218
admin-system/dashboard/src/stores/auth.js
Normal file
218
admin-system/dashboard/src/stores/auth.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { authAPI } from '@/services/api'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('auth_token') || null)
|
||||
const permissions = ref([])
|
||||
const roles = ref([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const userRole = computed(() => user.value?.role || 'guest')
|
||||
const userName = computed(() => user.value?.name || '未知用户')
|
||||
const userAvatar = computed(() => user.value?.avatar || '/default-avatar.png')
|
||||
|
||||
// 权限检查
|
||||
const hasPermission = computed(() => (permission) => {
|
||||
if (!permissions.value.length) return false
|
||||
return permissions.value.includes(permission) || permissions.value.includes('*')
|
||||
})
|
||||
|
||||
const hasRole = computed(() => (role) => {
|
||||
if (!roles.value.length) return false
|
||||
return roles.value.includes(role) || roles.value.includes('admin')
|
||||
})
|
||||
|
||||
const hasAnyRole = computed(() => (roleList) => {
|
||||
if (!roles.value.length) return false
|
||||
return roleList.some(role => roles.value.includes(role))
|
||||
})
|
||||
|
||||
// 动作
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await authAPI.login(credentials)
|
||||
|
||||
if (response.success) {
|
||||
token.value = response.data.token
|
||||
user.value = response.data.user
|
||||
permissions.value = response.data.permissions || []
|
||||
roles.value = response.data.roles || []
|
||||
|
||||
// 存储到本地存储
|
||||
localStorage.setItem('auth_token', token.value)
|
||||
localStorage.setItem('user_info', JSON.stringify(user.value))
|
||||
localStorage.setItem('user_permissions', JSON.stringify(permissions.value))
|
||||
localStorage.setItem('user_roles', JSON.stringify(roles.value))
|
||||
|
||||
return { success: true, message: '登录成功' }
|
||||
} else {
|
||||
return { success: false, message: response.message || '登录失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return { success: false, message: '网络错误,请稍后重试' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
// 调用后端登出接口
|
||||
if (token.value) {
|
||||
await authAPI.logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登出错误:', error)
|
||||
} finally {
|
||||
// 清除本地状态
|
||||
user.value = null
|
||||
token.value = null
|
||||
permissions.value = []
|
||||
roles.value = []
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('user_info')
|
||||
localStorage.removeItem('user_permissions')
|
||||
localStorage.removeItem('user_roles')
|
||||
}
|
||||
}
|
||||
|
||||
const refreshToken = async () => {
|
||||
try {
|
||||
const response = await authAPI.refreshToken()
|
||||
if (response.success) {
|
||||
token.value = response.data.token
|
||||
localStorage.setItem('auth_token', token.value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('刷新token错误:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfile = async (profileData) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await authAPI.updateProfile(profileData)
|
||||
|
||||
if (response.success) {
|
||||
user.value = { ...user.value, ...response.data }
|
||||
localStorage.setItem('user_info', JSON.stringify(user.value))
|
||||
return { success: true, message: '更新成功' }
|
||||
} else {
|
||||
return { success: false, message: response.message || '更新失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用户信息错误:', error)
|
||||
return { success: false, message: '网络错误,请稍后重试' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const changePassword = async (passwordData) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await authAPI.changePassword(passwordData)
|
||||
|
||||
if (response.success) {
|
||||
return { success: true, message: '密码修改成功' }
|
||||
} else {
|
||||
return { success: false, message: response.message || '密码修改失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码错误:', error)
|
||||
return { success: false, message: '网络错误,请稍后重试' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化用户信息(从本地存储恢复)
|
||||
const initializeAuth = () => {
|
||||
const storedUser = localStorage.getItem('user_info')
|
||||
const storedPermissions = localStorage.getItem('user_permissions')
|
||||
const storedRoles = localStorage.getItem('user_roles')
|
||||
|
||||
if (storedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(storedUser)
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (storedPermissions) {
|
||||
try {
|
||||
permissions.value = JSON.parse(storedPermissions)
|
||||
} catch (error) {
|
||||
console.error('解析权限信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (storedRoles) {
|
||||
try {
|
||||
roles.value = JSON.parse(storedRoles)
|
||||
} catch (error) {
|
||||
console.error('解析角色信息失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查token是否过期
|
||||
const checkTokenExpiry = () => {
|
||||
if (!token.value) return false
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.value.split('.')[1]))
|
||||
const currentTime = Date.now() / 1000
|
||||
|
||||
// 如果token在30分钟内过期,尝试刷新
|
||||
if (payload.exp - currentTime < 1800) {
|
||||
refreshToken()
|
||||
}
|
||||
|
||||
return payload.exp > currentTime
|
||||
} catch (error) {
|
||||
console.error('检查token过期时间失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
token,
|
||||
permissions,
|
||||
roles,
|
||||
isLoading,
|
||||
|
||||
// 计算属性
|
||||
isAuthenticated,
|
||||
userRole,
|
||||
userName,
|
||||
userAvatar,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasAnyRole,
|
||||
|
||||
// 动作
|
||||
login,
|
||||
logout,
|
||||
refreshToken,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
initializeAuth,
|
||||
checkTokenExpiry
|
||||
}
|
||||
})
|
||||
312
admin-system/dashboard/src/stores/permission.js
Normal file
312
admin-system/dashboard/src/stores/permission.js
Normal file
@@ -0,0 +1,312 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
export const usePermissionStore = defineStore('permission', () => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 状态
|
||||
const menuList = ref([])
|
||||
const permissionList = ref([])
|
||||
const roleList = ref([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 权限常量定义
|
||||
const PERMISSIONS = {
|
||||
// 用户管理
|
||||
USER_VIEW: 'user:view',
|
||||
USER_CREATE: 'user:create',
|
||||
USER_EDIT: 'user:edit',
|
||||
USER_DELETE: 'user:delete',
|
||||
|
||||
// 角色管理
|
||||
ROLE_VIEW: 'role:view',
|
||||
ROLE_CREATE: 'role:create',
|
||||
ROLE_EDIT: 'role:edit',
|
||||
ROLE_DELETE: 'role:delete',
|
||||
|
||||
// 权限管理
|
||||
PERMISSION_VIEW: 'permission:view',
|
||||
PERMISSION_CREATE: 'permission:create',
|
||||
PERMISSION_EDIT: 'permission:edit',
|
||||
PERMISSION_DELETE: 'permission:delete',
|
||||
|
||||
// 牛只管理
|
||||
CATTLE_VIEW: 'cattle:view',
|
||||
CATTLE_CREATE: 'cattle:create',
|
||||
CATTLE_EDIT: 'cattle:edit',
|
||||
CATTLE_DELETE: 'cattle:delete',
|
||||
|
||||
// 交易管理
|
||||
TRADING_VIEW: 'trading:view',
|
||||
TRADING_CREATE: 'trading:create',
|
||||
TRADING_EDIT: 'trading:edit',
|
||||
TRADING_DELETE: 'trading:delete',
|
||||
|
||||
// 财务管理
|
||||
FINANCE_VIEW: 'finance:view',
|
||||
FINANCE_CREATE: 'finance:create',
|
||||
FINANCE_EDIT: 'finance:edit',
|
||||
FINANCE_DELETE: 'finance:delete',
|
||||
|
||||
// 监控管理
|
||||
MONITOR_VIEW: 'monitor:view',
|
||||
MONITOR_MANAGE: 'monitor:manage',
|
||||
|
||||
// 系统管理
|
||||
SYSTEM_VIEW: 'system:view',
|
||||
SYSTEM_MANAGE: 'system:manage',
|
||||
|
||||
// 超级管理员
|
||||
ADMIN_ALL: '*'
|
||||
}
|
||||
|
||||
// 角色常量定义
|
||||
const ROLES = {
|
||||
SUPER_ADMIN: 'super_admin',
|
||||
ADMIN: 'admin',
|
||||
MANAGER: 'manager',
|
||||
OPERATOR: 'operator',
|
||||
VIEWER: 'viewer'
|
||||
}
|
||||
|
||||
// 默认菜单配置
|
||||
const defaultMenus = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
name: '首页',
|
||||
path: '/',
|
||||
icon: 'el-icon-house',
|
||||
permission: null, // 所有用户都可以访问
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 'monitor',
|
||||
name: '监控中心',
|
||||
path: '/monitor',
|
||||
icon: 'el-icon-monitor',
|
||||
permission: PERMISSIONS.MONITOR_VIEW,
|
||||
children: [
|
||||
{
|
||||
id: 'monitor-center',
|
||||
name: '实时监控',
|
||||
path: '/monitor-center',
|
||||
permission: PERMISSIONS.MONITOR_VIEW
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'cattle',
|
||||
name: '牛只管理',
|
||||
path: '/cattle',
|
||||
icon: 'el-icon-cow',
|
||||
permission: PERMISSIONS.CATTLE_VIEW,
|
||||
children: [
|
||||
{
|
||||
id: 'cattle-list',
|
||||
name: '牛只列表',
|
||||
path: '/cattle/list',
|
||||
permission: PERMISSIONS.CATTLE_VIEW
|
||||
},
|
||||
{
|
||||
id: 'cattle-add',
|
||||
name: '添加牛只',
|
||||
path: '/cattle/add',
|
||||
permission: PERMISSIONS.CATTLE_CREATE
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'trading',
|
||||
name: '交易管理',
|
||||
path: '/trading',
|
||||
icon: 'el-icon-goods',
|
||||
permission: PERMISSIONS.TRADING_VIEW,
|
||||
children: [
|
||||
{
|
||||
id: 'trading-list',
|
||||
name: '交易列表',
|
||||
path: '/trading/list',
|
||||
permission: PERMISSIONS.TRADING_VIEW
|
||||
},
|
||||
{
|
||||
id: 'trading-add',
|
||||
name: '新增交易',
|
||||
path: '/trading/add',
|
||||
permission: PERMISSIONS.TRADING_CREATE
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'finance',
|
||||
name: '财务管理',
|
||||
path: '/finance',
|
||||
icon: 'el-icon-money',
|
||||
permission: PERMISSIONS.FINANCE_VIEW,
|
||||
children: [
|
||||
{
|
||||
id: 'finance-records',
|
||||
name: '财务记录',
|
||||
path: '/finance/records',
|
||||
permission: PERMISSIONS.FINANCE_VIEW
|
||||
},
|
||||
{
|
||||
id: 'finance-statistics',
|
||||
name: '财务统计',
|
||||
path: '/finance/statistics',
|
||||
permission: PERMISSIONS.FINANCE_VIEW
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
name: '系统管理',
|
||||
path: '/system',
|
||||
icon: 'el-icon-setting',
|
||||
permission: PERMISSIONS.SYSTEM_VIEW,
|
||||
children: [
|
||||
{
|
||||
id: 'user-management',
|
||||
name: '用户管理',
|
||||
path: '/system/users',
|
||||
permission: PERMISSIONS.USER_VIEW
|
||||
},
|
||||
{
|
||||
id: 'role-management',
|
||||
name: '角色管理',
|
||||
path: '/system/roles',
|
||||
permission: PERMISSIONS.ROLE_VIEW
|
||||
},
|
||||
{
|
||||
id: 'permission-management',
|
||||
name: '权限管理',
|
||||
path: '/system/permissions',
|
||||
permission: PERMISSIONS.PERMISSION_VIEW
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const accessibleMenus = computed(() => {
|
||||
return filterMenusByPermission(defaultMenus)
|
||||
})
|
||||
|
||||
const hasPermission = computed(() => (permission) => {
|
||||
if (!permission) return true // 无权限要求的菜单所有人都可以访问
|
||||
return authStore.hasPermission.value(permission)
|
||||
})
|
||||
|
||||
const hasRole = computed(() => (role) => {
|
||||
return authStore.hasRole.value(role)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const filterMenusByPermission = (menus) => {
|
||||
return menus.filter(menu => {
|
||||
// 检查当前菜单权限
|
||||
if (menu.permission && !hasPermission.value(menu.permission)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 递归过滤子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children = filterMenusByPermission(menu.children)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const checkRoutePermission = (route) => {
|
||||
const routePath = route.path
|
||||
|
||||
// 查找对应的菜单项
|
||||
const findMenuByPath = (menus, path) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu
|
||||
}
|
||||
if (menu.children) {
|
||||
const found = findMenuByPath(menu.children, path)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const menuItem = findMenuByPath(defaultMenus, routePath)
|
||||
if (!menuItem) return true // 未配置的路由默认允许访问
|
||||
|
||||
return hasPermission.value(menuItem.permission)
|
||||
}
|
||||
|
||||
const getPermissionsByRole = (role) => {
|
||||
const rolePermissions = {
|
||||
[ROLES.SUPER_ADMIN]: [PERMISSIONS.ADMIN_ALL],
|
||||
[ROLES.ADMIN]: [
|
||||
PERMISSIONS.USER_VIEW, PERMISSIONS.USER_CREATE, PERMISSIONS.USER_EDIT, PERMISSIONS.USER_DELETE,
|
||||
PERMISSIONS.ROLE_VIEW, PERMISSIONS.ROLE_CREATE, PERMISSIONS.ROLE_EDIT, PERMISSIONS.ROLE_DELETE,
|
||||
PERMISSIONS.PERMISSION_VIEW, PERMISSIONS.PERMISSION_CREATE, PERMISSIONS.PERMISSION_EDIT, PERMISSIONS.PERMISSION_DELETE,
|
||||
PERMISSIONS.CATTLE_VIEW, PERMISSIONS.CATTLE_CREATE, PERMISSIONS.CATTLE_EDIT, PERMISSIONS.CATTLE_DELETE,
|
||||
PERMISSIONS.TRADING_VIEW, PERMISSIONS.TRADING_CREATE, PERMISSIONS.TRADING_EDIT, PERMISSIONS.TRADING_DELETE,
|
||||
PERMISSIONS.FINANCE_VIEW, PERMISSIONS.FINANCE_CREATE, PERMISSIONS.FINANCE_EDIT, PERMISSIONS.FINANCE_DELETE,
|
||||
PERMISSIONS.MONITOR_VIEW, PERMISSIONS.MONITOR_MANAGE,
|
||||
PERMISSIONS.SYSTEM_VIEW, PERMISSIONS.SYSTEM_MANAGE
|
||||
],
|
||||
[ROLES.MANAGER]: [
|
||||
PERMISSIONS.USER_VIEW,
|
||||
PERMISSIONS.CATTLE_VIEW, PERMISSIONS.CATTLE_CREATE, PERMISSIONS.CATTLE_EDIT,
|
||||
PERMISSIONS.TRADING_VIEW, PERMISSIONS.TRADING_CREATE, PERMISSIONS.TRADING_EDIT,
|
||||
PERMISSIONS.FINANCE_VIEW, PERMISSIONS.FINANCE_CREATE, PERMISSIONS.FINANCE_EDIT,
|
||||
PERMISSIONS.MONITOR_VIEW
|
||||
],
|
||||
[ROLES.OPERATOR]: [
|
||||
PERMISSIONS.CATTLE_VIEW, PERMISSIONS.CATTLE_CREATE, PERMISSIONS.CATTLE_EDIT,
|
||||
PERMISSIONS.TRADING_VIEW, PERMISSIONS.TRADING_CREATE,
|
||||
PERMISSIONS.FINANCE_VIEW,
|
||||
PERMISSIONS.MONITOR_VIEW
|
||||
],
|
||||
[ROLES.VIEWER]: [
|
||||
PERMISSIONS.CATTLE_VIEW,
|
||||
PERMISSIONS.TRADING_VIEW,
|
||||
PERMISSIONS.FINANCE_VIEW,
|
||||
PERMISSIONS.MONITOR_VIEW
|
||||
]
|
||||
}
|
||||
|
||||
return rolePermissions[role] || []
|
||||
}
|
||||
|
||||
const initializePermissions = () => {
|
||||
// 从认证store获取用户权限信息
|
||||
permissionList.value = authStore.permissions
|
||||
roleList.value = authStore.roles
|
||||
|
||||
// 生成可访问的菜单
|
||||
menuList.value = accessibleMenus.value
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
menuList,
|
||||
permissionList,
|
||||
roleList,
|
||||
isLoading,
|
||||
|
||||
// 常量
|
||||
PERMISSIONS,
|
||||
ROLES,
|
||||
|
||||
// 计算属性
|
||||
accessibleMenus,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
|
||||
// 方法
|
||||
filterMenusByPermission,
|
||||
checkRoutePermission,
|
||||
getPermissionsByRole,
|
||||
initializePermissions
|
||||
}
|
||||
})
|
||||
@@ -94,8 +94,21 @@
|
||||
<!-- 交易数据 -->
|
||||
<div class="transaction-card">
|
||||
<div class="chart-border card">
|
||||
<h3 class="chart-title">交易数据</h3>
|
||||
<div ref="transactionChart" class="chart-wrapper"></div>
|
||||
<TradingChart />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 牛只统计 -->
|
||||
<div class="cattle-card">
|
||||
<div class="chart-border card">
|
||||
<CattleChart />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 环境监测 -->
|
||||
<div class="environment-card">
|
||||
<div class="chart-border card">
|
||||
<EnvironmentChart />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -124,13 +137,19 @@ import * as echarts from 'echarts'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import ThreeDMap from '@/components/map/ThreeDMap.vue'
|
||||
import ApiTest from '@/components/ApiTest.vue'
|
||||
import TradingChart from '@/components/charts/TradingChart.vue'
|
||||
import CattleChart from '@/components/charts/CattleChart.vue'
|
||||
import EnvironmentChart from '@/components/charts/EnvironmentChart.vue'
|
||||
import { fetchMapData } from '@/services/dashboard.js'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
ThreeDMap,
|
||||
ApiTest
|
||||
ApiTest,
|
||||
TradingChart,
|
||||
CattleChart,
|
||||
EnvironmentChart
|
||||
},
|
||||
setup() {
|
||||
const currentTime = ref(new Date().toLocaleString())
|
||||
|
||||
@@ -2,185 +2,137 @@
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1>锡林郭勒盟智慧养殖平台</h1>
|
||||
<p>数字化管理系统</p>
|
||||
<h2>管理后台登录</h2>
|
||||
<p>欢迎使用牛只管理系统</p>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
@finish="handleLogin"
|
||||
<el-form
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
ref="loginFormRef"
|
||||
class="login-form"
|
||||
layout="vertical"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<a-form-item name="username" label="用户名">
|
||||
<a-input
|
||||
v-model:value="loginForm.username"
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
:prefix="renderIcon('user')"
|
||||
>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
prefix-icon="User"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<a-form-item name="password" label="密码">
|
||||
<a-input-password
|
||||
v-model:value="loginForm.password"
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
:prefix="renderIcon('lock')"
|
||||
prefix-icon="Lock"
|
||||
show-password
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</a-form-item>
|
||||
</el-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="loginForm.remember">
|
||||
记住登录状态
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="loginForm.remember">记住我</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
block
|
||||
@click="handleLogin"
|
||||
class="login-button"
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="demo-accounts">
|
||||
<h4>演示账户</h4>
|
||||
<div class="account-list">
|
||||
<div
|
||||
v-for="account in demoAccounts"
|
||||
:key="account.username"
|
||||
@click="setDemoAccount(account)"
|
||||
class="account-item"
|
||||
>
|
||||
<span class="username">{{ account.username }}</span>
|
||||
<span class="role">{{ account.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<p>© 2025 牛只管理系统. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>© 2024 锡林郭勒盟智慧养殖平台. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, h, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
||||
import { useAuthStore } from '../store/auth.js';
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 表单数据
|
||||
const loginForm = ref({
|
||||
const loading = ref(false)
|
||||
const loginFormRef = ref()
|
||||
|
||||
// 登录表单
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
remember: false
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, message: '用户名至少3个字符', trigger: 'blur' },
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码至少6个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 演示账户
|
||||
const demoAccounts = [
|
||||
{ username: 'admin', password: '123456', role: '系统管理员' },
|
||||
{ username: 'farmer001', password: '123456', role: '养殖户' },
|
||||
{ username: 'banker001', password: '123456', role: '银行职员' },
|
||||
{ username: 'insurer001', password: '123456', role: '保险员' },
|
||||
{ username: 'inspector001', password: '123456', role: '政府检查员' },
|
||||
{ username: 'trader001', password: '123456', role: '交易员' },
|
||||
];
|
||||
|
||||
// 渲染图标
|
||||
const renderIcon = (type) => {
|
||||
const icons = {
|
||||
user: UserOutlined,
|
||||
lock: LockOutlined,
|
||||
};
|
||||
return h(icons[type]);
|
||||
};
|
||||
|
||||
// 设置演示账户
|
||||
const setDemoAccount = (account) => {
|
||||
loginForm.value.username = account.username;
|
||||
loginForm.value.password = account.password;
|
||||
message.info(`已填入${account.role}演示账户信息`);
|
||||
};
|
||||
|
||||
// 处理登录
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const result = await authStore.login(loginForm.value);
|
||||
// 表单验证
|
||||
await loginFormRef.value.validate()
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 调用登录API
|
||||
await authStore.login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password,
|
||||
remember: loginForm.remember
|
||||
})
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
|
||||
// 跳转到首页
|
||||
router.push('/')
|
||||
|
||||
if (result.success) {
|
||||
message.success('登录成功!');
|
||||
|
||||
// 跳转到首页
|
||||
router.push('/');
|
||||
} else {
|
||||
message.error(result.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
message.error('登录失败,请稍后重试');
|
||||
ElMessage.error(error.message || '登录失败,请检查用户名和密码')
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时检查是否已登录
|
||||
onMounted(() => {
|
||||
if (authStore.isAuthenticated) {
|
||||
router.push('/');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-width: 400px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
@@ -188,97 +140,48 @@ onMounted(() => {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #2c3e50;
|
||||
.login-header h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #7f8c8d;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-accounts {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.demo-accounts h4 {
|
||||
color: #34495e;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.account-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.account-item:hover {
|
||||
background: #e3f2fd;
|
||||
border-color: #2196f3;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.account-item .username {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.account-item .role {
|
||||
font-size: 10px;
|
||||
color: #7f8c8d;
|
||||
margin-top: 2px;
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.login-box {
|
||||
padding: 30px 20px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
134
admin-system/dashboard/src/views/MonitorCenter.vue
Normal file
134
admin-system/dashboard/src/views/MonitorCenter.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="monitor-center">
|
||||
<div class="page-header">
|
||||
<h1>监控中心</h1>
|
||||
<p>实时监控系统运行状态和业务数据</p>
|
||||
</div>
|
||||
|
||||
<div class="monitor-content">
|
||||
<!-- 实时监控面板 -->
|
||||
<div class="monitor-panel">
|
||||
<RealTimeMonitor />
|
||||
</div>
|
||||
|
||||
<!-- 数据统计图表 -->
|
||||
<div class="charts-section">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<div class="chart-card">
|
||||
<CattleChart />
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="chart-card">
|
||||
<TradingChart />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="24" style="margin-top: 24px;">
|
||||
<a-col :span="24">
|
||||
<div class="chart-card">
|
||||
<EnvironmentChart />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import RealTimeMonitor from '@/components/RealTimeMonitor.vue';
|
||||
import CattleChart from '@/components/charts/CattleChart.vue';
|
||||
import TradingChart from '@/components/charts/TradingChart.vue';
|
||||
import EnvironmentChart from '@/components/charts/EnvironmentChart.vue';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
// 页面初始化逻辑
|
||||
console.log('监控中心页面已加载');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.monitor-center {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.monitor-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.monitor-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.monitor-center {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.charts-section .ant-col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
616
admin-system/dashboard/src/views/system/RoleManagement.vue
Normal file
616
admin-system/dashboard/src/views/system/RoleManagement.vue
Normal file
@@ -0,0 +1,616 @@
|
||||
<template>
|
||||
<div class="role-management">
|
||||
<div class="page-header">
|
||||
<h2>角色管理</h2>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="showAddDialog = true"
|
||||
v-if="hasPermission('role:create')"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="roleList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
border
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="角色名称" width="120" />
|
||||
<el-table-column prop="code" label="角色代码" width="120" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ row.status === 'active' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleViewPermissions(row)"
|
||||
v-if="hasPermission('role:view')"
|
||||
>
|
||||
查看权限
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEdit(row)"
|
||||
v-if="hasPermission('role:edit')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="handleSetPermissions(row)"
|
||||
v-if="hasPermission('role:permission')"
|
||||
>
|
||||
设置权限
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
v-if="hasPermission('role:delete') && !row.isSystem"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑角色对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
:title="editingRole ? '编辑角色' : '添加角色'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="roleForm"
|
||||
:rules="roleFormRules"
|
||||
ref="roleFormRef"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="roleForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色代码" prop="code">
|
||||
<el-input v-model="roleForm.code" :disabled="!!editingRole" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="roleForm.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入角色描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="roleForm.status">
|
||||
<el-radio label="active">启用</el-radio>
|
||||
<el-radio label="inactive">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showAddDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveRole" :loading="saving">
|
||||
{{ editingRole ? '更新' : '添加' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 权限设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="showPermissionDialog"
|
||||
title="设置角色权限"
|
||||
width="800px"
|
||||
>
|
||||
<div class="permission-setting" v-if="currentRole">
|
||||
<div class="role-info">
|
||||
<h4>角色:{{ currentRole.name }}</h4>
|
||||
<p>{{ currentRole.description }}</p>
|
||||
</div>
|
||||
|
||||
<el-tree
|
||||
ref="permissionTreeRef"
|
||||
:data="permissionTree"
|
||||
:props="treeProps"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="selectedPermissions"
|
||||
@check="handlePermissionCheck"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showPermissionDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSavePermissions" :loading="saving">
|
||||
保存权限
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看权限对话框 -->
|
||||
<el-dialog
|
||||
v-model="showViewPermissionDialog"
|
||||
title="角色权限详情"
|
||||
width="600px"
|
||||
>
|
||||
<div class="permission-view" v-if="viewingRole">
|
||||
<div class="role-info">
|
||||
<h4>角色:{{ viewingRole.name }}</h4>
|
||||
<p>{{ viewingRole.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="permission-list">
|
||||
<h5>拥有权限:</h5>
|
||||
<div class="permission-tags">
|
||||
<el-tag
|
||||
v-for="permission in viewingRole.permissions"
|
||||
:key="permission.id"
|
||||
class="permission-tag"
|
||||
>
|
||||
{{ permission.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 权限检查
|
||||
const hasPermission = (permission) => {
|
||||
return permissionStore.hasPermission.value(permission)
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const roleList = ref([])
|
||||
const showAddDialog = ref(false)
|
||||
const showPermissionDialog = ref(false)
|
||||
const showViewPermissionDialog = ref(false)
|
||||
const editingRole = ref(null)
|
||||
const currentRole = ref(null)
|
||||
const viewingRole = ref(null)
|
||||
const selectedPermissions = ref([])
|
||||
|
||||
// 角色表单
|
||||
const roleForm = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const roleFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入角色代码', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: '角色代码只能包含字母、数字和下划线,且以字母或下划线开头', trigger: 'blur' }
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: '请输入角色描述', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 权限树配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'name'
|
||||
}
|
||||
|
||||
// 权限树数据
|
||||
const permissionTree = ref([
|
||||
{
|
||||
id: 'user',
|
||||
name: '用户管理',
|
||||
children: [
|
||||
{ id: 'user:view', name: '查看用户' },
|
||||
{ id: 'user:create', name: '创建用户' },
|
||||
{ id: 'user:edit', name: '编辑用户' },
|
||||
{ id: 'user:delete', name: '删除用户' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
name: '角色管理',
|
||||
children: [
|
||||
{ id: 'role:view', name: '查看角色' },
|
||||
{ id: 'role:create', name: '创建角色' },
|
||||
{ id: 'role:edit', name: '编辑角色' },
|
||||
{ id: 'role:delete', name: '删除角色' },
|
||||
{ id: 'role:permission', name: '设置权限' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'cattle',
|
||||
name: '牛只管理',
|
||||
children: [
|
||||
{ id: 'cattle:view', name: '查看牛只' },
|
||||
{ id: 'cattle:create', name: '添加牛只' },
|
||||
{ id: 'cattle:edit', name: '编辑牛只' },
|
||||
{ id: 'cattle:delete', name: '删除牛只' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'trading',
|
||||
name: '交易管理',
|
||||
children: [
|
||||
{ id: 'trading:view', name: '查看交易' },
|
||||
{ id: 'trading:create', name: '创建交易' },
|
||||
{ id: 'trading:edit', name: '编辑交易' },
|
||||
{ id: 'trading:delete', name: '删除交易' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'monitor',
|
||||
name: '监控管理',
|
||||
children: [
|
||||
{ id: 'monitor:view', name: '查看监控' },
|
||||
{ id: 'monitor:config', name: '配置监控' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
name: '系统管理',
|
||||
children: [
|
||||
{ id: 'system:config', name: '系统配置' },
|
||||
{ id: 'system:log', name: '系统日志' },
|
||||
{ id: 'system:backup', name: '数据备份' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const loadRoleList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
const response = await mockRoleAPI.getRoles()
|
||||
roleList.value = response.data
|
||||
} catch (error) {
|
||||
ElMessage.error('加载角色列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (role) => {
|
||||
editingRole.value = role
|
||||
Object.assign(roleForm, {
|
||||
name: role.name,
|
||||
code: role.code,
|
||||
description: role.description,
|
||||
status: role.status
|
||||
})
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (role) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除角色 "${role.name}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await mockRoleAPI.deleteRole(role.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadRoleList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetPermissions = async (role) => {
|
||||
currentRole.value = role
|
||||
|
||||
// 获取角色当前权限
|
||||
try {
|
||||
const response = await mockRoleAPI.getRolePermissions(role.id)
|
||||
selectedPermissions.value = response.data.map(p => p.id)
|
||||
} catch (error) {
|
||||
ElMessage.error('获取角色权限失败')
|
||||
return
|
||||
}
|
||||
|
||||
showPermissionDialog.value = true
|
||||
}
|
||||
|
||||
const handleViewPermissions = async (role) => {
|
||||
try {
|
||||
const response = await mockRoleAPI.getRolePermissions(role.id)
|
||||
viewingRole.value = {
|
||||
...role,
|
||||
permissions: response.data
|
||||
}
|
||||
showViewPermissionDialog.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error('获取角色权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePermissionCheck = (data, checked) => {
|
||||
// 处理权限选择逻辑
|
||||
}
|
||||
|
||||
const handleSaveRole = async () => {
|
||||
try {
|
||||
await roleFormRef.value.validate()
|
||||
|
||||
saving.value = true
|
||||
|
||||
if (editingRole.value) {
|
||||
await mockRoleAPI.updateRole(editingRole.value.id, roleForm)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await mockRoleAPI.createRole(roleForm)
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
|
||||
showAddDialog.value = false
|
||||
resetRoleForm()
|
||||
loadRoleList()
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSavePermissions = async () => {
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
const checkedKeys = permissionTreeRef.value.getCheckedKeys()
|
||||
const halfCheckedKeys = permissionTreeRef.value.getHalfCheckedKeys()
|
||||
const allPermissions = [...checkedKeys, ...halfCheckedKeys]
|
||||
|
||||
await mockRoleAPI.updateRolePermissions(currentRole.value.id, allPermissions)
|
||||
ElMessage.success('权限设置成功')
|
||||
showPermissionDialog.value = false
|
||||
} catch (error) {
|
||||
ElMessage.error('权限设置失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetRoleForm = () => {
|
||||
Object.assign(roleForm, {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
editingRole.value = null
|
||||
}
|
||||
|
||||
// 模拟API
|
||||
const mockRoleAPI = {
|
||||
async getRoles() {
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
name: '超级管理员',
|
||||
code: 'super_admin',
|
||||
description: '系统超级管理员,拥有所有权限',
|
||||
status: 'active',
|
||||
isSystem: true,
|
||||
createdAt: '2025-01-01 00:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '管理员',
|
||||
code: 'admin',
|
||||
description: '系统管理员,拥有大部分管理权限',
|
||||
status: 'active',
|
||||
isSystem: false,
|
||||
createdAt: '2025-01-01 00:00:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '经理',
|
||||
code: 'manager',
|
||||
description: '业务经理,负责业务管理',
|
||||
status: 'active',
|
||||
isSystem: false,
|
||||
createdAt: '2025-01-01 00:00:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '操作员',
|
||||
code: 'operator',
|
||||
description: '日常操作员,负责数据录入和基础操作',
|
||||
status: 'active',
|
||||
isSystem: false,
|
||||
createdAt: '2025-01-01 00:00:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '查看者',
|
||||
code: 'viewer',
|
||||
description: '只读用户,只能查看数据',
|
||||
status: 'active',
|
||||
isSystem: false,
|
||||
createdAt: '2025-01-01 00:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: mockData
|
||||
}
|
||||
},
|
||||
|
||||
async createRole(roleData) {
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async updateRole(id, roleData) {
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async deleteRole(id) {
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async getRolePermissions(roleId) {
|
||||
// 模拟不同角色的权限
|
||||
const permissionMap = {
|
||||
1: [ // 超级管理员
|
||||
{ id: 'user:view', name: '查看用户' },
|
||||
{ id: 'user:create', name: '创建用户' },
|
||||
{ id: 'user:edit', name: '编辑用户' },
|
||||
{ id: 'user:delete', name: '删除用户' },
|
||||
{ id: 'role:view', name: '查看角色' },
|
||||
{ id: 'role:create', name: '创建角色' },
|
||||
{ id: 'role:edit', name: '编辑角色' },
|
||||
{ id: 'role:delete', name: '删除角色' },
|
||||
{ id: 'role:permission', name: '设置权限' }
|
||||
],
|
||||
2: [ // 管理员
|
||||
{ id: 'user:view', name: '查看用户' },
|
||||
{ id: 'user:create', name: '创建用户' },
|
||||
{ id: 'user:edit', name: '编辑用户' },
|
||||
{ id: 'cattle:view', name: '查看牛只' },
|
||||
{ id: 'cattle:create', name: '添加牛只' },
|
||||
{ id: 'cattle:edit', name: '编辑牛只' }
|
||||
],
|
||||
3: [ // 经理
|
||||
{ id: 'cattle:view', name: '查看牛只' },
|
||||
{ id: 'trading:view', name: '查看交易' },
|
||||
{ id: 'trading:create', name: '创建交易' },
|
||||
{ id: 'monitor:view', name: '查看监控' }
|
||||
],
|
||||
4: [ // 操作员
|
||||
{ id: 'cattle:view', name: '查看牛只' },
|
||||
{ id: 'cattle:create', name: '添加牛只' },
|
||||
{ id: 'trading:view', name: '查看交易' }
|
||||
],
|
||||
5: [ // 查看者
|
||||
{ id: 'cattle:view', name: '查看牛只' },
|
||||
{ id: 'trading:view', name: '查看交易' },
|
||||
{ id: 'monitor:view', name: '查看监控' }
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: permissionMap[roleId] || []
|
||||
}
|
||||
},
|
||||
|
||||
async updateRolePermissions(roleId, permissions) {
|
||||
return { success: true }
|
||||
}
|
||||
}
|
||||
|
||||
const roleFormRef = ref()
|
||||
const permissionTreeRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
loadRoleList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-management {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.permission-setting {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.role-info {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.role-info h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.role-info p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.permission-view {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.permission-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.permission-list h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.permission-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.permission-tag {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
617
admin-system/dashboard/src/views/system/UserManagement.vue
Normal file
617
admin-system/dashboard/src/views/system/UserManagement.vue
Normal file
@@ -0,0 +1,617 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<div class="page-header">
|
||||
<h2>用户管理</h2>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="showAddDialog = true"
|
||||
v-if="hasPermission('user:create')"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<div class="search-bar">
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="searchForm.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
|
||||
<el-option
|
||||
v-for="role in roleOptions"
|
||||
:key="role.value"
|
||||
:label="role.label"
|
||||
:value="role.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="inactive" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="userList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
border
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="email" label="邮箱" width="180" />
|
||||
<el-table-column prop="phone" label="手机号" width="120" />
|
||||
<el-table-column prop="role" label="角色" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getRoleTagType(row.role)">
|
||||
{{ getRoleLabel(row.role) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ row.status === 'active' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastLogin" label="最后登录" width="160" />
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleView(row)"
|
||||
v-if="hasPermission('user:view')"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEdit(row)"
|
||||
v-if="hasPermission('user:edit')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 'active' ? 'warning' : 'success'"
|
||||
@click="handleToggleStatus(row)"
|
||||
v-if="hasPermission('user:edit')"
|
||||
>
|
||||
{{ row.status === 'active' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
v-if="hasPermission('user:delete')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
:title="editingUser ? '编辑用户' : '添加用户'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
:model="userForm"
|
||||
:rules="userFormRules"
|
||||
ref="userFormRef"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="userForm.username" :disabled="!!editingUser" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="userForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="userForm.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="userForm.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="userForm.role" placeholder="请选择角色">
|
||||
<el-option
|
||||
v-for="role in roleOptions"
|
||||
:key="role.value"
|
||||
:label="role.label"
|
||||
:value="role.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password" v-if="!editingUser">
|
||||
<el-input v-model="userForm.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword" v-if="!editingUser">
|
||||
<el-input v-model="userForm.confirmPassword" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="userForm.status">
|
||||
<el-radio label="active">启用</el-radio>
|
||||
<el-radio label="inactive">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showAddDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveUser" :loading="saving">
|
||||
{{ editingUser ? '更新' : '添加' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户详情对话框 -->
|
||||
<el-dialog v-model="showViewDialog" title="用户详情" width="500px">
|
||||
<div class="user-detail" v-if="viewingUser">
|
||||
<div class="detail-item">
|
||||
<label>用户名:</label>
|
||||
<span>{{ viewingUser.username }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>姓名:</label>
|
||||
<span>{{ viewingUser.name }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>邮箱:</label>
|
||||
<span>{{ viewingUser.email }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>手机号:</label>
|
||||
<span>{{ viewingUser.phone }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>角色:</label>
|
||||
<el-tag :type="getRoleTagType(viewingUser.role)">
|
||||
{{ getRoleLabel(viewingUser.role) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>状态:</label>
|
||||
<el-tag :type="viewingUser.status === 'active' ? 'success' : 'danger'">
|
||||
{{ viewingUser.status === 'active' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>最后登录:</label>
|
||||
<span>{{ viewingUser.lastLogin || '从未登录' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>创建时间:</label>
|
||||
<span>{{ viewingUser.createdAt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { userAPI } from '@/services/api'
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 权限检查
|
||||
const hasPermission = (permission) => {
|
||||
return permissionStore.hasPermission.value(permission)
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const userList = ref([])
|
||||
const showAddDialog = ref(false)
|
||||
const showViewDialog = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const viewingUser = ref(null)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
role: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 用户表单
|
||||
const userForm = reactive({
|
||||
username: '',
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const userFormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入姓名', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value !== userForm.password) {
|
||||
callback(new Error('两次输入密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 角色选项
|
||||
const roleOptions = [
|
||||
{ label: '超级管理员', value: 'super_admin' },
|
||||
{ label: '管理员', value: 'admin' },
|
||||
{ label: '经理', value: 'manager' },
|
||||
{ label: '操作员', value: 'operator' },
|
||||
{ label: '查看者', value: 'viewer' }
|
||||
]
|
||||
|
||||
// 获取角色标签类型
|
||||
const getRoleTagType = (role) => {
|
||||
const typeMap = {
|
||||
'super_admin': 'danger',
|
||||
'admin': 'warning',
|
||||
'manager': 'primary',
|
||||
'operator': 'success',
|
||||
'viewer': 'info'
|
||||
}
|
||||
return typeMap[role] || 'info'
|
||||
}
|
||||
|
||||
// 获取角色标签
|
||||
const getRoleLabel = (role) => {
|
||||
const labelMap = {
|
||||
'super_admin': '超级管理员',
|
||||
'admin': '管理员',
|
||||
'manager': '经理',
|
||||
'operator': '操作员',
|
||||
'viewer': '查看者'
|
||||
}
|
||||
return labelMap[role] || role
|
||||
}
|
||||
|
||||
// 方法
|
||||
const loadUserList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 模拟API调用
|
||||
const response = await mockUserAPI.getUsers(params)
|
||||
userList.value = response.data.list
|
||||
pagination.total = response.data.total
|
||||
} catch (error) {
|
||||
ElMessage.error('加载用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadUserList()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
username: '',
|
||||
role: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadUserList()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadUserList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadUserList()
|
||||
}
|
||||
|
||||
const handleView = (user) => {
|
||||
viewingUser.value = user
|
||||
showViewDialog.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (user) => {
|
||||
editingUser.value = user
|
||||
Object.assign(userForm, {
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (user) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除用户 "${user.name}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 调用删除API
|
||||
await mockUserAPI.deleteUser(user.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadUserList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (user) => {
|
||||
try {
|
||||
const newStatus = user.status === 'active' ? 'inactive' : 'active'
|
||||
await mockUserAPI.updateUserStatus(user.id, newStatus)
|
||||
ElMessage.success(`${newStatus === 'active' ? '启用' : '禁用'}成功`)
|
||||
loadUserList()
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveUser = async () => {
|
||||
try {
|
||||
// 表单验证
|
||||
await userFormRef.value.validate()
|
||||
|
||||
saving.value = true
|
||||
|
||||
if (editingUser.value) {
|
||||
// 更新用户
|
||||
await mockUserAPI.updateUser(editingUser.value.id, userForm)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
// 添加用户
|
||||
await mockUserAPI.createUser(userForm)
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
|
||||
showAddDialog.value = false
|
||||
resetUserForm()
|
||||
loadUserList()
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetUserForm = () => {
|
||||
Object.assign(userForm, {
|
||||
username: '',
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
status: 'active'
|
||||
})
|
||||
editingUser.value = null
|
||||
}
|
||||
|
||||
// 模拟API
|
||||
const mockUserAPI = {
|
||||
async getUsers(params) {
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
name: '系统管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
role: 'super_admin',
|
||||
status: 'active',
|
||||
lastLogin: '2025-01-21 10:30:00',
|
||||
createdAt: '2025-01-01 00:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'manager',
|
||||
name: '张经理',
|
||||
email: 'manager@example.com',
|
||||
phone: '13800138001',
|
||||
role: 'manager',
|
||||
status: 'active',
|
||||
lastLogin: '2025-01-21 09:15:00',
|
||||
createdAt: '2025-01-02 00:00:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'operator',
|
||||
name: '李操作员',
|
||||
email: 'operator@example.com',
|
||||
phone: '13800138002',
|
||||
role: 'operator',
|
||||
status: 'active',
|
||||
lastLogin: '2025-01-20 16:45:00',
|
||||
createdAt: '2025-01-03 00:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
list: mockData,
|
||||
total: mockData.length
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async createUser(userData) {
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async updateUser(id, userData) {
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async deleteUser(id) {
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async updateUserStatus(id, status) {
|
||||
return { success: true }
|
||||
}
|
||||
}
|
||||
|
||||
const userFormRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-management {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.user-detail {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-item label {
|
||||
width: 80px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-item span {
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user