refactor: 重构数据库配置为SQLite开发环境并移除冗余文档

This commit is contained in:
2025-09-21 15:16:48 +08:00
parent d207610009
commit 3c8648a635
259 changed files with 88239 additions and 8379 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View 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)
}
}

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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()

View File

@@ -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

View File

@@ -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实例供其他地方使用

View 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
}
})

View 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
}
})

View File

@@ -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())

View File

@@ -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>&copy; 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>

View 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>

View 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>

View 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>