更新政府端和银行端
This commit is contained in:
25
government-admin/.env.example
Normal file
25
government-admin/.env.example
Normal file
@@ -0,0 +1,25 @@
|
||||
# 环境变量配置示例文件
|
||||
# 复制此文件为 .env 并根据实际情况修改配置
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_TITLE=宁夏智慧养殖监管平台 - 政府端管理后台
|
||||
VITE_APP_VERSION=1.0.0
|
||||
VITE_APP_ENV=development
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=http://localhost:5350/api
|
||||
VITE_API_FULL_URL=http://localhost:5350/api
|
||||
|
||||
# 百度地图API密钥
|
||||
VITE_BAIDU_MAP_AK=your_baidu_map_api_key
|
||||
|
||||
# WebSocket配置
|
||||
VITE_WS_URL=ws://localhost:5350
|
||||
|
||||
# 文件上传配置
|
||||
VITE_UPLOAD_URL=http://localhost:5350/api/upload
|
||||
VITE_MAX_FILE_SIZE=10485760
|
||||
|
||||
# 其他配置
|
||||
VITE_ENABLE_MOCK=false
|
||||
VITE_ENABLE_DEVTOOLS=true
|
||||
1
government-admin/.nvmrc
Normal file
1
government-admin/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
16
|
||||
191
government-admin/README.md
Normal file
191
government-admin/README.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 宁夏智慧养殖监管平台 - 政府端管理后台
|
||||
|
||||
## 项目简介
|
||||
|
||||
本项目是宁夏智慧养殖监管平台的政府端管理后台,基于 Vue 3 + Ant Design Vue 构建,为政府监管部门提供养殖场管理、设备监控、数据分析等功能。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端框架**: Vue 3.4+
|
||||
- **构建工具**: Vite 5.0+
|
||||
- **UI组件库**: Ant Design Vue 4.0+
|
||||
- **状态管理**: Pinia 2.1+
|
||||
- **路由管理**: Vue Router 4.2+
|
||||
- **HTTP客户端**: Axios 1.6+
|
||||
- **图表库**: ECharts 5.4+
|
||||
- **样式预处理**: Sass
|
||||
- **Node.js版本**: 16.x
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 核心功能
|
||||
- 🔐 用户认证与权限管理
|
||||
- 🏠 养殖场信息管理
|
||||
- 🗺️ 地图可视化展示
|
||||
- 📊 设备监控与状态管理
|
||||
- 🐄 动物健康管理
|
||||
- ⚠️ 预警管理系统
|
||||
- 📈 数据可视化与报表
|
||||
- 👥 用户管理
|
||||
- ⚙️ 系统设置
|
||||
|
||||
### 技术特性
|
||||
- 📱 响应式设计,支持多端适配
|
||||
- 🎨 现代化UI设计,用户体验优良
|
||||
- 🚀 基于Vite的快速开发体验
|
||||
- 🔄 实时数据更新(WebSocket)
|
||||
- 📦 组件化开发,代码复用性高
|
||||
- 🛡️ 完善的权限控制系统
|
||||
- 🌐 国际化支持(预留)
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js 16.x
|
||||
- npm 8.0+ 或 yarn 1.22+
|
||||
- 现代浏览器(Chrome 88+, Firefox 78+, Safari 14+)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 克隆项目
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd government-admin
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
```bash
|
||||
# 使用npm
|
||||
npm install
|
||||
|
||||
# 或使用yarn
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 3. 配置环境变量
|
||||
```bash
|
||||
# 复制环境变量示例文件
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑 .env 文件,配置API地址等信息
|
||||
```
|
||||
|
||||
### 4. 启动开发服务器
|
||||
```bash
|
||||
# 使用npm
|
||||
npm run dev
|
||||
|
||||
# 或使用yarn
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### 5. 构建生产版本
|
||||
```bash
|
||||
# 使用npm
|
||||
npm run build
|
||||
|
||||
# 或使用yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
government-admin/
|
||||
├── public/ # 静态资源
|
||||
├── src/
|
||||
│ ├── assets/ # 资源文件
|
||||
│ ├── components/ # 通用组件
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── stores/ # 状态管理
|
||||
│ ├── styles/ # 样式文件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── views/ # 页面组件
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.js # 入口文件
|
||||
├── .env.example # 环境变量示例
|
||||
├── .nvmrc # Node.js版本配置
|
||||
├── index.html # HTML模板
|
||||
├── package.json # 项目配置
|
||||
├── vite.config.js # Vite配置
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 代码规范
|
||||
- 使用 ESLint + Prettier 进行代码格式化
|
||||
- 组件命名使用 PascalCase
|
||||
- 文件命名使用 kebab-case
|
||||
- 变量命名使用 camelCase
|
||||
|
||||
### Git提交规范
|
||||
```
|
||||
feat: 新功能
|
||||
fix: 修复bug
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 测试相关
|
||||
chore: 构建过程或辅助工具的变动
|
||||
```
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 开发环境
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
访问: http://localhost:5400
|
||||
|
||||
### 生产环境
|
||||
```bash
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Docker部署
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t government-admin .
|
||||
|
||||
# 运行容器
|
||||
docker run -p 5400:80 government-admin
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
后端API服务地址: http://localhost:5350/api
|
||||
|
||||
主要接口:
|
||||
- `/auth/*` - 认证相关
|
||||
- `/farms/*` - 养殖场管理
|
||||
- `/devices/*` - 设备监控
|
||||
- `/animals/*` - 动物管理
|
||||
- `/alerts/*` - 预警管理
|
||||
- `/reports/*` - 报表数据
|
||||
- `/users/*` - 用户管理
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
| Chrome | Firefox | Safari | Edge |
|
||||
|--------|---------|--------|------|
|
||||
| 88+ | 78+ | 14+ | 88+ |
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 项目维护: NXXM Development Team
|
||||
- 技术支持: [技术支持邮箱]
|
||||
- 问题反馈: [GitHub Issues]
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2025-01-18)
|
||||
- 🎉 初始版本发布
|
||||
- ✨ 完成基础框架搭建
|
||||
- ✨ 实现用户认证系统
|
||||
- ✨ 完成基础布局和路由配置
|
||||
15
government-admin/index.html
Normal file
15
government-admin/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="宁夏智慧养殖监管平台 - 政府端管理后台" />
|
||||
<meta name="keywords" content="智慧养殖,监管平台,政府管理,数据监控" />
|
||||
<title>宁夏智慧养殖监管平台 - 政府端管理后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
6175
government-admin/package-lock.json
generated
Normal file
6175
government-admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
government-admin/package.json
Normal file
65
government-admin/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "government-admin-system",
|
||||
"version": "1.0.0",
|
||||
"description": "宁夏智慧养殖监管平台 - 政府端管理后台",
|
||||
"author": "NXXM Development Team",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"vue3",
|
||||
"vite",
|
||||
"ant-design-vue",
|
||||
"echarts",
|
||||
"pinia",
|
||||
"government-admin",
|
||||
"smart-farming",
|
||||
"monitoring-system"
|
||||
],
|
||||
"engines": {
|
||||
"node": "16.x",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"preview": "vite preview --port 5400",
|
||||
"lint": "eslint . --ext .vue,.js,.ts --fix",
|
||||
"lint:check": "eslint . --ext .vue,.js,.ts",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"clean": "rimraf dist node_modules/.vite",
|
||||
"analyze": "vite-bundle-analyzer dist/stats.html",
|
||||
"deploy": "npm run build && npm run preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"ant-design-vue": "^4.0.6",
|
||||
"axios": "^1.6.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.11.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.4",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"prettier": "^3.2.4",
|
||||
"rimraf": "^5.0.5",
|
||||
"vite": "^5.0.12",
|
||||
"vite-bundle-analyzer": "^0.7.0",
|
||||
"vitest": "^1.2.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
10
government-admin/public/favicon.svg
Normal file
10
government-admin/public/favicon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#1890ff"/>
|
||||
<circle cx="50" cy="50" r="30" fill="white"/>
|
||||
<circle cx="50" cy="50" r="20" fill="#1890ff"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#1890ff"/>
|
||||
<circle cx="50" cy="50" r="30" fill="white"/>
|
||||
<circle cx="50" cy="50" r="20" fill="#1890ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 435 B |
5
government-admin/public/static/favicon.svg
Normal file
5
government-admin/public/static/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#1890ff"/>
|
||||
<circle cx="50" cy="50" r="30" fill="white"/>
|
||||
<circle cx="50" cy="50" r="20" fill="#1890ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
60
government-admin/src/App.vue
Normal file
60
government-admin/src/App.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 应用初始化时检查用户登录状态
|
||||
authStore.checkAuthStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
// 全局样式重置
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
font-size: 14px;
|
||||
line-height: 1.5715;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
137
government-admin/src/api/farm.js
Normal file
137
government-admin/src/api/farm.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取养殖场列表
|
||||
export function getFarmList(params) {
|
||||
return request({
|
||||
url: '/api/farms',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取养殖场详情
|
||||
export function getFarmDetail(id) {
|
||||
return request({
|
||||
url: `/api/farms/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 创建养殖场
|
||||
export function createFarm(data) {
|
||||
return request({
|
||||
url: '/api/farms',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新养殖场
|
||||
export function updateFarm(id, data) {
|
||||
return request({
|
||||
url: `/api/farms/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除养殖场
|
||||
export function deleteFarm(id) {
|
||||
return request({
|
||||
url: `/api/farms/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除养殖场
|
||||
export function batchDeleteFarms(ids) {
|
||||
return request({
|
||||
url: '/api/farms/batch',
|
||||
method: 'delete',
|
||||
data: { ids }
|
||||
})
|
||||
}
|
||||
|
||||
// 更新养殖场状态
|
||||
export function updateFarmStatus(id, status) {
|
||||
return request({
|
||||
url: `/api/farms/${id}/status`,
|
||||
method: 'patch',
|
||||
data: { status }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取养殖场统计数据
|
||||
export function getFarmStats() {
|
||||
return request({
|
||||
url: '/api/farms/stats',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取养殖场地图数据
|
||||
export function getFarmMapData(params) {
|
||||
return request({
|
||||
url: '/api/farms/map',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 导出养殖场数据
|
||||
export function exportFarmData(params) {
|
||||
return request({
|
||||
url: '/api/farms/export',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 导入养殖场数据
|
||||
export function importFarmData(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request({
|
||||
url: '/api/farms/import',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取养殖场类型选项
|
||||
export function getFarmTypes() {
|
||||
return request({
|
||||
url: '/api/farms/types',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取养殖场规模选项
|
||||
export function getFarmScales() {
|
||||
return request({
|
||||
url: '/api/farms/scales',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证养殖场编号唯一性
|
||||
export function validateFarmCode(code, excludeId) {
|
||||
return request({
|
||||
url: '/api/farms/validate-code',
|
||||
method: 'post',
|
||||
data: { code, excludeId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取附近养殖场
|
||||
export function getNearbyFarms(lat, lng, radius = 5000) {
|
||||
return request({
|
||||
url: '/api/farms/nearby',
|
||||
method: 'get',
|
||||
params: { lat, lng, radius }
|
||||
})
|
||||
}
|
||||
391
government-admin/src/api/government.js
Normal file
391
government-admin/src/api/government.js
Normal file
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* 政府业务API接口
|
||||
*/
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 政府监管API
|
||||
export const supervisionApi = {
|
||||
// 获取监管数据
|
||||
getSupervisionData: () => request.get('/api/supervision/data'),
|
||||
|
||||
// 获取监管实体列表
|
||||
getEntities: (params) => request.get('/api/supervision/entities', { params }),
|
||||
|
||||
// 添加监管实体
|
||||
addEntity: (data) => request.post('/api/supervision/entities', data),
|
||||
|
||||
// 更新监管实体
|
||||
updateEntity: (id, data) => request.put(`/api/supervision/entities/${id}`, data),
|
||||
|
||||
// 删除监管实体
|
||||
deleteEntity: (id) => request.delete(`/api/supervision/entities/${id}`),
|
||||
|
||||
// 获取检查记录
|
||||
getInspections: (params) => request.get('/api/supervision/inspections', { params }),
|
||||
|
||||
// 创建检查记录
|
||||
createInspection: (data) => request.post('/api/supervision/inspections', data),
|
||||
|
||||
// 更新检查记录
|
||||
updateInspection: (id, data) => request.put(`/api/supervision/inspections/${id}`, data),
|
||||
|
||||
// 获取违规记录
|
||||
getViolations: (params) => request.get('/api/supervision/violations', { params }),
|
||||
|
||||
// 创建违规记录
|
||||
createViolation: (data) => request.post('/api/supervision/violations', data),
|
||||
|
||||
// 处理违规记录
|
||||
processViolation: (id, data) => request.put(`/api/supervision/violations/${id}/process`, data)
|
||||
}
|
||||
|
||||
// 审批管理API
|
||||
export const approvalApi = {
|
||||
// 获取审批数据
|
||||
getApprovalData: () => request.get('/api/approval/data'),
|
||||
|
||||
// 获取审批流程
|
||||
getWorkflows: (params) => request.get('/api/approval/workflows', { params }),
|
||||
|
||||
// 创建审批流程
|
||||
createWorkflow: (data) => request.post('/api/approval/workflows', data),
|
||||
|
||||
// 更新审批流程
|
||||
updateWorkflow: (id, data) => request.put(`/api/approval/workflows/${id}`, data),
|
||||
|
||||
// 获取审批记录
|
||||
getRecords: (params) => request.get('/api/approval/records', { params }),
|
||||
|
||||
// 提交审批申请
|
||||
submitApproval: (data) => request.post('/api/approval/records', data),
|
||||
|
||||
// 处理审批
|
||||
processApproval: (id, data) => request.put(`/api/approval/records/${id}/process`, data),
|
||||
|
||||
// 获取待办任务
|
||||
getTasks: (params) => request.get('/api/approval/tasks', { params }),
|
||||
|
||||
// 完成任务
|
||||
completeTask: (id, data) => request.put(`/api/approval/tasks/${id}/complete`, data),
|
||||
|
||||
// 转派任务
|
||||
transferTask: (id, data) => request.put(`/api/approval/tasks/${id}/transfer`, data)
|
||||
}
|
||||
|
||||
// 人员管理API
|
||||
export const personnelApi = {
|
||||
// 获取人员数据
|
||||
getPersonnelData: () => request.get('/api/personnel/data'),
|
||||
|
||||
// 获取员工列表
|
||||
getStaff: (params) => request.get('/api/personnel/staff', { params }),
|
||||
|
||||
// 添加员工
|
||||
addStaff: (data) => request.post('/api/personnel/staff', data),
|
||||
|
||||
// 更新员工信息
|
||||
updateStaff: (id, data) => request.put(`/api/personnel/staff/${id}`, data),
|
||||
|
||||
// 删除员工
|
||||
deleteStaff: (id) => request.delete(`/api/personnel/staff/${id}`),
|
||||
|
||||
// 获取部门列表
|
||||
getDepartments: (params) => request.get('/api/personnel/departments', { params }),
|
||||
|
||||
// 添加部门
|
||||
addDepartment: (data) => request.post('/api/personnel/departments', data),
|
||||
|
||||
// 更新部门信息
|
||||
updateDepartment: (id, data) => request.put(`/api/personnel/departments/${id}`, data),
|
||||
|
||||
// 删除部门
|
||||
deleteDepartment: (id) => request.delete(`/api/personnel/departments/${id}`),
|
||||
|
||||
// 获取职位列表
|
||||
getPositions: (params) => request.get('/api/personnel/positions', { params }),
|
||||
|
||||
// 添加职位
|
||||
addPosition: (data) => request.post('/api/personnel/positions', data),
|
||||
|
||||
// 更新职位信息
|
||||
updatePosition: (id, data) => request.put(`/api/personnel/positions/${id}`, data),
|
||||
|
||||
// 获取考勤记录
|
||||
getAttendance: (params) => request.get('/api/personnel/attendance', { params }),
|
||||
|
||||
// 记录考勤
|
||||
recordAttendance: (data) => request.post('/api/personnel/attendance', data),
|
||||
|
||||
// 获取员工详情
|
||||
getStaffDetail: (id) => request.get(`/api/personnel/staff/${id}`),
|
||||
|
||||
// 员工调岗
|
||||
transferStaff: (id, data) => request.put(`/api/personnel/staff/${id}/transfer`, data)
|
||||
}
|
||||
|
||||
// 设备仓库API
|
||||
export const warehouseApi = {
|
||||
// 获取仓库数据
|
||||
getWarehouseData: () => request.get('/api/warehouse/data'),
|
||||
|
||||
// 获取设备列表
|
||||
getEquipment: (params) => request.get('/api/warehouse/equipment', { params }),
|
||||
|
||||
// 添加设备
|
||||
addEquipment: (data) => request.post('/api/warehouse/equipment', data),
|
||||
|
||||
// 更新设备信息
|
||||
updateEquipment: (id, data) => request.put(`/api/warehouse/equipment/${id}`, data),
|
||||
|
||||
// 删除设备
|
||||
deleteEquipment: (id) => request.delete(`/api/warehouse/equipment/${id}`),
|
||||
|
||||
// 设备入库
|
||||
equipmentInbound: (data) => request.post('/api/warehouse/inbound', data),
|
||||
|
||||
// 设备出库
|
||||
equipmentOutbound: (data) => request.post('/api/warehouse/outbound', data),
|
||||
|
||||
// 获取入库记录
|
||||
getInboundRecords: (params) => request.get('/api/warehouse/inbound', { params }),
|
||||
|
||||
// 获取出库记录
|
||||
getOutboundRecords: (params) => request.get('/api/warehouse/outbound', { params }),
|
||||
|
||||
// 获取维护记录
|
||||
getMaintenanceRecords: (params) => request.get('/api/warehouse/maintenance', { params }),
|
||||
|
||||
// 创建维护记录
|
||||
createMaintenanceRecord: (data) => request.post('/api/warehouse/maintenance', data),
|
||||
|
||||
// 更新维护记录
|
||||
updateMaintenanceRecord: (id, data) => request.put(`/api/warehouse/maintenance/${id}`, data),
|
||||
|
||||
// 设备盘点
|
||||
inventoryCheck: (data) => request.post('/api/warehouse/inventory', data),
|
||||
|
||||
// 获取库存报告
|
||||
getInventoryReport: (params) => request.get('/api/warehouse/inventory/report', { params })
|
||||
}
|
||||
|
||||
// 防疫管理API
|
||||
export const epidemicApi = {
|
||||
// 获取防疫数据
|
||||
getEpidemicData: () => request.get('/api/epidemic/data'),
|
||||
|
||||
// 获取疫情案例
|
||||
getCases: (params) => request.get('/api/epidemic/cases', { params }),
|
||||
|
||||
// 添加疫情案例
|
||||
addCase: (data) => request.post('/api/epidemic/cases', data),
|
||||
|
||||
// 更新疫情案例
|
||||
updateCase: (id, data) => request.put(`/api/epidemic/cases/${id}`, data),
|
||||
|
||||
// 获取疫苗接种记录
|
||||
getVaccinations: (params) => request.get('/api/epidemic/vaccinations', { params }),
|
||||
|
||||
// 记录疫苗接种
|
||||
recordVaccination: (data) => request.post('/api/epidemic/vaccinations', data),
|
||||
|
||||
// 获取防疫措施
|
||||
getMeasures: (params) => request.get('/api/epidemic/measures', { params }),
|
||||
|
||||
// 创建防疫措施
|
||||
createMeasure: (data) => request.post('/api/epidemic/measures', data),
|
||||
|
||||
// 更新防疫措施
|
||||
updateMeasure: (id, data) => request.put(`/api/epidemic/measures/${id}`, data),
|
||||
|
||||
// 获取健康码数据
|
||||
getHealthCodes: (params) => request.get('/api/epidemic/health-codes', { params }),
|
||||
|
||||
// 生成健康码
|
||||
generateHealthCode: (data) => request.post('/api/epidemic/health-codes', data),
|
||||
|
||||
// 验证健康码
|
||||
verifyHealthCode: (code) => request.get(`/api/epidemic/health-codes/${code}/verify`),
|
||||
|
||||
// 获取疫情统计
|
||||
getEpidemicStats: (params) => request.get('/api/epidemic/stats', { params }),
|
||||
|
||||
// 获取疫情地图数据
|
||||
getEpidemicMapData: (params) => request.get('/api/epidemic/map', { params })
|
||||
}
|
||||
|
||||
// 服务管理API
|
||||
export const serviceApi = {
|
||||
// 获取服务数据
|
||||
getServiceData: () => request.get('/api/service/data'),
|
||||
|
||||
// 获取服务项目
|
||||
getServices: (params) => request.get('/api/service/services', { params }),
|
||||
|
||||
// 创建服务项目
|
||||
createService: (data) => request.post('/api/service/services', data),
|
||||
|
||||
// 更新服务项目
|
||||
updateService: (id, data) => request.put(`/api/service/services/${id}`, data),
|
||||
|
||||
// 删除服务项目
|
||||
deleteService: (id) => request.delete(`/api/service/services/${id}`),
|
||||
|
||||
// 获取服务申请
|
||||
getApplications: (params) => request.get('/api/service/applications', { params }),
|
||||
|
||||
// 提交服务申请
|
||||
submitApplication: (data) => request.post('/api/service/applications', data),
|
||||
|
||||
// 处理服务申请
|
||||
processApplication: (id, data) => request.put(`/api/service/applications/${id}/process`, data),
|
||||
|
||||
// 获取服务评价
|
||||
getEvaluations: (params) => request.get('/api/service/evaluations', { params }),
|
||||
|
||||
// 提交服务评价
|
||||
submitEvaluation: (data) => request.post('/api/service/evaluations', data),
|
||||
|
||||
// 获取服务指南
|
||||
getGuides: (params) => request.get('/api/service/guides', { params }),
|
||||
|
||||
// 创建服务指南
|
||||
createGuide: (data) => request.post('/api/service/guides', data),
|
||||
|
||||
// 更新服务指南
|
||||
updateGuide: (id, data) => request.put(`/api/service/guides/${id}`, data),
|
||||
|
||||
// 获取服务统计
|
||||
getServiceStats: (params) => request.get('/api/service/stats', { params })
|
||||
}
|
||||
|
||||
// 数据可视化API
|
||||
export const visualizationApi = {
|
||||
// 获取仪表盘数据
|
||||
getDashboardData: () => request.get('/api/visualization/dashboard'),
|
||||
|
||||
// 获取图表数据
|
||||
getChartData: (chartType, params) => request.get(`/api/visualization/charts/${chartType}`, { params }),
|
||||
|
||||
// 获取实时数据
|
||||
getRealTimeData: (dataType) => request.get(`/api/visualization/realtime/${dataType}`),
|
||||
|
||||
// 获取统计报告
|
||||
getStatisticsReport: (params) => request.get('/api/visualization/statistics', { params }),
|
||||
|
||||
// 导出数据
|
||||
exportData: (params) => request.get('/api/visualization/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
}),
|
||||
|
||||
// 获取地图数据
|
||||
getMapData: (params) => request.get('/api/visualization/map', { params }),
|
||||
|
||||
// 获取热力图数据
|
||||
getHeatmapData: (params) => request.get('/api/visualization/heatmap', { params })
|
||||
}
|
||||
|
||||
// 系统管理API
|
||||
export const systemApi = {
|
||||
// 获取系统信息
|
||||
getSystemInfo: () => request.get('/api/system/info'),
|
||||
|
||||
// 获取系统日志
|
||||
getSystemLogs: (params) => request.get('/api/system/logs', { params }),
|
||||
|
||||
// 获取操作日志
|
||||
getOperationLogs: (params) => request.get('/api/system/operation-logs', { params }),
|
||||
|
||||
// 系统备份
|
||||
systemBackup: () => request.post('/api/system/backup'),
|
||||
|
||||
// 系统恢复
|
||||
systemRestore: (data) => request.post('/api/system/restore', data),
|
||||
|
||||
// 清理缓存
|
||||
clearCache: () => request.post('/api/system/clear-cache'),
|
||||
|
||||
// 获取系统配置
|
||||
getSystemConfig: () => request.get('/api/system/config'),
|
||||
|
||||
// 更新系统配置
|
||||
updateSystemConfig: (data) => request.put('/api/system/config', data)
|
||||
}
|
||||
|
||||
// 文件管理API
|
||||
export const fileApi = {
|
||||
// 上传文件
|
||||
uploadFile: (file, onProgress) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return request.post('/api/files/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: onProgress
|
||||
})
|
||||
},
|
||||
|
||||
// 批量上传文件
|
||||
uploadFiles: (files, onProgress) => {
|
||||
const formData = new FormData()
|
||||
files.forEach(file => {
|
||||
formData.append('files', file)
|
||||
})
|
||||
|
||||
return request.post('/api/files/upload/batch', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: onProgress
|
||||
})
|
||||
},
|
||||
|
||||
// 删除文件
|
||||
deleteFile: (fileId) => request.delete(`/api/files/${fileId}`),
|
||||
|
||||
// 获取文件列表
|
||||
getFiles: (params) => request.get('/api/files', { params }),
|
||||
|
||||
// 下载文件
|
||||
downloadFile: (fileId) => request.get(`/api/files/${fileId}/download`, {
|
||||
responseType: 'blob'
|
||||
}),
|
||||
|
||||
// 获取文件信息
|
||||
getFileInfo: (fileId) => request.get(`/api/files/${fileId}`)
|
||||
}
|
||||
|
||||
// 统一导出政府业务API
|
||||
export const governmentApi = {
|
||||
// 获取所有模块数据
|
||||
getSupervisionData: supervisionApi.getSupervisionData,
|
||||
getApprovalData: approvalApi.getApprovalData,
|
||||
getPersonnelData: personnelApi.getPersonnelData,
|
||||
getWarehouseData: warehouseApi.getWarehouseData,
|
||||
getEpidemicData: epidemicApi.getEpidemicData,
|
||||
getServiceData: serviceApi.getServiceData,
|
||||
|
||||
// 常用操作
|
||||
submitApproval: approvalApi.submitApproval,
|
||||
processApproval: approvalApi.processApproval,
|
||||
addEquipment: warehouseApi.addEquipment,
|
||||
equipmentInbound: warehouseApi.equipmentInbound,
|
||||
equipmentOutbound: warehouseApi.equipmentOutbound,
|
||||
addStaff: personnelApi.addStaff,
|
||||
updateStaff: personnelApi.updateStaff,
|
||||
|
||||
// 子模块API
|
||||
supervision: supervisionApi,
|
||||
approval: approvalApi,
|
||||
personnel: personnelApi,
|
||||
warehouse: warehouseApi,
|
||||
epidemic: epidemicApi,
|
||||
service: serviceApi,
|
||||
visualization: visualizationApi,
|
||||
system: systemApi,
|
||||
file: fileApi
|
||||
}
|
||||
|
||||
export default governmentApi
|
||||
5
government-admin/src/assets/images/favicon.svg
Normal file
5
government-admin/src/assets/images/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#1890ff"/>
|
||||
<circle cx="50" cy="50" r="30" fill="white"/>
|
||||
<circle cx="50" cy="50" r="20" fill="#1890ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
76
government-admin/src/components/PageHeader.vue
Normal file
76
government-admin/src/components/PageHeader.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<div class="page-header-content">
|
||||
<div class="page-header-main">
|
||||
<div class="page-header-title">
|
||||
<h1>{{ title }}</h1>
|
||||
<p v-if="description" class="page-header-description">{{ description }}</p>
|
||||
</div>
|
||||
<div v-if="$slots.extra" class="page-header-extra">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.default" class="page-header-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.page-header-content {
|
||||
.page-header-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.page-header-title {
|
||||
flex: 1;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.page-header-description {
|
||||
margin: 4px 0 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-extra {
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-body {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
government-admin/src/components/PermissionButton.vue
Normal file
89
government-admin/src/components/PermissionButton.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<a-button
|
||||
v-if="hasPermission"
|
||||
v-bind="$attrs"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot />
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
const props = defineProps({
|
||||
// 权限码
|
||||
permission: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 角色
|
||||
role: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 权限列表(任一权限)
|
||||
permissions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 是否需要全部权限
|
||||
requireAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 角色列表
|
||||
roles: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 检查是否有权限
|
||||
const hasPermission = computed(() => {
|
||||
// 如果没有设置任何权限要求,默认显示
|
||||
if (!props.permission && !props.role && props.permissions.length === 0 && props.roles.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查单个权限
|
||||
if (props.permission) {
|
||||
return permissionStore.hasPermission(props.permission)
|
||||
}
|
||||
|
||||
// 检查单个角色
|
||||
if (props.role) {
|
||||
return permissionStore.hasRole(props.role)
|
||||
}
|
||||
|
||||
// 检查权限列表
|
||||
if (props.permissions.length > 0) {
|
||||
return props.requireAll
|
||||
? permissionStore.hasAllPermissions(props.permissions)
|
||||
: permissionStore.hasAnyPermission(props.permissions)
|
||||
}
|
||||
|
||||
// 检查角色列表
|
||||
if (props.roles.length > 0) {
|
||||
return props.roles.some(role => permissionStore.hasRole(role))
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const handleClick = (event) => {
|
||||
emit('click', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PermissionButton',
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
196
government-admin/src/components/charts/BarChart.vue
Normal file
196
government-admin/src/components/charts/BarChart.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="bar-chart" :style="{ width: width, height: height }">
|
||||
<div ref="chartRef" :style="{ width: '100%', height: '100%' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
xAxisData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
color: {
|
||||
type: Array,
|
||||
default: () => ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1']
|
||||
},
|
||||
horizontal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
stack: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
grid: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
top: '10%',
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
updateChart()
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return
|
||||
|
||||
const series = props.data.map((item, index) => ({
|
||||
name: item.name,
|
||||
type: 'bar',
|
||||
data: item.data,
|
||||
stack: props.stack,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: props.color[index % props.color.length] },
|
||||
{ offset: 1, color: echarts.color.lift(props.color[index % props.color.length], -0.3) }
|
||||
]),
|
||||
borderRadius: props.horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: props.color[index % props.color.length]
|
||||
}
|
||||
},
|
||||
barWidth: '60%'
|
||||
}))
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: props.title,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: props.data.map(item => item.name),
|
||||
bottom: 10,
|
||||
textStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
grid: props.grid,
|
||||
xAxis: {
|
||||
type: props.horizontal ? 'value' : 'category',
|
||||
data: props.horizontal ? null : props.xAxisData,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e8e8e8'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#666',
|
||||
rotate: props.horizontal ? 0 : (props.xAxisData.length > 6 ? 45 : 0)
|
||||
},
|
||||
splitLine: props.horizontal ? {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
} : null
|
||||
},
|
||||
yAxis: {
|
||||
type: props.horizontal ? 'category' : 'value',
|
||||
data: props.horizontal ? props.xAxisData : null,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e8e8e8'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#666'
|
||||
},
|
||||
splitLine: props.horizontal ? null : {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
series
|
||||
}
|
||||
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
|
||||
const resizeChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
window.addEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
window.removeEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
watch(() => [props.data, props.xAxisData], () => {
|
||||
updateChart()
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({
|
||||
getChartInstance: () => chartInstance,
|
||||
resize: resizeChart
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bar-chart {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
205
government-admin/src/components/charts/GaugeChart.vue
Normal file
205
government-admin/src/components/charts/GaugeChart.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="gauge-chart" :style="{ width: width, height: height }">
|
||||
<div ref="chartRef" :style="{ width: '100%', height: '100%' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
default: '%'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
color: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
[0.2, '#67e0e3'],
|
||||
[0.8, '#37a2da'],
|
||||
[1, '#fd666d']
|
||||
]
|
||||
},
|
||||
radius: {
|
||||
type: String,
|
||||
default: '75%'
|
||||
},
|
||||
center: {
|
||||
type: Array,
|
||||
default: () => ['50%', '60%']
|
||||
}
|
||||
})
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
updateChart()
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: props.title,
|
||||
left: 'center',
|
||||
top: 20,
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: '{a} <br/>{b} : {c}' + props.unit
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: props.title || '指标',
|
||||
type: 'gauge',
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
radius: props.radius,
|
||||
center: props.center,
|
||||
splitNumber: 10,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: props.color,
|
||||
width: 20,
|
||||
shadowColor: '#fff',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
fontWeight: 'bolder',
|
||||
color: '#fff',
|
||||
shadowColor: '#fff',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
length: 15,
|
||||
lineStyle: {
|
||||
color: 'auto',
|
||||
shadowColor: '#fff',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
length: 25,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#fff',
|
||||
shadowColor: '#fff',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
shadowColor: '#fff',
|
||||
shadowBlur: 5
|
||||
},
|
||||
title: {
|
||||
textStyle: {
|
||||
fontWeight: 'bolder',
|
||||
fontSize: 20,
|
||||
fontStyle: 'italic',
|
||||
color: '#fff',
|
||||
shadowColor: '#fff',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
backgroundColor: 'rgba(30,144,255,0.8)',
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
shadowColor: '#fff',
|
||||
shadowBlur: 5,
|
||||
offsetCenter: [0, '50%'],
|
||||
textStyle: {
|
||||
fontWeight: 'bolder',
|
||||
color: '#fff'
|
||||
},
|
||||
formatter: function(value) {
|
||||
return value + props.unit
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: props.value,
|
||||
name: props.title || '完成度'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
|
||||
const resizeChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
window.addEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
window.removeEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
watch(() => [props.value, props.max, props.min], () => {
|
||||
updateChart()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
getChartInstance: () => chartInstance,
|
||||
resize: resizeChart
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gauge-chart {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
200
government-admin/src/components/charts/LineChart.vue
Normal file
200
government-admin/src/components/charts/LineChart.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="line-chart" :style="{ width: width, height: height }">
|
||||
<div ref="chartRef" :style="{ width: '100%', height: '100%' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
xAxisData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
color: {
|
||||
type: Array,
|
||||
default: () => ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1']
|
||||
},
|
||||
smooth: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showArea: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showSymbol: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
grid: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
top: '10%',
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
updateChart()
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return
|
||||
|
||||
const series = props.data.map((item, index) => ({
|
||||
name: item.name,
|
||||
type: 'line',
|
||||
data: item.data,
|
||||
smooth: props.smooth,
|
||||
symbol: props.showSymbol ? 'circle' : 'none',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: props.color[index % props.color.length]
|
||||
},
|
||||
itemStyle: {
|
||||
color: props.color[index % props.color.length]
|
||||
},
|
||||
areaStyle: props.showArea ? {
|
||||
opacity: 0.3,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: props.color[index % props.color.length] },
|
||||
{ offset: 1, color: 'rgba(255, 255, 255, 0)' }
|
||||
])
|
||||
} : null
|
||||
}))
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: props.title,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: props.data.map(item => item.name),
|
||||
bottom: 10,
|
||||
textStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
grid: props.grid,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: props.xAxisData,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e8e8e8'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e8e8e8'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#666'
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
series
|
||||
}
|
||||
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
|
||||
const resizeChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
window.addEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
window.removeEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
watch(() => [props.data, props.xAxisData], () => {
|
||||
updateChart()
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({
|
||||
getChartInstance: () => chartInstance,
|
||||
resize: resizeChart
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.line-chart {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
185
government-admin/src/components/charts/MapChart.vue
Normal file
185
government-admin/src/components/charts/MapChart.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="map-chart" :style="{ width: width, height: height }">
|
||||
<div ref="chartRef" :style="{ width: '100%', height: '100%' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
mapName: {
|
||||
type: String,
|
||||
default: 'china'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
color: {
|
||||
type: Array,
|
||||
default: () => ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffcc', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']
|
||||
},
|
||||
visualMapMin: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
visualMapMax: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
roam: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
const initChart = async () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
|
||||
// 注册地图(这里需要根据实际情况加载地图数据)
|
||||
// 示例:加载中国地图数据
|
||||
try {
|
||||
// 这里应该加载实际的地图JSON数据
|
||||
// const mapData = await import('@/assets/maps/china.json')
|
||||
// echarts.registerMap(props.mapName, mapData.default)
|
||||
|
||||
// 临时使用内置地图
|
||||
updateChart()
|
||||
} catch (error) {
|
||||
console.warn('地图数据加载失败,使用默认配置')
|
||||
updateChart()
|
||||
}
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: props.title,
|
||||
left: 'center',
|
||||
top: 20,
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
if (params.data) {
|
||||
return `${params.name}<br/>${params.seriesName}: ${params.data.value}`
|
||||
}
|
||||
return `${params.name}<br/>暂无数据`
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
visualMap: {
|
||||
min: props.visualMapMin,
|
||||
max: props.visualMapMax,
|
||||
left: 'left',
|
||||
top: 'bottom',
|
||||
text: ['高', '低'],
|
||||
inRange: {
|
||||
color: props.color
|
||||
},
|
||||
textStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: props.title || '数据分布',
|
||||
type: 'map',
|
||||
map: props.mapName,
|
||||
roam: props.roam,
|
||||
data: props.data,
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
color: '#fff'
|
||||
},
|
||||
itemStyle: {
|
||||
areaColor: '#389BB7',
|
||||
borderWidth: 0
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1,
|
||||
areaColor: '#eee'
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 12,
|
||||
color: '#333'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
|
||||
const resizeChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
window.addEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
window.removeEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
watch(() => props.data, () => {
|
||||
updateChart()
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({
|
||||
getChartInstance: () => chartInstance,
|
||||
resize: resizeChart
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.map-chart {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
179
government-admin/src/components/charts/PieChart.vue
Normal file
179
government-admin/src/components/charts/PieChart.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="pie-chart" :style="{ width: width, height: height }">
|
||||
<div ref="chartRef" :style="{ width: '100%', height: '100%' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
color: {
|
||||
type: Array,
|
||||
default: () => ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#eb2f96', '#13c2c2', '#fa8c16']
|
||||
},
|
||||
radius: {
|
||||
type: Array,
|
||||
default: () => ['40%', '70%']
|
||||
},
|
||||
center: {
|
||||
type: Array,
|
||||
default: () => ['50%', '50%']
|
||||
},
|
||||
roseType: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showLabelLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
updateChart()
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: props.title,
|
||||
left: 'center',
|
||||
top: 20,
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#666'
|
||||
},
|
||||
formatter: function(name) {
|
||||
const item = props.data.find(d => d.name === name)
|
||||
return item ? `${name}: ${item.value}` : name
|
||||
}
|
||||
},
|
||||
color: props.color,
|
||||
series: [
|
||||
{
|
||||
name: props.title || '数据统计',
|
||||
type: 'pie',
|
||||
radius: props.radius,
|
||||
center: props.center,
|
||||
roseType: props.roseType,
|
||||
data: props.data,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: props.showLabel,
|
||||
position: 'outside',
|
||||
formatter: '{b}: {d}%',
|
||||
fontSize: 12,
|
||||
color: '#666'
|
||||
},
|
||||
labelLine: {
|
||||
show: props.showLabelLine,
|
||||
length: 15,
|
||||
length2: 10,
|
||||
lineStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 8,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
|
||||
const resizeChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
window.addEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
window.removeEventListener('resize', resizeChart)
|
||||
})
|
||||
|
||||
watch(() => props.data, () => {
|
||||
updateChart()
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({
|
||||
getChartInstance: () => chartInstance,
|
||||
resize: resizeChart
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pie-chart {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
22
government-admin/src/components/charts/index.js
Normal file
22
government-admin/src/components/charts/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// 图表组件统一导出
|
||||
import LineChart from './LineChart.vue'
|
||||
import BarChart from './BarChart.vue'
|
||||
import PieChart from './PieChart.vue'
|
||||
import GaugeChart from './GaugeChart.vue'
|
||||
import MapChart from './MapChart.vue'
|
||||
|
||||
export {
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
GaugeChart,
|
||||
MapChart
|
||||
}
|
||||
|
||||
export default {
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
GaugeChart,
|
||||
MapChart
|
||||
}
|
||||
272
government-admin/src/components/common/DataTable.vue
Normal file
272
government-admin/src/components/common/DataTable.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<div class="data-table">
|
||||
<div v-if="showToolbar" class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<slot name="toolbar-left">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="showAdd"
|
||||
type="primary"
|
||||
@click="$emit('add')"
|
||||
>
|
||||
<PlusOutlined />
|
||||
{{ addText || '新增' }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="showBatchDelete && selectedRowKeys.length > 0"
|
||||
danger
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
批量删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<slot name="toolbar-right">
|
||||
<a-space>
|
||||
<a-tooltip title="刷新">
|
||||
<a-button @click="$emit('refresh')">
|
||||
<ReloadOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="列设置">
|
||||
<a-button @click="showColumnSetting = true">
|
||||
<SettingOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="visibleColumns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="paginationConfig"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="scroll"
|
||||
:size="size"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
|
||||
<slot :name="name" v-bind="slotData"></slot>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 列设置弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showColumnSetting"
|
||||
title="列设置"
|
||||
@ok="handleColumnSettingOk"
|
||||
>
|
||||
<a-checkbox-group v-model:value="selectedColumns" class="column-setting">
|
||||
<div v-for="column in columns" :key="column.key || column.dataIndex" class="column-item">
|
||||
<a-checkbox :value="column.key || column.dataIndex">
|
||||
{{ column.title }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</a-checkbox-group>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Modal, message } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
dataSource: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
pagination: {
|
||||
type: [Object, Boolean],
|
||||
default: () => ({})
|
||||
},
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showAdd: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
addText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showBatchDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
scroll: {
|
||||
type: Object,
|
||||
default: () => ({ x: 'max-content' })
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'middle'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'add',
|
||||
'refresh',
|
||||
'change',
|
||||
'batchDelete',
|
||||
'selectionChange'
|
||||
])
|
||||
|
||||
const selectedRowKeys = ref([])
|
||||
const showColumnSetting = ref(false)
|
||||
const selectedColumns = ref([])
|
||||
|
||||
// 初始化选中的列
|
||||
const initSelectedColumns = () => {
|
||||
selectedColumns.value = props.columns
|
||||
.filter(col => col.key || col.dataIndex)
|
||||
.map(col => col.key || col.dataIndex)
|
||||
}
|
||||
|
||||
// 可见的列
|
||||
const visibleColumns = computed(() => {
|
||||
return props.columns.filter(col => {
|
||||
const key = col.key || col.dataIndex
|
||||
return !key || selectedColumns.value.includes(key)
|
||||
})
|
||||
})
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = computed(() => {
|
||||
if (!props.showBatchDelete) return null
|
||||
|
||||
return {
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys, rows) => {
|
||||
selectedRowKeys.value = keys
|
||||
emit('selectionChange', keys, rows)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const paginationConfig = computed(() => {
|
||||
if (props.pagination === false) return false
|
||||
|
||||
return {
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
...props.pagination
|
||||
}
|
||||
})
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
emit('change', { pagination, filters, sorter })
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除选中的 ${selectedRowKeys.value.length} 条数据吗?`,
|
||||
onOk: () => {
|
||||
emit('batchDelete', selectedRowKeys.value)
|
||||
selectedRowKeys.value = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 列设置确认
|
||||
const handleColumnSettingOk = () => {
|
||||
showColumnSetting.value = false
|
||||
message.success('列设置已保存')
|
||||
}
|
||||
|
||||
// 监听列变化,重新初始化选中的列
|
||||
watch(
|
||||
() => props.columns,
|
||||
() => {
|
||||
initSelectedColumns()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-table {
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.column-setting {
|
||||
.column-item {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.data-table {
|
||||
.table-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
80
government-admin/src/components/common/EmptyState.vue
Normal file
80
government-admin/src/components/common/EmptyState.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<component v-if="icon" :is="icon" />
|
||||
<InboxOutlined v-else />
|
||||
</div>
|
||||
<div class="empty-title">{{ title || '暂无数据' }}</div>
|
||||
<div v-if="description" class="empty-description">{{ description }}</div>
|
||||
<div v-if="showAction" class="empty-action">
|
||||
<a-button type="primary" @click="$emit('action')">
|
||||
{{ actionText || '重新加载' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
defineProps({
|
||||
icon: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['action'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
color: #262626;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 24px;
|
||||
max-width: 300px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
government-admin/src/components/common/LoadingSpinner.vue
Normal file
83
government-admin/src/components/common/LoadingSpinner.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="loading-spinner" :class="{ 'full-screen': fullScreen }">
|
||||
<div class="spinner-container">
|
||||
<div class="spinner" :style="{ width: size + 'px', height: size + 'px' }">
|
||||
<div class="spinner-inner"></div>
|
||||
</div>
|
||||
<div v-if="text" class="loading-text">{{ text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: Number,
|
||||
default: 40
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fullScreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
|
||||
&.full-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.spinner {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(from 0deg, #1890ff, #40a9ff, #69c0ff, #91d5ff, transparent);
|
||||
animation: spin 1s linear infinite;
|
||||
|
||||
.spinner-inner {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
government-admin/src/components/common/PageHeader.vue
Normal file
106
government-admin/src/components/common/PageHeader.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<component v-if="icon" :is="icon" class="title-icon" />
|
||||
<h1>{{ title }}</h1>
|
||||
</div>
|
||||
<div v-if="description" class="header-description">{{ description }}</div>
|
||||
</div>
|
||||
<div v-if="$slots.extra" class="header-extra">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.tabs" class="header-tabs">
|
||||
<slot name="tabs"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
icon: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.title-icon {
|
||||
font-size: 24px;
|
||||
color: #1890ff;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.header-description {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.header-extra {
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
padding: 0 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
|
||||
.header-extra {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
210
government-admin/src/components/common/SearchForm.vue
Normal file
210
government-admin/src/components/common/SearchForm.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="search-form">
|
||||
<a-form
|
||||
:model="formData"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
@reset="handleReset"
|
||||
>
|
||||
<template v-for="field in fields" :key="field.key">
|
||||
<!-- 输入框 -->
|
||||
<a-form-item
|
||||
v-if="field.type === 'input'"
|
||||
:label="field.label"
|
||||
:name="field.key"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder || `请输入${field.label}`"
|
||||
:style="{ width: field.width || '200px' }"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 选择框 -->
|
||||
<a-form-item
|
||||
v-else-if="field.type === 'select'"
|
||||
:label="field.label"
|
||||
:name="field.key"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder || `请选择${field.label}`"
|
||||
:style="{ width: field.width || '200px' }"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option
|
||||
v-for="option in field.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<a-form-item
|
||||
v-else-if="field.type === 'date'"
|
||||
:label="field.label"
|
||||
:name="field.key"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder || `请选择${field.label}`"
|
||||
:style="{ width: field.width || '200px' }"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 日期范围选择 -->
|
||||
<a-form-item
|
||||
v-else-if="field.type === 'dateRange'"
|
||||
:label="field.label"
|
||||
:name="field.key"
|
||||
>
|
||||
<a-range-picker
|
||||
v-model:value="formData[field.key]"
|
||||
:placeholder="field.placeholder || ['开始日期', '结束日期']"
|
||||
:style="{ width: field.width || '300px' }"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading">
|
||||
<SearchOutlined />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button html-type="reset">
|
||||
<ReloadOutlined />
|
||||
重置
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="showToggle && fields.length > 3"
|
||||
type="link"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
{{ expanded ? '收起' : '展开' }}
|
||||
<UpOutlined v-if="expanded" />
|
||||
<DownOutlined v-else />
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { SearchOutlined, ReloadOutlined, UpOutlined, DownOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showToggle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
initialValues: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['search', 'reset'])
|
||||
|
||||
const expanded = ref(false)
|
||||
const formData = reactive({})
|
||||
|
||||
// 初始化表单数据
|
||||
const initFormData = () => {
|
||||
props.fields.forEach(field => {
|
||||
formData[field.key] = props.initialValues[field.key] || field.defaultValue || undefined
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
const searchData = { ...formData }
|
||||
// 过滤空值
|
||||
Object.keys(searchData).forEach(key => {
|
||||
if (searchData[key] === undefined || searchData[key] === null || searchData[key] === '') {
|
||||
delete searchData[key]
|
||||
}
|
||||
})
|
||||
emit('search', searchData)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
props.fields.forEach(field => {
|
||||
formData[field.key] = field.defaultValue || undefined
|
||||
})
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
||||
// 监听初始值变化
|
||||
watch(
|
||||
() => props.initialValues,
|
||||
() => {
|
||||
initFormData()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 监听字段变化
|
||||
watch(
|
||||
() => props.fields,
|
||||
() => {
|
||||
initFormData()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-form {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.search-form {
|
||||
padding: 16px;
|
||||
|
||||
:deep(.ant-form) {
|
||||
.ant-form-item {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.ant-form-item-control-input {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
551
government-admin/src/components/common/TabsView.vue
Normal file
551
government-admin/src/components/common/TabsView.vue
Normal file
@@ -0,0 +1,551 @@
|
||||
<template>
|
||||
<div class="tabs-view">
|
||||
<!-- 标签页导航 -->
|
||||
<div class="tabs-nav" ref="tabsNavRef">
|
||||
<div class="tabs-nav-scroll" :style="{ transform: `translateX(${scrollOffset}px)` }">
|
||||
<div
|
||||
v-for="tab in tabsStore.openTabs"
|
||||
:key="tab.path"
|
||||
:class="[
|
||||
'tab-item',
|
||||
{ 'active': tab.active },
|
||||
{ 'closable': tab.closable }
|
||||
]"
|
||||
@click="handleTabClick(tab)"
|
||||
@contextmenu.prevent="handleTabContextMenu(tab, $event)"
|
||||
>
|
||||
<span class="tab-title">{{ tab.title }}</span>
|
||||
<CloseOutlined
|
||||
v-if="tab.closable"
|
||||
class="tab-close"
|
||||
@click.stop="handleTabClose(tab)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滚动控制按钮 -->
|
||||
<div class="tabs-nav-controls">
|
||||
<LeftOutlined
|
||||
:class="['nav-btn', { 'disabled': scrollOffset >= 0 }]"
|
||||
@click="scrollTabs('left')"
|
||||
/>
|
||||
<RightOutlined
|
||||
:class="['nav-btn', { 'disabled': scrollOffset <= maxScrollOffset }]"
|
||||
@click="scrollTabs('right')"
|
||||
/>
|
||||
<MoreOutlined
|
||||
class="nav-btn"
|
||||
@click="showTabsMenu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<div class="tabs-content">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<keep-alive :include="tabsStore.cachedViews">
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.path"
|
||||
v-if="Component"
|
||||
/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
:class="['context-menu']"
|
||||
:style="{
|
||||
left: contextMenu.x + 'px',
|
||||
top: contextMenu.y + 'px'
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<div class="menu-item" @click="refreshTab(contextMenu.tab)">
|
||||
<ReloadOutlined />
|
||||
刷新页面
|
||||
</div>
|
||||
<div
|
||||
v-if="contextMenu.tab.closable"
|
||||
class="menu-item"
|
||||
@click="closeTab(contextMenu.tab)"
|
||||
>
|
||||
<CloseOutlined />
|
||||
关闭标签
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-item" @click="closeOtherTabs(contextMenu.tab)">
|
||||
<CloseCircleOutlined />
|
||||
关闭其他
|
||||
</div>
|
||||
<div class="menu-item" @click="closeLeftTabs(contextMenu.tab)">
|
||||
<VerticalLeftOutlined />
|
||||
关闭左侧
|
||||
</div>
|
||||
<div class="menu-item" @click="closeRightTabs(contextMenu.tab)">
|
||||
<VerticalRightOutlined />
|
||||
关闭右侧
|
||||
</div>
|
||||
<div class="menu-item" @click="closeAllTabs">
|
||||
<CloseSquareOutlined />
|
||||
关闭全部
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页列表菜单 -->
|
||||
<a-dropdown
|
||||
v-model:open="tabsMenuVisible"
|
||||
:trigger="['click']"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item
|
||||
v-for="tab in tabsStore.openTabs"
|
||||
:key="tab.path"
|
||||
@click="handleTabClick(tab)"
|
||||
>
|
||||
<span :class="{ 'active-tab': tab.active }">
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<!-- 遮罩层,用于关闭右键菜单 -->
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu-overlay"
|
||||
@click="hideContextMenu"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
import {
|
||||
CloseOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
MoreOutlined,
|
||||
ReloadOutlined,
|
||||
CloseCircleOutlined,
|
||||
VerticalLeftOutlined,
|
||||
VerticalRightOutlined,
|
||||
CloseSquareOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tabsStore = useTabsStore()
|
||||
|
||||
// 标签页导航引用
|
||||
const tabsNavRef = ref(null)
|
||||
|
||||
// 滚动偏移量
|
||||
const scrollOffset = ref(0)
|
||||
|
||||
// 最大滚动偏移量
|
||||
const maxScrollOffset = computed(() => {
|
||||
if (!tabsNavRef.value) return 0
|
||||
const navWidth = tabsNavRef.value.clientWidth - 120 // 减去控制按钮宽度
|
||||
const scrollWidth = tabsNavRef.value.querySelector('.tabs-nav-scroll')?.scrollWidth || 0
|
||||
return Math.min(0, navWidth - scrollWidth)
|
||||
})
|
||||
|
||||
// 右键菜单状态
|
||||
const contextMenu = reactive({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
tab: null
|
||||
})
|
||||
|
||||
// 标签页菜单显示状态
|
||||
const tabsMenuVisible = ref(false)
|
||||
|
||||
/**
|
||||
* 处理标签页点击
|
||||
*/
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab.path !== route.path) {
|
||||
router.push(tab.path)
|
||||
}
|
||||
tabsStore.setActiveTab(tab.path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理标签页关闭
|
||||
*/
|
||||
const handleTabClose = (tab) => {
|
||||
if (!tab.closable) return
|
||||
|
||||
tabsStore.removeTab(tab.path)
|
||||
|
||||
// 如果关闭的是当前标签页,跳转到其他标签页
|
||||
if (tab.active && tabsStore.openTabs.length > 0) {
|
||||
const activeTab = tabsStore.openTabs.find(t => t.active)
|
||||
if (activeTab) {
|
||||
router.push(activeTab.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理标签页右键菜单
|
||||
*/
|
||||
const handleTabContextMenu = (tab, event) => {
|
||||
contextMenu.visible = true
|
||||
contextMenu.x = event.clientX
|
||||
contextMenu.y = event.clientY
|
||||
contextMenu.tab = tab
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏右键菜单
|
||||
*/
|
||||
const hideContextMenu = () => {
|
||||
contextMenu.visible = false
|
||||
contextMenu.tab = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新标签页
|
||||
*/
|
||||
const refreshTab = (tab) => {
|
||||
tabsStore.refreshTab(tab.path)
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭标签页
|
||||
*/
|
||||
const closeTab = (tab) => {
|
||||
handleTabClose(tab)
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他标签页
|
||||
*/
|
||||
const closeOtherTabs = (tab) => {
|
||||
tabsStore.closeOtherTabs(tab.path)
|
||||
if (tab.path !== route.path) {
|
||||
router.push(tab.path)
|
||||
}
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧标签页
|
||||
*/
|
||||
const closeLeftTabs = (tab) => {
|
||||
tabsStore.closeLeftTabs(tab.path)
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧标签页
|
||||
*/
|
||||
const closeRightTabs = (tab) => {
|
||||
tabsStore.closeRightTabs(tab.path)
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有标签页
|
||||
*/
|
||||
const closeAllTabs = () => {
|
||||
tabsStore.closeAllTabs()
|
||||
const activeTab = tabsStore.openTabs[0]
|
||||
if (activeTab && activeTab.path !== route.path) {
|
||||
router.push(activeTab.path)
|
||||
}
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动标签页
|
||||
*/
|
||||
const scrollTabs = (direction) => {
|
||||
const step = 200
|
||||
if (direction === 'left') {
|
||||
scrollOffset.value = Math.min(0, scrollOffset.value + step)
|
||||
} else {
|
||||
scrollOffset.value = Math.max(maxScrollOffset.value, scrollOffset.value - step)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示标签页菜单
|
||||
*/
|
||||
const showTabsMenu = () => {
|
||||
tabsMenuVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听路由变化,添加标签页
|
||||
*/
|
||||
const addCurrentRouteTab = () => {
|
||||
const { path, meta, name } = route
|
||||
|
||||
if (meta.hidden) return
|
||||
|
||||
const tab = {
|
||||
path,
|
||||
title: meta.title || name || '未命名页面',
|
||||
name: name,
|
||||
closable: meta.closable !== false
|
||||
}
|
||||
|
||||
tabsStore.addTab(tab)
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听点击事件,关闭右键菜单
|
||||
*/
|
||||
const handleDocumentClick = () => {
|
||||
if (contextMenu.visible) {
|
||||
hideContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听窗口大小变化,调整滚动偏移量
|
||||
*/
|
||||
const handleWindowResize = () => {
|
||||
nextTick(() => {
|
||||
if (scrollOffset.value < maxScrollOffset.value) {
|
||||
scrollOffset.value = maxScrollOffset.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 添加当前路由标签页
|
||||
addCurrentRouteTab()
|
||||
|
||||
// 监听文档点击事件
|
||||
document.addEventListener('click', handleDocumentClick)
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
})
|
||||
|
||||
// 监听路由变化
|
||||
router.afterEach((to) => {
|
||||
if (!to.meta.hidden) {
|
||||
const tab = {
|
||||
path: to.path,
|
||||
title: to.meta.title || to.name || '未命名页面',
|
||||
name: to.name,
|
||||
closable: to.meta.closable !== false
|
||||
}
|
||||
|
||||
tabsStore.addTab(tab)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabs-view {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
background: #fafafa;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.tabs-nav-scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
margin: 4px 2px;
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
max-width: 200px;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: #fff;
|
||||
|
||||
.tab-close {
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 8px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-nav-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
border-left: 1px solid #e8e8e8;
|
||||
background: #fafafa;
|
||||
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 2px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
min-width: 120px;
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
margin-right: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: #e8e8e8;
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.active-tab {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.tabs-nav {
|
||||
.tab-item {
|
||||
min-width: 60px;
|
||||
max-width: 120px;
|
||||
padding: 0 8px;
|
||||
|
||||
.tab-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
min-width: 100px;
|
||||
|
||||
.menu-item {
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
312
government-admin/src/components/layout/SidebarMenu.vue
Normal file
312
government-admin/src/components/layout/SidebarMenu.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<div class="sidebar-menu">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
>
|
||||
<template v-for="item in menuItems" :key="item.key">
|
||||
<a-menu-item
|
||||
v-if="!item.children"
|
||||
:key="item.key"
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="item.icon" />
|
||||
</template>
|
||||
<span>{{ item.title }}</span>
|
||||
</a-menu-item>
|
||||
|
||||
<a-sub-menu
|
||||
v-else
|
||||
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="item.icon" />
|
||||
</template>
|
||||
<template #title>{{ item.title }}</template>
|
||||
<!-- :key="item.key" -->
|
||||
<a-menu-item
|
||||
v-for="child in item.children"
|
||||
:key="child.key"
|
||||
:disabled="child.disabled"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="child.icon" />
|
||||
</template>
|
||||
<span>{{ child.title }}</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
</a-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
HomeOutlined,
|
||||
MonitorOutlined,
|
||||
AuditOutlined,
|
||||
LinkOutlined,
|
||||
AlertOutlined,
|
||||
FileTextOutlined,
|
||||
BarChartOutlined,
|
||||
SettingOutlined,
|
||||
SafetyOutlined,
|
||||
TeamOutlined,
|
||||
DatabaseOutlined,
|
||||
KeyOutlined,
|
||||
SolutionOutlined,
|
||||
MedicineBoxOutlined,
|
||||
CustomerServiceOutlined,
|
||||
EyeOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const selectedKeys = ref([])
|
||||
const openKeys = ref([])
|
||||
|
||||
// 菜单配置
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
key: '/dashboard',
|
||||
title: '仪表盘',
|
||||
icon: DashboardOutlined,
|
||||
path: '/dashboard'
|
||||
},
|
||||
{
|
||||
key: '/breeding',
|
||||
title: '养殖管理',
|
||||
icon: HomeOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/breeding/farms',
|
||||
title: '养殖场管理',
|
||||
icon: HomeOutlined,
|
||||
path: '/breeding/farms'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/monitoring',
|
||||
title: '健康监控',
|
||||
icon: MonitorOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/monitoring/health',
|
||||
title: '动物健康监控',
|
||||
icon: SafetyOutlined,
|
||||
path: '/monitoring/health'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/inspection',
|
||||
title: '检查管理',
|
||||
icon: AuditOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/inspection/management',
|
||||
title: '检查管理',
|
||||
icon: AuditOutlined,
|
||||
path: '/inspection/management'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/traceability',
|
||||
title: '溯源系统',
|
||||
icon: LinkOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/traceability/system',
|
||||
title: '产品溯源',
|
||||
icon: LinkOutlined,
|
||||
path: '/traceability/system'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/emergency',
|
||||
title: '应急响应',
|
||||
icon: AlertOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/emergency/response',
|
||||
title: '应急响应',
|
||||
icon: AlertOutlined,
|
||||
path: '/emergency/response'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/policy',
|
||||
title: '政策管理',
|
||||
icon: FileTextOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/policy/management',
|
||||
title: '政策管理',
|
||||
icon: FileTextOutlined,
|
||||
path: '/policy/management'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/statistics',
|
||||
title: '数据统计',
|
||||
icon: BarChartOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/statistics/data',
|
||||
title: '数据统计',
|
||||
icon: BarChartOutlined,
|
||||
path: '/statistics/data'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/reports',
|
||||
title: '报表中心',
|
||||
icon: FileTextOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/reports/center',
|
||||
title: '报表中心',
|
||||
icon: FileTextOutlined,
|
||||
path: '/reports/center'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/settings',
|
||||
title: '系统设置',
|
||||
icon: SettingOutlined,
|
||||
children: [
|
||||
{
|
||||
key: '/settings/system',
|
||||
title: '系统设置',
|
||||
icon: SettingOutlined,
|
||||
path: '/settings/system'
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = ({ key }) => {
|
||||
const findMenuItem = (items, targetKey) => {
|
||||
for (const item of items) {
|
||||
if (item.key === targetKey) {
|
||||
return item
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findMenuItem(item.children, targetKey)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const menuItem = findMenuItem(menuItems.value, key)
|
||||
if (menuItem && menuItem.path) {
|
||||
router.push(menuItem.path)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据当前路由设置选中状态
|
||||
const updateSelectedKeys = () => {
|
||||
const currentPath = route.path
|
||||
selectedKeys.value = [currentPath]
|
||||
|
||||
// 自动展开父级菜单
|
||||
const findParentKey = (items, targetPath, parentKey = null) => {
|
||||
for (const item of items) {
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
if (child.path === targetPath) {
|
||||
return item.key
|
||||
}
|
||||
}
|
||||
const found = findParentKey(item.children, targetPath, item.key)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return parentKey
|
||||
}
|
||||
|
||||
const parentKey = findParentKey(menuItems.value, currentPath)
|
||||
if (parentKey && !openKeys.value.includes(parentKey)) {
|
||||
openKeys.value = [...openKeys.value, parentKey]
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(route, updateSelectedKeys, { immediate: true })
|
||||
|
||||
// 监听折叠状态变化
|
||||
watch(() => props.collapsed, (collapsed) => {
|
||||
if (collapsed) {
|
||||
openKeys.value = []
|
||||
} else {
|
||||
updateSelectedKeys()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-menu {
|
||||
height: 100%;
|
||||
|
||||
:deep(.ant-menu) {
|
||||
border-right: none;
|
||||
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-title {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-item-selected {
|
||||
background-color: #1890ff !important;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-submenu-selected {
|
||||
.ant-menu-submenu-title {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-item-icon,
|
||||
.ant-menu-submenu-title .ant-menu-item-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
271
government-admin/src/components/layout/TabsView.vue
Normal file
271
government-admin/src/components/layout/TabsView.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="tabs-view">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeKey"
|
||||
type="editable-card"
|
||||
hide-add
|
||||
@edit="onEdit"
|
||||
@change="onChange"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:tab="tab.title"
|
||||
:closable="tab.closable"
|
||||
>
|
||||
<template #tab>
|
||||
<span class="tab-title">
|
||||
<component v-if="tab.icon" :is="tab.icon" class="tab-icon" />
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<a-dropdown
|
||||
v-model:open="contextMenuVisible"
|
||||
:trigger="['contextmenu']"
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<div ref="contextMenuTarget" class="context-menu-target"></div>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleContextMenu">
|
||||
<a-menu-item key="refresh">
|
||||
<ReloadOutlined />
|
||||
刷新页面
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="close">
|
||||
<CloseOutlined />
|
||||
关闭标签
|
||||
</a-menu-item>
|
||||
<a-menu-item key="closeOthers">
|
||||
<CloseCircleOutlined />
|
||||
关闭其他
|
||||
</a-menu-item>
|
||||
<a-menu-item key="closeAll">
|
||||
<CloseSquareOutlined />
|
||||
关闭全部
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="closeLeft">
|
||||
<VerticalLeftOutlined />
|
||||
关闭左侧
|
||||
</a-menu-item>
|
||||
<a-menu-item key="closeRight">
|
||||
<VerticalRightOutlined />
|
||||
关闭右侧
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
CloseCircleOutlined,
|
||||
CloseSquareOutlined,
|
||||
VerticalLeftOutlined,
|
||||
VerticalRightOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tabsStore = useTabsStore()
|
||||
|
||||
const activeKey = ref('')
|
||||
const contextMenuVisible = ref(false)
|
||||
const contextMenuTarget = ref(null)
|
||||
const currentContextTab = ref(null)
|
||||
|
||||
// 标签页列表
|
||||
const tabs = computed(() => tabsStore.tabs)
|
||||
|
||||
// 处理标签页变化
|
||||
const onChange = (key) => {
|
||||
const tab = tabs.value.find(t => t.key === key)
|
||||
if (tab) {
|
||||
router.push(tab.path)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签页编辑(关闭)
|
||||
const onEdit = (targetKey, action) => {
|
||||
if (action === 'remove') {
|
||||
closeTab(targetKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭标签页
|
||||
const closeTab = (key) => {
|
||||
const tab = tabs.value.find(t => t.key === key)
|
||||
if (tab && tab.closable) {
|
||||
tabsStore.removeTab(key)
|
||||
|
||||
// 如果关闭的是当前标签,跳转到最后一个标签
|
||||
if (key === activeKey.value && tabs.value.length > 0) {
|
||||
const lastTab = tabs.value[tabs.value.length - 1]
|
||||
router.push(lastTab.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单处理
|
||||
const handleContextMenu = ({ key }) => {
|
||||
const currentTab = currentContextTab.value
|
||||
if (!currentTab) return
|
||||
|
||||
switch (key) {
|
||||
case 'refresh':
|
||||
// 刷新当前页面
|
||||
router.go(0)
|
||||
break
|
||||
case 'close':
|
||||
closeTab(currentTab.key)
|
||||
break
|
||||
case 'closeOthers':
|
||||
tabsStore.closeOtherTabs(currentTab.key)
|
||||
break
|
||||
case 'closeAll':
|
||||
tabsStore.closeAllTabs()
|
||||
router.push('/dashboard')
|
||||
break
|
||||
case 'closeLeft':
|
||||
tabsStore.closeLeftTabs(currentTab.key)
|
||||
break
|
||||
case 'closeRight':
|
||||
tabsStore.closeRightTabs(currentTab.key)
|
||||
break
|
||||
}
|
||||
|
||||
contextMenuVisible.value = false
|
||||
}
|
||||
|
||||
// 监听路由变化,添加标签页
|
||||
watch(route, (newRoute) => {
|
||||
if (newRoute.meta && newRoute.meta.title) {
|
||||
const tab = {
|
||||
key: newRoute.path,
|
||||
path: newRoute.path,
|
||||
title: newRoute.meta.title,
|
||||
icon: newRoute.meta.icon,
|
||||
closable: !newRoute.meta.affix
|
||||
}
|
||||
|
||||
tabsStore.addTab(tab)
|
||||
activeKey.value = newRoute.path
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听标签页变化
|
||||
watch(tabs, (newTabs) => {
|
||||
if (newTabs.length === 0) {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 添加右键菜单事件监听
|
||||
const addContextMenuListener = () => {
|
||||
nextTick(() => {
|
||||
const tabsContainer = document.querySelector('.ant-tabs-nav')
|
||||
if (tabsContainer) {
|
||||
tabsContainer.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
// 查找被右键点击的标签
|
||||
const tabElement = e.target.closest('.ant-tabs-tab')
|
||||
if (tabElement) {
|
||||
const tabKey = tabElement.getAttribute('data-node-key')
|
||||
const tab = tabs.value.find(t => t.key === tabKey)
|
||||
if (tab) {
|
||||
currentContextTab.value = tab
|
||||
contextMenuVisible.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载后添加事件监听
|
||||
watch(tabs, addContextMenuListener, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabs-view {
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-nav {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
.ant-tabs-nav-list {
|
||||
.ant-tabs-tab {
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin-right: 4px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
|
||||
.tab-title {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.tab-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove {
|
||||
margin-left: 8px;
|
||||
color: #999;
|
||||
|
||||
&:hover {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-target {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
880
government-admin/src/layout/GovernmentLayout.vue
Normal file
880
government-admin/src/layout/GovernmentLayout.vue
Normal file
@@ -0,0 +1,880 @@
|
||||
<template>
|
||||
<div class="government-layout">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="layout-header">
|
||||
<div class="header-left">
|
||||
<div class="logo">
|
||||
<img src="/logo.svg" alt="政府管理后台" />
|
||||
<span class="logo-text">政府管理后台</span>
|
||||
</div>
|
||||
<div class="header-menu">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedMenuKeys"
|
||||
mode="horizontal"
|
||||
:items="headerMenuItems"
|
||||
@click="handleHeaderMenuClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- 通知中心 -->
|
||||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||||
<div class="header-action">
|
||||
<a-badge :count="notificationStore.unreadCount" :offset="[10, 0]">
|
||||
<BellOutlined />
|
||||
</a-badge>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<div class="notification-dropdown">
|
||||
<div class="notification-header">
|
||||
<span>通知中心</span>
|
||||
<a @click="notificationStore.markAllAsRead()">全部已读</a>
|
||||
</div>
|
||||
<div class="notification-list">
|
||||
<div
|
||||
v-for="notification in notificationStore.recentNotifications.slice(0, 5)"
|
||||
:key="notification.id"
|
||||
:class="['notification-item', { 'unread': !notification.read }]"
|
||||
@click="handleNotificationClick(notification)"
|
||||
>
|
||||
<div class="notification-content">
|
||||
<div class="notification-title">{{ notification.title }}</div>
|
||||
<div class="notification-desc">{{ notification.content }}</div>
|
||||
<div class="notification-time">{{ formatTime(notification.timestamp) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-footer">
|
||||
<a @click="$router.push('/notifications')">查看全部</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||||
<div class="header-action user-info">
|
||||
<a-avatar :src="userStore.userInfo.avatar" :size="32">
|
||||
{{ userStore.userInfo.name?.charAt(0) }}
|
||||
</a-avatar>
|
||||
<span class="user-name">{{ userStore.userInfo.name }}</span>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleUserMenuClick">
|
||||
<a-menu-item key="profile">
|
||||
<UserOutlined />
|
||||
个人中心
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<SettingOutlined />
|
||||
系统设置
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout">
|
||||
<LogoutOutlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主体内容区域 -->
|
||||
<div class="layout-content">
|
||||
<!-- 侧边栏 -->
|
||||
<aside :class="['layout-sider', { 'collapsed': siderCollapsed }]">
|
||||
<div class="sider-trigger" @click="toggleSider">
|
||||
<MenuUnfoldOutlined v-if="siderCollapsed" />
|
||||
<MenuFoldOutlined v-else />
|
||||
</div>
|
||||
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedSiderKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
:inline-collapsed="siderCollapsed"
|
||||
:items="siderMenuItems"
|
||||
@click="handleSiderMenuClick"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<main class="layout-main">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb-container">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.path">
|
||||
<router-link v-if="item.path && item.path !== $route.path" :to="item.path">
|
||||
{{ item.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ item.title }}</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<!-- 标签页视图 -->
|
||||
<TabsView />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 通知抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="notificationDrawerVisible"
|
||||
title="系统通知"
|
||||
placement="right"
|
||||
:width="400"
|
||||
>
|
||||
<div class="notification-drawer">
|
||||
<div class="notification-filters">
|
||||
<a-radio-group v-model:value="notificationFilter" size="small">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="unread">未读</a-radio-button>
|
||||
<a-radio-button value="system">系统</a-radio-button>
|
||||
<a-radio-button value="task">任务</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="notification-list">
|
||||
<div
|
||||
v-for="notification in filteredNotifications"
|
||||
:key="notification.id"
|
||||
:class="['notification-item', { 'unread': !notification.read }]"
|
||||
>
|
||||
<div class="notification-header">
|
||||
<span class="notification-title">{{ notification.title }}</span>
|
||||
<span class="notification-time">{{ formatTime(notification.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="notification-content">{{ notification.content }}</div>
|
||||
<div class="notification-actions">
|
||||
<a-button
|
||||
v-if="!notification.read"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="notificationStore.markAsRead(notification.id)"
|
||||
>
|
||||
标记已读
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="notificationStore.removeNotification(notification.id)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
BellOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
DownOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import TabsView from '@/components/layout/TabsView.vue'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// Store
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
const tabsStore = useTabsStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// 响应式数据
|
||||
const siderCollapsed = ref(false)
|
||||
const selectedMenuKeys = ref([])
|
||||
const selectedSiderKeys = ref([])
|
||||
const openKeys = ref([])
|
||||
const notificationDrawerVisible = ref(false)
|
||||
const notificationFilter = ref('all')
|
||||
|
||||
// 顶部菜单项
|
||||
const headerMenuItems = computed(() => [
|
||||
{
|
||||
key: 'dashboard',
|
||||
label: '工作台',
|
||||
onClick: () => router.push('/dashboard')
|
||||
},
|
||||
{
|
||||
key: 'supervision',
|
||||
label: '政府监管',
|
||||
onClick: () => router.push('/supervision')
|
||||
},
|
||||
{
|
||||
key: 'approval',
|
||||
label: '审批管理',
|
||||
onClick: () => router.push('/approval')
|
||||
},
|
||||
{
|
||||
key: 'visualization',
|
||||
label: '可视化大屏',
|
||||
onClick: () => router.push('/visualization')
|
||||
}
|
||||
])
|
||||
|
||||
// 侧边栏菜单项
|
||||
const siderMenuItems = computed(() => {
|
||||
const menuItems = [
|
||||
{
|
||||
key: '/dashboard',
|
||||
icon: 'DashboardOutlined',
|
||||
label: '工作台',
|
||||
permission: 'dashboard:view'
|
||||
},
|
||||
{
|
||||
key: '/supervision',
|
||||
icon: 'EyeOutlined',
|
||||
label: '政府监管',
|
||||
permission: 'supervision:view',
|
||||
children: [
|
||||
{
|
||||
key: '/supervision/enterprise',
|
||||
label: '企业监管',
|
||||
permission: 'supervision:enterprise:view'
|
||||
},
|
||||
{
|
||||
key: '/supervision/environment',
|
||||
label: '环境监管',
|
||||
permission: 'supervision:environment:view'
|
||||
},
|
||||
{
|
||||
key: '/supervision/safety',
|
||||
label: '安全监管',
|
||||
permission: 'supervision:safety:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/approval',
|
||||
icon: 'AuditOutlined',
|
||||
label: '审批管理',
|
||||
permission: 'approval:view',
|
||||
children: [
|
||||
{
|
||||
key: '/approval/business',
|
||||
label: '营业执照',
|
||||
permission: 'approval:business:view'
|
||||
},
|
||||
{
|
||||
key: '/approval/construction',
|
||||
label: '建设工程',
|
||||
permission: 'approval:construction:view'
|
||||
},
|
||||
{
|
||||
key: '/approval/environmental',
|
||||
label: '环保审批',
|
||||
permission: 'approval:environmental:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/personnel',
|
||||
icon: 'TeamOutlined',
|
||||
label: '人员管理',
|
||||
permission: 'personnel:view',
|
||||
children: [
|
||||
{
|
||||
key: '/personnel/staff',
|
||||
label: '员工管理',
|
||||
permission: 'personnel:staff:view'
|
||||
},
|
||||
{
|
||||
key: '/personnel/department',
|
||||
label: '部门管理',
|
||||
permission: 'personnel:department:view'
|
||||
},
|
||||
{
|
||||
key: '/personnel/role',
|
||||
label: '角色管理',
|
||||
permission: 'personnel:role:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/warehouse',
|
||||
icon: 'InboxOutlined',
|
||||
label: '设备仓库',
|
||||
permission: 'warehouse:view',
|
||||
children: [
|
||||
{
|
||||
key: '/warehouse/equipment',
|
||||
label: '设备管理',
|
||||
permission: 'warehouse:equipment:view'
|
||||
},
|
||||
{
|
||||
key: '/warehouse/inventory',
|
||||
label: '库存管理',
|
||||
permission: 'warehouse:inventory:view'
|
||||
},
|
||||
{
|
||||
key: '/warehouse/maintenance',
|
||||
label: '维护记录',
|
||||
permission: 'warehouse:maintenance:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/epidemic',
|
||||
icon: 'SafetyOutlined',
|
||||
label: '防疫管理',
|
||||
permission: 'epidemic:view',
|
||||
children: [
|
||||
{
|
||||
key: '/epidemic/monitoring',
|
||||
label: '疫情监控',
|
||||
permission: 'epidemic:monitoring:view'
|
||||
},
|
||||
{
|
||||
key: '/epidemic/prevention',
|
||||
label: '防控措施',
|
||||
permission: 'epidemic:prevention:view'
|
||||
},
|
||||
{
|
||||
key: '/epidemic/statistics',
|
||||
label: '统计报告',
|
||||
permission: 'epidemic:statistics:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/service',
|
||||
icon: 'CustomerServiceOutlined',
|
||||
label: '服务管理',
|
||||
permission: 'service:view',
|
||||
children: [
|
||||
{
|
||||
key: '/service/public',
|
||||
label: '公共服务',
|
||||
permission: 'service:public:view'
|
||||
},
|
||||
{
|
||||
key: '/service/online',
|
||||
label: '在线办事',
|
||||
permission: 'service:online:view'
|
||||
},
|
||||
{
|
||||
key: '/service/feedback',
|
||||
label: '意见反馈',
|
||||
permission: 'service:feedback:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/visualization',
|
||||
icon: 'BarChartOutlined',
|
||||
label: '可视化大屏',
|
||||
permission: 'visualization:view'
|
||||
},
|
||||
{
|
||||
key: '/system',
|
||||
icon: 'SettingOutlined',
|
||||
label: '系统管理',
|
||||
permission: 'system:view',
|
||||
children: [
|
||||
{
|
||||
key: '/system/user',
|
||||
label: '用户管理',
|
||||
permission: 'system:user:view'
|
||||
},
|
||||
{
|
||||
key: '/system/role',
|
||||
label: '角色管理',
|
||||
permission: 'system:role:view'
|
||||
},
|
||||
{
|
||||
key: '/system/permission',
|
||||
label: '权限管理',
|
||||
permission: 'system:permission:view'
|
||||
},
|
||||
{
|
||||
key: '/system/config',
|
||||
label: '系统配置',
|
||||
permission: 'system:config:view'
|
||||
},
|
||||
{
|
||||
key: '/system/log',
|
||||
label: '操作日志',
|
||||
permission: 'system:log:view'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 根据权限过滤菜单项
|
||||
return filterMenuByPermission(menuItems)
|
||||
})
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumbItems = computed(() => {
|
||||
const matched = route.matched.filter(item => item.meta && item.meta.title)
|
||||
return matched.map(item => ({
|
||||
path: item.path,
|
||||
title: item.meta.title
|
||||
}))
|
||||
})
|
||||
|
||||
// 过滤后的通知列表
|
||||
const filteredNotifications = computed(() => {
|
||||
let notifications = notificationStore.notifications
|
||||
|
||||
switch (notificationFilter.value) {
|
||||
case 'unread':
|
||||
notifications = notifications.filter(n => !n.read)
|
||||
break
|
||||
case 'system':
|
||||
notifications = notifications.filter(n => n.type === 'system')
|
||||
break
|
||||
case 'task':
|
||||
notifications = notifications.filter(n => n.type === 'task')
|
||||
break
|
||||
}
|
||||
|
||||
return notifications
|
||||
})
|
||||
|
||||
// 方法
|
||||
const toggleSider = () => {
|
||||
siderCollapsed.value = !siderCollapsed.value
|
||||
appStore.setSiderCollapsed(siderCollapsed.value)
|
||||
}
|
||||
|
||||
const handleHeaderMenuClick = ({ key }) => {
|
||||
selectedMenuKeys.value = [key]
|
||||
}
|
||||
|
||||
const handleSiderMenuClick = ({ key }) => {
|
||||
selectedSiderKeys.value = [key]
|
||||
router.push(key)
|
||||
|
||||
// 添加到标签页
|
||||
const route = router.resolve(key)
|
||||
if (route.meta?.title) {
|
||||
tabsStore.addTab({
|
||||
path: key,
|
||||
name: route.name,
|
||||
title: route.meta.title,
|
||||
closable: key !== '/dashboard'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserMenuClick = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'profile':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
router.push('/settings')
|
||||
break
|
||||
case 'logout':
|
||||
handleLogout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotificationClick = (notification) => {
|
||||
if (!notification.read) {
|
||||
notificationStore.markAsRead(notification.id)
|
||||
}
|
||||
|
||||
// 如果通知有关联的路由,跳转到对应页面
|
||||
if (notification.route) {
|
||||
router.push(notification.route)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await userStore.logout()
|
||||
message.success('退出登录成功')
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
message.error('退出登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
if (diff < 60000) { // 1分钟内
|
||||
return '刚刚'
|
||||
} else if (diff < 3600000) { // 1小时内
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
} else if (diff < 86400000) { // 1天内
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
} else {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
}
|
||||
|
||||
const filterMenuByPermission = (menuItems) => {
|
||||
return menuItems.filter(item => {
|
||||
// 检查当前菜单项权限
|
||||
if (item.permission && !hasPermission(item.permission)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 递归过滤子菜单
|
||||
if (item.children) {
|
||||
item.children = filterMenuByPermission(item.children)
|
||||
// 如果子菜单全部被过滤掉,则隐藏父菜单
|
||||
return item.children.length > 0
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(route, (newRoute) => {
|
||||
selectedSiderKeys.value = [newRoute.path]
|
||||
|
||||
// 更新面包屑
|
||||
const matched = newRoute.matched.filter(item => item.meta && item.meta.title)
|
||||
if (matched.length > 0) {
|
||||
// 自动展开对应的菜单
|
||||
const parentPath = matched[matched.length - 2]?.path
|
||||
if (parentPath && !openKeys.value.includes(parentPath)) {
|
||||
openKeys.value.push(parentPath)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 从store恢复状态
|
||||
siderCollapsed.value = appStore.siderCollapsed
|
||||
|
||||
// 初始化通知
|
||||
notificationStore.fetchNotifications()
|
||||
|
||||
// 设置当前选中的菜单
|
||||
selectedSiderKeys.value = [route.path]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.government-layout {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f0f2f5;
|
||||
|
||||
.layout-header {
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.system-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.breadcrumb-container {
|
||||
:deep(.el-breadcrumb__item) {
|
||||
.el-breadcrumb__inner {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.notification-center,
|
||||
.fullscreen-toggle {
|
||||
.el-button {
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.layout-sidebar {
|
||||
width: 240px;
|
||||
background: white;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
transition: width 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border-right: 3px solid #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.system-info {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
.version {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.tabs-container {
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 0 16px;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面切换动画
|
||||
.fade-transform-enter-active,
|
||||
.fade-transform-leave-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
// 通知列表样式
|
||||
.notification-list {
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background-color: #f6ffed;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
|
||||
.notification-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.government-layout {
|
||||
.layout-header {
|
||||
padding: 0 12px;
|
||||
|
||||
.header-center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
gap: 8px;
|
||||
|
||||
.user-info .username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-container {
|
||||
.layout-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 60px;
|
||||
height: calc(100vh - 60px);
|
||||
z-index: 999;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s;
|
||||
|
||||
&:not(.collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
388
government-admin/src/layouts/BasicLayout.vue
Normal file
388
government-admin/src/layouts/BasicLayout.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<a-layout class="basic-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
theme="dark"
|
||||
width="256"
|
||||
class="layout-sider"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/favicon.svg" alt="Logo" />
|
||||
<span v-show="!collapsed" class="logo-text">政府监管平台</span>
|
||||
</div>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
>
|
||||
<template v-for="route in menuRoutes" :key="route.name">
|
||||
<a-menu-item
|
||||
v-if="!route.children || route.children.length === 0"
|
||||
:key="route.name"
|
||||
>
|
||||
<component :is="getIcon(route.meta?.icon)" />
|
||||
<span>{{ route.meta?.title }}</span>
|
||||
</a-menu-item>
|
||||
|
||||
<a-sub-menu
|
||||
v-else
|
||||
|
||||
>
|
||||
<template #title>
|
||||
<component :is="getIcon(route.meta?.icon)" />
|
||||
<span>{{ route.meta?.title }}</span>
|
||||
</template>
|
||||
|
||||
<a-menu-item
|
||||
v-for="child in route.children"
|
||||
:key="child.name"
|
||||
>
|
||||
<component :is="getIcon(child.meta?.icon)" />
|
||||
<span>{{ child.meta?.title }}</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<!-- :key="route.name" -->
|
||||
<!-- 主体内容 -->
|
||||
<a-layout class="layout-content">
|
||||
<!-- 顶部导航 -->
|
||||
<a-layout-header class="layout-header">
|
||||
<div class="header-left">
|
||||
<a-button
|
||||
type="text"
|
||||
@click="collapsed = !collapsed"
|
||||
class="trigger"
|
||||
>
|
||||
<menu-unfold-outlined v-if="collapsed" />
|
||||
<menu-fold-outlined v-else />
|
||||
</a-button>
|
||||
|
||||
<!-- 面包屑 -->
|
||||
<a-breadcrumb class="breadcrumb">
|
||||
<a-breadcrumb-item
|
||||
v-for="item in breadcrumbItems"
|
||||
:key="item.path"
|
||||
>
|
||||
<router-link v-if="item.path && item.path !== $route.path" :to="item.path">
|
||||
{{ item.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ item.title }}</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- 通知 -->
|
||||
<a-badge :count="notificationCount" class="notification-badge">
|
||||
<a-button type="text" @click="showNotifications">
|
||||
<bell-outlined />
|
||||
</a-button>
|
||||
</a-badge>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<a-dropdown>
|
||||
<a-button type="text" class="user-info">
|
||||
<a-avatar :src="authStore.avatar" :size="32">
|
||||
{{ authStore.userName.charAt(0) }}
|
||||
</a-avatar>
|
||||
<span class="user-name">{{ authStore.userName }}</span>
|
||||
<down-outlined />
|
||||
</a-button>
|
||||
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="showProfile">
|
||||
<user-outlined />
|
||||
个人资料
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="showSettings">
|
||||
<setting-outlined />
|
||||
系统设置
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item @click="handleLogout">
|
||||
<logout-outlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<a-layout-content class="main-content">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<a-layout-footer class="layout-footer">
|
||||
<div class="footer-content">
|
||||
<span>© 2025 宁夏智慧养殖监管平台 - 政府端管理后台</span>
|
||||
<span>版本 v1.0.0</span>
|
||||
</div>
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import {
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
DashboardOutlined,
|
||||
HomeOutlined,
|
||||
MonitorOutlined,
|
||||
BugOutlined,
|
||||
AlertOutlined,
|
||||
BarChartOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
BellOutlined,
|
||||
DownOutlined,
|
||||
LogoutOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const collapsed = ref(false)
|
||||
const selectedKeys = ref([])
|
||||
const openKeys = ref([])
|
||||
const notificationCount = ref(5)
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
dashboard: DashboardOutlined,
|
||||
home: HomeOutlined,
|
||||
monitor: MonitorOutlined,
|
||||
bug: BugOutlined,
|
||||
alert: AlertOutlined,
|
||||
'bar-chart': BarChartOutlined,
|
||||
user: UserOutlined,
|
||||
setting: SettingOutlined
|
||||
}
|
||||
|
||||
// 获取图标组件
|
||||
const getIcon = (iconName) => {
|
||||
return iconMap[iconName] || DashboardOutlined
|
||||
}
|
||||
|
||||
// 菜单路由
|
||||
const menuRoutes = computed(() => {
|
||||
return router.getRoutes()
|
||||
.find(route => route.name === 'Layout')
|
||||
?.children?.filter(child =>
|
||||
!child.meta?.hidden &&
|
||||
(!child.meta?.roles || authStore.hasRole(child.meta.roles))
|
||||
) || []
|
||||
})
|
||||
|
||||
// 面包屑
|
||||
const breadcrumbItems = computed(() => {
|
||||
const matched = route.matched.filter(item => item.meta?.title)
|
||||
return matched.map(item => ({
|
||||
title: item.meta.title,
|
||||
path: item.path === route.path ? null : item.path
|
||||
}))
|
||||
})
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.name,
|
||||
(newName) => {
|
||||
if (newName) {
|
||||
selectedKeys.value = [newName]
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 菜单点击处理
|
||||
const handleMenuClick = ({ key }) => {
|
||||
router.push({ name: key })
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
const showNotifications = () => {
|
||||
// TODO: 实现通知功能
|
||||
console.log('显示通知')
|
||||
}
|
||||
|
||||
// 显示个人资料
|
||||
const showProfile = () => {
|
||||
// TODO: 实现个人资料功能
|
||||
console.log('显示个人资料')
|
||||
}
|
||||
|
||||
// 显示系统设置
|
||||
const showSettings = () => {
|
||||
router.push('/settings')
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
Modal.confirm({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
onOk: async () => {
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.basic-layout {
|
||||
height: 100vh;
|
||||
|
||||
.layout-sider {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
|
||||
.logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
margin-left: 256px;
|
||||
transition: margin-left 0.2s;
|
||||
|
||||
&.collapsed {
|
||||
margin-left: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.notification-badge {
|
||||
.ant-btn {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background: #f0f2f5;
|
||||
min-height: calc(100vh - 64px - 48px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.layout-footer {
|
||||
background: white;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 24px;
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 768px) {
|
||||
.basic-layout {
|
||||
.layout-sider {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.ant-layout-sider-collapsed {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
padding: 0 16px;
|
||||
|
||||
.header-left .breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
government-admin/src/main.js
Normal file
20
government-admin/src/main.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './styles/index.css'
|
||||
import { permissionDirective } from './stores/permission'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(Antd)
|
||||
app.use(router)
|
||||
|
||||
// 注册权限指令
|
||||
app.directive('permission', permissionDirective)
|
||||
|
||||
app.mount('#app')
|
||||
287
government-admin/src/router/guards.js
Normal file
287
government-admin/src/router/guards.js
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 路由守卫配置
|
||||
*/
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { checkRoutePermission } from '@/utils/permission'
|
||||
import { message } from 'ant-design-vue'
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
// 配置 NProgress
|
||||
NProgress.configure({
|
||||
showSpinner: false,
|
||||
minimum: 0.2,
|
||||
speed: 500
|
||||
})
|
||||
|
||||
// 白名单路由 - 不需要登录验证的路由
|
||||
const whiteList = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/forgot-password',
|
||||
'/404',
|
||||
'/403',
|
||||
'/500'
|
||||
]
|
||||
|
||||
// 公共路由 - 登录后都可以访问的路由
|
||||
const publicRoutes = [
|
||||
'/dashboard',
|
||||
'/profile',
|
||||
'/settings'
|
||||
]
|
||||
|
||||
/**
|
||||
* 前置守卫 - 路由跳转前的权限验证
|
||||
*/
|
||||
export async function beforeEach(to, from, next) {
|
||||
// 开始进度条
|
||||
NProgress.start()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 获取用户token
|
||||
const token = userStore.token || localStorage.getItem('token')
|
||||
|
||||
// 检查是否在白名单中
|
||||
if (whiteList.includes(to.path)) {
|
||||
// 如果已登录且访问登录页,重定向到首页
|
||||
if (token && to.path === '/login') {
|
||||
next({ path: '/dashboard' })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
if (!token) {
|
||||
message.warning('请先登录')
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户信息是否存在
|
||||
if (!userStore.userInfo || !userStore.userInfo.id) {
|
||||
try {
|
||||
// 获取用户信息
|
||||
await userStore.getUserInfo()
|
||||
// 初始化权限
|
||||
await permissionStore.initPermissions(userStore.userInfo)
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
message.error('获取用户信息失败,请重新登录')
|
||||
userStore.logout()
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为公共路由
|
||||
if (publicRoutes.includes(to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查路由权限
|
||||
if (!checkRoutePermission(to, userStore.userInfo)) {
|
||||
message.error('您没有访问该页面的权限')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查动态路由是否已生成
|
||||
if (!permissionStore.routesGenerated) {
|
||||
try {
|
||||
// 生成动态路由
|
||||
const accessRoutes = await permissionStore.generateRoutes(userStore.userInfo)
|
||||
|
||||
// 动态添加路由
|
||||
accessRoutes.forEach(route => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
|
||||
// 重新导航到目标路由
|
||||
next({ ...to, replace: true })
|
||||
} catch (error) {
|
||||
console.error('生成路由失败:', error)
|
||||
message.error('系统初始化失败')
|
||||
next({ path: '/500' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* 后置守卫 - 路由跳转后的处理
|
||||
*/
|
||||
export function afterEach(to, from) {
|
||||
// 结束进度条
|
||||
NProgress.done()
|
||||
|
||||
// 设置页面标题
|
||||
const title = to.meta?.title
|
||||
if (title) {
|
||||
document.title = `${title} - 政府管理后台`
|
||||
} else {
|
||||
document.title = '政府管理后台'
|
||||
}
|
||||
|
||||
// 记录路由访问日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`路由跳转: ${from.path} -> ${to.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由错误处理
|
||||
*/
|
||||
export function onError(error) {
|
||||
console.error('路由错误:', error)
|
||||
NProgress.done()
|
||||
|
||||
// 根据错误类型进行处理
|
||||
if (error.name === 'ChunkLoadError') {
|
||||
message.error('页面加载失败,请刷新重试')
|
||||
} else if (error.name === 'NavigationDuplicated') {
|
||||
// 重复导航错误,忽略
|
||||
return
|
||||
} else {
|
||||
message.error('页面访问异常')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限验证中间件
|
||||
*/
|
||||
export function requireAuth(permission) {
|
||||
return (to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (!userStore.token) {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
|
||||
if (permission && !permissionStore.hasPermission(permission)) {
|
||||
message.error('权限不足')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色验证中间件
|
||||
*/
|
||||
export function requireRole(role) {
|
||||
return (to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (!userStore.token) {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
|
||||
if (role && !permissionStore.hasRole(role)) {
|
||||
message.error('角色权限不足')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员权限验证
|
||||
*/
|
||||
export function requireAdmin(to, from, next) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (!userStore.token) {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
|
||||
const userRole = userStore.userInfo?.role
|
||||
if (!['super_admin', 'admin'].includes(userRole)) {
|
||||
message.error('需要管理员权限')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* 超级管理员权限验证
|
||||
*/
|
||||
export function requireSuperAdmin(to, from, next) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (!userStore.token) {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
|
||||
if (userStore.userInfo?.role !== 'super_admin') {
|
||||
message.error('需要超级管理员权限')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查页面访问权限
|
||||
*/
|
||||
export function checkPageAccess(requiredPermissions = []) {
|
||||
return (to, from, next) => {
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 检查是否有任一权限
|
||||
const hasAccess = requiredPermissions.length === 0 ||
|
||||
requiredPermissions.some(permission =>
|
||||
permissionStore.hasPermission(permission)
|
||||
)
|
||||
|
||||
if (!hasAccess) {
|
||||
message.error('您没有访问该页面的权限')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态路由加载守卫
|
||||
*/
|
||||
export function loadDynamicRoutes(to, from, next) {
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (!permissionStore.routesGenerated) {
|
||||
// 如果路由未生成,等待生成完成
|
||||
permissionStore.generateRoutes().then(() => {
|
||||
next({ ...to, replace: true })
|
||||
}).catch(() => {
|
||||
next({ path: '/500' })
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
498
government-admin/src/router/index.js
Normal file
498
government-admin/src/router/index.js
Normal file
@@ -0,0 +1,498 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { beforeEach, afterEach, onError } from './guards'
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
// 配置 NProgress
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
// 导入布局组件
|
||||
const Layout = () => import('@/layout/GovernmentLayout.vue')
|
||||
|
||||
// 基础路由配置
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/Login.vue'),
|
||||
meta: {
|
||||
title: '登录',
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/error/404.vue'),
|
||||
meta: {
|
||||
title: '页面不存在',
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: () => import('@/views/error/403.vue'),
|
||||
meta: {
|
||||
title: '权限不足',
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: 'ServerError',
|
||||
component: () => import('@/views/error/500.vue'),
|
||||
meta: {
|
||||
title: '服务器错误',
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/Dashboard.vue'),
|
||||
meta: {
|
||||
title: '仪表盘',
|
||||
icon: 'dashboard',
|
||||
affix: true,
|
||||
permission: 'dashboard:view'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/breeding',
|
||||
name: 'Breeding',
|
||||
meta: {
|
||||
title: '养殖管理',
|
||||
icon: 'home',
|
||||
permission: 'breeding:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'farms',
|
||||
name: 'BreedingFarmList',
|
||||
component: () => import('@/views/breeding/BreedingFarmList.vue'),
|
||||
meta: {
|
||||
title: '养殖场管理',
|
||||
permission: 'breeding:farm'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/monitoring',
|
||||
name: 'Monitoring',
|
||||
meta: {
|
||||
title: '健康监控',
|
||||
icon: 'monitor',
|
||||
permission: 'monitoring:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'health',
|
||||
name: 'AnimalHealthMonitor',
|
||||
component: () => import('@/views/monitoring/AnimalHealthMonitor.vue'),
|
||||
meta: {
|
||||
title: '动物健康监控',
|
||||
permission: 'monitoring:health'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/inspection',
|
||||
name: 'Inspection',
|
||||
meta: {
|
||||
title: '检查管理',
|
||||
icon: 'audit',
|
||||
permission: 'inspection:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'management',
|
||||
name: 'InspectionManagement',
|
||||
component: () => import('@/views/inspection/InspectionManagement.vue'),
|
||||
meta: {
|
||||
title: '检查管理',
|
||||
permission: 'inspection:manage'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/traceability',
|
||||
name: 'Traceability',
|
||||
meta: {
|
||||
title: '溯源系统',
|
||||
icon: 'link',
|
||||
permission: 'traceability:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'system',
|
||||
name: 'TraceabilitySystem',
|
||||
component: () => import('@/views/traceability/TraceabilitySystem.vue'),
|
||||
meta: {
|
||||
title: '产品溯源',
|
||||
permission: 'traceability:system'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/emergency',
|
||||
name: 'Emergency',
|
||||
meta: {
|
||||
title: '应急响应',
|
||||
icon: 'alert',
|
||||
permission: 'emergency:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'response',
|
||||
name: 'EmergencyResponse',
|
||||
component: () => import('@/views/emergency/EmergencyResponse.vue'),
|
||||
meta: {
|
||||
title: '应急响应',
|
||||
permission: 'emergency:response'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/policy',
|
||||
name: 'Policy',
|
||||
meta: {
|
||||
title: '政策管理',
|
||||
icon: 'file-text',
|
||||
permission: 'policy:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'management',
|
||||
name: 'PolicyManagement',
|
||||
component: () => import('@/views/policy/PolicyManagement.vue'),
|
||||
meta: {
|
||||
title: '政策管理',
|
||||
permission: 'policy:manage'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/statistics',
|
||||
name: 'Statistics',
|
||||
meta: {
|
||||
title: '数据统计',
|
||||
icon: 'bar-chart',
|
||||
permission: 'statistics:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'data',
|
||||
name: 'DataStatistics',
|
||||
component: () => import('@/views/statistics/DataStatistics.vue'),
|
||||
meta: {
|
||||
title: '数据统计',
|
||||
permission: 'statistics:data'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
name: 'Reports',
|
||||
meta: {
|
||||
title: '报表中心',
|
||||
icon: 'file-text',
|
||||
permission: 'reports:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'center',
|
||||
name: 'ReportCenter',
|
||||
component: () => import('@/views/reports/ReportCenter.vue'),
|
||||
meta: {
|
||||
title: '报表中心',
|
||||
permission: 'reports:center'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
icon: 'setting',
|
||||
permission: 'settings:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'system',
|
||||
name: 'SystemSettings',
|
||||
component: () => import('@/views/settings/SystemSettings.vue'),
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
permission: 'settings:system'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('@/views/monitor/MonitorDashboard.vue'),
|
||||
meta: {
|
||||
title: '实时监控',
|
||||
icon: 'eye',
|
||||
requiresAuth: true,
|
||||
permission: 'monitor:view'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
name: 'Reports',
|
||||
component: () => import('@/views/reports/ReportList.vue'),
|
||||
meta: {
|
||||
title: '报表管理',
|
||||
icon: 'file-text',
|
||||
requiresAuth: true,
|
||||
permission: 'monitor:report'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/data',
|
||||
name: 'Data',
|
||||
component: () => import('@/views/data/DataAnalysis.vue'),
|
||||
meta: {
|
||||
title: '数据分析',
|
||||
icon: 'bar-chart',
|
||||
requiresAuth: true,
|
||||
permission: 'data:view'
|
||||
}
|
||||
},
|
||||
// 业务管理
|
||||
{
|
||||
path: '/business',
|
||||
name: 'Business',
|
||||
meta: {
|
||||
title: '业务管理',
|
||||
icon: 'solution',
|
||||
permission: 'business:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'insurance',
|
||||
name: 'InsuranceManagement',
|
||||
component: () => import('@/views/business/InsuranceManagement.vue'),
|
||||
meta: {
|
||||
title: '保险管理',
|
||||
permission: 'business:insurance'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'trading',
|
||||
name: 'TradingManagement',
|
||||
component: () => import('@/views/business/TradingManagement.vue'),
|
||||
meta: {
|
||||
title: '生资交易',
|
||||
permission: 'business:trading'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'waste-collection',
|
||||
name: 'WasteCollection',
|
||||
component: () => import('@/views/business/WasteCollection.vue'),
|
||||
meta: {
|
||||
title: '粪污报收',
|
||||
permission: 'business:waste'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'subsidies',
|
||||
name: 'SubsidyManagement',
|
||||
component: () => import('@/views/business/SubsidyManagement.vue'),
|
||||
meta: {
|
||||
title: '奖补管理',
|
||||
permission: 'business:subsidy'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forage-enterprises',
|
||||
name: 'ForageEnterprises',
|
||||
component: () => import('@/views/business/ForageEnterprises.vue'),
|
||||
meta: {
|
||||
title: '饲草料企业管理',
|
||||
permission: 'business:forage'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'market-info',
|
||||
name: 'MarketInfo',
|
||||
component: () => import('@/views/business/MarketInfo.vue'),
|
||||
meta: {
|
||||
title: '市场行情',
|
||||
permission: 'business:market'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 防疫管理
|
||||
{
|
||||
path: '/epidemic-prevention',
|
||||
name: 'EpidemicPrevention',
|
||||
meta: {
|
||||
title: '防疫管理',
|
||||
icon: 'medicine-box',
|
||||
permission: 'epidemic:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'institutions',
|
||||
name: 'EpidemicInstitutions',
|
||||
component: () => import('@/views/epidemic/EpidemicInstitutions.vue'),
|
||||
meta: {
|
||||
title: '防疫机构管理',
|
||||
permission: 'epidemic:institution'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'records',
|
||||
name: 'EpidemicRecords',
|
||||
component: () => import('@/views/epidemic/EpidemicRecords.vue'),
|
||||
meta: {
|
||||
title: '防疫记录',
|
||||
permission: 'epidemic:record'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vaccines',
|
||||
name: 'VaccineManagement',
|
||||
component: () => import('@/views/epidemic/VaccineManagement.vue'),
|
||||
meta: {
|
||||
title: '疫苗管理',
|
||||
permission: 'epidemic:vaccine'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'activities',
|
||||
name: 'EpidemicActivities',
|
||||
component: () => import('@/views/epidemic/EpidemicActivities.vue'),
|
||||
meta: {
|
||||
title: '防疫活动管理',
|
||||
permission: 'epidemic:activity'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 服务管理
|
||||
{
|
||||
path: '/services',
|
||||
name: 'Services',
|
||||
meta: {
|
||||
title: '服务管理',
|
||||
icon: 'customer-service',
|
||||
permission: 'service:view'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'education',
|
||||
name: 'CattleEducation',
|
||||
component: () => import('@/views/services/CattleEducation.vue'),
|
||||
meta: {
|
||||
title: '养牛学院',
|
||||
permission: 'service:education'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'consultation',
|
||||
name: 'OnlineConsultation',
|
||||
component: () => import('@/views/services/OnlineConsultation.vue'),
|
||||
meta: {
|
||||
title: '线上问诊',
|
||||
permission: 'service:consultation'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'community',
|
||||
name: 'CommunityManagement',
|
||||
component: () => import('@/views/services/CommunityManagement.vue'),
|
||||
meta: {
|
||||
title: '交流社区',
|
||||
permission: 'service:community'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: () => import('@/views/users/UserList.vue'),
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'user',
|
||||
requiresAuth: true,
|
||||
permission: 'user:view'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/settings/SystemSettings.vue'),
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
icon: 'setting',
|
||||
requiresAuth: true,
|
||||
permission: 'system:config'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: () => import('@/views/error/403.vue'),
|
||||
meta: {
|
||||
title: '权限不足',
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/error/404.vue'),
|
||||
meta: {
|
||||
title: '页面不存在',
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404'
|
||||
}
|
||||
]
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
return { top: 0 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 注册路由守卫
|
||||
router.beforeEach(beforeEach)
|
||||
router.afterEach(afterEach)
|
||||
router.onError(onError)
|
||||
|
||||
export default router
|
||||
116
government-admin/src/stores/auth.js
Normal file
116
government-admin/src/stores/auth.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'))
|
||||
const permissions = ref([])
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => !!token.value && !!userInfo.value)
|
||||
const userName = computed(() => userInfo.value?.name || '')
|
||||
const userRole = computed(() => userInfo.value?.role || '')
|
||||
const avatar = computed(() => userInfo.value?.avatar || '')
|
||||
|
||||
// 方法
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
const response = await api.post('/auth/login', credentials)
|
||||
const { token: newToken, user, permissions: userPermissions } = response.data
|
||||
|
||||
// 保存认证信息
|
||||
token.value = newToken
|
||||
userInfo.value = user
|
||||
permissions.value = userPermissions || []
|
||||
|
||||
// 持久化存储
|
||||
localStorage.setItem('token', newToken)
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
localStorage.setItem('permissions', JSON.stringify(userPermissions || []))
|
||||
|
||||
message.success('登录成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
message.error(error.response?.data?.message || '登录失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await api.post('/auth/logout')
|
||||
} catch (error) {
|
||||
console.error('退出登录请求失败:', error)
|
||||
} finally {
|
||||
// 清除认证信息
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
permissions.value = []
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('permissions')
|
||||
|
||||
message.success('已退出登录')
|
||||
}
|
||||
}
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
if (!token.value) return false
|
||||
|
||||
try {
|
||||
const response = await api.get('/auth/me')
|
||||
userInfo.value = response.data.user
|
||||
permissions.value = response.data.permissions || []
|
||||
|
||||
// 更新本地存储
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data.user))
|
||||
localStorage.setItem('permissions', JSON.stringify(response.data.permissions || []))
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
// 认证失败,清除本地数据
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const hasPermission = (permission) => {
|
||||
return permissions.value.includes(permission)
|
||||
}
|
||||
|
||||
const hasRole = (roles) => {
|
||||
if (!Array.isArray(roles)) roles = [roles]
|
||||
return roles.includes(userRole.value)
|
||||
}
|
||||
|
||||
const updateUserInfo = (newUserInfo) => {
|
||||
userInfo.value = { ...userInfo.value, ...newUserInfo }
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
userInfo,
|
||||
permissions,
|
||||
|
||||
// 计算属性
|
||||
isAuthenticated,
|
||||
userName,
|
||||
userRole,
|
||||
avatar,
|
||||
|
||||
// 方法
|
||||
login,
|
||||
logout,
|
||||
checkAuthStatus,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
updateUserInfo
|
||||
}
|
||||
})
|
||||
292
government-admin/src/stores/farm.js
Normal file
292
government-admin/src/stores/farm.js
Normal file
@@ -0,0 +1,292 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
getFarmList,
|
||||
getFarmDetail,
|
||||
createFarm,
|
||||
updateFarm,
|
||||
deleteFarm,
|
||||
batchDeleteFarms,
|
||||
updateFarmStatus,
|
||||
getFarmStats,
|
||||
getFarmMapData,
|
||||
getFarmTypes,
|
||||
getFarmScales
|
||||
} from '@/api/farm'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
export const useFarmStore = defineStore('farm', () => {
|
||||
// 状态
|
||||
const farms = ref([])
|
||||
const currentFarm = ref(null)
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
pending: 0
|
||||
})
|
||||
const farmTypes = ref([])
|
||||
const farmScales = ref([])
|
||||
const mapData = ref([])
|
||||
|
||||
// 计算属性
|
||||
const activeFarms = computed(() =>
|
||||
farms.value.filter(farm => farm.status === 'active')
|
||||
)
|
||||
|
||||
const inactiveFarms = computed(() =>
|
||||
farms.value.filter(farm => farm.status === 'inactive')
|
||||
)
|
||||
|
||||
const pendingFarms = computed(() =>
|
||||
farms.value.filter(farm => farm.status === 'pending')
|
||||
)
|
||||
|
||||
// 获取养殖场列表
|
||||
const fetchFarms = async (params = {}) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getFarmList(params)
|
||||
farms.value = response.data.list || []
|
||||
total.value = response.data.total || 0
|
||||
return response
|
||||
} catch (error) {
|
||||
message.error('获取养殖场列表失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取养殖场详情
|
||||
const fetchFarmDetail = async (id) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getFarmDetail(id)
|
||||
currentFarm.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
message.error('获取养殖场详情失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建养殖场
|
||||
const addFarm = async (farmData) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await createFarm(farmData)
|
||||
message.success('创建养殖场成功')
|
||||
// 重新获取列表
|
||||
await fetchFarms()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
message.error('创建养殖场失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新养殖场
|
||||
const editFarm = async (id, farmData) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await updateFarm(id, farmData)
|
||||
message.success('更新养殖场成功')
|
||||
|
||||
// 更新本地数据
|
||||
const index = farms.value.findIndex(farm => farm.id === id)
|
||||
if (index !== -1) {
|
||||
farms.value[index] = { ...farms.value[index], ...response.data }
|
||||
}
|
||||
|
||||
// 如果是当前查看的养殖场,也更新
|
||||
if (currentFarm.value && currentFarm.value.id === id) {
|
||||
currentFarm.value = { ...currentFarm.value, ...response.data }
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
message.error('更新养殖场失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除养殖场
|
||||
const removeFarm = async (id) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await deleteFarm(id)
|
||||
message.success('删除养殖场成功')
|
||||
|
||||
// 从本地数据中移除
|
||||
const index = farms.value.findIndex(farm => farm.id === id)
|
||||
if (index !== -1) {
|
||||
farms.value.splice(index, 1)
|
||||
total.value -= 1
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
message.error('删除养殖场失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除养殖场
|
||||
const batchRemoveFarms = async (ids) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await batchDeleteFarms(ids)
|
||||
message.success(`成功删除 ${ids.length} 个养殖场`)
|
||||
|
||||
// 从本地数据中移除
|
||||
farms.value = farms.value.filter(farm => !ids.includes(farm.id))
|
||||
total.value -= ids.length
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
message.error('批量删除养殖场失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新养殖场状态
|
||||
const changeFarmStatus = async (id, status) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await updateFarmStatus(id, status)
|
||||
message.success('更新状态成功')
|
||||
|
||||
// 更新本地数据
|
||||
const index = farms.value.findIndex(farm => farm.id === id)
|
||||
if (index !== -1) {
|
||||
farms.value[index].status = status
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
message.error('更新状态失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await getFarmStats()
|
||||
stats.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
message.error('获取统计数据失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取地图数据
|
||||
const fetchMapData = async (params = {}) => {
|
||||
try {
|
||||
const response = await getFarmMapData(params)
|
||||
mapData.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
message.error('获取地图数据失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取养殖场类型选项
|
||||
const fetchFarmTypes = async () => {
|
||||
try {
|
||||
const response = await getFarmTypes()
|
||||
farmTypes.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取养殖场类型失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取养殖场规模选项
|
||||
const fetchFarmScales = async () => {
|
||||
try {
|
||||
const response = await getFarmScales()
|
||||
farmScales.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取养殖场规模失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
const resetState = () => {
|
||||
farms.value = []
|
||||
currentFarm.value = null
|
||||
loading.value = false
|
||||
total.value = 0
|
||||
stats.value = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
pending: 0
|
||||
}
|
||||
mapData.value = []
|
||||
}
|
||||
|
||||
// 设置当前养殖场
|
||||
const setCurrentFarm = (farm) => {
|
||||
currentFarm.value = farm
|
||||
}
|
||||
|
||||
// 清除当前养殖场
|
||||
const clearCurrentFarm = () => {
|
||||
currentFarm.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
farms,
|
||||
currentFarm,
|
||||
loading,
|
||||
total,
|
||||
stats,
|
||||
farmTypes,
|
||||
farmScales,
|
||||
mapData,
|
||||
|
||||
// 计算属性
|
||||
activeFarms,
|
||||
inactiveFarms,
|
||||
pendingFarms,
|
||||
|
||||
// 方法
|
||||
fetchFarms,
|
||||
fetchFarmDetail,
|
||||
addFarm,
|
||||
editFarm,
|
||||
removeFarm,
|
||||
batchRemoveFarms,
|
||||
changeFarmStatus,
|
||||
fetchStats,
|
||||
fetchMapData,
|
||||
fetchFarmTypes,
|
||||
fetchFarmScales,
|
||||
resetState,
|
||||
setCurrentFarm,
|
||||
clearCurrentFarm
|
||||
}
|
||||
})
|
||||
522
government-admin/src/stores/government.js
Normal file
522
government-admin/src/stores/government.js
Normal file
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* 政府业务状态管理
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { governmentApi } from '@/api/government'
|
||||
|
||||
export const useGovernmentStore = defineStore('government', {
|
||||
state: () => ({
|
||||
// 政府监管数据
|
||||
supervision: {
|
||||
// 监管统计
|
||||
stats: {
|
||||
totalEntities: 0,
|
||||
activeInspections: 0,
|
||||
pendingApprovals: 0,
|
||||
completedTasks: 0
|
||||
},
|
||||
// 监管实体列表
|
||||
entities: [],
|
||||
// 检查记录
|
||||
inspections: [],
|
||||
// 违规记录
|
||||
violations: []
|
||||
},
|
||||
|
||||
// 审批管理数据
|
||||
approval: {
|
||||
// 审批统计
|
||||
stats: {
|
||||
pending: 0,
|
||||
approved: 0,
|
||||
rejected: 0,
|
||||
total: 0
|
||||
},
|
||||
// 审批流程
|
||||
workflows: [],
|
||||
// 审批记录
|
||||
records: [],
|
||||
// 待办任务
|
||||
tasks: []
|
||||
},
|
||||
|
||||
// 人员管理数据
|
||||
personnel: {
|
||||
// 人员统计
|
||||
stats: {
|
||||
totalStaff: 0,
|
||||
activeStaff: 0,
|
||||
departments: 0,
|
||||
positions: 0
|
||||
},
|
||||
// 员工列表
|
||||
staff: [],
|
||||
// 部门列表
|
||||
departments: [],
|
||||
// 职位列表
|
||||
positions: [],
|
||||
// 考勤记录
|
||||
attendance: []
|
||||
},
|
||||
|
||||
// 设备仓库数据
|
||||
warehouse: {
|
||||
// 库存统计
|
||||
stats: {
|
||||
totalEquipment: 0,
|
||||
availableEquipment: 0,
|
||||
inUseEquipment: 0,
|
||||
maintenanceEquipment: 0
|
||||
},
|
||||
// 设备列表
|
||||
equipment: [],
|
||||
// 入库记录
|
||||
inboundRecords: [],
|
||||
// 出库记录
|
||||
outboundRecords: [],
|
||||
// 维护记录
|
||||
maintenanceRecords: []
|
||||
},
|
||||
|
||||
// 防疫管理数据
|
||||
epidemic: {
|
||||
// 防疫统计
|
||||
stats: {
|
||||
totalCases: 0,
|
||||
activeCases: 0,
|
||||
recoveredCases: 0,
|
||||
vaccinationRate: 0
|
||||
},
|
||||
// 疫情数据
|
||||
cases: [],
|
||||
// 疫苗接种记录
|
||||
vaccinations: [],
|
||||
// 防疫措施
|
||||
measures: [],
|
||||
// 健康码数据
|
||||
healthCodes: []
|
||||
},
|
||||
|
||||
// 服务管理数据
|
||||
service: {
|
||||
// 服务统计
|
||||
stats: {
|
||||
totalServices: 0,
|
||||
activeServices: 0,
|
||||
completedServices: 0,
|
||||
satisfactionRate: 0
|
||||
},
|
||||
// 服务项目
|
||||
services: [],
|
||||
// 服务申请
|
||||
applications: [],
|
||||
// 服务评价
|
||||
evaluations: [],
|
||||
// 服务指南
|
||||
guides: []
|
||||
},
|
||||
|
||||
// 数据可视化配置
|
||||
visualization: {
|
||||
// 图表配置
|
||||
charts: {},
|
||||
// 数据源配置
|
||||
dataSources: {},
|
||||
// 刷新间隔
|
||||
refreshInterval: 30000,
|
||||
// 实时数据开关
|
||||
realTimeEnabled: true
|
||||
},
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
supervision: false,
|
||||
approval: false,
|
||||
personnel: false,
|
||||
warehouse: false,
|
||||
epidemic: false,
|
||||
service: false
|
||||
},
|
||||
|
||||
// 错误信息
|
||||
errors: {}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 总体统计数据
|
||||
overallStats: (state) => ({
|
||||
supervision: state.supervision.stats,
|
||||
approval: state.approval.stats,
|
||||
personnel: state.personnel.stats,
|
||||
warehouse: state.warehouse.stats,
|
||||
epidemic: state.epidemic.stats,
|
||||
service: state.service.stats
|
||||
}),
|
||||
|
||||
// 待处理任务总数
|
||||
totalPendingTasks: (state) => {
|
||||
return state.approval.stats.pending +
|
||||
state.supervision.stats.pendingApprovals +
|
||||
state.service.stats.activeServices
|
||||
},
|
||||
|
||||
// 系统健康状态
|
||||
systemHealth: (state) => {
|
||||
const totalTasks = state.approval.stats.total
|
||||
const completedTasks = state.approval.stats.approved + state.approval.stats.rejected
|
||||
const completionRate = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 100
|
||||
|
||||
if (completionRate >= 90) return 'excellent'
|
||||
if (completionRate >= 75) return 'good'
|
||||
if (completionRate >= 60) return 'fair'
|
||||
return 'poor'
|
||||
},
|
||||
|
||||
// 最近活动
|
||||
recentActivities: (state) => {
|
||||
const activities = []
|
||||
|
||||
// 添加审批活动
|
||||
state.approval.records.slice(0, 5).forEach(record => {
|
||||
activities.push({
|
||||
type: 'approval',
|
||||
title: `审批:${record.title}`,
|
||||
time: record.updatedAt,
|
||||
status: record.status
|
||||
})
|
||||
})
|
||||
|
||||
// 添加监管活动
|
||||
state.supervision.inspections.slice(0, 5).forEach(inspection => {
|
||||
activities.push({
|
||||
type: 'inspection',
|
||||
title: `检查:${inspection.title}`,
|
||||
time: inspection.createdAt,
|
||||
status: inspection.status
|
||||
})
|
||||
})
|
||||
|
||||
return activities
|
||||
.sort((a, b) => new Date(b.time) - new Date(a.time))
|
||||
.slice(0, 10)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 初始化政府数据
|
||||
*/
|
||||
async initializeGovernmentData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadSupervisionData(),
|
||||
this.loadApprovalData(),
|
||||
this.loadPersonnelData(),
|
||||
this.loadWarehouseData(),
|
||||
this.loadEpidemicData(),
|
||||
this.loadServiceData()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('初始化政府数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载监管数据
|
||||
*/
|
||||
async loadSupervisionData() {
|
||||
this.loading.supervision = true
|
||||
try {
|
||||
const response = await governmentApi.getSupervisionData()
|
||||
this.supervision = { ...this.supervision, ...response.data }
|
||||
delete this.errors.supervision
|
||||
} catch (error) {
|
||||
this.errors.supervision = error.message
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.supervision = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载审批数据
|
||||
*/
|
||||
async loadApprovalData() {
|
||||
this.loading.approval = true
|
||||
try {
|
||||
const response = await governmentApi.getApprovalData()
|
||||
this.approval = { ...this.approval, ...response.data }
|
||||
delete this.errors.approval
|
||||
} catch (error) {
|
||||
this.errors.approval = error.message
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.approval = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载人员数据
|
||||
*/
|
||||
async loadPersonnelData() {
|
||||
this.loading.personnel = true
|
||||
try {
|
||||
const response = await governmentApi.getPersonnelData()
|
||||
this.personnel = { ...this.personnel, ...response.data }
|
||||
delete this.errors.personnel
|
||||
} catch (error) {
|
||||
this.errors.personnel = error.message
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.personnel = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载仓库数据
|
||||
*/
|
||||
async loadWarehouseData() {
|
||||
this.loading.warehouse = true
|
||||
try {
|
||||
const response = await governmentApi.getWarehouseData()
|
||||
this.warehouse = { ...this.warehouse, ...response.data }
|
||||
delete this.errors.warehouse
|
||||
} catch (error) {
|
||||
this.errors.warehouse = error.message
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.warehouse = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载防疫数据
|
||||
*/
|
||||
async loadEpidemicData() {
|
||||
this.loading.epidemic = true
|
||||
try {
|
||||
const response = await governmentApi.getEpidemicData()
|
||||
this.epidemic = { ...this.epidemic, ...response.data }
|
||||
delete this.errors.epidemic
|
||||
} catch (error) {
|
||||
this.errors.epidemic = error.message
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.epidemic = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载服务数据
|
||||
*/
|
||||
async loadServiceData() {
|
||||
this.loading.service = true
|
||||
try {
|
||||
const response = await governmentApi.getServiceData()
|
||||
this.service = { ...this.service, ...response.data }
|
||||
delete this.errors.service
|
||||
} catch (error) {
|
||||
this.errors.service = error.message
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.service = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交审批
|
||||
* @param {Object} approvalData - 审批数据
|
||||
*/
|
||||
async submitApproval(approvalData) {
|
||||
try {
|
||||
const response = await governmentApi.submitApproval(approvalData)
|
||||
|
||||
// 更新本地数据
|
||||
this.approval.records.unshift(response.data)
|
||||
this.approval.stats.pending += 1
|
||||
this.approval.stats.total += 1
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理审批
|
||||
* @param {string} id - 审批ID
|
||||
* @param {Object} decision - 审批决定
|
||||
*/
|
||||
async processApproval(id, decision) {
|
||||
try {
|
||||
const response = await governmentApi.processApproval(id, decision)
|
||||
|
||||
// 更新本地数据
|
||||
const index = this.approval.records.findIndex(r => r.id === id)
|
||||
if (index > -1) {
|
||||
this.approval.records[index] = response.data
|
||||
|
||||
// 更新统计
|
||||
this.approval.stats.pending -= 1
|
||||
if (decision.status === 'approved') {
|
||||
this.approval.stats.approved += 1
|
||||
} else if (decision.status === 'rejected') {
|
||||
this.approval.stats.rejected += 1
|
||||
}
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加设备
|
||||
* @param {Object} equipment - 设备数据
|
||||
*/
|
||||
async addEquipment(equipment) {
|
||||
try {
|
||||
const response = await governmentApi.addEquipment(equipment)
|
||||
|
||||
// 更新本地数据
|
||||
this.warehouse.equipment.unshift(response.data)
|
||||
this.warehouse.stats.totalEquipment += 1
|
||||
this.warehouse.stats.availableEquipment += 1
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设备入库
|
||||
* @param {Object} inboundData - 入库数据
|
||||
*/
|
||||
async equipmentInbound(inboundData) {
|
||||
try {
|
||||
const response = await governmentApi.equipmentInbound(inboundData)
|
||||
|
||||
// 更新本地数据
|
||||
this.warehouse.inboundRecords.unshift(response.data)
|
||||
|
||||
// 更新设备状态
|
||||
const equipment = this.warehouse.equipment.find(e => e.id === inboundData.equipmentId)
|
||||
if (equipment) {
|
||||
equipment.quantity += inboundData.quantity
|
||||
equipment.status = 'available'
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设备出库
|
||||
* @param {Object} outboundData - 出库数据
|
||||
*/
|
||||
async equipmentOutbound(outboundData) {
|
||||
try {
|
||||
const response = await governmentApi.equipmentOutbound(outboundData)
|
||||
|
||||
// 更新本地数据
|
||||
this.warehouse.outboundRecords.unshift(response.data)
|
||||
|
||||
// 更新设备状态
|
||||
const equipment = this.warehouse.equipment.find(e => e.id === outboundData.equipmentId)
|
||||
if (equipment) {
|
||||
equipment.quantity -= outboundData.quantity
|
||||
if (equipment.quantity <= 0) {
|
||||
equipment.status = 'out_of_stock'
|
||||
}
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加员工
|
||||
* @param {Object} staff - 员工数据
|
||||
*/
|
||||
async addStaff(staff) {
|
||||
try {
|
||||
const response = await governmentApi.addStaff(staff)
|
||||
|
||||
// 更新本地数据
|
||||
this.personnel.staff.unshift(response.data)
|
||||
this.personnel.stats.totalStaff += 1
|
||||
this.personnel.stats.activeStaff += 1
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新员工信息
|
||||
* @param {string} id - 员工ID
|
||||
* @param {Object} updates - 更新数据
|
||||
*/
|
||||
async updateStaff(id, updates) {
|
||||
try {
|
||||
const response = await governmentApi.updateStaff(id, updates)
|
||||
|
||||
// 更新本地数据
|
||||
const index = this.personnel.staff.findIndex(s => s.id === id)
|
||||
if (index > -1) {
|
||||
this.personnel.staff[index] = response.data
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新所有数据
|
||||
*/
|
||||
async refreshAllData() {
|
||||
await this.initializeGovernmentData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除错误信息
|
||||
* @param {string} module - 模块名称
|
||||
*/
|
||||
clearError(module) {
|
||||
if (module) {
|
||||
delete this.errors[module]
|
||||
} else {
|
||||
this.errors = {}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置模块数据
|
||||
* @param {string} module - 模块名称
|
||||
*/
|
||||
resetModuleData(module) {
|
||||
if (this[module]) {
|
||||
// 重置为初始状态
|
||||
const initialState = this.$state[module]
|
||||
this[module] = { ...initialState }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 持久化配置
|
||||
persist: {
|
||||
key: 'government-admin-data',
|
||||
storage: localStorage,
|
||||
paths: ['visualization.charts', 'visualization.dataSources']
|
||||
}
|
||||
})
|
||||
14
government-admin/src/stores/index.js
Normal file
14
government-admin/src/stores/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
export default pinia
|
||||
|
||||
// 导出所有store
|
||||
export { useUserStore } from './user'
|
||||
export { useAppStore } from './app'
|
||||
export { useTabsStore } from './tabs'
|
||||
export { useNotificationStore } from './notification'
|
||||
export { useGovernmentStore } from './government'
|
||||
407
government-admin/src/stores/notification.js
Normal file
407
government-admin/src/stores/notification.js
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* 通知状态管理
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useNotificationStore = defineStore('notification', {
|
||||
state: () => ({
|
||||
// 通知列表
|
||||
notifications: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'info',
|
||||
title: '系统通知',
|
||||
content: '政府管理后台系统已成功启动',
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
category: 'system'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'warning',
|
||||
title: '待办提醒',
|
||||
content: '您有3个审批任务待处理',
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString(),
|
||||
read: false,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'success',
|
||||
title: '操作成功',
|
||||
content: '设备入库操作已完成',
|
||||
timestamp: new Date(Date.now() - 7200000).toISOString(),
|
||||
read: true,
|
||||
category: 'operation'
|
||||
}
|
||||
],
|
||||
|
||||
// 通知设置
|
||||
settings: {
|
||||
// 是否启用桌面通知
|
||||
desktop: true,
|
||||
// 是否启用声音提醒
|
||||
sound: true,
|
||||
// 通知显示时长(毫秒)
|
||||
duration: 4500,
|
||||
// 最大显示数量
|
||||
maxVisible: 5,
|
||||
// 自动清理已读通知(天数)
|
||||
autoCleanDays: 7
|
||||
},
|
||||
|
||||
// 当前显示的toast通知
|
||||
toasts: []
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 未读通知数量
|
||||
unreadCount: (state) => {
|
||||
return state.notifications.filter(n => !n.read).length
|
||||
},
|
||||
|
||||
// 按类型分组的通知
|
||||
notificationsByType: (state) => {
|
||||
return state.notifications.reduce((acc, notification) => {
|
||||
const type = notification.type
|
||||
if (!acc[type]) {
|
||||
acc[type] = []
|
||||
}
|
||||
acc[type].push(notification)
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
|
||||
// 按分类分组的通知
|
||||
notificationsByCategory: (state) => {
|
||||
return state.notifications.reduce((acc, notification) => {
|
||||
const category = notification.category
|
||||
if (!acc[category]) {
|
||||
acc[category] = []
|
||||
}
|
||||
acc[category].push(notification)
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
|
||||
// 最近的通知(按时间排序)
|
||||
recentNotifications: (state) => {
|
||||
return [...state.notifications]
|
||||
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||
.slice(0, 10)
|
||||
},
|
||||
|
||||
// 未读通知
|
||||
unreadNotifications: (state) => {
|
||||
return state.notifications.filter(n => !n.read)
|
||||
},
|
||||
|
||||
// 今日通知
|
||||
todayNotifications: (state) => {
|
||||
const today = new Date().toDateString()
|
||||
return state.notifications.filter(n =>
|
||||
new Date(n.timestamp).toDateString() === today
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 添加通知
|
||||
* @param {Object} notification - 通知对象
|
||||
*/
|
||||
addNotification(notification) {
|
||||
const newNotification = {
|
||||
id: this.generateId(),
|
||||
type: notification.type || 'info',
|
||||
title: notification.title,
|
||||
content: notification.content,
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
category: notification.category || 'general',
|
||||
...notification
|
||||
}
|
||||
|
||||
this.notifications.unshift(newNotification)
|
||||
|
||||
// 显示toast通知
|
||||
if (notification.showToast !== false) {
|
||||
this.showToast(newNotification)
|
||||
}
|
||||
|
||||
// 桌面通知
|
||||
if (this.settings.desktop && notification.desktop !== false) {
|
||||
this.showDesktopNotification(newNotification)
|
||||
}
|
||||
|
||||
// 声音提醒
|
||||
if (this.settings.sound && notification.sound !== false) {
|
||||
this.playNotificationSound()
|
||||
}
|
||||
|
||||
return newNotification.id
|
||||
},
|
||||
|
||||
/**
|
||||
* 标记通知为已读
|
||||
* @param {string} id - 通知ID
|
||||
*/
|
||||
markAsRead(id) {
|
||||
const notification = this.notifications.find(n => n.id === id)
|
||||
if (notification) {
|
||||
notification.read = true
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 标记所有通知为已读
|
||||
*/
|
||||
markAllAsRead() {
|
||||
this.notifications.forEach(notification => {
|
||||
notification.read = true
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除通知
|
||||
* @param {string} id - 通知ID
|
||||
*/
|
||||
removeNotification(id) {
|
||||
const index = this.notifications.findIndex(n => n.id === id)
|
||||
if (index > -1) {
|
||||
this.notifications.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空所有通知
|
||||
*/
|
||||
clearAllNotifications() {
|
||||
this.notifications = []
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空已读通知
|
||||
*/
|
||||
clearReadNotifications() {
|
||||
this.notifications = this.notifications.filter(n => !n.read)
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示Toast通知
|
||||
* @param {Object} notification - 通知对象
|
||||
*/
|
||||
showToast(notification) {
|
||||
const toast = {
|
||||
id: notification.id,
|
||||
type: notification.type,
|
||||
title: notification.title,
|
||||
content: notification.content,
|
||||
duration: notification.duration || this.settings.duration
|
||||
}
|
||||
|
||||
this.toasts.push(toast)
|
||||
|
||||
// 限制显示数量
|
||||
if (this.toasts.length > this.settings.maxVisible) {
|
||||
this.toasts.shift()
|
||||
}
|
||||
|
||||
// 自动移除
|
||||
if (toast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
this.removeToast(toast.id)
|
||||
}, toast.duration)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除Toast通知
|
||||
* @param {string} id - 通知ID
|
||||
*/
|
||||
removeToast(id) {
|
||||
const index = this.toasts.findIndex(t => t.id === id)
|
||||
if (index > -1) {
|
||||
this.toasts.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示桌面通知
|
||||
* @param {Object} notification - 通知对象
|
||||
*/
|
||||
showDesktopNotification(notification) {
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
const desktopNotification = new Notification(notification.title, {
|
||||
body: notification.content,
|
||||
icon: '/favicon.ico',
|
||||
tag: notification.id
|
||||
})
|
||||
|
||||
desktopNotification.onclick = () => {
|
||||
window.focus()
|
||||
this.markAsRead(notification.id)
|
||||
desktopNotification.close()
|
||||
}
|
||||
|
||||
// 自动关闭
|
||||
setTimeout(() => {
|
||||
desktopNotification.close()
|
||||
}, this.settings.duration)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 请求桌面通知权限
|
||||
*/
|
||||
async requestDesktopPermission() {
|
||||
if ('Notification' in window) {
|
||||
const permission = await Notification.requestPermission()
|
||||
this.settings.desktop = permission === 'granted'
|
||||
return permission
|
||||
}
|
||||
return 'denied'
|
||||
},
|
||||
|
||||
/**
|
||||
* 播放通知声音
|
||||
*/
|
||||
playNotificationSound() {
|
||||
try {
|
||||
const audio = new Audio('/sounds/notification.mp3')
|
||||
audio.volume = 0.5
|
||||
audio.play().catch(() => {
|
||||
// 忽略播放失败的错误
|
||||
})
|
||||
} catch (error) {
|
||||
// 忽略音频播放错误
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新通知设置
|
||||
* @param {Object} newSettings - 新设置
|
||||
*/
|
||||
updateSettings(newSettings) {
|
||||
this.settings = { ...this.settings, ...newSettings }
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @returns {string}
|
||||
*/
|
||||
generateId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
},
|
||||
|
||||
/**
|
||||
* 按类型筛选通知
|
||||
* @param {string} type - 通知类型
|
||||
* @returns {Array}
|
||||
*/
|
||||
getNotificationsByType(type) {
|
||||
return this.notifications.filter(n => n.type === type)
|
||||
},
|
||||
|
||||
/**
|
||||
* 按分类筛选通知
|
||||
* @param {string} category - 通知分类
|
||||
* @returns {Array}
|
||||
*/
|
||||
getNotificationsByCategory(category) {
|
||||
return this.notifications.filter(n => n.category === category)
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索通知
|
||||
* @param {string} keyword - 搜索关键词
|
||||
* @returns {Array}
|
||||
*/
|
||||
searchNotifications(keyword) {
|
||||
if (!keyword) return this.notifications
|
||||
|
||||
const lowerKeyword = keyword.toLowerCase()
|
||||
return this.notifications.filter(n =>
|
||||
n.title.toLowerCase().includes(lowerKeyword) ||
|
||||
n.content.toLowerCase().includes(lowerKeyword)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动清理过期通知
|
||||
*/
|
||||
autoCleanNotifications() {
|
||||
if (this.settings.autoCleanDays <= 0) return
|
||||
|
||||
const cutoffDate = new Date()
|
||||
cutoffDate.setDate(cutoffDate.getDate() - this.settings.autoCleanDays)
|
||||
|
||||
this.notifications = this.notifications.filter(n => {
|
||||
const notificationDate = new Date(n.timestamp)
|
||||
return !n.read || notificationDate > cutoffDate
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量操作通知
|
||||
* @param {Array} ids - 通知ID列表
|
||||
* @param {string} action - 操作类型 ('read', 'delete')
|
||||
*/
|
||||
batchOperation(ids, action) {
|
||||
ids.forEach(id => {
|
||||
if (action === 'read') {
|
||||
this.markAsRead(id)
|
||||
} else if (action === 'delete') {
|
||||
this.removeNotification(id)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 导出通知数据
|
||||
* @param {Object} options - 导出选项
|
||||
* @returns {Array}
|
||||
*/
|
||||
exportNotifications(options = {}) {
|
||||
let notifications = [...this.notifications]
|
||||
|
||||
// 按时间范围筛选
|
||||
if (options.startDate) {
|
||||
const startDate = new Date(options.startDate)
|
||||
notifications = notifications.filter(n =>
|
||||
new Date(n.timestamp) >= startDate
|
||||
)
|
||||
}
|
||||
|
||||
if (options.endDate) {
|
||||
const endDate = new Date(options.endDate)
|
||||
notifications = notifications.filter(n =>
|
||||
new Date(n.timestamp) <= endDate
|
||||
)
|
||||
}
|
||||
|
||||
// 按类型筛选
|
||||
if (options.types && options.types.length > 0) {
|
||||
notifications = notifications.filter(n =>
|
||||
options.types.includes(n.type)
|
||||
)
|
||||
}
|
||||
|
||||
// 按分类筛选
|
||||
if (options.categories && options.categories.length > 0) {
|
||||
notifications = notifications.filter(n =>
|
||||
options.categories.includes(n.category)
|
||||
)
|
||||
}
|
||||
|
||||
return notifications
|
||||
}
|
||||
},
|
||||
|
||||
// 持久化配置
|
||||
persist: {
|
||||
key: 'government-admin-notifications',
|
||||
storage: localStorage,
|
||||
paths: ['notifications', 'settings']
|
||||
}
|
||||
})
|
||||
288
government-admin/src/stores/permission.js
Normal file
288
government-admin/src/stores/permission.js
Normal file
@@ -0,0 +1,288 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export const usePermissionStore = defineStore('permission', () => {
|
||||
// 状态
|
||||
const permissions = ref([])
|
||||
const roles = ref([])
|
||||
const userRole = ref('')
|
||||
const menuList = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const hasPermission = computed(() => {
|
||||
return (permission) => {
|
||||
if (!permission) return true
|
||||
return permissions.value.includes(permission)
|
||||
}
|
||||
})
|
||||
|
||||
const hasRole = computed(() => {
|
||||
return (role) => {
|
||||
if (!role) return true
|
||||
return roles.value.includes(role) || userRole.value === role
|
||||
}
|
||||
})
|
||||
|
||||
const hasAnyPermission = computed(() => {
|
||||
return (permissionList) => {
|
||||
if (!permissionList || !Array.isArray(permissionList)) return true
|
||||
return permissionList.some(permission => permissions.value.includes(permission))
|
||||
}
|
||||
})
|
||||
|
||||
const hasAllPermissions = computed(() => {
|
||||
return (permissionList) => {
|
||||
if (!permissionList || !Array.isArray(permissionList)) return true
|
||||
return permissionList.every(permission => permissions.value.includes(permission))
|
||||
}
|
||||
})
|
||||
|
||||
// 过滤后的菜单(根据权限)
|
||||
const filteredMenus = computed(() => {
|
||||
return filterMenusByPermission(menuList.value)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const setPermissions = (newPermissions) => {
|
||||
permissions.value = newPermissions || []
|
||||
}
|
||||
|
||||
const setRoles = (newRoles) => {
|
||||
roles.value = newRoles || []
|
||||
}
|
||||
|
||||
const setUserRole = (role) => {
|
||||
userRole.value = role || ''
|
||||
}
|
||||
|
||||
const setMenuList = (menus) => {
|
||||
menuList.value = menus || []
|
||||
}
|
||||
|
||||
// 获取用户权限
|
||||
const fetchUserPermissions = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get('/auth/permissions')
|
||||
|
||||
if (response.success) {
|
||||
setPermissions(response.data.permissions)
|
||||
setRoles(response.data.roles)
|
||||
setUserRole(response.data.role)
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.message || '获取权限失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户权限失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取菜单列表
|
||||
const fetchMenuList = async () => {
|
||||
try {
|
||||
const response = await api.get('/auth/menus')
|
||||
|
||||
if (response.success) {
|
||||
setMenuList(response.data)
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.message || '获取菜单失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 根据权限过滤菜单
|
||||
const filterMenusByPermission = (menus) => {
|
||||
return menus.filter(menu => {
|
||||
// 检查菜单权限
|
||||
if (menu.permission && !hasPermission.value(menu.permission)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if (menu.roles && menu.roles.length > 0) {
|
||||
const hasRequiredRole = menu.roles.some(role => hasRole.value(role))
|
||||
if (!hasRequiredRole) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 递归过滤子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children = filterMenusByPermission(menu.children)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 检查路由权限
|
||||
const checkRoutePermission = (route) => {
|
||||
// 检查路由元信息中的权限
|
||||
if (route.meta?.permission) {
|
||||
return hasPermission.value(route.meta.permission)
|
||||
}
|
||||
|
||||
// 检查路由元信息中的角色
|
||||
if (route.meta?.roles && route.meta.roles.length > 0) {
|
||||
return route.meta.roles.some(role => hasRole.value(role))
|
||||
}
|
||||
|
||||
// 默认允许访问
|
||||
return true
|
||||
}
|
||||
|
||||
// 重置权限数据
|
||||
const resetPermissions = () => {
|
||||
permissions.value = []
|
||||
roles.value = []
|
||||
userRole.value = ''
|
||||
menuList.value = []
|
||||
}
|
||||
|
||||
// 权限常量定义
|
||||
const PERMISSIONS = {
|
||||
// 养殖场管理
|
||||
FARM_VIEW: 'farm:view',
|
||||
FARM_CREATE: 'farm:create',
|
||||
FARM_UPDATE: 'farm:update',
|
||||
FARM_DELETE: 'farm:delete',
|
||||
FARM_EXPORT: 'farm:export',
|
||||
|
||||
// 设备管理
|
||||
DEVICE_VIEW: 'device:view',
|
||||
DEVICE_CREATE: 'device:create',
|
||||
DEVICE_UPDATE: 'device:update',
|
||||
DEVICE_DELETE: 'device:delete',
|
||||
DEVICE_CONTROL: 'device:control',
|
||||
|
||||
// 监控管理
|
||||
MONITOR_VIEW: 'monitor:view',
|
||||
MONITOR_ALERT: 'monitor:alert',
|
||||
MONITOR_REPORT: 'monitor:report',
|
||||
|
||||
// 数据管理
|
||||
DATA_VIEW: 'data:view',
|
||||
DATA_EXPORT: 'data:export',
|
||||
DATA_ANALYSIS: 'data:analysis',
|
||||
|
||||
// 用户管理
|
||||
USER_VIEW: 'user:view',
|
||||
USER_CREATE: 'user:create',
|
||||
USER_UPDATE: 'user:update',
|
||||
USER_DELETE: 'user:delete',
|
||||
|
||||
// 系统管理
|
||||
SYSTEM_CONFIG: 'system:config',
|
||||
SYSTEM_LOG: 'system:log',
|
||||
SYSTEM_BACKUP: 'system:backup'
|
||||
}
|
||||
|
||||
// 角色常量定义
|
||||
const ROLES = {
|
||||
SUPER_ADMIN: 'super_admin',
|
||||
ADMIN: 'admin',
|
||||
MANAGER: 'manager',
|
||||
OPERATOR: 'operator',
|
||||
VIEWER: 'viewer'
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
permissions,
|
||||
roles,
|
||||
userRole,
|
||||
menuList,
|
||||
loading,
|
||||
|
||||
// 计算属性
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
filteredMenus,
|
||||
|
||||
// 方法
|
||||
setPermissions,
|
||||
setRoles,
|
||||
setUserRole,
|
||||
setMenuList,
|
||||
fetchUserPermissions,
|
||||
fetchMenuList,
|
||||
filterMenusByPermission,
|
||||
checkRoutePermission,
|
||||
resetPermissions,
|
||||
|
||||
// 常量
|
||||
PERMISSIONS,
|
||||
ROLES
|
||||
}
|
||||
})
|
||||
|
||||
// 权限指令
|
||||
export const permissionDirective = {
|
||||
mounted(el, binding) {
|
||||
const permissionStore = usePermissionStore()
|
||||
const { value } = binding
|
||||
|
||||
if (value) {
|
||||
let hasPermission = false
|
||||
|
||||
if (typeof value === 'string') {
|
||||
hasPermission = permissionStore.hasPermission(value)
|
||||
} else if (Array.isArray(value)) {
|
||||
hasPermission = permissionStore.hasAnyPermission(value)
|
||||
} else if (typeof value === 'object') {
|
||||
if (value.permission) {
|
||||
hasPermission = permissionStore.hasPermission(value.permission)
|
||||
} else if (value.role) {
|
||||
hasPermission = permissionStore.hasRole(value.role)
|
||||
} else if (value.permissions) {
|
||||
hasPermission = value.all
|
||||
? permissionStore.hasAllPermissions(value.permissions)
|
||||
: permissionStore.hasAnyPermission(value.permissions)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPermission) {
|
||||
el.style.display = 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
const permissionStore = usePermissionStore()
|
||||
const { value } = binding
|
||||
|
||||
if (value) {
|
||||
let hasPermission = false
|
||||
|
||||
if (typeof value === 'string') {
|
||||
hasPermission = permissionStore.hasPermission(value)
|
||||
} else if (Array.isArray(value)) {
|
||||
hasPermission = permissionStore.hasAnyPermission(value)
|
||||
} else if (typeof value === 'object') {
|
||||
if (value.permission) {
|
||||
hasPermission = permissionStore.hasPermission(value.permission)
|
||||
} else if (value.role) {
|
||||
hasPermission = permissionStore.hasRole(value.role)
|
||||
} else if (value.permissions) {
|
||||
hasPermission = value.all
|
||||
? permissionStore.hasAllPermissions(value.permissions)
|
||||
: permissionStore.hasAnyPermission(value.permissions)
|
||||
}
|
||||
}
|
||||
|
||||
el.style.display = hasPermission ? '' : 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
294
government-admin/src/stores/tabs.js
Normal file
294
government-admin/src/stores/tabs.js
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 标签页状态管理
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useTabsStore = defineStore('tabs', {
|
||||
state: () => ({
|
||||
// 打开的标签页列表
|
||||
openTabs: [
|
||||
{
|
||||
path: '/dashboard',
|
||||
title: '仪表盘',
|
||||
closable: false
|
||||
}
|
||||
],
|
||||
// 缓存的视图组件名称列表
|
||||
cachedViews: ['Dashboard']
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取当前活跃的标签页
|
||||
activeTab: (state) => {
|
||||
return state.openTabs.find(tab => tab.active) || state.openTabs[0]
|
||||
},
|
||||
|
||||
// 获取可关闭的标签页
|
||||
closableTabs: (state) => {
|
||||
return state.openTabs.filter(tab => tab.closable)
|
||||
},
|
||||
|
||||
// 获取标签页数量
|
||||
tabCount: (state) => {
|
||||
return state.openTabs.length
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 添加标签页
|
||||
* @param {Object} tab - 标签页信息
|
||||
*/
|
||||
addTab(tab) {
|
||||
const existingTab = this.openTabs.find(t => t.path === tab.path)
|
||||
|
||||
if (!existingTab) {
|
||||
// 新标签页,添加到列表
|
||||
this.openTabs.push({
|
||||
path: tab.path,
|
||||
title: tab.title,
|
||||
closable: tab.closable !== false,
|
||||
active: true
|
||||
})
|
||||
|
||||
// 添加到缓存列表
|
||||
if (tab.name && !this.cachedViews.includes(tab.name)) {
|
||||
this.cachedViews.push(tab.name)
|
||||
}
|
||||
} else {
|
||||
// 已存在的标签页,激活它
|
||||
this.setActiveTab(tab.path)
|
||||
}
|
||||
|
||||
// 取消其他标签页的激活状态
|
||||
this.openTabs.forEach(t => {
|
||||
t.active = t.path === tab.path
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除标签页
|
||||
* @param {string} path - 标签页路径
|
||||
*/
|
||||
removeTab(path) {
|
||||
const index = this.openTabs.findIndex(tab => tab.path === path)
|
||||
|
||||
if (index > -1) {
|
||||
const removedTab = this.openTabs[index]
|
||||
this.openTabs.splice(index, 1)
|
||||
|
||||
// 从缓存中移除
|
||||
if (removedTab.name) {
|
||||
const cacheIndex = this.cachedViews.indexOf(removedTab.name)
|
||||
if (cacheIndex > -1) {
|
||||
this.cachedViews.splice(cacheIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果移除的是当前激活的标签页,需要激活其他标签页
|
||||
if (removedTab.active && this.openTabs.length > 0) {
|
||||
const nextTab = this.openTabs[Math.min(index, this.openTabs.length - 1)]
|
||||
this.setActiveTab(nextTab.path)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置活跃标签页
|
||||
* @param {string} path - 标签页路径
|
||||
*/
|
||||
setActiveTab(path) {
|
||||
this.openTabs.forEach(tab => {
|
||||
tab.active = tab.path === path
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭其他标签页
|
||||
* @param {string} currentPath - 当前标签页路径
|
||||
*/
|
||||
closeOtherTabs(currentPath) {
|
||||
const currentTab = this.openTabs.find(tab => tab.path === currentPath)
|
||||
const fixedTabs = this.openTabs.filter(tab => !tab.closable)
|
||||
|
||||
if (currentTab) {
|
||||
this.openTabs = [...fixedTabs, currentTab]
|
||||
|
||||
// 更新缓存列表
|
||||
const keepNames = this.openTabs
|
||||
.map(tab => tab.name)
|
||||
.filter(name => name)
|
||||
|
||||
this.cachedViews = this.cachedViews.filter(name =>
|
||||
keepNames.includes(name)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭所有可关闭的标签页
|
||||
*/
|
||||
closeAllTabs() {
|
||||
this.openTabs = this.openTabs.filter(tab => !tab.closable)
|
||||
|
||||
// 更新缓存列表
|
||||
const keepNames = this.openTabs
|
||||
.map(tab => tab.name)
|
||||
.filter(name => name)
|
||||
|
||||
this.cachedViews = this.cachedViews.filter(name =>
|
||||
keepNames.includes(name)
|
||||
)
|
||||
|
||||
// 激活第一个标签页
|
||||
if (this.openTabs.length > 0) {
|
||||
this.setActiveTab(this.openTabs[0].path)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭左侧标签页
|
||||
* @param {string} currentPath - 当前标签页路径
|
||||
*/
|
||||
closeLeftTabs(currentPath) {
|
||||
const currentIndex = this.openTabs.findIndex(tab => tab.path === currentPath)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
const leftTabs = this.openTabs.slice(0, currentIndex)
|
||||
const closableLeftTabs = leftTabs.filter(tab => tab.closable)
|
||||
|
||||
// 移除可关闭的左侧标签页
|
||||
closableLeftTabs.forEach(tab => {
|
||||
this.removeTab(tab.path)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭右侧标签页
|
||||
* @param {string} currentPath - 当前标签页路径
|
||||
*/
|
||||
closeRightTabs(currentPath) {
|
||||
const currentIndex = this.openTabs.findIndex(tab => tab.path === currentPath)
|
||||
|
||||
if (currentIndex < this.openTabs.length - 1) {
|
||||
const rightTabs = this.openTabs.slice(currentIndex + 1)
|
||||
const closableRightTabs = rightTabs.filter(tab => tab.closable)
|
||||
|
||||
// 移除可关闭的右侧标签页
|
||||
closableRightTabs.forEach(tab => {
|
||||
this.removeTab(tab.path)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新标签页
|
||||
* @param {string} path - 标签页路径
|
||||
*/
|
||||
refreshTab(path) {
|
||||
const tab = this.openTabs.find(t => t.path === path)
|
||||
|
||||
if (tab && tab.name) {
|
||||
// 从缓存中移除,强制重新渲染
|
||||
const cacheIndex = this.cachedViews.indexOf(tab.name)
|
||||
if (cacheIndex > -1) {
|
||||
this.cachedViews.splice(cacheIndex, 1)
|
||||
}
|
||||
|
||||
// 延迟重新添加到缓存
|
||||
setTimeout(() => {
|
||||
this.cachedViews.push(tab.name)
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新标签页标题
|
||||
* @param {string} path - 标签页路径
|
||||
* @param {string} title - 新标题
|
||||
*/
|
||||
updateTabTitle(path, title) {
|
||||
const tab = this.openTabs.find(t => t.path === path)
|
||||
if (tab) {
|
||||
tab.title = title
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查标签页是否存在
|
||||
* @param {string} path - 标签页路径
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasTab(path) {
|
||||
return this.openTabs.some(tab => tab.path === path)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取标签页信息
|
||||
* @param {string} path - 标签页路径
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
getTab(path) {
|
||||
return this.openTabs.find(tab => tab.path === path) || null
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置标签页状态
|
||||
*/
|
||||
resetTabs() {
|
||||
this.openTabs = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
title: '仪表盘',
|
||||
closable: false,
|
||||
active: true
|
||||
}
|
||||
]
|
||||
this.cachedViews = ['Dashboard']
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量添加标签页
|
||||
* @param {Array} tabs - 标签页列表
|
||||
*/
|
||||
addTabs(tabs) {
|
||||
tabs.forEach(tab => {
|
||||
this.addTab(tab)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 移动标签页位置
|
||||
* @param {number} oldIndex - 原位置
|
||||
* @param {number} newIndex - 新位置
|
||||
*/
|
||||
moveTab(oldIndex, newIndex) {
|
||||
if (oldIndex >= 0 && oldIndex < this.openTabs.length &&
|
||||
newIndex >= 0 && newIndex < this.openTabs.length) {
|
||||
const tab = this.openTabs.splice(oldIndex, 1)[0]
|
||||
this.openTabs.splice(newIndex, 0, tab)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 固定/取消固定标签页
|
||||
* @param {string} path - 标签页路径
|
||||
* @param {boolean} pinned - 是否固定
|
||||
*/
|
||||
pinTab(path, pinned = true) {
|
||||
const tab = this.openTabs.find(t => t.path === path)
|
||||
if (tab) {
|
||||
tab.closable = !pinned
|
||||
tab.pinned = pinned
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 持久化配置
|
||||
persist: {
|
||||
key: 'government-admin-tabs',
|
||||
storage: localStorage,
|
||||
paths: ['openTabs', 'cachedViews']
|
||||
}
|
||||
})
|
||||
116
government-admin/src/stores/user.js
Normal file
116
government-admin/src/stores/user.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 用户状态管理
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 状态
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'))
|
||||
const permissions = ref([])
|
||||
const roles = ref([])
|
||||
|
||||
// 计算属性
|
||||
const isLoggedIn = computed(() => !!token.value && !!userInfo.value)
|
||||
const userName = computed(() => userInfo.value?.name || '管理员')
|
||||
const userRole = computed(() => userInfo.value?.role || '系统管理员')
|
||||
|
||||
// 检查登录状态
|
||||
function checkLoginStatus() {
|
||||
const savedToken = localStorage.getItem('token')
|
||||
const savedUserInfo = localStorage.getItem('userInfo')
|
||||
|
||||
if (savedToken && savedUserInfo) {
|
||||
try {
|
||||
token.value = savedToken
|
||||
userInfo.value = JSON.parse(savedUserInfo)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('解析用户数据失败', error)
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 登录
|
||||
async function login(credentials) {
|
||||
try {
|
||||
// 这里应该调用实际的登录API
|
||||
// const response = await authApi.login(credentials)
|
||||
|
||||
// 模拟登录成功
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
name: credentials.username || '管理员',
|
||||
role: '系统管理员',
|
||||
avatar: '',
|
||||
email: 'admin@example.com'
|
||||
}
|
||||
|
||||
token.value = 'mock-token-' + Date.now()
|
||||
userInfo.value = mockUser
|
||||
|
||||
localStorage.setItem('token', token.value)
|
||||
localStorage.setItem('userInfo', JSON.stringify(mockUser))
|
||||
|
||||
return { success: true, data: mockUser }
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
return { success: false, message: '登录失败' }
|
||||
}
|
||||
}
|
||||
|
||||
// 登出
|
||||
function logout() {
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
permissions.value = []
|
||||
roles.value = []
|
||||
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
function updateUserInfo(newUserInfo) {
|
||||
userInfo.value = { ...userInfo.value, ...newUserInfo }
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
function hasPermission(permission) {
|
||||
if (!userInfo.value) return false
|
||||
|
||||
// 管理员拥有所有权限
|
||||
if (userInfo.value.role === '系统管理员' || userInfo.value.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
return permissions.value.includes(permission)
|
||||
}
|
||||
|
||||
// 角色检查
|
||||
function hasRole(role) {
|
||||
if (!userInfo.value) return false
|
||||
return roles.value.includes(role) || userInfo.value.role === role
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
permissions,
|
||||
roles,
|
||||
isLoggedIn,
|
||||
userName,
|
||||
userRole,
|
||||
checkLoginStatus,
|
||||
login,
|
||||
logout,
|
||||
updateUserInfo,
|
||||
hasPermission,
|
||||
hasRole
|
||||
}
|
||||
})
|
||||
11
government-admin/src/styles/index.css
Normal file
11
government-admin/src/styles/index.css
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 全局基础样式 */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
211
government-admin/src/styles/index.scss
Normal file
211
government-admin/src/styles/index.scss
Normal file
@@ -0,0 +1,211 @@
|
||||
// 全局样式文件
|
||||
|
||||
// 变量定义
|
||||
:root {
|
||||
--primary-color: #1890ff;
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
--error-color: #f5222d;
|
||||
--info-color: #1890ff;
|
||||
|
||||
--text-color: rgba(0, 0, 0, 0.85);
|
||||
--text-color-secondary: rgba(0, 0, 0, 0.65);
|
||||
--text-color-disabled: rgba(0, 0, 0, 0.25);
|
||||
|
||||
--background-color: #f0f2f5;
|
||||
--component-background: #ffffff;
|
||||
--border-color: #d9d9d9;
|
||||
--border-radius: 6px;
|
||||
|
||||
--shadow-1: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-2: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
// 通用工具类
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.flex { display: flex; }
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.flex-column { flex-direction: column; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-1 { flex: 1; }
|
||||
|
||||
.mb-0 { margin-bottom: 0 !important; }
|
||||
.mb-8 { margin-bottom: 8px; }
|
||||
.mb-16 { margin-bottom: 16px; }
|
||||
.mb-24 { margin-bottom: 24px; }
|
||||
.mb-32 { margin-bottom: 32px; }
|
||||
|
||||
.mt-0 { margin-top: 0 !important; }
|
||||
.mt-8 { margin-top: 8px; }
|
||||
.mt-16 { margin-top: 16px; }
|
||||
.mt-24 { margin-top: 24px; }
|
||||
.mt-32 { margin-top: 32px; }
|
||||
|
||||
.ml-8 { margin-left: 8px; }
|
||||
.ml-16 { margin-left: 16px; }
|
||||
.mr-8 { margin-right: 8px; }
|
||||
.mr-16 { margin-right: 16px; }
|
||||
|
||||
.p-16 { padding: 16px; }
|
||||
.p-24 { padding: 24px; }
|
||||
.pt-16 { padding-top: 16px; }
|
||||
.pb-16 { padding-bottom: 16px; }
|
||||
|
||||
// 卡片样式
|
||||
.card {
|
||||
background: var(--component-background);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-1);
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.small-padding {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面容器
|
||||
.page-container {
|
||||
padding: 24px;
|
||||
min-height: calc(100vh - 64px);
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表格样式增强
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
// 表单样式增强
|
||||
.ant-form {
|
||||
.ant-form-item-label > label {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态标签
|
||||
.status-tag {
|
||||
&.online { color: var(--success-color); }
|
||||
&.offline { color: var(--error-color); }
|
||||
&.warning { color: var(--warning-color); }
|
||||
&.maintenance { color: var(--text-color-secondary); }
|
||||
}
|
||||
|
||||
// 数据卡片
|
||||
.data-card {
|
||||
background: var(--component-background);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow-1);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.data-card-title {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-card-value {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.data-card-trend {
|
||||
font-size: 12px;
|
||||
|
||||
&.up { color: var(--success-color); }
|
||||
&.down { color: var(--error-color); }
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ant-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
padding: 16px;
|
||||
|
||||
.data-card-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
color: var(--text-color-secondary);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
447
government-admin/src/styles/mixins.scss
Normal file
447
government-admin/src/styles/mixins.scss
Normal file
@@ -0,0 +1,447 @@
|
||||
@import './variables.scss';
|
||||
|
||||
// 清除浮动
|
||||
@mixin clearfix {
|
||||
&::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本省略
|
||||
@mixin text-ellipsis($lines: 1) {
|
||||
@if $lines == 1 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
} @else {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// 居中对齐
|
||||
@mixin center($type: 'both') {
|
||||
position: absolute;
|
||||
@if $type == 'both' {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
} @else if $type == 'horizontal' {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
} @else if $type == 'vertical' {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
// Flex 布局
|
||||
@mixin flex($direction: row, $justify: flex-start, $align: stretch, $wrap: nowrap) {
|
||||
display: flex;
|
||||
flex-direction: $direction;
|
||||
justify-content: $justify;
|
||||
align-items: $align;
|
||||
flex-wrap: $wrap;
|
||||
}
|
||||
|
||||
// 响应式断点
|
||||
@mixin respond-to($breakpoint) {
|
||||
@if $breakpoint == xs {
|
||||
@media (max-width: #{$screen-xs - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == sm {
|
||||
@media (min-width: #{$screen-sm}) and (max-width: #{$screen-md - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == md {
|
||||
@media (min-width: #{$screen-md}) and (max-width: #{$screen-lg - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == lg {
|
||||
@media (min-width: #{$screen-lg}) and (max-width: #{$screen-xl - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == xl {
|
||||
@media (min-width: #{$screen-xl}) and (max-width: #{$screen-xxl - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == xxl {
|
||||
@media (min-width: #{$screen-xxl}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 阴影效果
|
||||
@mixin box-shadow($level: 1) {
|
||||
@if $level == 1 {
|
||||
box-shadow: $box-shadow-sm;
|
||||
} @else if $level == 2 {
|
||||
box-shadow: $box-shadow-base;
|
||||
} @else if $level == 3 {
|
||||
box-shadow: $box-shadow-lg;
|
||||
} @else if $level == 4 {
|
||||
box-shadow: $box-shadow-xl;
|
||||
}
|
||||
}
|
||||
|
||||
// 渐变背景
|
||||
@mixin gradient-bg($direction: 135deg, $start-color: $primary-color, $end-color: lighten($primary-color, 10%)) {
|
||||
background: linear-gradient($direction, $start-color 0%, $end-color 100%);
|
||||
}
|
||||
|
||||
// 按钮样式
|
||||
@mixin button-variant($color, $background, $border: $background) {
|
||||
color: $color;
|
||||
background-color: $background;
|
||||
border-color: $border;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $color;
|
||||
background-color: lighten($background, 5%);
|
||||
border-color: lighten($border, 5%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: $color;
|
||||
background-color: darken($background, 5%);
|
||||
border-color: darken($border, 5%);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
color: $text-color-disabled;
|
||||
background-color: $background-color;
|
||||
border-color: $border-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
@mixin input-variant($border-color: $border-color, $focus-color: $primary-color) {
|
||||
border-color: $border-color;
|
||||
|
||||
&:hover {
|
||||
border-color: lighten($focus-color, 20%);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.focused {
|
||||
border-color: $focus-color;
|
||||
box-shadow: 0 0 0 2px fade($focus-color, 20%);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
@mixin card-variant($padding: $card-padding-base, $radius: $border-radius-base, $shadow: $box-shadow-base) {
|
||||
background: $background-color-light;
|
||||
border-radius: $radius;
|
||||
box-shadow: $shadow;
|
||||
padding: $padding;
|
||||
transition: box-shadow $animation-duration-base $ease-out;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $box-shadow-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式
|
||||
@mixin tag-variant($color, $background, $border: $background) {
|
||||
color: $color;
|
||||
background-color: $background;
|
||||
border-color: $border;
|
||||
border-radius: $tag-border-radius;
|
||||
padding: 0 7px;
|
||||
font-size: $tag-font-size;
|
||||
line-height: $tag-line-height;
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
transition: all $animation-duration-base;
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
@mixin fade-in($duration: $animation-duration-base) {
|
||||
animation: fadeIn $duration $ease-out;
|
||||
}
|
||||
|
||||
@mixin slide-up($duration: $animation-duration-base) {
|
||||
animation: slideUp $duration $ease-out;
|
||||
}
|
||||
|
||||
@mixin slide-down($duration: $animation-duration-base) {
|
||||
animation: slideDown $duration $ease-out;
|
||||
}
|
||||
|
||||
@mixin zoom-in($duration: $animation-duration-base) {
|
||||
animation: zoomIn $duration $ease-out;
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
@mixin loading-spin($size: 14px, $color: $primary-color) {
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
border: 2px solid fade($color, 20%);
|
||||
border-top-color: $color;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
// 脉冲动画
|
||||
@mixin pulse($color: $primary-color) {
|
||||
animation: pulse 2s infinite;
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 fade($color, 70%);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px fade($color, 0%);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 fade($color, 0%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
@mixin scrollbar($width: 6px, $track-color: $background-color, $thumb-color: $border-color) {
|
||||
&::-webkit-scrollbar {
|
||||
width: $width;
|
||||
height: $width;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $track-color;
|
||||
border-radius: $width / 2;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $thumb-color;
|
||||
border-radius: $width / 2;
|
||||
|
||||
&:hover {
|
||||
background: darken($thumb-color, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表格斑马纹
|
||||
@mixin table-striped($odd-color: $background-color-light, $even-color: transparent) {
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: $odd-color;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: $even-color;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具提示箭头
|
||||
@mixin tooltip-arrow($direction: top, $size: 4px, $color: $tooltip-bg) {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: $size solid transparent;
|
||||
|
||||
@if $direction == top {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
margin-left: -$size;
|
||||
border-bottom-color: $color;
|
||||
} @else if $direction == bottom {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -$size;
|
||||
border-top-color: $color;
|
||||
} @else if $direction == left {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
margin-top: -$size;
|
||||
border-right-color: $color;
|
||||
} @else if $direction == right {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
margin-top: -$size;
|
||||
border-left-color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文字渐变
|
||||
@mixin text-gradient($start-color: $primary-color, $end-color: lighten($primary-color, 20%)) {
|
||||
background: linear-gradient(45deg, $start-color, $end-color);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
// 毛玻璃效果
|
||||
@mixin glass-morphism($blur: 10px, $opacity: 0.1) {
|
||||
backdrop-filter: blur($blur);
|
||||
background: rgba(255, 255, 255, $opacity);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
// 网格布局
|
||||
@mixin grid($columns: 12, $gap: $spacing-md) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat($columns, 1fr);
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
// 粘性定位
|
||||
@mixin sticky($top: 0, $z-index: $zindex-sticky) {
|
||||
position: sticky;
|
||||
top: $top;
|
||||
z-index: $z-index;
|
||||
}
|
||||
|
||||
// 隐藏文本(用于图标替换)
|
||||
@mixin hide-text {
|
||||
font: 0/0 a;
|
||||
color: transparent;
|
||||
text-shadow: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// 三角形
|
||||
@mixin triangle($direction: up, $size: 6px, $color: $text-color) {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
@if $direction == up {
|
||||
border-left: $size solid transparent;
|
||||
border-right: $size solid transparent;
|
||||
border-bottom: $size solid $color;
|
||||
} @else if $direction == down {
|
||||
border-left: $size solid transparent;
|
||||
border-right: $size solid transparent;
|
||||
border-top: $size solid $color;
|
||||
} @else if $direction == left {
|
||||
border-top: $size solid transparent;
|
||||
border-bottom: $size solid transparent;
|
||||
border-right: $size solid $color;
|
||||
} @else if $direction == right {
|
||||
border-top: $size solid transparent;
|
||||
border-bottom: $size solid transparent;
|
||||
border-left: $size solid $color;
|
||||
}
|
||||
}
|
||||
|
||||
// 关键帧动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 53%, 80%, 100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
40%, 43% {
|
||||
transform: translate3d(0, -30px, 0);
|
||||
}
|
||||
70% {
|
||||
transform: translate3d(0, -15px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, -4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%, 30%, 50%, 70%, 90% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
20%, 40%, 60%, 80% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
14% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
28% {
|
||||
transform: scale(1);
|
||||
}
|
||||
42% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
134
government-admin/src/utils/api.js
Normal file
134
government-admin/src/utils/api.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import axios from 'axios'
|
||||
import { message } from 'ant-design-vue'
|
||||
import router from '@/router'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 添加时间戳防止缓存
|
||||
if (config.method === 'get') {
|
||||
config.params = {
|
||||
...config.params,
|
||||
_t: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
const { code, message: msg } = response.data
|
||||
|
||||
// 处理业务错误码
|
||||
if (code && code !== 200) {
|
||||
message.error(msg || '请求失败')
|
||||
return Promise.reject(new Error(msg || '请求失败'))
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
const { response } = error
|
||||
|
||||
if (response) {
|
||||
const { status, data } = response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 未授权,清除token并跳转到登录页
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('permissions')
|
||||
router.push('/login')
|
||||
message.error('登录已过期,请重新登录')
|
||||
break
|
||||
|
||||
case 403:
|
||||
message.error('没有权限访问该资源')
|
||||
break
|
||||
|
||||
case 404:
|
||||
message.error('请求的资源不存在')
|
||||
break
|
||||
|
||||
case 500:
|
||||
message.error('服务器内部错误')
|
||||
break
|
||||
|
||||
default:
|
||||
message.error(data?.message || `请求失败 (${status})`)
|
||||
}
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
message.error('请求超时,请稍后重试')
|
||||
} else {
|
||||
message.error('网络错误,请检查网络连接')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 封装常用请求方法
|
||||
export const request = {
|
||||
get: (url, params = {}) => api.get(url, { params }),
|
||||
post: (url, data = {}) => api.post(url, data),
|
||||
put: (url, data = {}) => api.put(url, data),
|
||||
delete: (url, params = {}) => api.delete(url, { params }),
|
||||
patch: (url, data = {}) => api.patch(url, data)
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
export const upload = (url, formData, onProgress) => {
|
||||
return api.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: onProgress
|
||||
})
|
||||
}
|
||||
|
||||
// 文件下载
|
||||
export const download = async (url, filename, params = {}) => {
|
||||
try {
|
||||
const response = await api.get(url, {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
const blob = new Blob([response.data])
|
||||
const downloadUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(downloadUrl)
|
||||
} catch (error) {
|
||||
message.error('文件下载失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
508
government-admin/src/utils/permission.js
Normal file
508
government-admin/src/utils/permission.js
Normal file
@@ -0,0 +1,508 @@
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
/**
|
||||
* 权限检查工具函数
|
||||
*/
|
||||
|
||||
// 检查单个权限
|
||||
export function hasPermission(permission) {
|
||||
const permissionStore = usePermissionStore()
|
||||
return permissionStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
export function hasRole(role) {
|
||||
const permissionStore = usePermissionStore()
|
||||
return permissionStore.hasRole(role)
|
||||
}
|
||||
|
||||
// 检查任一权限
|
||||
export function hasAnyPermission(permissions) {
|
||||
const permissionStore = usePermissionStore()
|
||||
return permissionStore.hasAnyPermission(permissions)
|
||||
}
|
||||
|
||||
// 检查全部权限
|
||||
export function hasAllPermissions(permissions) {
|
||||
const permissionStore = usePermissionStore()
|
||||
return permissionStore.hasAllPermissions(permissions)
|
||||
}
|
||||
|
||||
// 检查路由权限
|
||||
export function checkRoutePermission(route) {
|
||||
const permissionStore = usePermissionStore()
|
||||
return permissionStore.checkRoutePermission(route)
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限装饰器
|
||||
* 用于方法级别的权限控制
|
||||
*/
|
||||
export function requirePermission(permission) {
|
||||
return function(target, propertyKey, descriptor) {
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
descriptor.value = function(...args) {
|
||||
if (hasPermission(permission)) {
|
||||
return originalMethod.apply(this, args)
|
||||
} else {
|
||||
console.warn(`权限不足: ${permission}`)
|
||||
return Promise.reject(new Error('权限不足'))
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色装饰器
|
||||
* 用于方法级别的角色控制
|
||||
*/
|
||||
export function requireRole(role) {
|
||||
return function(target, propertyKey, descriptor) {
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
descriptor.value = function(...args) {
|
||||
if (hasRole(role)) {
|
||||
return originalMethod.apply(this, args)
|
||||
} else {
|
||||
console.warn(`角色权限不足: ${role}`)
|
||||
return Promise.reject(new Error('角色权限不足'))
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限混入
|
||||
* 为组件提供权限检查方法
|
||||
*/
|
||||
export const permissionMixin = {
|
||||
methods: {
|
||||
$hasPermission: hasPermission,
|
||||
$hasRole: hasRole,
|
||||
$hasAnyPermission: hasAnyPermission,
|
||||
$hasAllPermissions: hasAllPermissions,
|
||||
|
||||
// 权限检查快捷方法
|
||||
$canView(resource) {
|
||||
return hasPermission(`${resource}:view`)
|
||||
},
|
||||
|
||||
$canCreate(resource) {
|
||||
return hasPermission(`${resource}:create`)
|
||||
},
|
||||
|
||||
$canUpdate(resource) {
|
||||
return hasPermission(`${resource}:update`)
|
||||
},
|
||||
|
||||
$canDelete(resource) {
|
||||
return hasPermission(`${resource}:delete`)
|
||||
},
|
||||
|
||||
$canExport(resource) {
|
||||
return hasPermission(`${resource}:export`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限常量
|
||||
*/
|
||||
export const PERMISSIONS = {
|
||||
// 养殖场管理
|
||||
FARM_VIEW: 'farm:view',
|
||||
FARM_CREATE: 'farm:create',
|
||||
FARM_UPDATE: 'farm:update',
|
||||
FARM_DELETE: 'farm:delete',
|
||||
FARM_EXPORT: 'farm:export',
|
||||
|
||||
// 设备管理
|
||||
DEVICE_VIEW: 'device:view',
|
||||
DEVICE_CREATE: 'device:create',
|
||||
DEVICE_UPDATE: 'device:update',
|
||||
DEVICE_DELETE: 'device:delete',
|
||||
DEVICE_CONTROL: 'device:control',
|
||||
|
||||
// 监控管理
|
||||
MONITOR_VIEW: 'monitor:view',
|
||||
MONITOR_ALERT: 'monitor:alert',
|
||||
MONITOR_REPORT: 'monitor:report',
|
||||
|
||||
// 数据管理
|
||||
DATA_VIEW: 'data:view',
|
||||
DATA_EXPORT: 'data:export',
|
||||
DATA_ANALYSIS: 'data:analysis',
|
||||
|
||||
// 用户管理
|
||||
USER_VIEW: 'user:view',
|
||||
USER_CREATE: 'user:create',
|
||||
USER_UPDATE: 'user:update',
|
||||
USER_DELETE: 'user:delete',
|
||||
|
||||
// 系统管理
|
||||
SYSTEM_CONFIG: 'system:config',
|
||||
SYSTEM_LOG: 'system:log',
|
||||
SYSTEM_BACKUP: 'system:backup',
|
||||
|
||||
// 政府监管
|
||||
SUPERVISION_VIEW: 'supervision:view',
|
||||
SUPERVISION_CREATE: 'supervision:create',
|
||||
SUPERVISION_UPDATE: 'supervision:update',
|
||||
SUPERVISION_DELETE: 'supervision:delete',
|
||||
SUPERVISION_APPROVE: 'supervision:approve',
|
||||
SUPERVISION_EXPORT: 'supervision:export',
|
||||
|
||||
// 审批管理
|
||||
APPROVAL_VIEW: 'approval:view',
|
||||
APPROVAL_CREATE: 'approval:create',
|
||||
APPROVAL_UPDATE: 'approval:update',
|
||||
APPROVAL_DELETE: 'approval:delete',
|
||||
APPROVAL_APPROVE: 'approval:approve',
|
||||
APPROVAL_REJECT: 'approval:reject',
|
||||
APPROVAL_EXPORT: 'approval:export',
|
||||
|
||||
// 人员管理
|
||||
PERSONNEL_VIEW: 'personnel:view',
|
||||
PERSONNEL_CREATE: 'personnel:create',
|
||||
PERSONNEL_UPDATE: 'personnel:update',
|
||||
PERSONNEL_DELETE: 'personnel:delete',
|
||||
PERSONNEL_ASSIGN: 'personnel:assign',
|
||||
PERSONNEL_EXPORT: 'personnel:export',
|
||||
|
||||
// 设备仓库
|
||||
WAREHOUSE_VIEW: 'warehouse:view',
|
||||
WAREHOUSE_CREATE: 'warehouse:create',
|
||||
WAREHOUSE_UPDATE: 'warehouse:update',
|
||||
WAREHOUSE_DELETE: 'warehouse:delete',
|
||||
WAREHOUSE_IN: 'warehouse:in',
|
||||
WAREHOUSE_OUT: 'warehouse:out',
|
||||
WAREHOUSE_EXPORT: 'warehouse:export',
|
||||
|
||||
// 防疫管理
|
||||
EPIDEMIC_VIEW: 'epidemic:view',
|
||||
EPIDEMIC_CREATE: 'epidemic:create',
|
||||
EPIDEMIC_UPDATE: 'epidemic:update',
|
||||
EPIDEMIC_DELETE: 'epidemic:delete',
|
||||
EPIDEMIC_PLAN: 'epidemic:plan',
|
||||
EPIDEMIC_REPORT: 'epidemic:report',
|
||||
|
||||
// 服务管理
|
||||
SERVICE_VIEW: 'service:view',
|
||||
SERVICE_CREATE: 'service:create',
|
||||
SERVICE_UPDATE: 'service:update',
|
||||
SERVICE_DELETE: 'service:delete',
|
||||
SERVICE_ASSIGN: 'service:assign',
|
||||
|
||||
// 可视化大屏
|
||||
VISUALIZATION_VIEW: 'visualization:view',
|
||||
VISUALIZATION_CONFIG: 'visualization:config',
|
||||
VISUALIZATION_EXPORT: 'visualization:export'
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色常量
|
||||
*/
|
||||
export const ROLES = {
|
||||
SUPER_ADMIN: 'super_admin',
|
||||
ADMIN: 'admin',
|
||||
MANAGER: 'manager',
|
||||
OPERATOR: 'operator',
|
||||
VIEWER: 'viewer'
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限组合
|
||||
*/
|
||||
export const PERMISSION_GROUPS = {
|
||||
// 养殖场管理权限组
|
||||
FARM_MANAGEMENT: [
|
||||
PERMISSIONS.FARM_VIEW,
|
||||
PERMISSIONS.FARM_CREATE,
|
||||
PERMISSIONS.FARM_UPDATE,
|
||||
PERMISSIONS.FARM_DELETE,
|
||||
PERMISSIONS.FARM_EXPORT
|
||||
],
|
||||
|
||||
// 设备管理权限组
|
||||
DEVICE_MANAGEMENT: [
|
||||
PERMISSIONS.DEVICE_VIEW,
|
||||
PERMISSIONS.DEVICE_CREATE,
|
||||
PERMISSIONS.DEVICE_UPDATE,
|
||||
PERMISSIONS.DEVICE_DELETE,
|
||||
PERMISSIONS.DEVICE_CONTROL
|
||||
],
|
||||
|
||||
// 监控管理权限组
|
||||
MONITOR_MANAGEMENT: [
|
||||
PERMISSIONS.MONITOR_VIEW,
|
||||
PERMISSIONS.MONITOR_ALERT,
|
||||
PERMISSIONS.MONITOR_REPORT
|
||||
],
|
||||
|
||||
// 数据管理权限组
|
||||
DATA_MANAGEMENT: [
|
||||
PERMISSIONS.DATA_VIEW,
|
||||
PERMISSIONS.DATA_EXPORT,
|
||||
PERMISSIONS.DATA_ANALYSIS
|
||||
],
|
||||
|
||||
// 用户管理权限组
|
||||
USER_MANAGEMENT: [
|
||||
PERMISSIONS.USER_VIEW,
|
||||
PERMISSIONS.USER_CREATE,
|
||||
PERMISSIONS.USER_UPDATE,
|
||||
PERMISSIONS.USER_DELETE
|
||||
],
|
||||
|
||||
// 系统管理权限组
|
||||
SYSTEM_MANAGEMENT: [
|
||||
PERMISSIONS.SYSTEM_CONFIG,
|
||||
PERMISSIONS.SYSTEM_LOG,
|
||||
PERMISSIONS.SYSTEM_BACKUP
|
||||
],
|
||||
|
||||
// 政府监管
|
||||
SUPERVISION_MANAGEMENT: [
|
||||
PERMISSIONS.SUPERVISION_VIEW,
|
||||
PERMISSIONS.SUPERVISION_CREATE,
|
||||
PERMISSIONS.SUPERVISION_UPDATE,
|
||||
PERMISSIONS.SUPERVISION_DELETE,
|
||||
PERMISSIONS.SUPERVISION_APPROVE,
|
||||
PERMISSIONS.SUPERVISION_EXPORT
|
||||
],
|
||||
|
||||
// 审批管理
|
||||
APPROVAL_MANAGEMENT: [
|
||||
PERMISSIONS.APPROVAL_VIEW,
|
||||
PERMISSIONS.APPROVAL_CREATE,
|
||||
PERMISSIONS.APPROVAL_UPDATE,
|
||||
PERMISSIONS.APPROVAL_DELETE,
|
||||
PERMISSIONS.APPROVAL_APPROVE,
|
||||
PERMISSIONS.APPROVAL_REJECT,
|
||||
PERMISSIONS.APPROVAL_EXPORT
|
||||
],
|
||||
|
||||
// 人员管理
|
||||
PERSONNEL_MANAGEMENT: [
|
||||
PERMISSIONS.PERSONNEL_VIEW,
|
||||
PERMISSIONS.PERSONNEL_CREATE,
|
||||
PERMISSIONS.PERSONNEL_UPDATE,
|
||||
PERMISSIONS.PERSONNEL_DELETE,
|
||||
PERMISSIONS.PERSONNEL_ASSIGN,
|
||||
PERMISSIONS.PERSONNEL_EXPORT
|
||||
],
|
||||
|
||||
// 设备仓库
|
||||
WAREHOUSE_MANAGEMENT: [
|
||||
PERMISSIONS.WAREHOUSE_VIEW,
|
||||
PERMISSIONS.WAREHOUSE_CREATE,
|
||||
PERMISSIONS.WAREHOUSE_UPDATE,
|
||||
PERMISSIONS.WAREHOUSE_DELETE,
|
||||
PERMISSIONS.WAREHOUSE_IN,
|
||||
PERMISSIONS.WAREHOUSE_OUT,
|
||||
PERMISSIONS.WAREHOUSE_EXPORT
|
||||
],
|
||||
|
||||
// 防疫管理
|
||||
EPIDEMIC_MANAGEMENT: [
|
||||
PERMISSIONS.EPIDEMIC_VIEW,
|
||||
PERMISSIONS.EPIDEMIC_CREATE,
|
||||
PERMISSIONS.EPIDEMIC_UPDATE,
|
||||
PERMISSIONS.EPIDEMIC_DELETE,
|
||||
PERMISSIONS.EPIDEMIC_PLAN,
|
||||
PERMISSIONS.EPIDEMIC_REPORT
|
||||
],
|
||||
|
||||
// 服务管理
|
||||
SERVICE_MANAGEMENT: [
|
||||
PERMISSIONS.SERVICE_VIEW,
|
||||
PERMISSIONS.SERVICE_CREATE,
|
||||
PERMISSIONS.SERVICE_UPDATE,
|
||||
PERMISSIONS.SERVICE_DELETE,
|
||||
PERMISSIONS.SERVICE_ASSIGN
|
||||
],
|
||||
|
||||
// 可视化大屏
|
||||
VISUALIZATION_MANAGEMENT: [
|
||||
PERMISSIONS.VISUALIZATION_VIEW,
|
||||
PERMISSIONS.VISUALIZATION_CONFIG,
|
||||
PERMISSIONS.VISUALIZATION_EXPORT
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限映射
|
||||
*/
|
||||
export const ROLE_PERMISSIONS = {
|
||||
[ROLES.SUPER_ADMIN]: [
|
||||
...PERMISSION_GROUPS.FARM_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.DEVICE_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.MONITOR_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.DATA_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.USER_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.SYSTEM_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.SUPERVISION_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.APPROVAL_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.PERSONNEL_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.WAREHOUSE_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.EPIDEMIC_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.SERVICE_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.VISUALIZATION_MANAGEMENT
|
||||
],
|
||||
|
||||
[ROLES.ADMIN]: [
|
||||
...PERMISSION_GROUPS.FARM_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.DEVICE_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.MONITOR_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.DATA_MANAGEMENT,
|
||||
PERMISSIONS.USER_VIEW,
|
||||
PERMISSIONS.USER_CREATE,
|
||||
PERMISSIONS.USER_UPDATE,
|
||||
...PERMISSION_GROUPS.SUPERVISION_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.APPROVAL_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.PERSONNEL_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.WAREHOUSE_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.EPIDEMIC_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.SERVICE_MANAGEMENT,
|
||||
PERMISSIONS.VISUALIZATION_VIEW,
|
||||
PERMISSIONS.VISUALIZATION_CONFIG
|
||||
],
|
||||
|
||||
[ROLES.MANAGER]: [
|
||||
...PERMISSION_GROUPS.FARM_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.DEVICE_MANAGEMENT,
|
||||
...PERMISSION_GROUPS.MONITOR_MANAGEMENT,
|
||||
PERMISSIONS.DATA_VIEW,
|
||||
PERMISSIONS.DATA_EXPORT,
|
||||
PERMISSIONS.SUPERVISION_VIEW,
|
||||
PERMISSIONS.SUPERVISION_CREATE,
|
||||
PERMISSIONS.SUPERVISION_UPDATE,
|
||||
PERMISSIONS.SUPERVISION_EXPORT,
|
||||
PERMISSIONS.APPROVAL_VIEW,
|
||||
PERMISSIONS.APPROVAL_APPROVE,
|
||||
PERMISSIONS.APPROVAL_REJECT,
|
||||
PERMISSIONS.PERSONNEL_VIEW,
|
||||
PERMISSIONS.PERSONNEL_ASSIGN,
|
||||
PERMISSIONS.WAREHOUSE_VIEW,
|
||||
PERMISSIONS.WAREHOUSE_IN,
|
||||
PERMISSIONS.WAREHOUSE_OUT,
|
||||
PERMISSIONS.EPIDEMIC_VIEW,
|
||||
PERMISSIONS.EPIDEMIC_PLAN,
|
||||
PERMISSIONS.SERVICE_VIEW,
|
||||
PERMISSIONS.SERVICE_ASSIGN,
|
||||
PERMISSIONS.VISUALIZATION_VIEW
|
||||
],
|
||||
|
||||
[ROLES.OPERATOR]: [
|
||||
PERMISSIONS.FARM_VIEW,
|
||||
PERMISSIONS.FARM_UPDATE,
|
||||
PERMISSIONS.DEVICE_VIEW,
|
||||
PERMISSIONS.DEVICE_CONTROL,
|
||||
PERMISSIONS.MONITOR_VIEW,
|
||||
PERMISSIONS.MONITOR_ALERT,
|
||||
PERMISSIONS.DATA_VIEW,
|
||||
PERMISSIONS.SUPERVISION_VIEW,
|
||||
PERMISSIONS.SUPERVISION_CREATE,
|
||||
PERMISSIONS.APPROVAL_VIEW,
|
||||
PERMISSIONS.PERSONNEL_VIEW,
|
||||
PERMISSIONS.WAREHOUSE_VIEW,
|
||||
PERMISSIONS.WAREHOUSE_IN,
|
||||
PERMISSIONS.WAREHOUSE_OUT,
|
||||
PERMISSIONS.EPIDEMIC_VIEW,
|
||||
PERMISSIONS.EPIDEMIC_CREATE,
|
||||
PERMISSIONS.SERVICE_VIEW,
|
||||
PERMISSIONS.VISUALIZATION_VIEW
|
||||
],
|
||||
|
||||
[ROLES.VIEWER]: [
|
||||
PERMISSIONS.FARM_VIEW,
|
||||
PERMISSIONS.DEVICE_VIEW,
|
||||
PERMISSIONS.MONITOR_VIEW,
|
||||
PERMISSIONS.DATA_VIEW,
|
||||
PERMISSIONS.SUPERVISION_VIEW,
|
||||
PERMISSIONS.APPROVAL_VIEW,
|
||||
PERMISSIONS.PERSONNEL_VIEW,
|
||||
PERMISSIONS.WAREHOUSE_VIEW,
|
||||
PERMISSIONS.EPIDEMIC_VIEW,
|
||||
PERMISSIONS.SERVICE_VIEW,
|
||||
PERMISSIONS.VISUALIZATION_VIEW
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色对应的权限列表
|
||||
*/
|
||||
export function getRolePermissions(role) {
|
||||
return ROLE_PERMISSIONS[role] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限是否属于某个权限组
|
||||
*/
|
||||
export function isPermissionInGroup(permission, group) {
|
||||
return PERMISSION_GROUPS[group]?.includes(permission) || false
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化权限显示名称
|
||||
*/
|
||||
export function formatPermissionName(permission) {
|
||||
const permissionNames = {
|
||||
// 养殖场管理
|
||||
'farm:view': '查看养殖场',
|
||||
'farm:create': '新增养殖场',
|
||||
'farm:update': '编辑养殖场',
|
||||
'farm:delete': '删除养殖场',
|
||||
'farm:export': '导出养殖场数据',
|
||||
|
||||
// 设备管理
|
||||
'device:view': '查看设备',
|
||||
'device:create': '新增设备',
|
||||
'device:update': '编辑设备',
|
||||
'device:delete': '删除设备',
|
||||
'device:control': '控制设备',
|
||||
|
||||
// 监控管理
|
||||
'monitor:view': '查看监控',
|
||||
'monitor:alert': '处理预警',
|
||||
'monitor:report': '生成报表',
|
||||
|
||||
// 数据管理
|
||||
'data:view': '查看数据',
|
||||
'data:export': '导出数据',
|
||||
'data:analysis': '数据分析',
|
||||
|
||||
// 用户管理
|
||||
'user:view': '查看用户',
|
||||
'user:create': '新增用户',
|
||||
'user:update': '编辑用户',
|
||||
'user:delete': '删除用户',
|
||||
|
||||
// 系统管理
|
||||
'system:config': '系统配置',
|
||||
'system:log': '系统日志',
|
||||
'system:backup': '系统备份'
|
||||
}
|
||||
|
||||
return permissionNames[permission] || permission
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化角色显示名称
|
||||
*/
|
||||
export function formatRoleName(role) {
|
||||
const roleNames = {
|
||||
'super_admin': '超级管理员',
|
||||
'admin': '管理员',
|
||||
'manager': '经理',
|
||||
'operator': '操作员',
|
||||
'viewer': '查看者'
|
||||
}
|
||||
|
||||
return roleNames[role] || role
|
||||
}
|
||||
307
government-admin/src/utils/request.js
Normal file
307
government-admin/src/utils/request.js
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* HTTP请求工具
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import router from '@/router'
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 添加认证token
|
||||
if (userStore.token) {
|
||||
config.headers.Authorization = `Bearer ${userStore.token}`
|
||||
}
|
||||
|
||||
// 添加请求ID用于追踪
|
||||
config.headers['X-Request-ID'] = generateRequestId()
|
||||
|
||||
// 添加时间戳防止缓存
|
||||
if (config.method === 'get') {
|
||||
config.params = {
|
||||
...config.params,
|
||||
_t: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
// 开发环境下打印请求信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🚀 Request:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
params: config.params,
|
||||
data: config.data
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ Request Error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const { data, config } = response
|
||||
|
||||
// 开发环境下打印响应信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ Response:', {
|
||||
url: config.url,
|
||||
status: response.status,
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 统一处理响应格式
|
||||
if (data && typeof data === 'object') {
|
||||
// 标准响应格式: { code, data, message }
|
||||
if (data.hasOwnProperty('code')) {
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
return {
|
||||
data: data.data,
|
||||
message: data.message,
|
||||
success: true
|
||||
}
|
||||
} else {
|
||||
// 业务错误
|
||||
const errorMessage = data.message || '请求失败'
|
||||
message.error(errorMessage)
|
||||
return Promise.reject(new Error(errorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
// 直接返回数据
|
||||
return {
|
||||
data: data,
|
||||
success: true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: data,
|
||||
success: true
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
const { response, config } = error
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
console.error('❌ Response Error:', error)
|
||||
|
||||
// 网络错误
|
||||
if (!response) {
|
||||
const errorMessage = '网络连接失败,请检查网络设置'
|
||||
message.error(errorMessage)
|
||||
|
||||
// 添加系统通知
|
||||
notificationStore.addNotification({
|
||||
type: 'error',
|
||||
title: '网络错误',
|
||||
content: errorMessage,
|
||||
category: 'system'
|
||||
})
|
||||
|
||||
return Promise.reject(new Error(errorMessage))
|
||||
}
|
||||
|
||||
const { status, data } = response
|
||||
let errorMessage = '请求失败'
|
||||
|
||||
// 根据状态码处理不同错误
|
||||
switch (status) {
|
||||
case 400:
|
||||
errorMessage = data?.message || '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
errorMessage = '登录已过期,请重新登录'
|
||||
handleUnauthorized()
|
||||
break
|
||||
case 403:
|
||||
errorMessage = '没有权限访问该资源'
|
||||
break
|
||||
case 404:
|
||||
errorMessage = '请求的资源不存在'
|
||||
break
|
||||
case 422:
|
||||
errorMessage = data?.message || '数据验证失败'
|
||||
break
|
||||
case 429:
|
||||
errorMessage = '请求过于频繁,请稍后再试'
|
||||
break
|
||||
case 500:
|
||||
errorMessage = '服务器内部错误'
|
||||
break
|
||||
case 502:
|
||||
errorMessage = '网关错误'
|
||||
break
|
||||
case 503:
|
||||
errorMessage = '服务暂时不可用'
|
||||
break
|
||||
case 504:
|
||||
errorMessage = '请求超时'
|
||||
break
|
||||
default:
|
||||
errorMessage = data?.message || `请求失败 (${status})`
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
if (status !== 401) { // 401错误由handleUnauthorized处理
|
||||
message.error(errorMessage)
|
||||
}
|
||||
|
||||
// 添加错误通知
|
||||
notificationStore.addNotification({
|
||||
type: 'error',
|
||||
title: '请求错误',
|
||||
content: `${config.url}: ${errorMessage}`,
|
||||
category: 'system'
|
||||
})
|
||||
|
||||
return Promise.reject(new Error(errorMessage))
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 处理未授权错误
|
||||
*/
|
||||
function handleUnauthorized() {
|
||||
const userStore = useUserStore()
|
||||
|
||||
Modal.confirm({
|
||||
title: '登录已过期',
|
||||
content: '您的登录状态已过期,请重新登录',
|
||||
okText: '重新登录',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求ID
|
||||
*/
|
||||
function generateRequestId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求方法封装
|
||||
*/
|
||||
export const http = {
|
||||
get: (url, config = {}) => request.get(url, config),
|
||||
post: (url, data = {}, config = {}) => request.post(url, data, config),
|
||||
put: (url, data = {}, config = {}) => request.put(url, data, config),
|
||||
patch: (url, data = {}, config = {}) => request.patch(url, data, config),
|
||||
delete: (url, config = {}) => request.delete(url, config),
|
||||
upload: (url, formData, config = {}) => {
|
||||
return request.post(url, formData, {
|
||||
...config,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
...config.headers
|
||||
}
|
||||
})
|
||||
},
|
||||
download: (url, config = {}) => {
|
||||
return request.get(url, {
|
||||
...config,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量请求
|
||||
*/
|
||||
export const batchRequest = (requests) => {
|
||||
return Promise.allSettled(requests.map(req => {
|
||||
const { method = 'get', url, data, config } = req
|
||||
return http[method](url, data, config)
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试请求
|
||||
*/
|
||||
export const retryRequest = async (requestFn, maxRetries = 3, delay = 1000) => {
|
||||
let lastError
|
||||
|
||||
for (let i = 0; i <= maxRetries; i++) {
|
||||
try {
|
||||
return await requestFn()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
|
||||
if (i < maxRetries) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消请求的控制器
|
||||
*/
|
||||
export const createCancelToken = () => {
|
||||
return axios.CancelToken.source()
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查请求是否被取消
|
||||
*/
|
||||
export const isCancel = axios.isCancel
|
||||
|
||||
/**
|
||||
* 请求缓存
|
||||
*/
|
||||
const requestCache = new Map()
|
||||
|
||||
export const cachedRequest = (key, requestFn, ttl = 5 * 60 * 1000) => {
|
||||
const cached = requestCache.get(key)
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < ttl) {
|
||||
return Promise.resolve(cached.data)
|
||||
}
|
||||
|
||||
return requestFn().then(data => {
|
||||
requestCache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除请求缓存
|
||||
*/
|
||||
export const clearRequestCache = (key) => {
|
||||
if (key) {
|
||||
requestCache.delete(key)
|
||||
} else {
|
||||
requestCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export default request
|
||||
681
government-admin/src/views/Dashboard.vue
Normal file
681
government-admin/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,681 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">仪表盘</h1>
|
||||
<p class="page-description">宁夏智慧养殖监管平台数据概览</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="data-card">
|
||||
<div class="data-card-content">
|
||||
<div class="data-card-icon farms">
|
||||
<home-outlined />
|
||||
</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-title">养殖场总数</div>
|
||||
<div class="data-card-value">{{ stats.totalFarms }}</div>
|
||||
<div class="data-card-trend up">
|
||||
<arrow-up-outlined />
|
||||
较上月 +12%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="data-card">
|
||||
<div class="data-card-content">
|
||||
<div class="data-card-icon devices">
|
||||
<monitor-outlined />
|
||||
</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-title">在线设备</div>
|
||||
<div class="data-card-value">{{ stats.onlineDevices }}</div>
|
||||
<div class="data-card-trend up">
|
||||
<arrow-up-outlined />
|
||||
在线率 95.2%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="data-card">
|
||||
<div class="data-card-content">
|
||||
<div class="data-card-icon animals">
|
||||
<bug-outlined />
|
||||
</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-title">动物总数</div>
|
||||
<div class="data-card-value">{{ stats.totalAnimals }}</div>
|
||||
<div class="data-card-trend up">
|
||||
<arrow-up-outlined />
|
||||
较上月 +8%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="data-card">
|
||||
<div class="data-card-content">
|
||||
<div class="data-card-icon alerts">
|
||||
<alert-outlined />
|
||||
</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-title">待处理预警</div>
|
||||
<div class="data-card-value">{{ stats.pendingAlerts }}</div>
|
||||
<div class="data-card-trend down">
|
||||
<arrow-down-outlined />
|
||||
较昨日 -5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="[16, 16]" class="charts-row">
|
||||
<!-- 养殖场分布地图 -->
|
||||
<a-col :xs="24" :lg="12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>养殖场分布地图</h3>
|
||||
<a-button type="link" @click="viewFullMap">查看详情</a-button>
|
||||
</div>
|
||||
<div class="map-container" ref="mapRef">
|
||||
<!-- 地图将在这里渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 设备状态统计 -->
|
||||
<a-col :xs="24" :lg="12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>设备状态统计</h3>
|
||||
<a-select v-model:value="deviceTimeRange" style="width: 120px">
|
||||
<a-select-option value="today">今日</a-select-option>
|
||||
<a-select-option value="week">本周</a-select-option>
|
||||
<a-select-option value="month">本月</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="chart-container" ref="deviceChartRef"></div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="charts-row">
|
||||
<!-- 动物健康趋势 -->
|
||||
<a-col :xs="24" :lg="16">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>动物健康趋势</h3>
|
||||
<a-radio-group v-model:value="healthTimeRange" button-style="solid" size="small">
|
||||
<a-radio-button value="7d">7天</a-radio-button>
|
||||
<a-radio-button value="30d">30天</a-radio-button>
|
||||
<a-radio-button value="90d">90天</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="chart-container" ref="healthChartRef"></div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 预警统计 -->
|
||||
<a-col :xs="24" :lg="8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>预警统计</h3>
|
||||
</div>
|
||||
<div class="alert-stats">
|
||||
<div class="alert-item">
|
||||
<div class="alert-level high">
|
||||
<exclamation-circle-outlined />
|
||||
</div>
|
||||
<div class="alert-info">
|
||||
<div class="alert-type">高级预警</div>
|
||||
<div class="alert-count">{{ alertStats.high }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item">
|
||||
<div class="alert-level medium">
|
||||
<warning-outlined />
|
||||
</div>
|
||||
<div class="alert-info">
|
||||
<div class="alert-type">中级预警</div>
|
||||
<div class="alert-count">{{ alertStats.medium }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item">
|
||||
<div class="alert-level low">
|
||||
<info-circle-outlined />
|
||||
</div>
|
||||
<div class="alert-info">
|
||||
<div class="alert-type">低级预警</div>
|
||||
<div class="alert-count">{{ alertStats.low }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>最新动态</h3>
|
||||
<a-button type="link" @click="viewAllNews">查看全部</a-button>
|
||||
</div>
|
||||
<a-list
|
||||
:data-source="recentNews"
|
||||
:loading="newsLoading"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<a href="#">{{ item.title }}</a>
|
||||
</template>
|
||||
<template #description>
|
||||
{{ item.description }}
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<div class="news-time">{{ item.time }}</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>快捷操作</h3>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<a-button type="primary" @click="addFarm" class="action-btn">
|
||||
<plus-outlined />
|
||||
新增养殖场
|
||||
</a-button>
|
||||
<a-button @click="deviceMonitor" class="action-btn">
|
||||
<monitor-outlined />
|
||||
设备监控
|
||||
</a-button>
|
||||
<a-button @click="generateReport" class="action-btn">
|
||||
<file-text-outlined />
|
||||
生成报表
|
||||
</a-button>
|
||||
<a-button @click="systemSettings" class="action-btn">
|
||||
<setting-outlined />
|
||||
系统设置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
HomeOutlined,
|
||||
MonitorOutlined,
|
||||
BugOutlined,
|
||||
AlertOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
WarningOutlined,
|
||||
InfoCircleOutlined,
|
||||
PlusOutlined,
|
||||
FileTextOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const mapRef = ref()
|
||||
const deviceChartRef = ref()
|
||||
const healthChartRef = ref()
|
||||
const deviceTimeRange = ref('today')
|
||||
const healthTimeRange = ref('7d')
|
||||
const newsLoading = ref(false)
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalFarms: 156,
|
||||
onlineDevices: 1248,
|
||||
totalAnimals: 25680,
|
||||
pendingAlerts: 12
|
||||
})
|
||||
|
||||
// 预警统计
|
||||
const alertStats = reactive({
|
||||
high: 3,
|
||||
medium: 7,
|
||||
low: 15
|
||||
})
|
||||
|
||||
// 最新动态
|
||||
const recentNews = ref([
|
||||
{
|
||||
title: '新增5家养殖场接入监管平台',
|
||||
description: '本月新增5家大型养殖场接入智慧监管平台,覆盖范围进一步扩大',
|
||||
time: '2小时前'
|
||||
},
|
||||
{
|
||||
title: '设备维护通知',
|
||||
description: '计划于本周末对部分监控设备进行例行维护,请提前做好准备',
|
||||
time: '5小时前'
|
||||
},
|
||||
{
|
||||
title: '月度数据报表已生成',
|
||||
description: '12月份养殖监管数据报表已生成完成,可在报表管理中查看',
|
||||
time: '1天前'
|
||||
},
|
||||
{
|
||||
title: '系统升级公告',
|
||||
description: '系统将于下周进行功能升级,新增多项智能分析功能',
|
||||
time: '2天前'
|
||||
}
|
||||
])
|
||||
|
||||
// 图表实例
|
||||
let deviceChart = null
|
||||
let healthChart = null
|
||||
|
||||
// 初始化设备状态图表
|
||||
const initDeviceChart = () => {
|
||||
if (!deviceChartRef.value) return
|
||||
|
||||
deviceChart = echarts.init(deviceChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '设备状态',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [
|
||||
{ value: 1186, name: '在线' },
|
||||
{ value: 45, name: '离线' },
|
||||
{ value: 17, name: '维护中' }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
deviceChart.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化健康趋势图表
|
||||
const initHealthChart = () => {
|
||||
if (!healthChartRef.value) return
|
||||
|
||||
healthChart = echarts.init(healthChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['健康', '亚健康', '异常']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '健康',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [120, 132, 101, 134, 90, 230, 210]
|
||||
},
|
||||
{
|
||||
name: '亚健康',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [220, 182, 191, 234, 290, 330, 310]
|
||||
},
|
||||
{
|
||||
name: '异常',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [150, 232, 201, 154, 190, 330, 410]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
healthChart.setOption(option)
|
||||
}
|
||||
|
||||
// 快捷操作
|
||||
const addFarm = () => {
|
||||
router.push('/farms?action=add')
|
||||
}
|
||||
|
||||
const deviceMonitor = () => {
|
||||
router.push('/devices')
|
||||
}
|
||||
|
||||
const generateReport = () => {
|
||||
router.push('/reports')
|
||||
}
|
||||
|
||||
const systemSettings = () => {
|
||||
router.push('/settings')
|
||||
}
|
||||
|
||||
const viewFullMap = () => {
|
||||
router.push('/farms?view=map')
|
||||
}
|
||||
|
||||
const viewAllNews = () => {
|
||||
// TODO: 实现查看全部动态功能
|
||||
console.log('查看全部动态')
|
||||
}
|
||||
|
||||
// 窗口大小变化处理
|
||||
const handleResize = () => {
|
||||
if (deviceChart) {
|
||||
deviceChart.resize()
|
||||
}
|
||||
if (healthChart) {
|
||||
healthChart.resize()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
// 初始化图表
|
||||
initDeviceChart()
|
||||
initHealthChart()
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
// 销毁图表实例
|
||||
if (deviceChart) {
|
||||
deviceChart.dispose()
|
||||
}
|
||||
if (healthChart) {
|
||||
healthChart.dispose()
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stats-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.data-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.data-card-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
margin-right: 16px;
|
||||
|
||||
&.farms {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
&.devices {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
&.animals {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
&.alerts {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.data-card-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.data-card-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-card-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.data-card-trend {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&.up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #f5222d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.card-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: 300px;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.alert-stats {
|
||||
padding: 24px;
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.alert-level {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
margin-right: 16px;
|
||||
|
||||
&.high {
|
||||
background: #f5222d;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background: #faad14;
|
||||
}
|
||||
|
||||
&.low {
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
flex: 1;
|
||||
|
||||
.alert-type {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alert-count {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.news-time {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
padding: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
.action-btn {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.data-card {
|
||||
padding: 16px;
|
||||
|
||||
.data-card-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-card-icon {
|
||||
margin-right: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
445
government-admin/src/views/Login.vue
Normal file
445
government-admin/src/views/Login.vue
Normal file
@@ -0,0 +1,445 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-background">
|
||||
<div class="background-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="login-content">
|
||||
<div class="login-form-container">
|
||||
<!-- Logo和标题 -->
|
||||
<div class="login-header">
|
||||
<img src="@/assets/images/favicon.svg" alt="Logo" class="logo" />
|
||||
<h1 class="title">宁夏养殖政府管理平台</h1>
|
||||
<p class="subtitle">政府端管理后台</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
class="login-form"
|
||||
@finish="handleLogin"
|
||||
>
|
||||
<a-form-item name="username">
|
||||
<a-input
|
||||
v-model:value="formData.username"
|
||||
size="large"
|
||||
placeholder="请输入用户名"
|
||||
:prefix="h(UserOutlined)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
v-model:value="formData.password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
:prefix="h(LockOutlined)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="captcha" v-if="showCaptcha">
|
||||
<div class="captcha-container">
|
||||
<a-input
|
||||
v-model:value="formData.captcha"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:prefix="h(SafetyOutlined)"
|
||||
style="flex: 1; margin-right: 12px;"
|
||||
/>
|
||||
<div class="captcha-image" @click="refreshCaptcha">
|
||||
<img :src="captchaUrl" alt="验证码" />
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<div class="login-options">
|
||||
<a-checkbox v-model:checked="formData.remember">
|
||||
记住我
|
||||
</a-checkbox>
|
||||
<a href="#" class="forgot-password">忘记密码?</a>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
block
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 其他登录方式 -->
|
||||
<div class="other-login" v-if="false">
|
||||
<a-divider>其他登录方式</a-divider>
|
||||
<div class="other-login-buttons">
|
||||
<a-button type="text" class="other-login-btn">
|
||||
<wechat-outlined />
|
||||
</a-button>
|
||||
<a-button type="text" class="other-login-btn">
|
||||
<alipay-outlined />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 版权信息 -->
|
||||
<div class="login-footer">
|
||||
<p>© 2025 宁夏智慧养殖监管平台 版权所有</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
UserOutlined,
|
||||
LockOutlined,
|
||||
SafetyOutlined,
|
||||
WechatOutlined,
|
||||
AlipayOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const showCaptcha = ref(false)
|
||||
const captchaUrl = ref('')
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
username: 'admin',
|
||||
password: '123456',
|
||||
captcha: '',
|
||||
remember: true
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度为6-20个字符', trigger: 'blur' }
|
||||
],
|
||||
captcha: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur', validator: () => showCaptcha.value }
|
||||
]
|
||||
}
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async (values) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const success = await authStore.login({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
captcha: values.captcha,
|
||||
remember: values.remember
|
||||
})
|
||||
|
||||
if (success) {
|
||||
message.success('登录成功')
|
||||
|
||||
// 跳转到首页或之前访问的页面
|
||||
const redirect = router.currentRoute.value.query.redirect || '/'
|
||||
router.push(redirect)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
|
||||
// 登录失败后显示验证码
|
||||
if (!showCaptcha.value) {
|
||||
showCaptcha.value = true
|
||||
refreshCaptcha()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新验证码
|
||||
const refreshCaptcha = () => {
|
||||
captchaUrl.value = `/api/auth/captcha?t=${Date.now()}`
|
||||
}
|
||||
|
||||
// 组件挂载时的处理
|
||||
onMounted(() => {
|
||||
// 如果已经登录,直接跳转到首页
|
||||
if (authStore.isAuthenticated) {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 初始化验证码(如果需要)
|
||||
if (showCaptcha.value) {
|
||||
refreshCaptcha()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f0f2f5; /* 使用纯色背景替代渐变色 */
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 119, 198, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.login-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper,
|
||||
.ant-input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.captcha-image {
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.forgot-password {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: #1890ff; /* 使用纯色背景替代渐变色 */
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: #40a9ff; /* 使用纯色背景替代渐变色 */
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.other-login {
|
||||
margin-top: 24px;
|
||||
|
||||
.ant-divider {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.other-login-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
|
||||
.other-login-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 480px) {
|
||||
.login-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
</style>
|
||||
63
government-admin/src/views/approval/ApprovedList.vue
Normal file
63
government-admin/src/views/approval/ApprovedList.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="approved-list">
|
||||
<a-card title="已审批" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="approvedList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'approved' ? 'green' : 'red'">
|
||||
{{ record.status === 'approved' ? '已通过' : '已拒绝' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">导出</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const approvedList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '申请标题', dataIndex: 'title', key: 'title' },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant' },
|
||||
{ title: '申请类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '审批人', dataIndex: 'approver', key: 'approver' },
|
||||
{ title: '审批时间', dataIndex: 'approveTime', key: 'approveTime' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
approvedList.value = [
|
||||
{
|
||||
id: 1,
|
||||
title: '养殖场许可证申请',
|
||||
applicant: '李四',
|
||||
type: '许可证',
|
||||
status: 'approved',
|
||||
approver: '管理员',
|
||||
approveTime: '2024-01-10'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approved-list {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
98
government-admin/src/views/approval/IdentityAuth.vue
Normal file
98
government-admin/src/views/approval/IdentityAuth.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="identity-auth">
|
||||
<a-card title="电子身份认证" :bordered="false">
|
||||
<a-tabs>
|
||||
<a-tab-pane key="pending" tab="待认证">
|
||||
<a-table
|
||||
:columns="authColumns"
|
||||
:data-source="pendingList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="primary" size="small">认证</a-button>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="completed" tab="已认证">
|
||||
<a-table
|
||||
:columns="completedColumns"
|
||||
:data-source="completedList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag color="green">已认证</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">证书</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const pendingList = ref([])
|
||||
const completedList = ref([])
|
||||
|
||||
const authColumns = [
|
||||
{ title: '申请人', dataIndex: 'name', key: 'name' },
|
||||
{ title: '身份证号', dataIndex: 'idCard', key: 'idCard' },
|
||||
{ title: '联系电话', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '申请时间', dataIndex: 'createTime', key: 'createTime' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const completedColumns = [
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '身份证号', dataIndex: 'idCard', key: 'idCard' },
|
||||
{ title: '认证状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '认证时间', dataIndex: 'authTime', key: 'authTime' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
pendingList.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '王五',
|
||||
idCard: '123456789012345678',
|
||||
phone: '13800138000',
|
||||
createTime: '2024-01-15'
|
||||
}
|
||||
]
|
||||
|
||||
completedList.value = [
|
||||
{
|
||||
id: 2,
|
||||
name: '赵六',
|
||||
idCard: '987654321098765432',
|
||||
status: 'verified',
|
||||
authTime: '2024-01-10'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.identity-auth {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
887
government-admin/src/views/approval/LicenseApproval.vue
Normal file
887
government-admin/src/views/approval/LicenseApproval.vue
Normal file
@@ -0,0 +1,887 @@
|
||||
<template>
|
||||
<div class="license-approval">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1 class="page-title">许可证审批</h1>
|
||||
<p class="page-description">管理养殖许可证申请、审批和证书发放</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增申请
|
||||
</a-button>
|
||||
<a-button @click="exportData" style="margin-left: 8px">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card pending">
|
||||
<a-statistic
|
||||
title="待审批"
|
||||
:value="stats.pending"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
>
|
||||
<template #prefix><ClockCircleOutlined /></template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card approved">
|
||||
<a-statistic
|
||||
title="已通过"
|
||||
:value="stats.approved"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix><CheckCircleOutlined /></template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card rejected">
|
||||
<a-statistic
|
||||
title="已拒绝"
|
||||
:value="stats.rejected"
|
||||
:value-style="{ color: '#ff4d4f' }"
|
||||
>
|
||||
<template #prefix><CloseCircleOutlined /></template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card expired">
|
||||
<a-statistic
|
||||
title="即将到期"
|
||||
:value="stats.expiring"
|
||||
:value-style="{ color: '#fa8c16' }"
|
||||
>
|
||||
<template #prefix><ExclamationCircleOutlined /></template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="searchForm" @finish="handleSearch">
|
||||
<a-form-item label="申请编号" name="applicationNo">
|
||||
<a-input v-model:value="searchForm.applicationNo" placeholder="请输入申请编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="申请人" name="applicant">
|
||||
<a-input v-model:value="searchForm.applicant" placeholder="请输入申请人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="许可证类型" name="licenseType">
|
||||
<a-select v-model:value="searchForm.licenseType" placeholder="请选择类型" style="width: 150px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="breeding">养殖许可证</a-select-option>
|
||||
<a-select-option value="transport">运输许可证</a-select-option>
|
||||
<a-select-option value="slaughter">屠宰许可证</a-select-option>
|
||||
<a-select-option value="feed">饲料生产许可证</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="审批状态" name="status">
|
||||
<a-select v-model:value="searchForm.status" placeholder="请选择状态" style="width: 120px">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="pending">待审批</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="expired">已过期</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="申请时间" name="dateRange">
|
||||
<a-range-picker v-model:value="searchForm.dateRange" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-card class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'licenseType'">
|
||||
<a-tag color="blue">{{ getLicenseTypeText(record.licenseType) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'validPeriod'">
|
||||
<span :class="{ 'text-warning': isExpiringSoon(record.validPeriod) }">
|
||||
{{ record.validPeriod }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="viewDetail(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleApproval(record)"
|
||||
v-if="record.status === 'pending'"
|
||||
>
|
||||
审批
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="downloadCertificate(record)"
|
||||
v-if="record.status === 'approved'"
|
||||
>
|
||||
下载证书
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="editRecord(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这条记录吗?"
|
||||
@confirm="deleteRecord(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="900px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<a-tab-pane key="basic" tab="基本信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="申请编号" name="applicationNo">
|
||||
<a-input v-model:value="formData.applicationNo" placeholder="系统自动生成" disabled />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="许可证类型" name="licenseType" required>
|
||||
<a-select v-model:value="formData.licenseType" placeholder="请选择许可证类型">
|
||||
<a-select-option value="breeding">养殖许可证</a-select-option>
|
||||
<a-select-option value="transport">运输许可证</a-select-option>
|
||||
<a-select-option value="slaughter">屠宰许可证</a-select-option>
|
||||
<a-select-option value="feed">饲料生产许可证</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="申请人" name="applicant" required>
|
||||
<a-input v-model:value="formData.applicant" placeholder="请输入申请人姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="phone" required>
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="身份证号" name="idCard" required>
|
||||
<a-input v-model:value="formData.idCard" placeholder="请输入身份证号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="企业名称" name="companyName">
|
||||
<a-input v-model:value="formData.companyName" placeholder="请输入企业名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="经营地址" name="address" required>
|
||||
<a-input v-model:value="formData.address" placeholder="请输入经营地址" />
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="business" tab="经营信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="经营范围" name="businessScope" required>
|
||||
<a-select
|
||||
v-model:value="formData.businessScope"
|
||||
mode="multiple"
|
||||
placeholder="请选择经营范围"
|
||||
>
|
||||
<a-select-option value="cattle">牛类养殖</a-select-option>
|
||||
<a-select-option value="sheep">羊类养殖</a-select-option>
|
||||
<a-select-option value="pig">猪类养殖</a-select-option>
|
||||
<a-select-option value="poultry">家禽养殖</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖规模" name="scale" required>
|
||||
<a-input-number
|
||||
v-model:value="formData.scale"
|
||||
placeholder="请输入养殖规模"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
addon-after="头/只"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="占地面积" name="area" required>
|
||||
<a-input-number
|
||||
v-model:value="formData.area"
|
||||
placeholder="请输入占地面积"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
addon-after="平方米"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="投资金额" name="investment">
|
||||
<a-input-number
|
||||
v-model:value="formData.investment"
|
||||
placeholder="请输入投资金额"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
addon-after="万元"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="经营计划" name="businessPlan">
|
||||
<a-textarea
|
||||
v-model:value="formData.businessPlan"
|
||||
placeholder="请输入经营计划"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="documents" tab="申请材料">
|
||||
<a-form-item label="申请材料" name="documents">
|
||||
<a-upload
|
||||
v-model:file-list="formData.documents"
|
||||
name="file"
|
||||
multiple
|
||||
:before-upload="beforeUpload"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon><UploadOutlined /></template>
|
||||
上传文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div class="upload-tips">
|
||||
<p>请上传以下材料:</p>
|
||||
<ul>
|
||||
<li>身份证复印件</li>
|
||||
<li>营业执照(企业申请)</li>
|
||||
<li>土地使用证明</li>
|
||||
<li>环评报告</li>
|
||||
<li>其他相关证明材料</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 审批弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="approvalModalVisible"
|
||||
title="许可证审批"
|
||||
width="700px"
|
||||
@ok="handleApprovalSubmit"
|
||||
@cancel="approvalModalVisible = false"
|
||||
>
|
||||
<a-form
|
||||
ref="approvalFormRef"
|
||||
:model="approvalForm"
|
||||
:rules="approvalRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="审批结果" name="result" required>
|
||||
<a-radio-group v-model:value="approvalForm.result">
|
||||
<a-radio value="approved">通过</a-radio>
|
||||
<a-radio value="rejected">拒绝</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审批意见" name="opinion" required>
|
||||
<a-textarea
|
||||
v-model:value="approvalForm.opinion"
|
||||
placeholder="请输入审批意见"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div v-if="approvalForm.result === 'approved'">
|
||||
<a-form-item label="许可证编号" name="licenseNo">
|
||||
<a-input v-model:value="approvalForm.licenseNo" placeholder="请输入许可证编号" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="有效期开始" name="validFrom">
|
||||
<a-date-picker
|
||||
v-model:value="approvalForm.validFrom"
|
||||
style="width: 100%"
|
||||
placeholder="请选择开始日期"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="有效期结束" name="validTo">
|
||||
<a-date-picker
|
||||
v-model:value="approvalForm.validTo"
|
||||
style="width: 100%"
|
||||
placeholder="请选择结束日期"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined,
|
||||
ClockCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
UploadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const approvalModalVisible = ref(false)
|
||||
const modalTitle = ref('新增申请')
|
||||
const currentRecord = ref(null)
|
||||
const activeTab = ref('basic')
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
pending: 25,
|
||||
approved: 186,
|
||||
rejected: 12,
|
||||
expiring: 8
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
applicationNo: '',
|
||||
applicant: '',
|
||||
licenseType: '',
|
||||
status: '',
|
||||
dateRange: null
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
applicationNo: '',
|
||||
licenseType: '',
|
||||
applicant: '',
|
||||
phone: '',
|
||||
idCard: '',
|
||||
companyName: '',
|
||||
address: '',
|
||||
businessScope: [],
|
||||
scale: null,
|
||||
area: null,
|
||||
investment: null,
|
||||
businessPlan: '',
|
||||
documents: []
|
||||
})
|
||||
|
||||
// 审批表单
|
||||
const approvalForm = reactive({
|
||||
result: '',
|
||||
opinion: '',
|
||||
licenseNo: '',
|
||||
validFrom: null,
|
||||
validTo: null
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '申请编号',
|
||||
dataIndex: 'applicationNo',
|
||||
key: 'applicationNo',
|
||||
width: 150,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '许可证类型',
|
||||
dataIndex: 'licenseType',
|
||||
key: 'licenseType',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '申请人',
|
||||
dataIndex: 'applicant',
|
||||
key: 'applicant',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '经营地址',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '有效期',
|
||||
dataIndex: 'validPeriod',
|
||||
key: 'validPeriod',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 250,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
licenseType: [{ required: true, message: '请选择许可证类型' }],
|
||||
applicant: [{ required: true, message: '请输入申请人姓名' }],
|
||||
phone: [{ required: true, message: '请输入联系电话' }],
|
||||
idCard: [{ required: true, message: '请输入身份证号' }],
|
||||
address: [{ required: true, message: '请输入经营地址' }],
|
||||
businessScope: [{ required: true, message: '请选择经营范围' }],
|
||||
scale: [{ required: true, message: '请输入养殖规模' }],
|
||||
area: [{ required: true, message: '请输入占地面积' }]
|
||||
}
|
||||
|
||||
const approvalRules = {
|
||||
result: [{ required: true, message: '请选择审批结果' }],
|
||||
opinion: [{ required: true, message: '请输入审批意见' }]
|
||||
}
|
||||
|
||||
// 方法
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
tableData.value = [
|
||||
{
|
||||
id: 1,
|
||||
applicationNo: 'XK202401001',
|
||||
licenseType: 'breeding',
|
||||
applicant: '张三',
|
||||
phone: '13800138001',
|
||||
address: '内蒙古呼和浩特市某养殖场',
|
||||
status: 'pending',
|
||||
createTime: '2024-01-15 09:30:00',
|
||||
validPeriod: '2024-02-01 至 2027-02-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
applicationNo: 'XK202401002',
|
||||
licenseType: 'transport',
|
||||
applicant: '李四',
|
||||
phone: '13800138002',
|
||||
address: '内蒙古包头市某运输公司',
|
||||
status: 'approved',
|
||||
createTime: '2024-01-14 14:20:00',
|
||||
validPeriod: '2024-01-20 至 2027-01-20'
|
||||
}
|
||||
]
|
||||
|
||||
pagination.total = 50
|
||||
} catch (error) {
|
||||
message.error('获取数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.keys(searchForm).forEach(key => {
|
||||
searchForm[key] = key === 'dateRange' ? null : ''
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const showAddModal = () => {
|
||||
modalTitle.value = '新增申请'
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const editRecord = (record) => {
|
||||
modalTitle.value = '编辑申请'
|
||||
currentRecord.value = record
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key === 'businessScope') {
|
||||
formData[key] = record[key] || []
|
||||
} else if (key === 'documents') {
|
||||
formData[key] = record[key] || []
|
||||
} else {
|
||||
formData[key] = record[key] || (typeof formData[key] === 'number' ? null : '')
|
||||
}
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key === 'businessScope' || key === 'documents') {
|
||||
formData[key] = []
|
||||
} else if (typeof formData[key] === 'number') {
|
||||
formData[key] = null
|
||||
} else {
|
||||
formData[key] = ''
|
||||
}
|
||||
})
|
||||
formData.applicationNo = 'XK' + Date.now()
|
||||
activeTab.value = 'basic'
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 表单验证
|
||||
// await formRef.value.validate()
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
message.success(currentRecord.value ? '更新成功' : '创建成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
currentRecord.value = null
|
||||
}
|
||||
|
||||
const handleApproval = (record) => {
|
||||
currentRecord.value = record
|
||||
Object.keys(approvalForm).forEach(key => {
|
||||
approvalForm[key] = key.includes('valid') ? null : ''
|
||||
})
|
||||
approvalModalVisible.value = true
|
||||
}
|
||||
|
||||
const handleApprovalSubmit = async () => {
|
||||
try {
|
||||
// 表单验证
|
||||
// await approvalFormRef.value.validate()
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
message.success('审批完成')
|
||||
approvalModalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
message.error('审批失败')
|
||||
}
|
||||
}
|
||||
|
||||
const viewDetail = (record) => {
|
||||
message.info('查看详情功能待实现')
|
||||
}
|
||||
|
||||
const downloadCertificate = (record) => {
|
||||
message.info('下载证书功能待实现')
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
message.info('导出数据功能待实现')
|
||||
}
|
||||
|
||||
const deleteRecord = async (id) => {
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
message.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isLt10M) {
|
||||
message.error('文件大小不能超过10MB!')
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
const handleRemove = (file) => {
|
||||
const index = formData.documents.indexOf(file)
|
||||
const newFileList = formData.documents.slice()
|
||||
newFileList.splice(index, 1)
|
||||
formData.documents = newFileList
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
expired: 'default'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: '待审批',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
expired: '已过期'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
const getLicenseTypeText = (type) => {
|
||||
const texts = {
|
||||
breeding: '养殖许可证',
|
||||
transport: '运输许可证',
|
||||
slaughter: '屠宰许可证',
|
||||
feed: '饲料生产许可证'
|
||||
}
|
||||
return texts[type] || '未知'
|
||||
}
|
||||
|
||||
const isExpiringSoon = (validPeriod) => {
|
||||
// 简单判断是否即将到期的逻辑
|
||||
return validPeriod && validPeriod.includes('2024-02')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.license-approval {
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.header-content {
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.pending {
|
||||
border-left: 4px solid #faad14;
|
||||
}
|
||||
|
||||
&.approved {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
border-left: 4px solid #ff4d4f;
|
||||
}
|
||||
|
||||
&.expired {
|
||||
border-left: 4px solid #fa8c16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-card,
|
||||
.table-card {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-card {
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #fa8c16;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-tips {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
background: #f6f8fa;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-modal) {
|
||||
.ant-modal-header {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-tab {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
111
government-admin/src/views/approval/LoanSupervision.vue
Normal file
111
government-admin/src/views/approval/LoanSupervision.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="loan-supervision">
|
||||
<a-card title="惠农贷款监管" :bordered="false">
|
||||
<a-row :gutter="16" style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="贷款总额" :value="statistics.totalAmount" suffix="万元" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="已放贷" :value="statistics.loanedAmount" suffix="万元" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="待审核" :value="statistics.pendingCount" suffix="笔" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="逾期率" :value="statistics.overdueRate" suffix="%" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="loanList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">监管</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const loanList = ref([])
|
||||
|
||||
const statistics = reactive({
|
||||
totalAmount: 5000,
|
||||
loanedAmount: 3500,
|
||||
pendingCount: 25,
|
||||
overdueRate: 2.5
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '借款人', dataIndex: 'borrower', key: 'borrower' },
|
||||
{ title: '贷款金额', dataIndex: 'amount', key: 'amount' },
|
||||
{ title: '贷款用途', dataIndex: 'purpose', key: 'purpose' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '申请时间', dataIndex: 'applyTime', key: 'applyTime' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
overdue: 'red'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已批准',
|
||||
rejected: '已拒绝',
|
||||
overdue: '逾期'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loanList.value = [
|
||||
{
|
||||
id: 1,
|
||||
borrower: '农户张三',
|
||||
amount: '50万元',
|
||||
purpose: '养殖场扩建',
|
||||
status: 'pending',
|
||||
applyTime: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
borrower: '农户李四',
|
||||
amount: '30万元',
|
||||
purpose: '购买饲料',
|
||||
status: 'approved',
|
||||
applyTime: '2024-01-10'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loan-supervision {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
59
government-admin/src/views/approval/PendingApproval.vue
Normal file
59
government-admin/src/views/approval/PendingApproval.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="pending-approval">
|
||||
<a-card title="待审批" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="approvalList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag color="orange">待审批</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="primary" size="small">审批</a-button>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const approvalList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '申请标题', dataIndex: 'title', key: 'title' },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant' },
|
||||
{ title: '申请类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '申请时间', dataIndex: 'createTime', key: 'createTime' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
approvalList.value = [
|
||||
{
|
||||
id: 1,
|
||||
title: '养殖场许可证申请',
|
||||
applicant: '张三',
|
||||
type: '许可证',
|
||||
status: 'pending',
|
||||
createTime: '2024-01-15'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pending-approval {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
144
government-admin/src/views/breeding/AnimalTracking.vue
Normal file
144
government-admin/src/views/breeding/AnimalTracking.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="animal-tracking">
|
||||
<a-card title="动物溯源" :bordered="false">
|
||||
<a-tabs>
|
||||
<a-tab-pane key="search" tab="溯源查询">
|
||||
<div class="tracking-search">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="耳标号">
|
||||
<a-input v-model:value="searchForm.earTag" placeholder="请输入耳标号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="养殖场">
|
||||
<a-select v-model:value="searchForm.farm" placeholder="请选择养殖场" style="width: 200px">
|
||||
<a-select-option value="farm1">阳光养殖场</a-select-option>
|
||||
<a-select-option value="farm2">绿野养牛场</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="trackingColumns"
|
||||
:data-source="trackingList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">查看轨迹</a-button>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="register" tab="动物登记">
|
||||
<a-form :model="registerForm" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="耳标号">
|
||||
<a-input v-model:value="registerForm.earTag" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="动物种类">
|
||||
<a-select v-model:value="registerForm.species">
|
||||
<a-select-option value="pig">猪</a-select-option>
|
||||
<a-select-option value="cattle">牛</a-select-option>
|
||||
<a-select-option value="sheep">羊</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="出生日期">
|
||||
<a-date-picker v-model:value="registerForm.birthDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖场">
|
||||
<a-select v-model:value="registerForm.farm">
|
||||
<a-select-option value="farm1">阳光养殖场</a-select-option>
|
||||
<a-select-option value="farm2">绿野养牛场</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item>
|
||||
<a-button type="primary">登记</a-button>
|
||||
<a-button style="margin-left: 8px">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const trackingList = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
earTag: '',
|
||||
farm: undefined
|
||||
})
|
||||
|
||||
const registerForm = reactive({
|
||||
earTag: '',
|
||||
species: undefined,
|
||||
birthDate: null,
|
||||
farm: undefined
|
||||
})
|
||||
|
||||
const trackingColumns = [
|
||||
{ title: '耳标号', dataIndex: 'earTag', key: 'earTag' },
|
||||
{ title: '动物种类', dataIndex: 'species', key: 'species' },
|
||||
{ title: '出生日期', dataIndex: 'birthDate', key: 'birthDate' },
|
||||
{ title: '当前位置', dataIndex: 'location', key: 'location' },
|
||||
{ title: '健康状态', dataIndex: 'health', key: 'health' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
trackingList.value = [
|
||||
{
|
||||
id: 1,
|
||||
earTag: 'PIG001',
|
||||
species: '猪',
|
||||
birthDate: '2023-06-15',
|
||||
location: '阳光养殖场',
|
||||
health: '健康'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
earTag: 'COW001',
|
||||
species: '牛',
|
||||
birthDate: '2023-03-20',
|
||||
location: '绿野养牛场',
|
||||
health: '健康'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animal-tracking {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.tracking-search {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
578
government-admin/src/views/breeding/BreedingFarmList.vue
Normal file
578
government-admin/src/views/breeding/BreedingFarmList.vue
Normal file
@@ -0,0 +1,578 @@
|
||||
<template>
|
||||
<div class="breeding-farm-list">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="养殖场管理"
|
||||
description="管理和监控所有注册的养殖场信息"
|
||||
icon="bank"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增养殖场
|
||||
</a-button>
|
||||
<a-button @click="handleExport">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出数据
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<SearchForm
|
||||
:fields="searchFields"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'scale'">
|
||||
<a-tag color="blue">{{ getScaleText(record.scale) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个养殖场吗?"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="800px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖场名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入养殖场名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="负责人" name="manager">
|
||||
<a-input v-model:value="formData.manager" placeholder="请输入负责人姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖规模" name="scale">
|
||||
<a-select v-model:value="formData.scale" placeholder="请选择养殖规模">
|
||||
<a-select-option value="small">小型(<1000头)</a-select-option>
|
||||
<a-select-option value="medium">中型(1000-5000头)</a-select-option>
|
||||
<a-select-option value="large">大型(>5000头)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖品种" name="breed">
|
||||
<a-select v-model:value="formData.breed" placeholder="请选择养殖品种">
|
||||
<a-select-option value="pig">生猪</a-select-option>
|
||||
<a-select-option value="cattle">牛</a-select-option>
|
||||
<a-select-option value="sheep">羊</a-select-option>
|
||||
<a-select-option value="chicken">鸡</a-select-option>
|
||||
<a-select-option value="duck">鸭</a-select-option>
|
||||
<a-select-option value="fish">鱼</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formData.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">正常运营</a-select-option>
|
||||
<a-select-option value="suspended">暂停运营</a-select-option>
|
||||
<a-select-option value="closed">已关闭</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="详细地址" name="address">
|
||||
<a-textarea
|
||||
v-model:value="formData.address"
|
||||
placeholder="请输入详细地址"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="detailVisible"
|
||||
title="养殖场详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="养殖场名称">
|
||||
{{ currentRecord?.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="负责人">
|
||||
{{ currentRecord?.manager }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ currentRecord?.phone }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="养殖规模">
|
||||
<a-tag color="blue">{{ getScaleText(currentRecord?.scale) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="养殖品种">
|
||||
{{ getBreedText(currentRecord?.breed) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(currentRecord?.status)">
|
||||
{{ getStatusText(currentRecord?.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="注册时间" :span="2">
|
||||
{{ formatDate(currentRecord?.createdAt) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="详细地址" :span="2">
|
||||
{{ currentRecord?.address }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ currentRecord?.remark || '无' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ExportOutlined,
|
||||
BankOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
import SearchForm from '@/components/common/SearchForm.vue'
|
||||
import DataTable from '@/components/common/DataTable.vue'
|
||||
import { useBreedingStore } from '@/stores/breeding'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
const breedingStore = useBreedingStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const currentRecord = ref(null)
|
||||
const formRef = ref()
|
||||
|
||||
// 搜索条件
|
||||
const searchParams = reactive({
|
||||
name: '',
|
||||
manager: '',
|
||||
status: '',
|
||||
scale: '',
|
||||
breed: ''
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
scale: '',
|
||||
breed: '',
|
||||
status: 'active',
|
||||
address: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 搜索字段配置
|
||||
const searchFields = [
|
||||
{
|
||||
type: 'input',
|
||||
key: 'name',
|
||||
label: '养殖场名称',
|
||||
placeholder: '请输入养殖场名称'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
key: 'manager',
|
||||
label: '负责人',
|
||||
placeholder: '请输入负责人姓名'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '正常运营', value: 'active' },
|
||||
{ label: '暂停运营', value: 'suspended' },
|
||||
{ label: '已关闭', value: 'closed' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'scale',
|
||||
label: '养殖规模',
|
||||
placeholder: '请选择养殖规模',
|
||||
options: [
|
||||
{ label: '小型(<1000头)', value: 'small' },
|
||||
{ label: '中型(1000-5000头)', value: 'medium' },
|
||||
{ label: '大型(>5000头)', value: 'large' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'breed',
|
||||
label: '养殖品种',
|
||||
placeholder: '请选择养殖品种',
|
||||
options: [
|
||||
{ label: '生猪', value: 'pig' },
|
||||
{ label: '牛', value: 'cattle' },
|
||||
{ label: '羊', value: 'sheep' },
|
||||
{ label: '鸡', value: 'chicken' },
|
||||
{ label: '鸭', value: 'duck' },
|
||||
{ label: '鱼', value: 'fish' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '养殖场名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'manager',
|
||||
key: 'manager',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '养殖规模',
|
||||
dataIndex: 'scale',
|
||||
key: 'scale',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '养殖品种',
|
||||
dataIndex: 'breed',
|
||||
key: 'breed',
|
||||
width: 100,
|
||||
customRender: ({ text }) => getBreedText(text)
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180,
|
||||
customRender: ({ text }) => formatDate(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入养殖场名称', trigger: 'blur' }
|
||||
],
|
||||
manager: [
|
||||
{ required: true, message: '请输入负责人姓名', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
],
|
||||
scale: [
|
||||
{ required: true, message: '请选择养殖规模', trigger: 'change' }
|
||||
],
|
||||
breed: [
|
||||
{ required: true, message: '请选择养殖品种', trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
],
|
||||
address: [
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const dataSource = computed(() => breedingStore.farmList || [])
|
||||
const modalTitle = computed(() => formData.id ? '编辑养殖场' : '新增养殖场')
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
active: 'success',
|
||||
suspended: 'warning',
|
||||
closed: 'error'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
active: '正常运营',
|
||||
suspended: '暂停运营',
|
||||
closed: '已关闭'
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取规模文本
|
||||
const getScaleText = (scale) => {
|
||||
const textMap = {
|
||||
small: '小型(<1000头)',
|
||||
medium: '中型(1000-5000头)',
|
||||
large: '大型(>5000头)'
|
||||
}
|
||||
return textMap[scale] || '未知'
|
||||
}
|
||||
|
||||
// 获取品种文本
|
||||
const getBreedText = (breed) => {
|
||||
const textMap = {
|
||||
pig: '生猪',
|
||||
cattle: '牛',
|
||||
sheep: '羊',
|
||||
chicken: '鸡',
|
||||
duck: '鸭',
|
||||
fish: '鱼'
|
||||
}
|
||||
return textMap[breed] || '未知'
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
...searchParams,
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
|
||||
const result = await breedingStore.fetchFarmList(params)
|
||||
pagination.total = result.total
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (values) => {
|
||||
Object.assign(searchParams, values)
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.keys(searchParams).forEach(key => {
|
||||
searchParams[key] = ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag, filters, sorter) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增处理
|
||||
const handleAdd = () => {
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑处理
|
||||
const handleEdit = (record) => {
|
||||
Object.assign(formData, record)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看处理
|
||||
const handleView = (record) => {
|
||||
currentRecord.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 删除处理
|
||||
const handleDelete = async (record) => {
|
||||
try {
|
||||
await breedingStore.deleteFarm(record.id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出处理
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await breedingStore.exportFarmList(searchParams)
|
||||
message.success('导出成功')
|
||||
} catch (error) {
|
||||
message.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formData.id) {
|
||||
await breedingStore.updateFarm(formData.id, formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await breedingStore.createFarm(formData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
message.error('请检查表单输入')
|
||||
} else {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消处理
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
name: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
scale: '',
|
||||
breed: '',
|
||||
status: 'active',
|
||||
address: '',
|
||||
remark: ''
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breeding-farm-list {
|
||||
.ant-descriptions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
government-admin/src/views/breeding/FarmManagement.vue
Normal file
104
government-admin/src/views/breeding/FarmManagement.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="farm-management">
|
||||
<a-card title="养殖场管理" :bordered="false">
|
||||
<div class="search-form">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="养殖场名称">
|
||||
<a-input v-model:value="searchForm.name" placeholder="请输入养殖场名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="养殖类型">
|
||||
<a-select v-model:value="searchForm.type" placeholder="请选择养殖类型" style="width: 120px">
|
||||
<a-select-option value="pig">生猪</a-select-option>
|
||||
<a-select-option value="cattle">牛</a-select-option>
|
||||
<a-select-option value="poultry">家禽</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary">查询</a-button>
|
||||
<a-button style="margin-left: 8px">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="farmList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
|
||||
{{ record.status === 'active' ? '正常' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">编辑</a-button>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const farmList = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
type: undefined
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '养殖场名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '负责人', dataIndex: 'owner', key: 'owner' },
|
||||
{ title: '养殖类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '规模', dataIndex: 'scale', key: 'scale' },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
farmList.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '阳光养殖场',
|
||||
owner: '张三',
|
||||
type: '生猪',
|
||||
scale: '500头',
|
||||
address: '某县某镇某村',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '绿野养牛场',
|
||||
owner: '李四',
|
||||
type: '牛',
|
||||
scale: '200头',
|
||||
address: '某县某镇某村',
|
||||
status: 'active'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.farm-management {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
63
government-admin/src/views/business/ForageEnterprises.vue
Normal file
63
government-admin/src/views/business/ForageEnterprises.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="forage-enterprises">
|
||||
<a-card title="饲料企业管理" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="enterprisesList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
|
||||
{{ record.status === 'active' ? '正常' : '停业' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">监管</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const enterprisesList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '企业名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '法人代表', dataIndex: 'representative', key: 'representative' },
|
||||
{ title: '许可证号', dataIndex: 'licenseNo', key: 'licenseNo' },
|
||||
{ title: '联系电话', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
enterprisesList.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '绿源饲料有限公司',
|
||||
representative: '王总',
|
||||
licenseNo: 'FL2024001',
|
||||
phone: '0123-4567890',
|
||||
address: '工业园区A区',
|
||||
status: 'active'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.forage-enterprises {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
79
government-admin/src/views/business/InsuranceManagement.vue
Normal file
79
government-admin/src/views/business/InsuranceManagement.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="insurance-management">
|
||||
<a-card title="保险管理" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="insuranceList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">理赔</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const insuranceList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '保单号', dataIndex: 'policyNo', key: 'policyNo' },
|
||||
{ title: '投保人', dataIndex: 'insured', key: 'insured' },
|
||||
{ title: '保险类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '保险金额', dataIndex: 'amount', key: 'amount' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
active: 'green',
|
||||
expired: 'red',
|
||||
pending: 'orange'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
active: '有效',
|
||||
expired: '已过期',
|
||||
pending: '待审核'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
insuranceList.value = [
|
||||
{
|
||||
id: 1,
|
||||
policyNo: 'INS2024001',
|
||||
insured: '张三',
|
||||
type: '养殖保险',
|
||||
amount: '50万元',
|
||||
status: 'active'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.insurance-management {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
133
government-admin/src/views/business/MarketInfo.vue
Normal file
133
government-admin/src/views/business/MarketInfo.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="market-info">
|
||||
<a-card title="市场信息" :bordered="false">
|
||||
<a-row :gutter="16" style="margin-bottom: 24px;">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="生猪价格"
|
||||
:value="marketData.pigPrice"
|
||||
suffix="元/公斤"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="牛肉价格"
|
||||
:value="marketData.beefPrice"
|
||||
suffix="元/公斤"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="鸡蛋价格"
|
||||
:value="marketData.eggPrice"
|
||||
suffix="元/公斤"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="饲料价格"
|
||||
:value="marketData.feedPrice"
|
||||
suffix="元/吨"
|
||||
:value-style="{ color: '#fa8c16' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="priceList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'trend'">
|
||||
<a-tag :color="getTrendColor(record.trend)">
|
||||
{{ getTrendText(record.trend) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const priceList = ref([])
|
||||
|
||||
const marketData = reactive({
|
||||
pigPrice: 16.8,
|
||||
beefPrice: 68.5,
|
||||
eggPrice: 12.3,
|
||||
feedPrice: 3200
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '商品名称', dataIndex: 'product', key: 'product' },
|
||||
{ title: '当前价格', dataIndex: 'currentPrice', key: 'currentPrice' },
|
||||
{ title: '昨日价格', dataIndex: 'yesterdayPrice', key: 'yesterdayPrice' },
|
||||
{ title: '涨跌幅', dataIndex: 'change', key: 'change' },
|
||||
{ title: '趋势', dataIndex: 'trend', key: 'trend' },
|
||||
{ title: '更新时间', dataIndex: 'updateTime', key: 'updateTime' }
|
||||
]
|
||||
|
||||
const getTrendColor = (trend) => {
|
||||
const colors = {
|
||||
up: 'red',
|
||||
down: 'green',
|
||||
stable: 'blue'
|
||||
}
|
||||
return colors[trend] || 'default'
|
||||
}
|
||||
|
||||
const getTrendText = (trend) => {
|
||||
const texts = {
|
||||
up: '上涨',
|
||||
down: '下跌',
|
||||
stable: '持平'
|
||||
}
|
||||
return texts[trend] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
priceList.value = [
|
||||
{
|
||||
id: 1,
|
||||
product: '生猪',
|
||||
currentPrice: '16.8元/公斤',
|
||||
yesterdayPrice: '16.5元/公斤',
|
||||
change: '+1.8%',
|
||||
trend: 'up',
|
||||
updateTime: '2024-01-15 14:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
product: '牛肉',
|
||||
currentPrice: '68.5元/公斤',
|
||||
yesterdayPrice: '69.0元/公斤',
|
||||
change: '-0.7%',
|
||||
trend: 'down',
|
||||
updateTime: '2024-01-15 14:00'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.market-info {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
83
government-admin/src/views/business/SubsidyManagement.vue
Normal file
83
government-admin/src/views/business/SubsidyManagement.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="subsidy-management">
|
||||
<a-card title="补贴管理" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="subsidyList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">发放</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const subsidyList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '补贴编号', dataIndex: 'subsidyNo', key: 'subsidyNo' },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant' },
|
||||
{ title: '补贴类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '补贴金额', dataIndex: 'amount', key: 'amount' },
|
||||
{ title: '申请时间', dataIndex: 'applyTime', key: 'applyTime' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
paid: 'blue'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已批准',
|
||||
rejected: '已拒绝',
|
||||
paid: '已发放'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
subsidyList.value = [
|
||||
{
|
||||
id: 1,
|
||||
subsidyNo: 'SUB2024001',
|
||||
applicant: '张三',
|
||||
type: '养殖补贴',
|
||||
amount: '5000元',
|
||||
applyTime: '2024-01-10',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.subsidy-management {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
85
government-admin/src/views/business/TradingManagement.vue
Normal file
85
government-admin/src/views/business/TradingManagement.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="trading-management">
|
||||
<a-card title="交易管理" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tradingList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">审核</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tradingList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '交易编号', dataIndex: 'tradeNo', key: 'tradeNo' },
|
||||
{ title: '卖方', dataIndex: 'seller', key: 'seller' },
|
||||
{ title: '买方', dataIndex: 'buyer', key: 'buyer' },
|
||||
{ title: '商品', dataIndex: 'product', key: 'product' },
|
||||
{ title: '数量', dataIndex: 'quantity', key: 'quantity' },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
completed: 'blue'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已批准',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tradingList.value = [
|
||||
{
|
||||
id: 1,
|
||||
tradeNo: 'TR2024001',
|
||||
seller: '阳光养殖场',
|
||||
buyer: '某肉类加工厂',
|
||||
product: '生猪',
|
||||
quantity: '100头',
|
||||
amount: '50万元',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trading-management {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
81
government-admin/src/views/business/WasteCollection.vue
Normal file
81
government-admin/src/views/business/WasteCollection.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="waste-collection">
|
||||
<a-card title="废料收集管理" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="wasteList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">处理</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const wasteList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '收集编号', dataIndex: 'collectionNo', key: 'collectionNo' },
|
||||
{ title: '养殖场', dataIndex: 'farm', key: 'farm' },
|
||||
{ title: '废料类型', dataIndex: 'wasteType', key: 'wasteType' },
|
||||
{ title: '数量', dataIndex: 'quantity', key: 'quantity' },
|
||||
{ title: '收集时间', dataIndex: 'collectionTime', key: 'collectionTime' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
collected: 'blue',
|
||||
processing: 'orange',
|
||||
completed: 'green'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
collected: '已收集',
|
||||
processing: '处理中',
|
||||
completed: '已完成'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
wasteList.value = [
|
||||
{
|
||||
id: 1,
|
||||
collectionNo: 'WC2024001',
|
||||
farm: '阳光养殖场',
|
||||
wasteType: '粪便',
|
||||
quantity: '5吨',
|
||||
collectionTime: '2024-01-15',
|
||||
status: 'collected'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.waste-collection {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
975
government-admin/src/views/dashboard/Dashboard.vue
Normal file
975
government-admin/src/views/dashboard/Dashboard.vue
Normal file
@@ -0,0 +1,975 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="数据概览"
|
||||
description="畜牧业监管系统数据统计与分析"
|
||||
icon="dashboard"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-select v-model:value="timeRange" style="width: 120px" @change="handleTimeRangeChange">
|
||||
<a-select-option value="today">今日</a-select-option>
|
||||
<a-select-option value="week">本周</a-select-option>
|
||||
<a-select-option value="month">本月</a-select-option>
|
||||
<a-select-option value="year">本年</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="handleRefresh" :loading="refreshLoading">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleExportReport">
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
导出报表
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 核心指标卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="stats-cards">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stat-card">
|
||||
<a-statistic
|
||||
title="养殖场总数"
|
||||
:value="dashboardData.totalFarms"
|
||||
:precision="0"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<HomeOutlined />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<span class="stat-suffix">家</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span :class="['trend-text', getTrendClass(dashboardData.farmTrend)]">
|
||||
<CaretUpOutlined v-if="dashboardData.farmTrend > 0" />
|
||||
<CaretDownOutlined v-else-if="dashboardData.farmTrend < 0" />
|
||||
{{ Math.abs(dashboardData.farmTrend) }}%
|
||||
</span>
|
||||
<span class="trend-desc">较上期</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stat-card">
|
||||
<a-statistic
|
||||
title="动物总数"
|
||||
:value="dashboardData.totalAnimals"
|
||||
:precision="0"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<BugOutlined />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<span class="stat-suffix">头</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span :class="['trend-text', getTrendClass(dashboardData.animalTrend)]">
|
||||
<CaretUpOutlined v-if="dashboardData.animalTrend > 0" />
|
||||
<CaretDownOutlined v-else-if="dashboardData.animalTrend < 0" />
|
||||
{{ Math.abs(dashboardData.animalTrend) }}%
|
||||
</span>
|
||||
<span class="trend-desc">较上期</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stat-card">
|
||||
<a-statistic
|
||||
title="检查次数"
|
||||
:value="dashboardData.totalInspections"
|
||||
:precision="0"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<span class="stat-suffix">次</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span :class="['trend-text', getTrendClass(dashboardData.inspectionTrend)]">
|
||||
<CaretUpOutlined v-if="dashboardData.inspectionTrend > 0" />
|
||||
<CaretDownOutlined v-else-if="dashboardData.inspectionTrend < 0" />
|
||||
{{ Math.abs(dashboardData.inspectionTrend) }}%
|
||||
</span>
|
||||
<span class="trend-desc">较上期</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stat-card">
|
||||
<a-statistic
|
||||
title="预警事件"
|
||||
:value="dashboardData.totalAlerts"
|
||||
:precision="0"
|
||||
:value-style="{ color: '#ff4d4f' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<AlertOutlined />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<span class="stat-suffix">件</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span :class="['trend-text', getTrendClass(dashboardData.alertTrend)]">
|
||||
<CaretUpOutlined v-if="dashboardData.alertTrend > 0" />
|
||||
<CaretDownOutlined v-else-if="dashboardData.alertTrend < 0" />
|
||||
{{ Math.abs(dashboardData.alertTrend) }}%
|
||||
</span>
|
||||
<span class="trend-desc">较上期</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-section">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :span="12">
|
||||
<a-card title="养殖场数量趋势" class="chart-card">
|
||||
<LineChart
|
||||
:data="farmTrendData"
|
||||
:x-axis-data="trendXAxisData"
|
||||
title=""
|
||||
height="300px"
|
||||
:show-area="true"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="动物健康状况分布" class="chart-card">
|
||||
<PieChart
|
||||
:data="healthStatusData"
|
||||
title=""
|
||||
height="300px"
|
||||
:radius="['30%', '60%']"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card title="检查完成率" class="chart-card">
|
||||
<GaugeChart
|
||||
:value="inspectionCompletionRate"
|
||||
title=""
|
||||
height="250px"
|
||||
unit="%"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card title="月度检查数量" class="chart-card">
|
||||
<BarChart
|
||||
:data="monthlyInspectionData"
|
||||
:x-axis-data="monthXAxisData"
|
||||
title=""
|
||||
height="250px"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card title="应急响应时效" class="chart-card">
|
||||
<GaugeChart
|
||||
:value="emergencyResponseRate"
|
||||
title=""
|
||||
height="250px"
|
||||
unit="%"
|
||||
:color="[
|
||||
[0.3, '#fd666d'],
|
||||
[0.7, '#faad14'],
|
||||
[1, '#52c41a']
|
||||
]"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-card title="各地区养殖场分布" class="chart-card">
|
||||
<MapChart
|
||||
:data="regionDistributionData"
|
||||
title=""
|
||||
height="400px"
|
||||
map-name="china"
|
||||
:visual-map-max="500"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 快捷操作和最新动态 -->
|
||||
<a-row :gutter="[16, 16]" class="action-section">
|
||||
<!-- 快捷操作 -->
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="快捷操作" class="action-card">
|
||||
<div class="quick-actions">
|
||||
<a-row :gutter="[8, 8]">
|
||||
<a-col :span="12">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
@click="handleQuickAction('addFarm')"
|
||||
class="quick-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增养殖场
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-button
|
||||
block
|
||||
@click="handleQuickAction('inspection')"
|
||||
class="quick-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
开始检查
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-button
|
||||
block
|
||||
@click="handleQuickAction('emergency')"
|
||||
class="quick-btn emergency"
|
||||
>
|
||||
<template #icon>
|
||||
<AlertOutlined />
|
||||
</template>
|
||||
应急响应
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-button
|
||||
block
|
||||
@click="handleQuickAction('report')"
|
||||
class="quick-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<FileTextOutlined />
|
||||
</template>
|
||||
生成报表
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="最新动态" class="activity-card">
|
||||
<template #extra>
|
||||
<a @click="handleViewAllActivities">查看全部</a>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="recentActivities"
|
||||
size="small"
|
||||
:loading="activitiesLoading"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-avatar :style="{ backgroundColor: getActivityColor(item.type) }">
|
||||
<component :is="getActivityIcon(item.type)" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<span class="activity-title">{{ item.title }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="activity-desc">
|
||||
<div>{{ item.description }}</div>
|
||||
<div class="activity-time">{{ formatDateTime(item.createTime) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="待办事项" class="todo-card">
|
||||
<template #extra>
|
||||
<a-badge :count="todoList.length" :offset="[10, 0]">
|
||||
<a @click="handleViewAllTodos">查看全部</a>
|
||||
</a-badge>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="todoList"
|
||||
size="small"
|
||||
:loading="todoLoading"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div class="todo-item">
|
||||
<span class="todo-title">{{ item.title }}</span>
|
||||
<a-tag
|
||||
:color="getPriorityColor(item.priority)"
|
||||
size="small"
|
||||
>
|
||||
{{ getPriorityText(item.priority) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="todo-desc">
|
||||
<div>{{ item.description }}</div>
|
||||
<div class="todo-time">
|
||||
截止:{{ formatDateTime(item.deadline) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a @click="handleCompleteTodo(item)">完成</a>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 系统状态 -->
|
||||
<a-row :gutter="[16, 16]" class="system-section">
|
||||
<a-col :span="24">
|
||||
<a-card title="系统状态" class="system-card">
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<div class="system-item">
|
||||
<div class="system-label">数据库状态</div>
|
||||
<div class="system-value">
|
||||
<a-badge status="success" text="正常" />
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<div class="system-item">
|
||||
<div class="system-label">API服务</div>
|
||||
<div class="system-value">
|
||||
<a-badge status="success" text="运行中" />
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<div class="system-item">
|
||||
<div class="system-label">定时任务</div>
|
||||
<div class="system-value">
|
||||
<a-badge status="processing" text="执行中" />
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<div class="system-item">
|
||||
<div class="system-label">存储空间</div>
|
||||
<div class="system-value">
|
||||
<span>75.2% 已使用</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
HomeOutlined,
|
||||
BugOutlined,
|
||||
SearchOutlined,
|
||||
AlertOutlined,
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined,
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
import { LineChart, BarChart, PieChart, GaugeChart, MapChart } from '@/components/charts'
|
||||
import { useDashboardStore } from '@/stores/dashboard'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const router = useRouter()
|
||||
const dashboardStore = useDashboardStore()
|
||||
|
||||
// 响应式数据
|
||||
const timeRange = ref('month')
|
||||
const refreshLoading = ref(false)
|
||||
const activitiesLoading = ref(false)
|
||||
const todoLoading = ref(false)
|
||||
|
||||
// 图表引用
|
||||
const farmDistributionChart = ref()
|
||||
const healthStatusChart = ref()
|
||||
const inspectionTrendChart = ref()
|
||||
const alertStatChart = ref()
|
||||
|
||||
// 图表数据
|
||||
const farmTrendData = ref([
|
||||
{
|
||||
name: '养殖场数量',
|
||||
data: [120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330]
|
||||
}
|
||||
])
|
||||
|
||||
const trendXAxisData = ref([
|
||||
'1月', '2月', '3月', '4月', '5月', '6月',
|
||||
'7月', '8月', '9月', '10月', '11月', '12月'
|
||||
])
|
||||
|
||||
const healthStatusData = ref([
|
||||
{ name: '健康', value: 1548 },
|
||||
{ name: '亚健康', value: 735 },
|
||||
{ name: '疑似患病', value: 580 },
|
||||
{ name: '确诊患病', value: 484 },
|
||||
{ name: '康复中', value: 300 }
|
||||
])
|
||||
|
||||
const inspectionCompletionRate = ref(87.5)
|
||||
|
||||
const monthlyInspectionData = ref([
|
||||
{
|
||||
name: '检查数量',
|
||||
data: [820, 932, 901, 934, 1290, 1330, 1320, 1200, 1100, 1400, 1500, 1600]
|
||||
}
|
||||
])
|
||||
|
||||
const monthXAxisData = ref([
|
||||
'1月', '2月', '3月', '4月', '5月', '6月',
|
||||
'7月', '8月', '9月', '10月', '11月', '12月'
|
||||
])
|
||||
|
||||
const emergencyResponseRate = ref(92.3)
|
||||
|
||||
const regionDistributionData = ref([
|
||||
{ name: '北京', value: 177 },
|
||||
{ name: '天津', value: 42 },
|
||||
{ name: '河北', value: 102 },
|
||||
{ name: '山西', value: 81 },
|
||||
{ name: '内蒙古', value: 47 },
|
||||
{ name: '辽宁', value: 67 },
|
||||
{ name: '吉林', value: 82 },
|
||||
{ name: '黑龙江', value: 123 },
|
||||
{ name: '上海', value: 24 },
|
||||
{ name: '江苏', value: 215 },
|
||||
{ name: '浙江', value: 189 },
|
||||
{ name: '安徽', value: 134 },
|
||||
{ name: '福建', value: 156 },
|
||||
{ name: '江西', value: 98 },
|
||||
{ name: '山东', value: 345 },
|
||||
{ name: '河南', value: 267 },
|
||||
{ name: '湖北', value: 187 },
|
||||
{ name: '湖南', value: 234 },
|
||||
{ name: '广东', value: 456 },
|
||||
{ name: '广西', value: 123 },
|
||||
{ name: '海南', value: 45 },
|
||||
{ name: '重庆', value: 89 },
|
||||
{ name: '四川', value: 278 },
|
||||
{ name: '贵州', value: 67 },
|
||||
{ name: '云南', value: 134 },
|
||||
{ name: '西藏', value: 12 },
|
||||
{ name: '陕西', value: 156 },
|
||||
{ name: '甘肃', value: 78 },
|
||||
{ name: '青海', value: 23 },
|
||||
{ name: '宁夏', value: 34 },
|
||||
{ name: '新疆', value: 89 }
|
||||
])
|
||||
|
||||
// 仪表盘数据
|
||||
const dashboardData = reactive({
|
||||
totalFarms: 1245,
|
||||
farmTrend: 8.5,
|
||||
totalAnimals: 156789,
|
||||
animalTrend: 12.3,
|
||||
totalInspections: 2456,
|
||||
inspectionTrend: -3.2,
|
||||
totalAlerts: 23,
|
||||
alertTrend: -15.6
|
||||
})
|
||||
|
||||
// 最新动态
|
||||
const recentActivities = ref([
|
||||
{
|
||||
id: 1,
|
||||
type: 'inspection',
|
||||
title: '完成例行检查',
|
||||
description: '对阳光养殖场进行了例行检查,发现2项轻微问题',
|
||||
createTime: new Date(Date.now() - 2 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'alert',
|
||||
title: '健康预警',
|
||||
description: '绿野养殖场出现动物健康异常,已通知相关人员',
|
||||
createTime: new Date(Date.now() - 4 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'farm',
|
||||
title: '新增养殖场',
|
||||
description: '春天养殖场完成注册审核,正式纳入监管范围',
|
||||
createTime: new Date(Date.now() - 6 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'report',
|
||||
title: '月度报表生成',
|
||||
description: '2024年3月畜牧业监管月报已生成完成',
|
||||
createTime: new Date(Date.now() - 8 * 60 * 60 * 1000)
|
||||
}
|
||||
])
|
||||
|
||||
// 待办事项
|
||||
const todoList = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '处理健康预警',
|
||||
description: '绿野养殖场动物健康异常需要跟进处理',
|
||||
priority: 'high',
|
||||
deadline: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '审核养殖场申请',
|
||||
description: '3家新申请养殖场的资质审核',
|
||||
priority: 'medium',
|
||||
deadline: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '制定检查计划',
|
||||
description: '下月度检查计划制定和人员安排',
|
||||
priority: 'low',
|
||||
deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
])
|
||||
|
||||
// 获取趋势样式类
|
||||
const getTrendClass = (trend) => {
|
||||
if (trend > 0) return 'trend-up'
|
||||
if (trend < 0) return 'trend-down'
|
||||
return 'trend-flat'
|
||||
}
|
||||
|
||||
// 获取活动颜色
|
||||
const getActivityColor = (type) => {
|
||||
const colorMap = {
|
||||
inspection: '#1890ff',
|
||||
alert: '#ff4d4f',
|
||||
farm: '#52c41a',
|
||||
report: '#faad14'
|
||||
}
|
||||
return colorMap[type] || '#d9d9d9'
|
||||
}
|
||||
|
||||
// 获取活动图标
|
||||
const getActivityIcon = (type) => {
|
||||
const iconMap = {
|
||||
inspection: 'SearchOutlined',
|
||||
alert: 'AlertOutlined',
|
||||
farm: 'HomeOutlined',
|
||||
report: 'FileTextOutlined'
|
||||
}
|
||||
return iconMap[type] || 'InfoCircleOutlined'
|
||||
}
|
||||
|
||||
// 获取优先级颜色
|
||||
const getPriorityColor = (priority) => {
|
||||
const colorMap = {
|
||||
high: 'red',
|
||||
medium: 'orange',
|
||||
low: 'blue'
|
||||
}
|
||||
return colorMap[priority] || 'default'
|
||||
}
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority) => {
|
||||
const textMap = {
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
}
|
||||
return textMap[priority] || '普通'
|
||||
}
|
||||
|
||||
// 时间范围变化处理
|
||||
const handleTimeRangeChange = (value) => {
|
||||
loadDashboardData()
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
refreshLoading.value = true
|
||||
await loadDashboardData()
|
||||
message.success('数据刷新成功')
|
||||
} catch (error) {
|
||||
message.error('数据刷新失败')
|
||||
} finally {
|
||||
refreshLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出报表
|
||||
const handleExportReport = async () => {
|
||||
try {
|
||||
await dashboardStore.exportReport(timeRange.value)
|
||||
message.success('报表导出成功')
|
||||
} catch (error) {
|
||||
message.error('报表导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 图表操作
|
||||
const handleChartAction = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'refresh':
|
||||
loadChartData()
|
||||
break
|
||||
case 'export':
|
||||
message.info('图表导出功能开发中')
|
||||
break
|
||||
case 'fullscreen':
|
||||
message.info('全屏查看功能开发中')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷操作
|
||||
const handleQuickAction = (action) => {
|
||||
switch (action) {
|
||||
case 'addFarm':
|
||||
router.push('/breeding/farms')
|
||||
break
|
||||
case 'inspection':
|
||||
router.push('/inspection/management')
|
||||
break
|
||||
case 'emergency':
|
||||
router.push('/emergency/response')
|
||||
break
|
||||
case 'report':
|
||||
router.push('/reports/center')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 查看所有活动
|
||||
const handleViewAllActivities = () => {
|
||||
message.info('查看全部活动功能开发中')
|
||||
}
|
||||
|
||||
// 查看所有待办
|
||||
const handleViewAllTodos = () => {
|
||||
message.info('查看全部待办功能开发中')
|
||||
}
|
||||
|
||||
// 完成待办
|
||||
const handleCompleteTodo = async (todo) => {
|
||||
try {
|
||||
await dashboardStore.completeTodo(todo.id)
|
||||
const index = todoList.value.findIndex(item => item.id === todo.id)
|
||||
if (index > -1) {
|
||||
todoList.value.splice(index, 1)
|
||||
}
|
||||
message.success('待办事项已完成')
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载仪表盘数据
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
const data = await dashboardStore.fetchDashboardData(timeRange.value)
|
||||
Object.assign(dashboardData, data)
|
||||
|
||||
// 加载图表数据
|
||||
loadChartData()
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载图表数据
|
||||
const loadChartData = () => {
|
||||
// 这里应该初始化ECharts图表
|
||||
// 暂时用占位符代替
|
||||
if (farmDistributionChart.value) {
|
||||
farmDistributionChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">养殖场分布图表</div>'
|
||||
}
|
||||
|
||||
if (healthStatusChart.value) {
|
||||
healthStatusChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">健康状况图表</div>'
|
||||
}
|
||||
|
||||
if (inspectionTrendChart.value) {
|
||||
inspectionTrendChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">检查趋势图表</div>'
|
||||
}
|
||||
|
||||
if (alertStatChart.value) {
|
||||
alertStatChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">预警统计图表</div>'
|
||||
}
|
||||
}
|
||||
|
||||
// 加载活动数据
|
||||
const loadActivities = async () => {
|
||||
try {
|
||||
activitiesLoading.value = true
|
||||
const data = await dashboardStore.fetchRecentActivities()
|
||||
recentActivities.value = data
|
||||
} catch (error) {
|
||||
message.error('加载活动数据失败')
|
||||
} finally {
|
||||
activitiesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载待办数据
|
||||
const loadTodos = async () => {
|
||||
try {
|
||||
todoLoading.value = true
|
||||
const data = await dashboardStore.fetchTodoList()
|
||||
todoList.value = data
|
||||
} catch (error) {
|
||||
message.error('加载待办数据失败')
|
||||
} finally {
|
||||
todoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
loadDashboardData()
|
||||
loadActivities()
|
||||
loadTodos()
|
||||
|
||||
// 设置定时刷新
|
||||
const refreshInterval = setInterval(() => {
|
||||
loadDashboardData()
|
||||
}, 5 * 60 * 1000) // 5分钟刷新一次
|
||||
|
||||
// 保存定时器引用
|
||||
onUnmounted(() => {
|
||||
clearInterval(refreshInterval)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
PageHeader,
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
GaugeChart,
|
||||
MapChart
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.stat-card {
|
||||
.ant-statistic {
|
||||
.ant-statistic-title {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ant-statistic-content {
|
||||
.ant-statistic-content-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-suffix {
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.trend-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.trend-up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.trend-down {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.trend-flat {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.chart-card {
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.action-card {
|
||||
.quick-actions {
|
||||
.quick-btn {
|
||||
height: 48px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.emergency {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
.activity-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.todo-card {
|
||||
.todo-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.todo-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-desc {
|
||||
.todo-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.system-section {
|
||||
.system-card {
|
||||
.system-item {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
|
||||
.system-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.system-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard {
|
||||
.stats-cards {
|
||||
.stat-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
.chart-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-section {
|
||||
.action-card,
|
||||
.activity-card,
|
||||
.todo-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
948
government-admin/src/views/dashboard/GovernmentDashboard.vue
Normal file
948
government-admin/src/views/dashboard/GovernmentDashboard.vue
Normal file
@@ -0,0 +1,948 @@
|
||||
<template>
|
||||
<div class="government-dashboard">
|
||||
<!-- 欢迎区域 -->
|
||||
<div class="welcome-section">
|
||||
<a-card class="welcome-card">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-text">
|
||||
<h2>欢迎回来,{{ userInfo.name }}</h2>
|
||||
<p>今天是 {{ currentDate }},{{ currentWeather }}</p>
|
||||
<p class="welcome-desc">内蒙古畜牧业管理系统为您提供全面的数据监控和管理服务</p>
|
||||
</div>
|
||||
<div class="welcome-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ todayStats.newApplications }}</div>
|
||||
<div class="stat-label">今日新增申请</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ todayStats.pendingApprovals }}</div>
|
||||
<div class="stat-label">待处理审批</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ todayStats.completedTasks }}</div>
|
||||
<div class="stat-label">已完成任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 核心数据统计 -->
|
||||
<div class="core-stats">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card farms">
|
||||
<a-statistic
|
||||
title="注册养殖场"
|
||||
:value="coreStats.totalFarms"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix><HomeOutlined /></template>
|
||||
<template #suffix>
|
||||
<span class="stat-trend up">
|
||||
<ArrowUpOutlined /> +12%
|
||||
</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-detail">
|
||||
<span>本月新增: {{ coreStats.newFarmsThisMonth }}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card animals">
|
||||
<a-statistic
|
||||
title="监管动物总数"
|
||||
:value="coreStats.totalAnimals"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix><BugOutlined /></template>
|
||||
<template #suffix>
|
||||
<span class="stat-trend up">
|
||||
<ArrowUpOutlined /> +8%
|
||||
</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-detail">
|
||||
<span>健康率: {{ coreStats.healthRate }}%</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card veterinarians">
|
||||
<a-statistic
|
||||
title="注册兽医"
|
||||
:value="coreStats.totalVeterinarians"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix><UserOutlined /></template>
|
||||
<template #suffix>
|
||||
<span class="stat-trend up">
|
||||
<ArrowUpOutlined /> +5%
|
||||
</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-detail">
|
||||
<span>在线: {{ coreStats.onlineVeterinarians }}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card alerts">
|
||||
<a-statistic
|
||||
title="预警信息"
|
||||
:value="coreStats.totalAlerts"
|
||||
:value-style="{ color: '#fa8c16' }"
|
||||
>
|
||||
<template #prefix><AlertOutlined /></template>
|
||||
<template #suffix>
|
||||
<span class="stat-trend down">
|
||||
<ArrowDownOutlined /> -15%
|
||||
</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-detail">
|
||||
<span>紧急: {{ coreStats.urgentAlerts }}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-section">
|
||||
<a-row :gutter="16">
|
||||
<!-- 养殖场分布图 -->
|
||||
<a-col :span="12">
|
||||
<a-card title="养殖场地区分布" class="chart-card">
|
||||
<template #extra>
|
||||
<a-select v-model:value="farmDistributionPeriod" style="width: 120px">
|
||||
<a-select-option value="month">本月</a-select-option>
|
||||
<a-select-option value="quarter">本季度</a-select-option>
|
||||
<a-select-option value="year">本年</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<div ref="farmDistributionChart" class="chart-container"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 动物健康趋势 -->
|
||||
<a-col :span="12">
|
||||
<a-card title="动物健康趋势" class="chart-card">
|
||||
<template #extra>
|
||||
<a-radio-group v-model:value="healthTrendType" size="small">
|
||||
<a-radio-button value="week">周</a-radio-button>
|
||||
<a-radio-button value="month">月</a-radio-button>
|
||||
<a-radio-button value="year">年</a-radio-button>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<div ref="healthTrendChart" class="chart-container"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" style="margin-top: 16px">
|
||||
<!-- 审批流程统计 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="审批流程统计" class="chart-card">
|
||||
<div ref="approvalChart" class="chart-container small"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 设备监控状态 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="设备监控状态" class="chart-card">
|
||||
<div ref="deviceStatusChart" class="chart-container small"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 预警类型分布 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="预警类型分布" class="chart-card">
|
||||
<div ref="alertTypeChart" class="chart-container small"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 快捷操作和最新动态 -->
|
||||
<div class="bottom-section">
|
||||
<a-row :gutter="16">
|
||||
<!-- 快捷操作 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="快捷操作" class="quick-actions-card">
|
||||
<div class="quick-actions">
|
||||
<div class="action-item" @click="navigateTo('/farms/list')">
|
||||
<div class="action-icon farms">
|
||||
<HomeOutlined />
|
||||
</div>
|
||||
<div class="action-content">
|
||||
<div class="action-title">养殖场管理</div>
|
||||
<div class="action-desc">查看和管理养殖场信息</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-item" @click="navigateTo('/approval/license')">
|
||||
<div class="action-icon approval">
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
<div class="action-content">
|
||||
<div class="action-title">许可证审批</div>
|
||||
<div class="action-desc">处理许可证申请和审批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-item" @click="navigateTo('/monitoring/devices')">
|
||||
<div class="action-icon monitoring">
|
||||
<MonitorOutlined />
|
||||
</div>
|
||||
<div class="action-content">
|
||||
<div class="action-title">设备监控</div>
|
||||
<div class="action-desc">实时监控设备运行状态</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-item" @click="navigateTo('/personnel/veterinarians')">
|
||||
<div class="action-icon personnel">
|
||||
<TeamOutlined />
|
||||
</div>
|
||||
<div class="action-content">
|
||||
<div class="action-title">人员管理</div>
|
||||
<div class="action-desc">管理兽医和工作人员</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="最新动态" class="recent-activities-card">
|
||||
<template #extra>
|
||||
<a @click="viewAllActivities">查看全部</a>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="recentActivities"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-avatar :style="{ backgroundColor: item.color }">
|
||||
<component :is="item.icon" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<span>{{ item.title }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="activity-desc">
|
||||
<span>{{ item.description }}</span>
|
||||
<span class="activity-time">{{ item.time }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="待办事项" class="todo-card">
|
||||
<template #extra>
|
||||
<a-badge :count="todoList.filter(item => !item.completed).length">
|
||||
<BellOutlined />
|
||||
</a-badge>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="todoList"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="completeTodo(item.id)"
|
||||
v-if="!item.completed"
|
||||
>
|
||||
完成
|
||||
</a-button>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<span :class="{ 'completed': item.completed }">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="todo-desc">
|
||||
<a-tag :color="getPriorityColor(item.priority)" size="small">
|
||||
{{ item.priority }}
|
||||
</a-tag>
|
||||
<span class="todo-time">{{ item.dueDate }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, nextTick, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
HomeOutlined,
|
||||
BugOutlined,
|
||||
UserOutlined,
|
||||
AlertOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
FileTextOutlined,
|
||||
MonitorOutlined,
|
||||
TeamOutlined,
|
||||
BellOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useGovernmentStore } from '@/stores/government'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
|
||||
const router = useRouter()
|
||||
const governmentStore = useGovernmentStore()
|
||||
const userStore = useUserStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// 响应式数据
|
||||
const farmDistributionPeriod = ref('month')
|
||||
const healthTrendType = ref('month')
|
||||
|
||||
// 用户信息 - 从store获取
|
||||
const userInfo = computed(() => ({
|
||||
name: userStore.userInfo?.name || '管理员',
|
||||
role: userStore.userInfo?.role || '系统管理员'
|
||||
}))
|
||||
|
||||
// 当前日期和天气
|
||||
const currentDate = ref('')
|
||||
const currentWeather = ref('晴朗,适宜户外作业')
|
||||
|
||||
// 今日统计 - 从store获取
|
||||
const todayStats = computed(() => ({
|
||||
newApplications: governmentStore.approval.todayApplications || 15,
|
||||
pendingApprovals: governmentStore.approval.pendingCount || 8,
|
||||
completedTasks: governmentStore.approval.completedToday || 23
|
||||
}))
|
||||
|
||||
// 核心统计数据 - 从store获取
|
||||
const coreStats = computed(() => ({
|
||||
totalFarms: governmentStore.supervision.totalEnterprises || 1248,
|
||||
newFarmsThisMonth: governmentStore.supervision.newThisMonth || 45,
|
||||
totalAnimals: governmentStore.supervision.totalAnimals || 156780,
|
||||
healthRate: governmentStore.supervision.healthRate || 98.5,
|
||||
totalVeterinarians: governmentStore.personnel.totalStaff || 156,
|
||||
onlineVeterinarians: governmentStore.personnel.onlineStaff || 89,
|
||||
totalAlerts: governmentStore.supervision.totalAlerts || 23,
|
||||
urgentAlerts: governmentStore.supervision.urgentAlerts || 3
|
||||
}))
|
||||
|
||||
// 最新动态 - 从store获取
|
||||
const recentActivities = computed(() => {
|
||||
return notificationStore.recentActivities.length > 0
|
||||
? notificationStore.recentActivities.slice(0, 4)
|
||||
: [
|
||||
{
|
||||
id: 1,
|
||||
title: '新养殖场注册',
|
||||
description: '呼和浩特市某养殖场完成注册',
|
||||
time: '2小时前',
|
||||
icon: 'HomeOutlined',
|
||||
color: '#1890ff'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '许可证审批通过',
|
||||
description: '包头市运输许可证审批完成',
|
||||
time: '4小时前',
|
||||
icon: 'CheckCircleOutlined',
|
||||
color: '#52c41a'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '设备预警',
|
||||
description: '某养殖场温度监控设备异常',
|
||||
time: '6小时前',
|
||||
icon: 'ExclamationCircleOutlined',
|
||||
color: '#fa8c16'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '兽医认证',
|
||||
description: '新增3名兽医完成资质认证',
|
||||
time: '1天前',
|
||||
icon: 'UserOutlined',
|
||||
color: '#722ed1'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 待办事项 - 从store获取
|
||||
const todoList = computed(() => {
|
||||
return governmentStore.todoList.length > 0
|
||||
? governmentStore.todoList
|
||||
: [
|
||||
{
|
||||
id: 1,
|
||||
title: '审批养殖许可证申请',
|
||||
priority: '高',
|
||||
dueDate: '今天',
|
||||
completed: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '检查设备维护报告',
|
||||
priority: '中',
|
||||
dueDate: '明天',
|
||||
completed: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '更新兽医资质信息',
|
||||
priority: '低',
|
||||
dueDate: '本周',
|
||||
completed: true
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '处理养殖场投诉',
|
||||
priority: '高',
|
||||
dueDate: '今天',
|
||||
completed: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 图表引用
|
||||
const farmDistributionChart = ref(null)
|
||||
const healthTrendChart = ref(null)
|
||||
const approvalChart = ref(null)
|
||||
const deviceStatusChart = ref(null)
|
||||
const alertTypeChart = ref(null)
|
||||
|
||||
// 方法
|
||||
const initCharts = () => {
|
||||
nextTick(() => {
|
||||
initFarmDistributionChart()
|
||||
initHealthTrendChart()
|
||||
initApprovalChart()
|
||||
initDeviceStatusChart()
|
||||
initAlertTypeChart()
|
||||
})
|
||||
}
|
||||
|
||||
const initFarmDistributionChart = () => {
|
||||
if (!farmDistributionChart.value) return
|
||||
|
||||
const chart = echarts.init(farmDistributionChart.value)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '养殖场分布',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [
|
||||
{ value: 456, name: '呼和浩特市' },
|
||||
{ value: 312, name: '包头市' },
|
||||
{ value: 234, name: '乌海市' },
|
||||
{ value: 156, name: '赤峰市' },
|
||||
{ value: 90, name: '其他地区' }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
const initHealthTrendChart = () => {
|
||||
if (!healthTrendChart.value) return
|
||||
|
||||
const chart = echarts.init(healthTrendChart.value)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['健康率', '疫苗接种率', '治疗成功率']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 90,
|
||||
max: 100
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '健康率',
|
||||
type: 'line',
|
||||
data: [98.2, 98.5, 98.1, 98.8, 98.6, 98.5],
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '疫苗接种率',
|
||||
type: 'line',
|
||||
data: [96.5, 97.2, 97.8, 98.1, 98.3, 98.0],
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '治疗成功率',
|
||||
type: 'line',
|
||||
data: [94.8, 95.2, 95.6, 96.1, 96.5, 96.8],
|
||||
smooth: true
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
const initApprovalChart = () => {
|
||||
if (!approvalChart.value) return
|
||||
|
||||
const chart = echarts.init(approvalChart.value)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '审批状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 45, name: '已通过' },
|
||||
{ value: 15, name: '待审批' },
|
||||
{ value: 8, name: '已拒绝' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
const initDeviceStatusChart = () => {
|
||||
if (!deviceStatusChart.value) return
|
||||
|
||||
const chart = echarts.init(deviceStatusChart.value)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '设备状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 156, name: '正常' },
|
||||
{ value: 23, name: '预警' },
|
||||
{ value: 8, name: '故障' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
const initAlertTypeChart = () => {
|
||||
if (!alertTypeChart.value) return
|
||||
|
||||
const chart = echarts.init(alertTypeChart.value)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['温度', '湿度', '疫情', '设备', '其他']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '预警数量',
|
||||
type: 'bar',
|
||||
data: [8, 5, 3, 4, 3]
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
const navigateTo = (path) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const viewAllActivities = () => {
|
||||
message.info('查看全部动态功能待实现')
|
||||
}
|
||||
|
||||
const completeTodo = async (id) => {
|
||||
try {
|
||||
await governmentStore.completeTodo(id)
|
||||
message.success('任务已完成')
|
||||
} catch (error) {
|
||||
message.error('完成任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const getPriorityColor = (priority) => {
|
||||
const colors = {
|
||||
'高': 'red',
|
||||
'中': 'orange',
|
||||
'低': 'green'
|
||||
}
|
||||
return colors[priority] || 'default'
|
||||
}
|
||||
|
||||
const updateCurrentDate = () => {
|
||||
const now = new Date()
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
}
|
||||
currentDate.value = now.toLocaleDateString('zh-CN', options)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
updateCurrentDate()
|
||||
|
||||
// 加载数据
|
||||
try {
|
||||
await Promise.all([
|
||||
governmentStore.fetchDashboardData(),
|
||||
notificationStore.fetchNotifications(),
|
||||
userStore.fetchUserInfo()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error)
|
||||
message.error('数据加载失败,请刷新页面重试')
|
||||
}
|
||||
|
||||
initCharts()
|
||||
|
||||
// 监听窗口大小变化,重新调整图表
|
||||
window.addEventListener('resize', () => {
|
||||
// 重新调整所有图表大小
|
||||
setTimeout(() => {
|
||||
const charts = [
|
||||
farmDistributionChart,
|
||||
healthTrendChart,
|
||||
approvalChart,
|
||||
deviceStatusChart,
|
||||
alertTypeChart
|
||||
]
|
||||
charts.forEach(chartRef => {
|
||||
if (chartRef.value) {
|
||||
const chart = echarts.getInstanceByDom(chartRef.value)
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.government-dashboard {
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
|
||||
.welcome-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.welcome-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: white;
|
||||
|
||||
.welcome-text {
|
||||
h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.welcome-desc {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-stats {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-stats {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.stat-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.farms {
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
|
||||
&.animals {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
|
||||
&.veterinarians {
|
||||
border-left: 4px solid #722ed1;
|
||||
}
|
||||
|
||||
&.alerts {
|
||||
border-left: 4px solid #fa8c16;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
|
||||
&.up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-detail {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
|
||||
&.small {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-section {
|
||||
.quick-actions-card,
|
||||
.recent-activities-card,
|
||||
.todo-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
height: 400px;
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
height: calc(100% - 57px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
margin-right: 16px;
|
||||
|
||||
&.farms {
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
&.approval {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
&.monitoring {
|
||||
background: #fa8c16;
|
||||
}
|
||||
|
||||
&.personnel {
|
||||
background: #722ed1;
|
||||
}
|
||||
}
|
||||
|
||||
.action-content {
|
||||
.action-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.action-desc {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-desc {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.todo-time {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.completed {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-title) {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head-title) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-list-item-meta-title) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.ant-list-item-meta-description) {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
63
government-admin/src/views/data/DataAnalysis.vue
Normal file
63
government-admin/src/views/data/DataAnalysis.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="data-analysis">
|
||||
<a-card title="数据分析" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-card title="养殖数据趋势" size="small">
|
||||
<div ref="trendChart" style="height: 300px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="区域分布" size="small">
|
||||
<div ref="regionChart" style="height: 300px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" style="margin-top: 16px;">
|
||||
<a-col :span="24">
|
||||
<a-card title="数据统计表" size="small">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
size="small"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataList = ref([])
|
||||
const trendChart = ref()
|
||||
const regionChart = ref()
|
||||
|
||||
const columns = [
|
||||
{ title: '指标名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '数值', dataIndex: 'value', key: 'value' },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit' },
|
||||
{ title: '更新时间', dataIndex: 'updateTime', key: 'updateTime' }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
// 模拟数据
|
||||
dataList.value = [
|
||||
{ id: 1, name: '养殖场总数', value: 150, unit: '个', updateTime: '2024-01-15' },
|
||||
{ id: 2, name: '牲畜总数', value: 5000, unit: '头', updateTime: '2024-01-15' },
|
||||
{ id: 3, name: '疫苗接种率', value: 95, unit: '%', updateTime: '2024-01-15' }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-analysis {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
1708
government-admin/src/views/emergency/EmergencyResponse.vue
Normal file
1708
government-admin/src/views/emergency/EmergencyResponse.vue
Normal file
File diff suppressed because it is too large
Load Diff
147
government-admin/src/views/epidemic/EpidemicActivities.vue
Normal file
147
government-admin/src/views/epidemic/EpidemicActivities.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="epidemic-activities">
|
||||
<a-card title="疫情防控活动" :bordered="false">
|
||||
<a-tabs>
|
||||
<a-tab-pane key="ongoing" tab="进行中活动">
|
||||
<a-table
|
||||
:columns="activityColumns"
|
||||
:data-source="ongoingActivities"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag color="blue">进行中</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">参与</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="completed" tab="已完成活动">
|
||||
<a-table
|
||||
:columns="completedColumns"
|
||||
:data-source="completedActivities"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag color="green">已完成</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">报告</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="planned" tab="计划活动">
|
||||
<a-table
|
||||
:columns="plannedColumns"
|
||||
:data-source="plannedActivities"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag color="orange">计划中</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">编辑</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const ongoingActivities = ref([])
|
||||
const completedActivities = ref([])
|
||||
const plannedActivities = ref([])
|
||||
|
||||
const activityColumns = [
|
||||
{ title: '活动名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '活动类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '负责机构', dataIndex: 'organization', key: 'organization' },
|
||||
{ title: '开始时间', dataIndex: 'startTime', key: 'startTime' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const completedColumns = [
|
||||
{ title: '活动名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '活动类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '负责机构', dataIndex: 'organization', key: 'organization' },
|
||||
{ title: '完成时间', dataIndex: 'endTime', key: 'endTime' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const plannedColumns = [
|
||||
{ title: '活动名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '活动类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '负责机构', dataIndex: 'organization', key: 'organization' },
|
||||
{ title: '计划时间', dataIndex: 'plannedTime', key: 'plannedTime' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
ongoingActivities.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '春季疫苗接种活动',
|
||||
type: '疫苗接种',
|
||||
organization: '县疫控中心',
|
||||
startTime: '2024-01-10',
|
||||
status: 'ongoing'
|
||||
}
|
||||
]
|
||||
|
||||
completedActivities.value = [
|
||||
{
|
||||
id: 2,
|
||||
name: '冬季消毒活动',
|
||||
type: '环境消毒',
|
||||
organization: '县疫控中心',
|
||||
endTime: '2024-01-05',
|
||||
status: 'completed'
|
||||
}
|
||||
]
|
||||
|
||||
plannedActivities.value = [
|
||||
{
|
||||
id: 3,
|
||||
name: '夏季防疫培训',
|
||||
type: '培训教育',
|
||||
organization: '市畜牧局',
|
||||
plannedTime: '2024-06-01',
|
||||
status: 'planned'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.epidemic-activities {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
92
government-admin/src/views/epidemic/EpidemicInstitutions.vue
Normal file
92
government-admin/src/views/epidemic/EpidemicInstitutions.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="epidemic-institutions">
|
||||
<a-card title="疫情防控机构" :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="institutionsList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'level'">
|
||||
<a-tag :color="getLevelColor(record.level)">
|
||||
{{ getLevelText(record.level) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">联系</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const institutionsList = ref([])
|
||||
|
||||
const columns = [
|
||||
{ title: '机构名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '机构类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '级别', dataIndex: 'level', key: 'level' },
|
||||
{ title: '负责人', dataIndex: 'director', key: 'director' },
|
||||
{ title: '联系电话', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const getLevelColor = (level) => {
|
||||
const colors = {
|
||||
national: 'red',
|
||||
provincial: 'orange',
|
||||
city: 'blue',
|
||||
county: 'green'
|
||||
}
|
||||
return colors[level] || 'default'
|
||||
}
|
||||
|
||||
const getLevelText = (level) => {
|
||||
const texts = {
|
||||
national: '国家级',
|
||||
provincial: '省级',
|
||||
city: '市级',
|
||||
county: '县级'
|
||||
}
|
||||
return texts[level] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
institutionsList.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '县动物疫病预防控制中心',
|
||||
type: '疫病防控',
|
||||
level: 'county',
|
||||
director: '李主任',
|
||||
phone: '0123-4567890',
|
||||
address: '县城中心路123号'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '市畜牧兽医局',
|
||||
type: '行政管理',
|
||||
level: 'city',
|
||||
director: '王局长',
|
||||
phone: '0123-7890123',
|
||||
address: '市政府大楼5楼'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.epidemic-institutions {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
134
government-admin/src/views/epidemic/EpidemicRecords.vue
Normal file
134
government-admin/src/views/epidemic/EpidemicRecords.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="epidemic-records">
|
||||
<a-card title="疫情记录" :bordered="false">
|
||||
<div class="search-form">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="疫情类型">
|
||||
<a-select v-model:value="searchForm.type" placeholder="请选择疫情类型" style="width: 150px">
|
||||
<a-select-option value="bird_flu">禽流感</a-select-option>
|
||||
<a-select-option value="swine_fever">猪瘟</a-select-option>
|
||||
<a-select-option value="foot_mouth">口蹄疫</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="发生地区">
|
||||
<a-input v-model:value="searchForm.location" placeholder="请输入地区" />
|
||||
</a-form-item>
|
||||
<a-form-item label="时间范围">
|
||||
<a-range-picker v-model:value="searchForm.dateRange" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary">查询</a-button>
|
||||
<a-button style="margin-left: 8px">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="recordsList"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'severity'">
|
||||
<a-tag :color="getSeverityColor(record.severity)">
|
||||
{{ getSeverityText(record.severity) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'controlled' ? 'green' : 'orange'">
|
||||
{{ record.status === 'controlled' ? '已控制' : '处理中' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
<a-button type="link" size="small">编辑</a-button>
|
||||
<a-button type="link" size="small">报告</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const recordsList = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
type: undefined,
|
||||
location: '',
|
||||
dateRange: null
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '疫情编号', dataIndex: 'code', key: 'code' },
|
||||
{ title: '疫情类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '发生地区', dataIndex: 'location', key: 'location' },
|
||||
{ title: '严重程度', dataIndex: 'severity', key: 'severity' },
|
||||
{ title: '影响范围', dataIndex: 'scope', key: 'scope' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '发生时间', dataIndex: 'occurTime', key: 'occurTime' },
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
const getSeverityColor = (severity) => {
|
||||
const colors = {
|
||||
low: 'green',
|
||||
medium: 'orange',
|
||||
high: 'red'
|
||||
}
|
||||
return colors[severity] || 'default'
|
||||
}
|
||||
|
||||
const getSeverityText = (severity) => {
|
||||
const texts = {
|
||||
low: '轻微',
|
||||
medium: '中等',
|
||||
high: '严重'
|
||||
}
|
||||
return texts[severity] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
recordsList.value = [
|
||||
{
|
||||
id: 1,
|
||||
code: 'EP2024001',
|
||||
type: '禽流感',
|
||||
location: '某县某镇',
|
||||
severity: 'medium',
|
||||
scope: '3个养殖场',
|
||||
status: 'controlled',
|
||||
occurTime: '2024-01-10'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: 'EP2024002',
|
||||
type: '猪瘟',
|
||||
location: '某县某村',
|
||||
severity: 'high',
|
||||
scope: '1个养殖场',
|
||||
status: 'processing',
|
||||
occurTime: '2024-01-15'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.epidemic-records {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
441
government-admin/src/views/epidemic/VaccineManagement.vue
Normal file
441
government-admin/src/views/epidemic/VaccineManagement.vue
Normal file
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<div class="vaccine-management">
|
||||
<a-card title="疫苗管理" :bordered="false">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加疫苗
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<div class="search-form">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="疫苗名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="疫苗类型"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="preventive">预防性疫苗</a-select-option>
|
||||
<a-select-option value="therapeutic">治疗性疫苗</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="状态"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="available">可用</a-select-option>
|
||||
<a-select-option value="expired">过期</a-select-option>
|
||||
<a-select-option value="shortage">库存不足</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSearch">
|
||||
重置
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 疫苗列表 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="vaccineList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="showEditModal(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="showDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个疫苗吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑疫苗模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="疫苗名称" name="name">
|
||||
<a-input v-model:value="formData.name" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="疫苗类型" name="type">
|
||||
<a-select v-model:value="formData.type">
|
||||
<a-select-option value="preventive">预防性疫苗</a-select-option>
|
||||
<a-select-option value="therapeutic">治疗性疫苗</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生产厂家" name="manufacturer">
|
||||
<a-input v-model:value="formData.manufacturer" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="批次号" name="batchNumber">
|
||||
<a-input v-model:value="formData.batchNumber" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生产日期" name="productionDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.productionDate"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="有效期至" name="expiryDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.expiryDate"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="库存数量" name="stock">
|
||||
<a-input-number
|
||||
v-model:value="formData.stock"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="单位" name="unit">
|
||||
<a-select v-model:value="formData.unit">
|
||||
<a-select-option value="支">支</a-select-option>
|
||||
<a-select-option value="瓶">瓶</a-select-option>
|
||||
<a-select-option value="盒">盒</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('添加疫苗')
|
||||
const formRef = ref()
|
||||
const vaccineList = ref([])
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
type: '',
|
||||
manufacturer: '',
|
||||
batchNumber: '',
|
||||
productionDate: null,
|
||||
expiryDate: null,
|
||||
stock: 0,
|
||||
unit: '支',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '疫苗名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '疫苗类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type'
|
||||
},
|
||||
{
|
||||
title: '生产厂家',
|
||||
dataIndex: 'manufacturer',
|
||||
key: 'manufacturer'
|
||||
},
|
||||
{
|
||||
title: '批次号',
|
||||
dataIndex: 'batchNumber',
|
||||
key: 'batchNumber'
|
||||
},
|
||||
{
|
||||
title: '库存数量',
|
||||
dataIndex: 'stock',
|
||||
key: 'stock'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200
|
||||
}
|
||||
]
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入疫苗名称' }],
|
||||
type: [{ required: true, message: '请选择疫苗类型' }],
|
||||
manufacturer: [{ required: true, message: '请输入生产厂家' }],
|
||||
batchNumber: [{ required: true, message: '请输入批次号' }],
|
||||
productionDate: [{ required: true, message: '请选择生产日期' }],
|
||||
expiryDate: [{ required: true, message: '请选择有效期' }],
|
||||
stock: [{ required: true, message: '请输入库存数量' }],
|
||||
unit: [{ required: true, message: '请选择单位' }]
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
available: 'green',
|
||||
expired: 'red',
|
||||
shortage: 'orange'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
available: '可用',
|
||||
expired: '过期',
|
||||
shortage: '库存不足'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
// 加载疫苗列表
|
||||
const loadVaccineList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟数据
|
||||
vaccineList.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '口蹄疫疫苗',
|
||||
type: 'preventive',
|
||||
manufacturer: '中牧股份',
|
||||
batchNumber: 'FMD20240101',
|
||||
productionDate: '2024-01-01',
|
||||
expiryDate: '2025-01-01',
|
||||
stock: 500,
|
||||
unit: '支',
|
||||
status: 'available',
|
||||
remark: '预防口蹄疫'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '布鲁氏菌病疫苗',
|
||||
type: 'preventive',
|
||||
manufacturer: '兰州生物制品研究所',
|
||||
batchNumber: 'BRU20240201',
|
||||
productionDate: '2024-02-01',
|
||||
expiryDate: '2025-02-01',
|
||||
stock: 200,
|
||||
unit: '支',
|
||||
status: 'shortage',
|
||||
remark: '预防布鲁氏菌病'
|
||||
}
|
||||
]
|
||||
|
||||
pagination.total = vaccineList.value.length
|
||||
} catch (error) {
|
||||
message.error('加载疫苗列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
loadVaccineList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.keys(searchForm).forEach(key => {
|
||||
searchForm[key] = ''
|
||||
})
|
||||
loadVaccineList()
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadVaccineList()
|
||||
}
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = () => {
|
||||
modalTitle.value = '添加疫苗'
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑模态框
|
||||
const showEditModal = (record) => {
|
||||
modalTitle.value = '编辑疫苗'
|
||||
Object.assign(formData, record)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 显示详情
|
||||
const showDetail = (record) => {
|
||||
message.info('查看疫苗详情功能待实现')
|
||||
}
|
||||
|
||||
// 删除疫苗
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
message.success('删除成功')
|
||||
loadVaccineList()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
message.success(formData.id ? '更新成功' : '添加成功')
|
||||
modalVisible.value = false
|
||||
loadVaccineList()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key === 'stock') {
|
||||
formData[key] = 0
|
||||
} else if (key === 'unit') {
|
||||
formData[key] = '支'
|
||||
} else {
|
||||
formData[key] = key === 'id' ? null : ''
|
||||
}
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadVaccineList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vaccine-management {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
262
government-admin/src/views/error/403.vue
Normal file
262
government-admin/src/views/error/403.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-content">
|
||||
<div class="error-icon">
|
||||
<stop-outlined />
|
||||
</div>
|
||||
<h1 class="error-title">403</h1>
|
||||
<h2 class="error-subtitle">权限不足</h2>
|
||||
<p class="error-description">
|
||||
抱歉,您没有权限访问此页面。请联系管理员获取相应权限。
|
||||
</p>
|
||||
<div class="error-actions">
|
||||
<a-button type="primary" @click="goBack">
|
||||
<arrow-left-outlined />
|
||||
返回上页
|
||||
</a-button>
|
||||
<a-button @click="goHome">
|
||||
<home-outlined />
|
||||
回到首页
|
||||
</a-button>
|
||||
<a-button @click="contactAdmin">
|
||||
<phone-outlined />
|
||||
联系管理员
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-illustration">
|
||||
<svg viewBox="0 0 400 300" class="illustration-svg">
|
||||
<!-- 锁图标 -->
|
||||
<g transform="translate(150, 80)">
|
||||
<rect x="0" y="40" width="100" height="80" rx="10" fill="#f5f5f5" stroke="#d9d9d9" stroke-width="2"/>
|
||||
<circle cx="50" cy="80" r="8" fill="#ff4d4f"/>
|
||||
<path d="M20 40 L20 20 Q20 0 50 0 Q80 0 80 20 L80 40" fill="none" stroke="#d9d9d9" stroke-width="4"/>
|
||||
</g>
|
||||
<!-- 装饰元素 -->
|
||||
<circle cx="80" cy="50" r="3" fill="#faad14" opacity="0.6"/>
|
||||
<circle cx="320" cy="80" r="4" fill="#52c41a" opacity="0.6"/>
|
||||
<circle cx="60" cy="200" r="2" fill="#1890ff" opacity="0.6"/>
|
||||
<circle cx="340" cy="180" r="3" fill="#722ed1" opacity="0.6"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
StopOutlined,
|
||||
ArrowLeftOutlined,
|
||||
HomeOutlined,
|
||||
PhoneOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1)
|
||||
} else {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
|
||||
const contactAdmin = () => {
|
||||
message.info('请联系系统管理员:admin@example.com')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-height: 500px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
padding: 60px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
color: #ff4d4f;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.anticon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #ff4d4f;
|
||||
margin: 0 0 10px 0;
|
||||
line-height: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-subtitle {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0 0 16px 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 16px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 32px 0;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
|
||||
.ant-btn {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ant-btn-primary) {
|
||||
border-color: #d9d9d9;
|
||||
color: #595959;
|
||||
|
||||
&:hover {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.error-illustration {
|
||||
background: linear-gradient(135deg, #f6f9fc 0%, #e9f2ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.illustration-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 300px;
|
||||
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.error-icon {
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.error-subtitle {
|
||||
animation: fadeInUp 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
animation: fadeInUp 0.8s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
animation: fadeInUp 0.8s ease-out 0.6s both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
299
government-admin/src/views/error/404.vue
Normal file
299
government-admin/src/views/error/404.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-content">
|
||||
<div class="error-icon">
|
||||
<question-circle-outlined />
|
||||
</div>
|
||||
<h1 class="error-title">404</h1>
|
||||
<h2 class="error-subtitle">页面不存在</h2>
|
||||
<p class="error-description">
|
||||
抱歉,您访问的页面不存在或已被移除。请检查网址是否正确。
|
||||
</p>
|
||||
<div class="error-actions">
|
||||
<a-button type="primary" @click="goBack">
|
||||
<arrow-left-outlined />
|
||||
返回上页
|
||||
</a-button>
|
||||
<a-button @click="goHome">
|
||||
<home-outlined />
|
||||
回到首页
|
||||
</a-button>
|
||||
<a-button @click="reportProblem">
|
||||
<bug-outlined />
|
||||
报告问题
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-illustration">
|
||||
<svg viewBox="0 0 400 300" class="illustration-svg">
|
||||
<!-- 404 数字 -->
|
||||
<g transform="translate(50, 100)">
|
||||
<!-- 4 -->
|
||||
<path d="M0 0 L0 40 L20 40 L20 0 L20 20 L40 20 M20 0 L20 60" stroke="#1890ff" stroke-width="4" fill="none"/>
|
||||
<!-- 0 -->
|
||||
<ellipse cx="80" cy="30" rx="20" ry="30" stroke="#52c41a" stroke-width="4" fill="none"/>
|
||||
<!-- 4 -->
|
||||
<path d="M120 0 L120 40 L140 40 L140 0 L140 20 L160 20 M140 0 L140 60" stroke="#faad14" stroke-width="4" fill="none"/>
|
||||
</g>
|
||||
<!-- 装饰元素 -->
|
||||
<circle cx="300" cy="60" r="8" fill="#ff4d4f" opacity="0.3">
|
||||
<animate attributeName="r" values="8;12;8" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="80" cy="250" r="6" fill="#722ed1" opacity="0.4">
|
||||
<animate attributeName="r" values="6;10;6" dur="1.5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="320" cy="220" r="4" fill="#13c2c2" opacity="0.5">
|
||||
<animate attributeName="r" values="4;8;4" dur="1.8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<!-- 搜索图标 -->
|
||||
<g transform="translate(280, 150)">
|
||||
<circle cx="0" cy="0" r="15" stroke="#bfbfbf" stroke-width="3" fill="none"/>
|
||||
<path d="M12 12 L25 25" stroke="#bfbfbf" stroke-width="3"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
QuestionCircleOutlined,
|
||||
ArrowLeftOutlined,
|
||||
HomeOutlined,
|
||||
BugOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1)
|
||||
} else {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
|
||||
const reportProblem = () => {
|
||||
message.info('问题已记录,我们会尽快处理')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-height: 500px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
padding: 60px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
color: #1890ff;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.anticon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #1890ff;
|
||||
margin: 0 0 10px 0;
|
||||
line-height: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-subtitle {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0 0 16px 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 16px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 32px 0;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
|
||||
.ant-btn {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #0f7ae5 0%, #6526c7 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ant-btn-primary) {
|
||||
border-color: #d9d9d9;
|
||||
color: #595959;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.error-illustration {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.illustration-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 300px;
|
||||
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.error-icon {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.error-subtitle {
|
||||
animation: fadeInUp 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
animation: fadeInUp 0.8s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
animation: fadeInUp 0.8s ease-out 0.6s both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 404数字动画
|
||||
.illustration-svg g path,
|
||||
.illustration-svg g ellipse {
|
||||
stroke-dasharray: 200;
|
||||
stroke-dashoffset: 200;
|
||||
animation: drawPath 2s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.illustration-svg g path:nth-child(2),
|
||||
.illustration-svg g ellipse:nth-child(2) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.illustration-svg g path:nth-child(3),
|
||||
.illustration-svg g ellipse:nth-child(3) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes drawPath {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
government-admin/src/views/error/500.vue
Normal file
54
government-admin/src/views/error/500.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
<a-result
|
||||
status="500"
|
||||
title="500"
|
||||
sub-title="抱歉,服务器出现错误"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="goHome">
|
||||
返回首页
|
||||
</a-button>
|
||||
<a-button @click="goBack">
|
||||
返回上页
|
||||
</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
869
government-admin/src/views/farms/FarmDetail.vue
Normal file
869
government-admin/src/views/farms/FarmDetail.vue
Normal file
@@ -0,0 +1,869 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<a-button @click="goBack" class="back-btn">
|
||||
<arrow-left-outlined />
|
||||
返回
|
||||
</a-button>
|
||||
<div class="title-info">
|
||||
<h1 class="page-title">{{ farmDetail.name }}</h1>
|
||||
<div class="farm-meta">
|
||||
<a-tag :color="getStatusColor(farmDetail.status)">
|
||||
{{ getStatusText(farmDetail.status) }}
|
||||
</a-tag>
|
||||
<span class="farm-code">编号:{{ farmDetail.code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-space>
|
||||
<permission-button
|
||||
permission="farm:update"
|
||||
@click="editFarm"
|
||||
>
|
||||
<edit-outlined />
|
||||
编辑
|
||||
</permission-button>
|
||||
<permission-button
|
||||
type="primary"
|
||||
permission="monitor:view"
|
||||
@click="realTimeMonitor"
|
||||
>
|
||||
<monitor-outlined />
|
||||
实时监控
|
||||
</permission-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧主要信息 -->
|
||||
<a-col :xs="24" :lg="16">
|
||||
<!-- 基本信息卡片 -->
|
||||
<a-card title="基本信息" class="info-card" :bordered="false">
|
||||
<template #extra>
|
||||
<a-button type="link" @click="showEditModal">
|
||||
<edit-outlined />
|
||||
编辑
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">养殖场名称</div>
|
||||
<div class="info-value">{{ farmDetail.name }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">养殖场编号</div>
|
||||
<div class="info-value">{{ farmDetail.code }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">所属区域</div>
|
||||
<div class="info-value">{{ getRegionText(farmDetail.region) }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">养殖类型</div>
|
||||
<div class="info-value">{{ getFarmTypeText(farmDetail.farmType) }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">养殖规模</div>
|
||||
<div class="info-value">{{ getScaleText(farmDetail.scale) }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">养殖面积</div>
|
||||
<div class="info-value">{{ farmDetail.area }} 平方米</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<div class="info-item">
|
||||
<div class="info-label">详细地址</div>
|
||||
<div class="info-value">
|
||||
<environment-outlined />
|
||||
{{ farmDetail.address }}
|
||||
<a-button type="link" size="small" @click="showOnMap">
|
||||
在地图中查看
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<div class="info-item">
|
||||
<div class="info-label">养殖场描述</div>
|
||||
<div class="info-value">{{ farmDetail.description || '暂无描述' }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 负责人信息卡片 -->
|
||||
<a-card title="负责人信息" class="info-card" :bordered="false">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">负责人姓名</div>
|
||||
<div class="info-value">{{ farmDetail.manager }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">联系电话</div>
|
||||
<div class="info-value">
|
||||
<phone-outlined />
|
||||
{{ farmDetail.phone }}
|
||||
<a-button type="link" size="small" @click="callPhone(farmDetail.phone)">
|
||||
拨打
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">邮箱地址</div>
|
||||
<div class="info-value">
|
||||
<mail-outlined />
|
||||
{{ farmDetail.email || '暂无' }}
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">身份证号</div>
|
||||
<div class="info-value">{{ maskIdCard(farmDetail.idCard) }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 养殖信息卡片 -->
|
||||
<a-card title="养殖信息" class="info-card" :bordered="false">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">动物种类</div>
|
||||
<div class="info-value">
|
||||
<a-space wrap>
|
||||
<a-tag
|
||||
v-for="type in farmDetail.animalTypes"
|
||||
:key="type"
|
||||
color="blue"
|
||||
>
|
||||
{{ getAnimalTypeText(type) }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">动物总数</div>
|
||||
<div class="info-value">
|
||||
<span class="number-highlight">{{ farmDetail.animalCount }}</span> 头
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">建设时间</div>
|
||||
<div class="info-value">{{ formatDate(farmDetail.buildDate) }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">投产时间</div>
|
||||
<div class="info-value">{{ formatDate(farmDetail.operationDate) }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 证照信息卡片 -->
|
||||
<a-card title="证照信息" class="info-card" :bordered="false">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">营业执照号</div>
|
||||
<div class="info-value">{{ farmDetail.licenseNumber }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">动物防疫证号</div>
|
||||
<div class="info-value">{{ farmDetail.vaccineNumber }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<div class="info-item">
|
||||
<div class="info-label">环保证号</div>
|
||||
<div class="info-value">{{ farmDetail.environmentNumber }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧统计和操作 -->
|
||||
<a-col :xs="24" :lg="8">
|
||||
<!-- 实时统计卡片 -->
|
||||
<a-card title="实时统计" class="stats-card" :bordered="false">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon devices">
|
||||
<monitor-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ farmDetail.onlineDevices }}/{{ farmDetail.totalDevices }}</div>
|
||||
<div class="stat-label">设备在线</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon alerts">
|
||||
<bell-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ farmDetail.alertCount || 0 }}</div>
|
||||
<div class="stat-label">当前预警</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon temperature">
|
||||
<fire-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ farmDetail.avgTemperature || '--' }}°C</div>
|
||||
<div class="stat-label">平均温度</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon humidity">
|
||||
<cloud-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ farmDetail.avgHumidity || '--' }}%</div>
|
||||
<div class="stat-label">平均湿度</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 快捷操作卡片 -->
|
||||
<a-card title="快捷操作" class="action-card" :bordered="false">
|
||||
<div class="action-grid">
|
||||
<permission-button
|
||||
class="action-btn"
|
||||
permission="device:view"
|
||||
@click="viewDevices"
|
||||
>
|
||||
<monitor-outlined />
|
||||
<span>设备管理</span>
|
||||
</permission-button>
|
||||
|
||||
<permission-button
|
||||
class="action-btn"
|
||||
permission="alert:view"
|
||||
@click="viewAlerts"
|
||||
>
|
||||
<bell-outlined />
|
||||
<span>预警管理</span>
|
||||
</permission-button>
|
||||
|
||||
<permission-button
|
||||
class="action-btn"
|
||||
permission="report:view"
|
||||
@click="viewReports"
|
||||
>
|
||||
<file-text-outlined />
|
||||
<span>报表查看</span>
|
||||
</permission-button>
|
||||
|
||||
<permission-button
|
||||
class="action-btn"
|
||||
permission="data:export"
|
||||
@click="exportData"
|
||||
>
|
||||
<download-outlined />
|
||||
<span>数据导出</span>
|
||||
</permission-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 位置地图卡片 -->
|
||||
<a-card title="位置信息" class="map-card" :bordered="false">
|
||||
<div class="mini-map" id="farmDetailMap">
|
||||
<!-- 地图容器 -->
|
||||
</div>
|
||||
<div class="location-info">
|
||||
<div class="coordinate">
|
||||
<span>经度:{{ farmDetail.longitude }}</span>
|
||||
<span>纬度:{{ farmDetail.latitude }}</span>
|
||||
</div>
|
||||
<a-button type="link" @click="openFullMap">
|
||||
查看大图
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 最近活动卡片 -->
|
||||
<a-card title="最近活动" class="activity-card" :bordered="false">
|
||||
<a-timeline size="small">
|
||||
<a-timeline-item
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
:color="getActivityColor(activity.type)"
|
||||
>
|
||||
<div class="activity-item">
|
||||
<div class="activity-title">{{ activity.title }}</div>
|
||||
<div class="activity-time">{{ formatTime(activity.time) }}</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
|
||||
<div class="activity-more">
|
||||
<a-button type="link" @click="viewAllActivities">
|
||||
查看全部活动
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-spin>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="editModalVisible"
|
||||
title="编辑养殖场"
|
||||
width="800px"
|
||||
@ok="handleUpdate"
|
||||
@cancel="handleEditCancel"
|
||||
:confirm-loading="updateLoading"
|
||||
>
|
||||
<farm-form
|
||||
ref="editFormRef"
|
||||
:form-data="editFormData"
|
||||
:is-edit="true"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 地图弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="mapModalVisible"
|
||||
title="位置地图"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div id="fullMap" style="height: 400px;"></div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
EditOutlined,
|
||||
MonitorOutlined,
|
||||
EnvironmentOutlined,
|
||||
PhoneOutlined,
|
||||
MailOutlined,
|
||||
BellOutlined,
|
||||
FireOutlined,
|
||||
CloudOutlined,
|
||||
FileTextOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PermissionButton from '@/components/PermissionButton.vue'
|
||||
import FarmForm from './components/FarmForm.vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const updateLoading = ref(false)
|
||||
const editModalVisible = ref(false)
|
||||
const mapModalVisible = ref(false)
|
||||
const farmDetail = ref({})
|
||||
const editFormData = ref({})
|
||||
const editFormRef = ref()
|
||||
const recentActivities = ref([])
|
||||
|
||||
// 获取养殖场ID
|
||||
const farmId = route.params.id
|
||||
|
||||
// 方法
|
||||
const fetchFarmDetail = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get(`/farms/${farmId}`)
|
||||
|
||||
if (response.success) {
|
||||
farmDetail.value = response.data
|
||||
// 获取最近活动
|
||||
fetchRecentActivities()
|
||||
// 初始化地图
|
||||
initMiniMap()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取养殖场详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRecentActivities = async () => {
|
||||
try {
|
||||
const response = await api.get(`/farms/${farmId}/activities`, {
|
||||
params: { limit: 5 }
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
recentActivities.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取活动记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
const editFarm = () => {
|
||||
editFormData.value = { ...farmDetail.value }
|
||||
editModalVisible.value = true
|
||||
}
|
||||
|
||||
const showEditModal = () => {
|
||||
editFarm()
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
const formData = await editFormRef.value.validate()
|
||||
updateLoading.value = true
|
||||
|
||||
const response = await api.put(`/farms/${farmId}`, formData)
|
||||
|
||||
if (response.success) {
|
||||
message.success('更新成功')
|
||||
editModalVisible.value = false
|
||||
fetchFarmDetail()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
message.error('请检查表单信息')
|
||||
} else {
|
||||
message.error('更新失败')
|
||||
}
|
||||
} finally {
|
||||
updateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditCancel = () => {
|
||||
editModalVisible.value = false
|
||||
editFormData.value = {}
|
||||
}
|
||||
|
||||
const realTimeMonitor = () => {
|
||||
router.push(`/monitor?farmId=${farmId}`)
|
||||
}
|
||||
|
||||
const viewDevices = () => {
|
||||
router.push(`/devices?farmId=${farmId}`)
|
||||
}
|
||||
|
||||
const viewAlerts = () => {
|
||||
router.push(`/alerts?farmId=${farmId}`)
|
||||
}
|
||||
|
||||
const viewReports = () => {
|
||||
router.push(`/reports?farmId=${farmId}`)
|
||||
}
|
||||
|
||||
const viewAllActivities = () => {
|
||||
router.push(`/activities?farmId=${farmId}`)
|
||||
}
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
const response = await api.get(`/farms/${farmId}/export`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(new Blob([response]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${farmDetail.value.name}_数据导出_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
message.success('导出成功')
|
||||
} catch (error) {
|
||||
message.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
const showOnMap = () => {
|
||||
mapModalVisible.value = true
|
||||
// 延迟初始化地图,确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
initFullMap()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const openFullMap = () => {
|
||||
showOnMap()
|
||||
}
|
||||
|
||||
const callPhone = (phone) => {
|
||||
window.open(`tel:${phone}`)
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
active: 'green',
|
||||
inactive: 'red',
|
||||
maintenance: 'orange'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
active: '正常',
|
||||
inactive: '停用',
|
||||
maintenance: '维护中'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
const getRegionText = (region) => {
|
||||
const regions = {
|
||||
yinchuan: '银川市',
|
||||
shizuishan: '石嘴山市',
|
||||
wuzhong: '吴忠市',
|
||||
guyuan: '固原市',
|
||||
zhongwei: '中卫市'
|
||||
}
|
||||
return regions[region] || region
|
||||
}
|
||||
|
||||
const getFarmTypeText = (type) => {
|
||||
const types = {
|
||||
cattle: '肉牛',
|
||||
dairy: '奶牛',
|
||||
sheep: '羊',
|
||||
pig: '猪',
|
||||
chicken: '鸡',
|
||||
mixed: '混合养殖'
|
||||
}
|
||||
return types[type] || type
|
||||
}
|
||||
|
||||
const getScaleText = (scale) => {
|
||||
const scales = {
|
||||
small: '小型(<100头)',
|
||||
medium: '中型(100-500头)',
|
||||
large: '大型(500-1000头)',
|
||||
xlarge: '超大型(>1000头)'
|
||||
}
|
||||
return scales[scale] || scale
|
||||
}
|
||||
|
||||
const getAnimalTypeText = (type) => {
|
||||
const types = {
|
||||
cattle: '肉牛',
|
||||
dairy_cow: '奶牛',
|
||||
sheep: '羊',
|
||||
goat: '山羊',
|
||||
pig: '猪',
|
||||
chicken: '鸡',
|
||||
duck: '鸭',
|
||||
goose: '鹅'
|
||||
}
|
||||
return types[type] || type
|
||||
}
|
||||
|
||||
const maskIdCard = (idCard) => {
|
||||
if (!idCard) return '暂无'
|
||||
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2')
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '暂无'
|
||||
return new Date(date).toLocaleDateString()
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return ''
|
||||
return new Date(time).toLocaleString()
|
||||
}
|
||||
|
||||
const getActivityColor = (type) => {
|
||||
const colors = {
|
||||
info: 'blue',
|
||||
warning: 'orange',
|
||||
error: 'red',
|
||||
success: 'green'
|
||||
}
|
||||
return colors[type] || 'blue'
|
||||
}
|
||||
|
||||
// 地图相关方法
|
||||
const initMiniMap = () => {
|
||||
// TODO: 初始化小地图
|
||||
console.log('初始化小地图')
|
||||
}
|
||||
|
||||
const initFullMap = () => {
|
||||
// TODO: 初始化完整地图
|
||||
console.log('初始化完整地图')
|
||||
}
|
||||
|
||||
// 组件挂载和卸载
|
||||
onMounted(() => {
|
||||
fetchFarmDetail()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理地图资源
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
|
||||
.back-btn {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.title-info {
|
||||
.page-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.farm-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.farm-code {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.info-item {
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.number-highlight {
|
||||
color: #1890ff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
margin-right: 12px;
|
||||
|
||||
&.devices {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
&.alerts {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||
}
|
||||
|
||||
&.temperature {
|
||||
background: linear-gradient(135deg, #ff9500 0%, #ff6b35 100%);
|
||||
}
|
||||
|
||||
&.humidity {
|
||||
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.mini-map {
|
||||
height: 200px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.coordinate {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
.activity-item {
|
||||
.activity-title {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-more {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.header-left {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
931
government-admin/src/views/farms/FarmList.vue
Normal file
931
government-admin/src/views/farms/FarmList.vue
Normal file
@@ -0,0 +1,931 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">养殖场管理</h1>
|
||||
<p class="page-description">管理和监控所有养殖场信息</p>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-left">
|
||||
<a-input-search
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="搜索养殖场名称、编号或负责人"
|
||||
style="width: 300px"
|
||||
@search="handleSearch"
|
||||
allow-clear
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="状态筛选"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="inactive">停用</a-select-option>
|
||||
<a-select-option value="maintenance">维护中</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-model:value="searchForm.region"
|
||||
placeholder="区域筛选"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="yinchuan">银川市</a-select-option>
|
||||
<a-select-option value="shizuishan">石嘴山市</a-select-option>
|
||||
<a-select-option value="wuzhong">吴忠市</a-select-option>
|
||||
<a-select-option value="guyuan">固原市</a-select-option>
|
||||
<a-select-option value="zhongwei">中卫市</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
</div>
|
||||
<div class="search-right">
|
||||
<permission-button
|
||||
permission="farm:export"
|
||||
@click="exportData"
|
||||
:loading="exportLoading"
|
||||
>
|
||||
<download-outlined />
|
||||
导出数据
|
||||
</permission-button>
|
||||
<permission-button
|
||||
type="primary"
|
||||
permission="farm:create"
|
||||
@click="showAddModal"
|
||||
>
|
||||
<plus-outlined />
|
||||
新增养殖场
|
||||
</permission-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-cards">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon farms">
|
||||
<home-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.total }}</div>
|
||||
<div class="stat-label">总数量</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon active">
|
||||
<check-circle-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.active }}</div>
|
||||
<div class="stat-label">正常运营</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<exclamation-circle-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.warning }}</div>
|
||||
<div class="stat-label">预警中</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon offline">
|
||||
<stop-outlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.offline }}</div>
|
||||
<div class="stat-label">离线</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<div class="view-controls">
|
||||
<a-radio-group v-model:value="viewMode" button-style="solid">
|
||||
<a-radio-button value="table">
|
||||
<table-outlined />
|
||||
列表视图
|
||||
</a-radio-button>
|
||||
<a-radio-button value="map">
|
||||
<environment-outlined />
|
||||
地图视图
|
||||
</a-radio-button>
|
||||
<a-radio-button value="card">
|
||||
<appstore-outlined />
|
||||
卡片视图
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<div v-if="viewMode === 'table'" class="table-container">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="farmList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="farm-name">
|
||||
<a @click="viewDetail(record.id)">{{ record.name }}</a>
|
||||
<div class="farm-code">{{ record.code }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'location'">
|
||||
<div class="location-info">
|
||||
<environment-outlined />
|
||||
{{ record.region }} {{ record.address }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'animals'">
|
||||
<div class="animal-stats">
|
||||
<div class="animal-count">{{ record.animalCount }}</div>
|
||||
<div class="animal-types">{{ record.animalTypes?.join('、') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'devices'">
|
||||
<div class="device-stats">
|
||||
<span class="online">{{ record.onlineDevices }}</span>
|
||||
/
|
||||
<span class="total">{{ record.totalDevices }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'alerts'">
|
||||
<a-badge
|
||||
:count="record.alertCount"
|
||||
:number-style="{ backgroundColor: record.alertCount > 0 ? '#ff4d4f' : '#52c41a' }"
|
||||
>
|
||||
<a-button size="small" @click="viewAlerts(record.id)">
|
||||
查看预警
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<permission-button
|
||||
type="link"
|
||||
size="small"
|
||||
permission="farm:view"
|
||||
@click="viewDetail(record.id)"
|
||||
>
|
||||
详情
|
||||
</permission-button>
|
||||
<permission-button
|
||||
type="link"
|
||||
size="small"
|
||||
permission="farm:update"
|
||||
@click="editFarm(record)"
|
||||
>
|
||||
编辑
|
||||
</permission-button>
|
||||
<permission-button
|
||||
type="link"
|
||||
size="small"
|
||||
permission="device:view"
|
||||
@click="viewDevices(record.id)"
|
||||
>
|
||||
设备
|
||||
</permission-button>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="monitor" @click="monitorFarm(record.id)">
|
||||
<monitor-outlined />
|
||||
实时监控
|
||||
</a-menu-item>
|
||||
<a-menu-item key="report" @click="generateReport(record.id)">
|
||||
<file-text-outlined />
|
||||
生成报表
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item
|
||||
key="delete"
|
||||
danger
|
||||
@click="deleteFarm(record)"
|
||||
v-if="$hasPermission('farm:delete')"
|
||||
>
|
||||
<delete-outlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="link" size="small">
|
||||
更多
|
||||
<down-outlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 地图视图 -->
|
||||
<div v-if="viewMode === 'map'" class="map-container">
|
||||
<div id="farmMap" class="farm-map"></div>
|
||||
<div class="map-controls">
|
||||
<a-card size="small" title="图例">
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot active"></span>
|
||||
正常运营
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot warning"></span>
|
||||
预警中
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot offline"></span>
|
||||
离线
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="card-container">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col
|
||||
v-for="farm in farmList"
|
||||
:key="farm.id"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:lg="8"
|
||||
:xl="6"
|
||||
>
|
||||
<a-card
|
||||
:hoverable="true"
|
||||
class="farm-card"
|
||||
@click="viewDetail(farm.id)"
|
||||
>
|
||||
<template #cover>
|
||||
<div class="farm-cover">
|
||||
<img
|
||||
:src="farm.image || '/images/default-farm.jpg'"
|
||||
:alt="farm.name"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div class="farm-status">
|
||||
<a-tag :color="getStatusColor(farm.status)">
|
||||
{{ getStatusText(farm.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<permission-button
|
||||
type="text"
|
||||
permission="farm:view"
|
||||
@click.stop="viewDetail(farm.id)"
|
||||
>
|
||||
<eye-outlined />
|
||||
</permission-button>
|
||||
<permission-button
|
||||
type="text"
|
||||
permission="farm:update"
|
||||
@click.stop="editFarm(farm)"
|
||||
>
|
||||
<edit-outlined />
|
||||
</permission-button>
|
||||
<permission-button
|
||||
type="text"
|
||||
permission="monitor:view"
|
||||
@click.stop="monitorFarm(farm.id)"
|
||||
>
|
||||
<monitor-outlined />
|
||||
</permission-button>
|
||||
</template>
|
||||
|
||||
<a-card-meta
|
||||
:title="farm.name"
|
||||
:description="farm.description"
|
||||
/>
|
||||
|
||||
<div class="farm-info">
|
||||
<div class="info-item">
|
||||
<environment-outlined />
|
||||
{{ farm.region }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<user-outlined />
|
||||
{{ farm.manager }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<bug-outlined />
|
||||
{{ farm.animalCount }} 头
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<monitor-outlined />
|
||||
{{ farm.onlineDevices }}/{{ farm.totalDevices }}
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑养殖场弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="800px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="submitLoading"
|
||||
>
|
||||
<farm-form
|
||||
ref="farmFormRef"
|
||||
:form-data="currentFarm"
|
||||
:is-edit="isEdit"
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
DownloadOutlined,
|
||||
HomeOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
StopOutlined,
|
||||
TableOutlined,
|
||||
EnvironmentOutlined,
|
||||
AppstoreOutlined,
|
||||
MonitorOutlined,
|
||||
FileTextOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
UserOutlined,
|
||||
BugOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PermissionButton from '@/components/PermissionButton.vue'
|
||||
import FarmForm from './components/FarmForm.vue'
|
||||
import { permissionMixin } from '@/utils/permission'
|
||||
import api from '@/utils/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const viewMode = ref('table')
|
||||
const farmList = ref([])
|
||||
const currentFarm = ref({})
|
||||
const isEdit = ref(false)
|
||||
const farmFormRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: undefined,
|
||||
region: undefined
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
total: 156,
|
||||
active: 142,
|
||||
warning: 8,
|
||||
offline: 6
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '养殖场信息',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '位置',
|
||||
key: 'location',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'manager',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
title: '动物数量',
|
||||
key: 'animals',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '设备状态',
|
||||
key: 'devices',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '预警',
|
||||
key: 'alerts',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const modalTitle = computed(() => {
|
||||
return isEdit.value ? '编辑养殖场' : '新增养殖场'
|
||||
})
|
||||
|
||||
// 混入权限方法
|
||||
const { $hasPermission } = permissionMixin.methods
|
||||
|
||||
// 方法
|
||||
const fetchFarmList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
const response = await api.get('/farms', { params })
|
||||
|
||||
if (response.success) {
|
||||
farmList.value = response.data.list
|
||||
pagination.total = response.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取养殖场列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchFarmList()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
status: undefined,
|
||||
region: undefined
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchFarmList()
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
active: 'green',
|
||||
inactive: 'red',
|
||||
maintenance: 'orange',
|
||||
warning: 'orange'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
active: '正常',
|
||||
inactive: '停用',
|
||||
maintenance: '维护中',
|
||||
warning: '预警中'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
const showAddModal = () => {
|
||||
currentFarm.value = {}
|
||||
isEdit.value = false
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const editFarm = (farm) => {
|
||||
currentFarm.value = { ...farm }
|
||||
isEdit.value = true
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const formData = await farmFormRef.value.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
const url = isEdit.value ? `/farms/${currentFarm.value.id}` : '/farms'
|
||||
const method = isEdit.value ? 'put' : 'post'
|
||||
|
||||
const response = await api[method](url, formData)
|
||||
|
||||
if (response.success) {
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
modalVisible.value = false
|
||||
fetchFarmList()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
message.error('请检查表单信息')
|
||||
} else {
|
||||
message.error(isEdit.value ? '编辑失败' : '新增失败')
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
currentFarm.value = {}
|
||||
}
|
||||
|
||||
const viewDetail = (id) => {
|
||||
router.push(`/farms/detail/${id}`)
|
||||
}
|
||||
|
||||
const viewDevices = (id) => {
|
||||
router.push(`/devices?farmId=${id}`)
|
||||
}
|
||||
|
||||
const viewAlerts = (id) => {
|
||||
router.push(`/alerts?farmId=${id}`)
|
||||
}
|
||||
|
||||
const monitorFarm = (id) => {
|
||||
router.push(`/monitor?farmId=${id}`)
|
||||
}
|
||||
|
||||
const generateReport = (id) => {
|
||||
router.push(`/reports?farmId=${id}`)
|
||||
}
|
||||
|
||||
const deleteFarm = (farm) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除养殖场"${farm.name}"吗?此操作不可恢复。`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await api.delete(`/farms/${farm.id}`)
|
||||
if (response.success) {
|
||||
message.success('删除成功')
|
||||
fetchFarmList()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
exportLoading.value = true
|
||||
const response = await api.get('/farms/export', {
|
||||
params: searchForm,
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(new Blob([response]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `养殖场数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
message.success('导出成功')
|
||||
} catch (error) {
|
||||
message.error('导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageError = (e) => {
|
||||
e.target.src = '/images/default-farm.jpg'
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
const initMap = () => {
|
||||
// TODO: 集成百度地图
|
||||
console.log('初始化地图')
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
fetchFarmList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
margin-right: 16px;
|
||||
|
||||
&.farms {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-controls {
|
||||
margin-bottom: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.farm-name {
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.farm-code {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.location-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.animal-stats {
|
||||
.animal-count {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.animal-types {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.device-stats {
|
||||
.online {
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.total {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-container {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.farm-map {
|
||||
height: 600px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.map-controls {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.active {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: #faad14;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-container {
|
||||
.farm-card {
|
||||
.farm-cover {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.farm-status {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.farm-info {
|
||||
margin-top: 12px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.search-left,
|
||||
.search-right {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-left {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.view-controls {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
566
government-admin/src/views/farms/components/FarmForm.vue
Normal file
566
government-admin/src/views/farms/components/FarmForm.vue
Normal file
@@ -0,0 +1,566 @@
|
||||
<template>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
class="farm-form"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<!-- 基本信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">基本信息</h3>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="养殖场名称" name="name">
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入养殖场名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="养殖场编号" name="code">
|
||||
<a-input
|
||||
v-model:value="formData.code"
|
||||
placeholder="请输入养殖场编号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="所属区域" name="region">
|
||||
<a-select
|
||||
v-model:value="formData.region"
|
||||
placeholder="请选择所属区域"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="yinchuan">银川市</a-select-option>
|
||||
<a-select-option value="shizuishan">石嘴山市</a-select-option>
|
||||
<a-select-option value="wuzhong">吴忠市</a-select-option>
|
||||
<a-select-option value="guyuan">固原市</a-select-option>
|
||||
<a-select-option value="zhongwei">中卫市</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="养殖类型" name="farmType">
|
||||
<a-select
|
||||
v-model:value="formData.farmType"
|
||||
placeholder="请选择养殖类型"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="cattle">肉牛</a-select-option>
|
||||
<a-select-option value="dairy">奶牛</a-select-option>
|
||||
<a-select-option value="sheep">羊</a-select-option>
|
||||
<a-select-option value="pig">猪</a-select-option>
|
||||
<a-select-option value="chicken">鸡</a-select-option>
|
||||
<a-select-option value="mixed">混合养殖</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="养殖规模" name="scale">
|
||||
<a-select
|
||||
v-model:value="formData.scale"
|
||||
placeholder="请选择养殖规模"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="small">小型(<100头)</a-select-option>
|
||||
<a-select-option value="medium">中型(100-500头)</a-select-option>
|
||||
<a-select-option value="large">大型(500-1000头)</a-select-option>
|
||||
<a-select-option value="xlarge">超大型(>1000头)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="详细地址" name="address">
|
||||
<a-input
|
||||
v-model:value="formData.address"
|
||||
placeholder="请输入详细地址"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="经度" name="longitude">
|
||||
<a-input-number
|
||||
v-model:value="formData.longitude"
|
||||
placeholder="请输入经度"
|
||||
:precision="6"
|
||||
:min="-180"
|
||||
:max="180"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="纬度" name="latitude">
|
||||
<a-input-number
|
||||
v-model:value="formData.latitude"
|
||||
placeholder="请输入纬度"
|
||||
:precision="6"
|
||||
:min="-90"
|
||||
:max="90"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="养殖场描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入养殖场描述"
|
||||
:rows="3"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 负责人信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">负责人信息</h3>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="负责人姓名" name="manager">
|
||||
<a-input
|
||||
v-model:value="formData.manager"
|
||||
placeholder="请输入负责人姓名"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input
|
||||
v-model:value="formData.phone"
|
||||
placeholder="请输入联系电话"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="邮箱地址" name="email">
|
||||
<a-input
|
||||
v-model:value="formData.email"
|
||||
placeholder="请输入邮箱地址"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="身份证号" name="idCard">
|
||||
<a-input
|
||||
v-model:value="formData.idCard"
|
||||
placeholder="请输入身份证号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 养殖信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">养殖信息</h3>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="动物种类" name="animalTypes">
|
||||
<a-select
|
||||
v-model:value="formData.animalTypes"
|
||||
mode="multiple"
|
||||
placeholder="请选择动物种类"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="cattle">肉牛</a-select-option>
|
||||
<a-select-option value="dairy_cow">奶牛</a-select-option>
|
||||
<a-select-option value="sheep">羊</a-select-option>
|
||||
<a-select-option value="goat">山羊</a-select-option>
|
||||
<a-select-option value="pig">猪</a-select-option>
|
||||
<a-select-option value="chicken">鸡</a-select-option>
|
||||
<a-select-option value="duck">鸭</a-select-option>
|
||||
<a-select-option value="goose">鹅</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="动物总数" name="animalCount">
|
||||
<a-input-number
|
||||
v-model:value="formData.animalCount"
|
||||
placeholder="请输入动物总数"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="养殖面积(平方米)" name="area">
|
||||
<a-input-number
|
||||
v-model:value="formData.area"
|
||||
placeholder="请输入养殖面积"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="建设时间" name="buildDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.buildDate"
|
||||
placeholder="请选择建设时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="投产时间" name="operationDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.operationDate"
|
||||
placeholder="请选择投产时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 设备信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">设备信息</h3>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="监控设备数量" name="monitorDevices">
|
||||
<a-input-number
|
||||
v-model:value="formData.monitorDevices"
|
||||
placeholder="请输入监控设备数量"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="传感器数量" name="sensorDevices">
|
||||
<a-input-number
|
||||
v-model:value="formData.sensorDevices"
|
||||
placeholder="请输入传感器数量"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="自动化设备数量" name="autoDevices">
|
||||
<a-input-number
|
||||
v-model:value="formData.autoDevices"
|
||||
placeholder="请输入自动化设备数量"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">其他信息</h3>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="营业执照号" name="licenseNumber">
|
||||
<a-input
|
||||
v-model:value="formData.licenseNumber"
|
||||
placeholder="请输入营业执照号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="动物防疫证号" name="vaccineNumber">
|
||||
<a-input
|
||||
v-model:value="formData.vaccineNumber"
|
||||
placeholder="请输入动物防疫证号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="环保证号" name="environmentNumber">
|
||||
<a-input
|
||||
v-model:value="formData.environmentNumber"
|
||||
placeholder="请输入环保证号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select
|
||||
v-model:value="formData.status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="inactive">停用</a-select-option>
|
||||
<a-select-option value="maintenance">维护中</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="备注" name="remarks">
|
||||
<a-textarea
|
||||
v-model:value="formData.remarks"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:formData'])
|
||||
|
||||
const formRef = ref()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
region: '',
|
||||
farmType: '',
|
||||
scale: '',
|
||||
address: '',
|
||||
longitude: null,
|
||||
latitude: null,
|
||||
description: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
idCard: '',
|
||||
animalTypes: [],
|
||||
animalCount: null,
|
||||
area: null,
|
||||
buildDate: null,
|
||||
operationDate: null,
|
||||
monitorDevices: 0,
|
||||
sensorDevices: 0,
|
||||
autoDevices: 0,
|
||||
licenseNumber: '',
|
||||
vaccineNumber: '',
|
||||
environmentNumber: '',
|
||||
status: 'active',
|
||||
remarks: '',
|
||||
...props.formData
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入养殖场名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入养殖场编号', trigger: 'blur' },
|
||||
{ pattern: /^[A-Z0-9]{6,20}$/, message: '编号格式不正确(6-20位大写字母和数字)', trigger: 'blur' }
|
||||
],
|
||||
region: [
|
||||
{ required: true, message: '请选择所属区域', trigger: 'change' }
|
||||
],
|
||||
farmType: [
|
||||
{ required: true, message: '请选择养殖类型', trigger: 'change' }
|
||||
],
|
||||
scale: [
|
||||
{ required: true, message: '请选择养殖规模', trigger: 'change' }
|
||||
],
|
||||
address: [
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
||||
{ min: 5, max: 200, message: '地址长度在 5 到 200 个字符', trigger: 'blur' }
|
||||
],
|
||||
longitude: [
|
||||
{ required: true, message: '请输入经度', trigger: 'blur' },
|
||||
{ type: 'number', min: -180, max: 180, message: '经度范围在 -180 到 180 之间', trigger: 'blur' }
|
||||
],
|
||||
latitude: [
|
||||
{ required: true, message: '请输入纬度', trigger: 'blur' },
|
||||
{ type: 'number', min: -90, max: 90, message: '纬度范围在 -90 到 90 之间', trigger: 'blur' }
|
||||
],
|
||||
manager: [
|
||||
{ required: true, message: '请输入负责人姓名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
idCard: [
|
||||
{ required: true, message: '请输入身份证号', trigger: 'blur' },
|
||||
{ pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入正确的身份证号', trigger: 'blur' }
|
||||
],
|
||||
animalTypes: [
|
||||
{ required: true, message: '请选择动物种类', trigger: 'change' }
|
||||
],
|
||||
animalCount: [
|
||||
{ required: true, message: '请输入动物总数', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '动物总数必须大于0', trigger: 'blur' }
|
||||
],
|
||||
area: [
|
||||
{ required: true, message: '请输入养殖面积', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '养殖面积必须大于0', trigger: 'blur' }
|
||||
],
|
||||
buildDate: [
|
||||
{ required: true, message: '请选择建设时间', trigger: 'change' }
|
||||
],
|
||||
operationDate: [
|
||||
{ required: true, message: '请选择投产时间', trigger: 'change' }
|
||||
],
|
||||
licenseNumber: [
|
||||
{ required: true, message: '请输入营业执照号', trigger: 'blur' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听表单数据变化
|
||||
watch(
|
||||
() => props.formData,
|
||||
(newData) => {
|
||||
Object.assign(formData, newData)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// 表单验证方法
|
||||
const validate = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
return formData
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetFields = () => {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
validate,
|
||||
resetFields
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.farm-form {
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-select-selector),
|
||||
:deep(.ant-picker),
|
||||
:deep(.ant-input-number) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:deep(.ant-select-multiple .ant-select-selection-item) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.farm-form {
|
||||
.form-section {
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1065
government-admin/src/views/inspection/InspectionManagement.vue
Normal file
1065
government-admin/src/views/inspection/InspectionManagement.vue
Normal file
File diff suppressed because it is too large
Load Diff
618
government-admin/src/views/monitor/MonitorDashboard.vue
Normal file
618
government-admin/src/views/monitor/MonitorDashboard.vue
Normal file
@@ -0,0 +1,618 @@
|
||||
<template>
|
||||
<div class="monitor-dashboard">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="监控仪表盘"
|
||||
description="实时监控系统运行状态、性能指标和关键业务数据"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="refreshData">
|
||||
<ReloadOutlined />
|
||||
刷新数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="exportReport">
|
||||
<ExportOutlined />
|
||||
导出报告
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 系统状态概览 -->
|
||||
<div class="system-overview">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="系统状态"
|
||||
value="正常"
|
||||
:value-style="{ color: '#52c41a', fontSize: '24px' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<CheckCircleOutlined style="color: #52c41a" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="status-detail">
|
||||
<a-progress :percent="98" size="small" status="active" />
|
||||
<span style="font-size: 12px; color: #666;">系统健康度: 98%</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="在线用户"
|
||||
:value="stats.onlineUsers"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="trend-info">
|
||||
<ArrowUpOutlined style="color: #52c41a" />
|
||||
<span style="color: #52c41a; font-size: 12px;">较昨日 +12%</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="今日访问量"
|
||||
:value="stats.todayVisits"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<EyeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="trend-info">
|
||||
<ArrowUpOutlined style="color: #52c41a" />
|
||||
<span style="color: #52c41a; font-size: 12px;">较昨日 +8%</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="系统负载"
|
||||
:value="stats.systemLoad"
|
||||
suffix="%"
|
||||
:value-style="{ color: '#f5222d' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<DashboardOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="trend-info">
|
||||
<ArrowDownOutlined style="color: #f5222d" />
|
||||
<span style="color: #f5222d; font-size: 12px;">较昨日 +5%</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 实时监控图表 -->
|
||||
<div class="monitor-charts">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-card title="系统性能监控" :bordered="false">
|
||||
<template #extra>
|
||||
<a-radio-group v-model:value="performanceTimeRange" size="small">
|
||||
<a-radio-button value="1h">1小时</a-radio-button>
|
||||
<a-radio-button value="6h">6小时</a-radio-button>
|
||||
<a-radio-button value="24h">24小时</a-radio-button>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<div ref="performanceChartRef" style="height: 300px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="用户访问统计" :bordered="false">
|
||||
<template #extra>
|
||||
<a-radio-group v-model:value="visitTimeRange" size="small">
|
||||
<a-radio-button value="today">今日</a-radio-button>
|
||||
<a-radio-button value="week">本周</a-radio-button>
|
||||
<a-radio-button value="month">本月</a-radio-button>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<div ref="visitChartRef" style="height: 300px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 服务状态监控 -->
|
||||
<div class="service-monitor">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="16">
|
||||
<a-card title="服务状态监控" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-tag color="green">{{ serviceStats.running }}个服务正常</a-tag>
|
||||
<a-tag color="red">{{ serviceStats.error }}个服务异常</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
:columns="serviceColumns"
|
||||
:data-source="serviceList"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-badge
|
||||
:status="record.status === 'running' ? 'success' : 'error'"
|
||||
:text="record.status === 'running' ? '正常' : '异常'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'responseTime'">
|
||||
<span :style="{ color: record.responseTime > 1000 ? '#f5222d' : '#52c41a' }">
|
||||
{{ record.responseTime }}ms
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="viewServiceDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="restartService(record)">
|
||||
重启
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card title="系统资源使用" :bordered="false">
|
||||
<div class="resource-monitor">
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">CPU使用率</div>
|
||||
<a-progress
|
||||
:percent="resourceUsage.cpu"
|
||||
:stroke-color="getProgressColor(resourceUsage.cpu)"
|
||||
/>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">内存使用率</div>
|
||||
<a-progress
|
||||
:percent="resourceUsage.memory"
|
||||
:stroke-color="getProgressColor(resourceUsage.memory)"
|
||||
/>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">磁盘使用率</div>
|
||||
<a-progress
|
||||
:percent="resourceUsage.disk"
|
||||
:stroke-color="getProgressColor(resourceUsage.disk)"
|
||||
/>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">网络带宽</div>
|
||||
<a-progress
|
||||
:percent="resourceUsage.network"
|
||||
:stroke-color="getProgressColor(resourceUsage.network)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 告警信息 -->
|
||||
<div class="alert-panel">
|
||||
<a-card title="系统告警" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-badge :count="alertList.filter(item => item.level === 'error').length" />
|
||||
<span>严重告警</span>
|
||||
<a-badge :count="alertList.filter(item => item.level === 'warning').length" />
|
||||
<span>警告</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="alertList"
|
||||
size="small"
|
||||
:pagination="{ pageSize: 5, size: 'small' }"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a @click="handleAlert(item)">处理</a>
|
||||
<a @click="ignoreAlert(item)">忽略</a>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-badge
|
||||
:status="item.level === 'error' ? 'error' : 'warning'"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
<span>{{ item.title }}</span>
|
||||
<a-tag
|
||||
:color="item.level === 'error' ? 'red' : 'orange'"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ item.level === 'error' ? '严重' : '警告' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<div>{{ item.description }}</div>
|
||||
<div style="color: #999; font-size: 12px; margin-top: 4px;">
|
||||
{{ item.time }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
ExportOutlined,
|
||||
CheckCircleOutlined,
|
||||
UserOutlined,
|
||||
EyeOutlined,
|
||||
DashboardOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const performanceChartRef = ref()
|
||||
const visitChartRef = ref()
|
||||
const performanceTimeRange = ref('6h')
|
||||
const visitTimeRange = ref('today')
|
||||
let performanceChart = null
|
||||
let visitChart = null
|
||||
let refreshTimer = null
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
onlineUsers: 1248,
|
||||
todayVisits: 8652,
|
||||
systemLoad: 75
|
||||
})
|
||||
|
||||
// 服务状态统计
|
||||
const serviceStats = reactive({
|
||||
running: 12,
|
||||
error: 2
|
||||
})
|
||||
|
||||
// 资源使用情况
|
||||
const resourceUsage = reactive({
|
||||
cpu: 68,
|
||||
memory: 72,
|
||||
disk: 45,
|
||||
network: 35
|
||||
})
|
||||
|
||||
// 服务列表表格列
|
||||
const serviceColumns = [
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status'
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'responseTime',
|
||||
key: 'responseTime'
|
||||
},
|
||||
{
|
||||
title: '最后检查',
|
||||
dataIndex: 'lastCheck',
|
||||
key: 'lastCheck'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action'
|
||||
}
|
||||
]
|
||||
|
||||
// 服务列表数据
|
||||
const serviceList = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Web服务',
|
||||
status: 'running',
|
||||
responseTime: 245,
|
||||
lastCheck: '2024-01-20 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '数据库服务',
|
||||
status: 'running',
|
||||
responseTime: 156,
|
||||
lastCheck: '2024-01-20 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Redis缓存',
|
||||
status: 'error',
|
||||
responseTime: 0,
|
||||
lastCheck: '2024-01-20 14:25:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'API网关',
|
||||
status: 'running',
|
||||
responseTime: 89,
|
||||
lastCheck: '2024-01-20 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '消息队列',
|
||||
status: 'running',
|
||||
responseTime: 123,
|
||||
lastCheck: '2024-01-20 14:30:00'
|
||||
}
|
||||
])
|
||||
|
||||
// 告警列表
|
||||
const alertList = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Redis服务连接失败',
|
||||
description: 'Redis服务无法连接,可能影响缓存功能',
|
||||
level: 'error',
|
||||
time: '2024-01-20 14:25:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'CPU使用率过高',
|
||||
description: 'CPU使用率达到75%,建议检查系统负载',
|
||||
level: 'warning',
|
||||
time: '2024-01-20 14:20:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '磁盘空间不足',
|
||||
description: '系统磁盘使用率达到85%,建议清理日志文件',
|
||||
level: 'warning',
|
||||
time: '2024-01-20 14:15:00'
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const getProgressColor = (percent) => {
|
||||
if (percent >= 80) return '#f5222d'
|
||||
if (percent >= 60) return '#faad14'
|
||||
return '#52c41a'
|
||||
}
|
||||
|
||||
const initPerformanceChart = () => {
|
||||
if (!performanceChartRef.value) return
|
||||
|
||||
performanceChart = echarts.init(performanceChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['CPU使用率', '内存使用率', '网络IO']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['14:00', '14:05', '14:10', '14:15', '14:20', '14:25', '14:30']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
max: 100,
|
||||
axisLabel: {
|
||||
formatter: '{value}%'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'CPU使用率',
|
||||
type: 'line',
|
||||
data: [65, 68, 72, 70, 75, 73, 68],
|
||||
smooth: true,
|
||||
itemStyle: { color: '#1890ff' }
|
||||
},
|
||||
{
|
||||
name: '内存使用率',
|
||||
type: 'line',
|
||||
data: [70, 72, 75, 74, 76, 75, 72],
|
||||
smooth: true,
|
||||
itemStyle: { color: '#52c41a' }
|
||||
},
|
||||
{
|
||||
name: '网络IO',
|
||||
type: 'line',
|
||||
data: [30, 35, 40, 38, 42, 40, 35],
|
||||
smooth: true,
|
||||
itemStyle: { color: '#faad14' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
performanceChart.setOption(option)
|
||||
}
|
||||
|
||||
const initVisitChart = () => {
|
||||
if (!visitChartRef.value) return
|
||||
|
||||
visitChart = echarts.init(visitChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['页面访问量', '用户访问量']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '页面访问量',
|
||||
type: 'bar',
|
||||
data: [320, 280, 450, 680, 890, 750, 420],
|
||||
itemStyle: { color: '#1890ff' }
|
||||
},
|
||||
{
|
||||
name: '用户访问量',
|
||||
type: 'line',
|
||||
data: [180, 160, 280, 420, 520, 450, 280],
|
||||
smooth: true,
|
||||
itemStyle: { color: '#52c41a' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
visitChart.setOption(option)
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
// 模拟数据刷新
|
||||
stats.onlineUsers = Math.floor(Math.random() * 2000) + 1000
|
||||
stats.todayVisits = Math.floor(Math.random() * 10000) + 5000
|
||||
stats.systemLoad = Math.floor(Math.random() * 30) + 60
|
||||
|
||||
resourceUsage.cpu = Math.floor(Math.random() * 40) + 50
|
||||
resourceUsage.memory = Math.floor(Math.random() * 40) + 50
|
||||
resourceUsage.disk = Math.floor(Math.random() * 30) + 30
|
||||
resourceUsage.network = Math.floor(Math.random() * 50) + 20
|
||||
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const exportReport = () => {
|
||||
message.success('监控报告导出成功')
|
||||
}
|
||||
|
||||
const viewServiceDetail = (service) => {
|
||||
message.info(`查看服务"${service.name}"的详细信息`)
|
||||
}
|
||||
|
||||
const restartService = (service) => {
|
||||
message.success(`服务"${service.name}"重启成功`)
|
||||
}
|
||||
|
||||
const handleAlert = (alert) => {
|
||||
message.success(`告警"${alert.title}"已处理`)
|
||||
}
|
||||
|
||||
const ignoreAlert = (alert) => {
|
||||
message.info(`告警"${alert.title}"已忽略`)
|
||||
}
|
||||
|
||||
// 自动刷新数据
|
||||
const startAutoRefresh = () => {
|
||||
refreshTimer = setInterval(() => {
|
||||
refreshData()
|
||||
}, 30000) // 30秒刷新一次
|
||||
}
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
initPerformanceChart()
|
||||
initVisitChart()
|
||||
startAutoRefresh()
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', () => {
|
||||
performanceChart?.resize()
|
||||
visitChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
performanceChart?.dispose()
|
||||
visitChart?.dispose()
|
||||
window.removeEventListener('resize', () => {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-dashboard {
|
||||
.system-overview {
|
||||
margin: 24px 0;
|
||||
|
||||
.status-detail {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.trend-info {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-charts {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.service-monitor {
|
||||
margin: 24px 0;
|
||||
|
||||
.resource-monitor {
|
||||
.resource-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.resource-label {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-panel {
|
||||
margin: 24px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
902
government-admin/src/views/monitoring/AnimalHealthMonitor.vue
Normal file
902
government-admin/src/views/monitoring/AnimalHealthMonitor.vue
Normal file
@@ -0,0 +1,902 @@
|
||||
<template>
|
||||
<div class="animal-health-monitor">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="动物健康监控"
|
||||
description="实时监控动物健康状态,及时发现和处理疫病风险"
|
||||
icon="heart"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAddRecord">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增健康记录
|
||||
</a-button>
|
||||
<a-button @click="handleExport">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出报告
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-cards">
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card healthy">
|
||||
<a-statistic
|
||||
title="健康动物"
|
||||
:value="healthStats.healthy"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<CheckCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card sick">
|
||||
<a-statistic
|
||||
title="患病动物"
|
||||
:value="healthStats.sick"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#ff4d4f' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<ExclamationCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card quarantine">
|
||||
<a-statistic
|
||||
title="隔离观察"
|
||||
:value="healthStats.quarantine"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<EyeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card vaccination">
|
||||
<a-statistic
|
||||
title="待疫苗接种"
|
||||
:value="healthStats.vaccination"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<MedicineBoxOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 健康趋势图表 -->
|
||||
<a-card title="健康状态趋势" class="chart-card">
|
||||
<div ref="healthTrendChart" class="chart-container"></div>
|
||||
</a-card>
|
||||
|
||||
<!-- 疫病预警 -->
|
||||
<a-card title="疫病预警" class="alert-card">
|
||||
<a-list
|
||||
:data-source="diseaseAlerts"
|
||||
:loading="alertLoading"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-avatar :style="{ backgroundColor: getAlertColor(item.level) }">
|
||||
<WarningOutlined />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<span>{{ item.title }}</span>
|
||||
<a-tag :color="getAlertColor(item.level)" style="margin-left: 8px">
|
||||
{{ getAlertLevelText(item.level) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<p>{{ item.description }}</p>
|
||||
<p class="alert-meta">
|
||||
<span>影响范围:{{ item.scope }}</span>
|
||||
<span style="margin-left: 16px">发生时间:{{ formatDate(item.createdAt) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a @click="handleAlertDetail(item)">查看详情</a>
|
||||
<a @click="handleAlertProcess(item)">处理</a>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
|
||||
<!-- 健康记录表格 -->
|
||||
<a-card title="健康记录" class="table-card">
|
||||
<!-- 搜索表单 -->
|
||||
<SearchForm
|
||||
:fields="searchFields"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'healthStatus'">
|
||||
<a-tag :color="getHealthStatusColor(record.healthStatus)">
|
||||
{{ getHealthStatusText(record.healthStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'temperature'">
|
||||
<span :class="{ 'high-temperature': record.temperature > 39 }">
|
||||
{{ record.temperature }}°C
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这条记录吗?"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</DataTable>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑健康记录弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="800px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="动物编号" name="animalId">
|
||||
<a-input v-model:value="formData.animalId" placeholder="请输入动物编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="检查日期" name="checkDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.checkDate"
|
||||
style="width: 100%"
|
||||
placeholder="请选择检查日期"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="体温(°C)" name="temperature">
|
||||
<a-input-number
|
||||
v-model:value="formData.temperature"
|
||||
:min="35"
|
||||
:max="45"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
placeholder="请输入体温"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="体重(kg)" name="weight">
|
||||
<a-input-number
|
||||
v-model:value="formData.weight"
|
||||
:min="0"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
placeholder="请输入体重"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="健康状态" name="healthStatus">
|
||||
<a-select v-model:value="formData.healthStatus" placeholder="请选择健康状态">
|
||||
<a-select-option value="healthy">健康</a-select-option>
|
||||
<a-select-option value="sick">患病</a-select-option>
|
||||
<a-select-option value="quarantine">隔离观察</a-select-option>
|
||||
<a-select-option value="recovery">康复中</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="疫苗接种" name="vaccination">
|
||||
<a-select v-model:value="formData.vaccination" placeholder="请选择疫苗类型">
|
||||
<a-select-option value="">无</a-select-option>
|
||||
<a-select-option value="foot_mouth">口蹄疫疫苗</a-select-option>
|
||||
<a-select-option value="swine_fever">猪瘟疫苗</a-select-option>
|
||||
<a-select-option value="blue_ear">蓝耳病疫苗</a-select-option>
|
||||
<a-select-option value="porcine_parvovirus">细小病毒疫苗</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="检查人员" name="inspector">
|
||||
<a-input v-model:value="formData.inspector" placeholder="请输入检查人员" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="症状描述" name="symptoms">
|
||||
<a-textarea
|
||||
v-model:value="formData.symptoms"
|
||||
placeholder="请描述观察到的症状"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="治疗措施" name="treatment">
|
||||
<a-textarea
|
||||
v-model:value="formData.treatment"
|
||||
placeholder="请描述采取的治疗措施"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="detailVisible"
|
||||
title="健康记录详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="动物编号">
|
||||
{{ currentRecord?.animalId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="检查日期">
|
||||
{{ formatDate(currentRecord?.checkDate) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="体温">
|
||||
<span :class="{ 'high-temperature': currentRecord?.temperature > 39 }">
|
||||
{{ currentRecord?.temperature }}°C
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="体重">
|
||||
{{ currentRecord?.weight }}kg
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="健康状态">
|
||||
<a-tag :color="getHealthStatusColor(currentRecord?.healthStatus)">
|
||||
{{ getHealthStatusText(currentRecord?.healthStatus) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="疫苗接种">
|
||||
{{ getVaccinationText(currentRecord?.vaccination) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="检查人员">
|
||||
{{ currentRecord?.inspector }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="记录时间">
|
||||
{{ formatDate(currentRecord?.createdAt) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="症状描述" :span="2">
|
||||
{{ currentRecord?.symptoms || '无' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="治疗措施" :span="2">
|
||||
{{ currentRecord?.treatment || '无' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ currentRecord?.remark || '无' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ExportOutlined,
|
||||
HeartOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
EyeOutlined,
|
||||
MedicineBoxOutlined,
|
||||
WarningOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
import SearchForm from '@/components/common/SearchForm.vue'
|
||||
import DataTable from '@/components/common/DataTable.vue'
|
||||
import { useMonitoringStore } from '@/stores/monitoring'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
const monitoringStore = useMonitoringStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const alertLoading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const currentRecord = ref(null)
|
||||
const formRef = ref()
|
||||
const healthTrendChart = ref()
|
||||
|
||||
// 健康统计数据
|
||||
const healthStats = reactive({
|
||||
healthy: 0,
|
||||
sick: 0,
|
||||
quarantine: 0,
|
||||
vaccination: 0
|
||||
})
|
||||
|
||||
// 疫病预警数据
|
||||
const diseaseAlerts = ref([])
|
||||
|
||||
// 搜索条件
|
||||
const searchParams = reactive({
|
||||
animalId: '',
|
||||
healthStatus: '',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
inspector: ''
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
animalId: '',
|
||||
checkDate: null,
|
||||
temperature: null,
|
||||
weight: null,
|
||||
healthStatus: '',
|
||||
vaccination: '',
|
||||
inspector: '',
|
||||
symptoms: '',
|
||||
treatment: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 搜索字段配置
|
||||
const searchFields = [
|
||||
{
|
||||
type: 'input',
|
||||
key: 'animalId',
|
||||
label: '动物编号',
|
||||
placeholder: '请输入动物编号'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'healthStatus',
|
||||
label: '健康状态',
|
||||
placeholder: '请选择健康状态',
|
||||
options: [
|
||||
{ label: '健康', value: 'healthy' },
|
||||
{ label: '患病', value: 'sick' },
|
||||
{ label: '隔离观察', value: 'quarantine' },
|
||||
{ label: '康复中', value: 'recovery' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'date-range',
|
||||
key: 'dateRange',
|
||||
label: '检查日期',
|
||||
placeholder: ['开始日期', '结束日期']
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
key: 'inspector',
|
||||
label: '检查人员',
|
||||
placeholder: '请输入检查人员'
|
||||
}
|
||||
]
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '动物编号',
|
||||
dataIndex: 'animalId',
|
||||
key: 'animalId',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '检查日期',
|
||||
dataIndex: 'checkDate',
|
||||
key: 'checkDate',
|
||||
width: 120,
|
||||
customRender: ({ text }) => formatDate(text, 'YYYY-MM-DD')
|
||||
},
|
||||
{
|
||||
title: '体温',
|
||||
dataIndex: 'temperature',
|
||||
key: 'temperature',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '体重(kg)',
|
||||
dataIndex: 'weight',
|
||||
key: 'weight',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '健康状态',
|
||||
dataIndex: 'healthStatus',
|
||||
key: 'healthStatus',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '疫苗接种',
|
||||
dataIndex: 'vaccination',
|
||||
key: 'vaccination',
|
||||
width: 120,
|
||||
customRender: ({ text }) => getVaccinationText(text)
|
||||
},
|
||||
{
|
||||
title: '检查人员',
|
||||
dataIndex: 'inspector',
|
||||
key: 'inspector',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '记录时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
customRender: ({ text }) => formatDate(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
animalId: [
|
||||
{ required: true, message: '请输入动物编号', trigger: 'blur' }
|
||||
],
|
||||
checkDate: [
|
||||
{ required: true, message: '请选择检查日期', trigger: 'change' }
|
||||
],
|
||||
temperature: [
|
||||
{ required: true, message: '请输入体温', trigger: 'blur' }
|
||||
],
|
||||
healthStatus: [
|
||||
{ required: true, message: '请选择健康状态', trigger: 'change' }
|
||||
],
|
||||
inspector: [
|
||||
{ required: true, message: '请输入检查人员', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const dataSource = computed(() => monitoringStore.healthRecords || [])
|
||||
const modalTitle = computed(() => formData.id ? '编辑健康记录' : '新增健康记录')
|
||||
|
||||
// 获取健康状态颜色
|
||||
const getHealthStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
healthy: 'success',
|
||||
sick: 'error',
|
||||
quarantine: 'warning',
|
||||
recovery: 'processing'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取健康状态文本
|
||||
const getHealthStatusText = (status) => {
|
||||
const textMap = {
|
||||
healthy: '健康',
|
||||
sick: '患病',
|
||||
quarantine: '隔离观察',
|
||||
recovery: '康复中'
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取疫苗接种文本
|
||||
const getVaccinationText = (vaccination) => {
|
||||
const textMap = {
|
||||
foot_mouth: '口蹄疫疫苗',
|
||||
swine_fever: '猪瘟疫苗',
|
||||
blue_ear: '蓝耳病疫苗',
|
||||
porcine_parvovirus: '细小病毒疫苗'
|
||||
}
|
||||
return textMap[vaccination] || '无'
|
||||
}
|
||||
|
||||
// 获取预警级别颜色
|
||||
const getAlertColor = (level) => {
|
||||
const colorMap = {
|
||||
high: '#ff4d4f',
|
||||
medium: '#faad14',
|
||||
low: '#1890ff'
|
||||
}
|
||||
return colorMap[level] || '#d9d9d9'
|
||||
}
|
||||
|
||||
// 获取预警级别文本
|
||||
const getAlertLevelText = (level) => {
|
||||
const textMap = {
|
||||
high: '高风险',
|
||||
medium: '中风险',
|
||||
low: '低风险'
|
||||
}
|
||||
return textMap[level] || '未知'
|
||||
}
|
||||
|
||||
// 初始化健康趋势图表
|
||||
const initHealthTrendChart = () => {
|
||||
nextTick(() => {
|
||||
if (healthTrendChart.value) {
|
||||
const chart = echarts.init(healthTrendChart.value)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '近30天健康状态趋势',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['健康', '患病', '隔离观察', '康复中'],
|
||||
bottom: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [] // 从store获取日期数据
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '健康',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [], // 从store获取数据
|
||||
itemStyle: { color: '#52c41a' }
|
||||
},
|
||||
{
|
||||
name: '患病',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [], // 从store获取数据
|
||||
itemStyle: { color: '#ff4d4f' }
|
||||
},
|
||||
{
|
||||
name: '隔离观察',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [], // 从store获取数据
|
||||
itemStyle: { color: '#faad14' }
|
||||
},
|
||||
{
|
||||
name: '康复中',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [], // 从store获取数据
|
||||
itemStyle: { color: '#1890ff' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadHealthStats = async () => {
|
||||
try {
|
||||
const stats = await monitoringStore.fetchHealthStats()
|
||||
Object.assign(healthStats, stats)
|
||||
} catch (error) {
|
||||
message.error('加载统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载疫病预警
|
||||
const loadDiseaseAlerts = async () => {
|
||||
try {
|
||||
alertLoading.value = true
|
||||
diseaseAlerts.value = await monitoringStore.fetchDiseaseAlerts()
|
||||
} catch (error) {
|
||||
message.error('加载预警信息失败')
|
||||
} finally {
|
||||
alertLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载健康记录数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
...searchParams,
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
|
||||
const result = await monitoringStore.fetchHealthRecords(params)
|
||||
pagination.total = result.total
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (values) => {
|
||||
Object.assign(searchParams, values)
|
||||
if (values.dateRange) {
|
||||
searchParams.startDate = values.dateRange[0]
|
||||
searchParams.endDate = values.dateRange[1]
|
||||
}
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.keys(searchParams).forEach(key => {
|
||||
searchParams[key] = key.includes('Date') ? null : ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag, filters, sorter) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增记录处理
|
||||
const handleAddRecord = () => {
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑处理
|
||||
const handleEdit = (record) => {
|
||||
Object.assign(formData, record)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看处理
|
||||
const handleView = (record) => {
|
||||
currentRecord.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 删除处理
|
||||
const handleDelete = async (record) => {
|
||||
try {
|
||||
await monitoringStore.deleteHealthRecord(record.id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
loadHealthStats()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出处理
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await monitoringStore.exportHealthReport(searchParams)
|
||||
message.success('导出成功')
|
||||
} catch (error) {
|
||||
message.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 预警详情处理
|
||||
const handleAlertDetail = (alert) => {
|
||||
// 跳转到预警详情页面或显示详情弹窗
|
||||
console.log('查看预警详情:', alert)
|
||||
}
|
||||
|
||||
// 预警处理
|
||||
const handleAlertProcess = (alert) => {
|
||||
// 处理预警
|
||||
console.log('处理预警:', alert)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formData.id) {
|
||||
await monitoringStore.updateHealthRecord(formData.id, formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await monitoringStore.createHealthRecord(formData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
loadHealthStats()
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
message.error('请检查表单输入')
|
||||
} else {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消处理
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
animalId: '',
|
||||
checkDate: null,
|
||||
temperature: null,
|
||||
weight: null,
|
||||
healthStatus: '',
|
||||
vaccination: '',
|
||||
inspector: '',
|
||||
symptoms: '',
|
||||
treatment: '',
|
||||
remark: ''
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
loadHealthStats()
|
||||
loadDiseaseAlerts()
|
||||
loadData()
|
||||
initHealthTrendChart()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.animal-health-monitor {
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
|
||||
&.healthy {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
|
||||
&.sick {
|
||||
border-left: 4px solid #ff4d4f;
|
||||
}
|
||||
|
||||
&.quarantine {
|
||||
border-left: 4px solid #faad14;
|
||||
}
|
||||
|
||||
&.vaccination {
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.chart-container {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-card {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.alert-meta {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
.high-temperature {
|
||||
color: #ff4d4f;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-descriptions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
752
government-admin/src/views/monitoring/DeviceList.vue
Normal file
752
government-admin/src/views/monitoring/DeviceList.vue
Normal file
@@ -0,0 +1,752 @@
|
||||
<template>
|
||||
<div class="device-list">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h2>设备监控</h2>
|
||||
<p>实时监控养殖场设备运行状态</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加设备
|
||||
</a-button>
|
||||
<a-button @click="refreshData">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card online">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<CheckCircleOutlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.online }}</div>
|
||||
<div class="stat-label">在线设备</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card offline">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<ExclamationCircleOutlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.offline }}</div>
|
||||
<div class="stat-label">离线设备</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card warning">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<WarningOutlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.warning }}</div>
|
||||
<div class="stat-label">预警设备</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card total">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<DatabaseOutlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.total }}</div>
|
||||
<div class="stat-label">设备总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<a-card class="search-card">
|
||||
<a-form layout="inline" :model="searchForm" @finish="handleSearch">
|
||||
<a-form-item label="设备名称">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="请输入设备名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="请选择设备类型"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
>
|
||||
<a-select-option value="sensor">传感器</a-select-option>
|
||||
<a-select-option value="camera">摄像头</a-select-option>
|
||||
<a-select-option value="feeder">投料机</a-select-option>
|
||||
<a-select-option value="aerator">增氧机</a-select-option>
|
||||
<a-select-option value="pump">水泵</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="请选择设备状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="online">在线</a-select-option>
|
||||
<a-select-option value="offline">离线</a-select-option>
|
||||
<a-select-option value="warning">预警</a-select-option>
|
||||
<a-select-option value="error">故障</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属养殖场">
|
||||
<a-select
|
||||
v-model:value="searchForm.farmId"
|
||||
placeholder="请选择养殖场"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
show-search
|
||||
:filter-option="filterFarmOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="farm in farms"
|
||||
:key="farm.id"
|
||||
:value="farm.id"
|
||||
>
|
||||
{{ farm.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<a-card class="table-card">
|
||||
<template #title>
|
||||
<div class="table-header">
|
||||
<span>设备列表</span>
|
||||
<div class="table-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
:disabled="!selectedRowKeys.length"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
</a-button>
|
||||
<a-button @click="handleExport">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="devices"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
<template #icon>
|
||||
<component :is="getStatusIcon(record.status)" />
|
||||
</template>
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag color="blue">
|
||||
{{ getDeviceTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'lastHeartbeat'">
|
||||
<span :class="getHeartbeatClass(record.lastHeartbeat)">
|
||||
{{ formatTime(record.lastHeartbeat) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="viewDevice(record)"
|
||||
>
|
||||
查看
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="editDevice(record)"
|
||||
v-permission="'device:edit'"
|
||||
>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="controlDevice(record)"
|
||||
v-permission="'device:control'"
|
||||
>
|
||||
控制
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个设备吗?"
|
||||
@confirm="deleteDevice(record.id)"
|
||||
>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
v-permission="'device:delete'"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑设备弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="800px"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<DeviceForm
|
||||
ref="deviceFormRef"
|
||||
:device="currentDevice"
|
||||
:farms="farms"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 设备控制弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="controlModalVisible"
|
||||
title="设备控制"
|
||||
width="600px"
|
||||
@ok="handleControlOk"
|
||||
@cancel="controlModalVisible = false"
|
||||
>
|
||||
<DeviceControl
|
||||
ref="deviceControlRef"
|
||||
:device="currentDevice"
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
WarningOutlined,
|
||||
DatabaseOutlined,
|
||||
WifiOutlined,
|
||||
DisconnectOutlined,
|
||||
CloseCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import DeviceForm from './components/DeviceForm.vue'
|
||||
import DeviceControl from './components/DeviceControl.vue'
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { useFarmStore } from '@/stores/farm'
|
||||
import { formatTime } from '@/utils/date'
|
||||
|
||||
// Store
|
||||
const deviceStore = useDeviceStore()
|
||||
const farmStore = useFarmStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const devices = ref([])
|
||||
const farms = ref([])
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
online: 0,
|
||||
offline: 0,
|
||||
warning: 0
|
||||
})
|
||||
|
||||
const selectedRowKeys = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const controlModalVisible = ref(false)
|
||||
const currentDevice = ref(null)
|
||||
const deviceFormRef = ref()
|
||||
const deviceControlRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
status: '',
|
||||
farmId: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '设备编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '所属养殖场',
|
||||
dataIndex: 'farmName',
|
||||
key: 'farmName',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '设备状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '最后心跳',
|
||||
dataIndex: 'lastHeartbeat',
|
||||
key: 'lastHeartbeat',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '位置信息',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
}
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const modalTitle = computed(() => {
|
||||
return currentDevice.value?.id ? '编辑设备' : '添加设备'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
const [devicesRes, statsRes] = await Promise.all([
|
||||
deviceStore.fetchDevices(params),
|
||||
deviceStore.fetchStats()
|
||||
])
|
||||
|
||||
devices.value = devicesRes.data.list
|
||||
pagination.total = devicesRes.data.total
|
||||
stats.value = statsRes
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchFarms = async () => {
|
||||
try {
|
||||
const response = await farmStore.fetchFarms({ pageSize: 1000 })
|
||||
farms.value = response.data.list
|
||||
} catch (error) {
|
||||
console.error('获取养殖场列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.keys(searchForm).forEach(key => {
|
||||
searchForm[key] = ''
|
||||
})
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const showAddModal = () => {
|
||||
currentDevice.value = null
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const viewDevice = (device) => {
|
||||
// 跳转到设备详情页
|
||||
// router.push(`/monitoring/device/${device.id}`)
|
||||
}
|
||||
|
||||
const editDevice = (device) => {
|
||||
currentDevice.value = { ...device }
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const controlDevice = (device) => {
|
||||
currentDevice.value = device
|
||||
controlModalVisible.value = true
|
||||
}
|
||||
|
||||
const deleteDevice = async (id) => {
|
||||
try {
|
||||
await deviceStore.removeDevice(id)
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除设备失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个设备吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deviceStore.batchRemoveDevices(selectedRowKeys.value)
|
||||
selectedRowKeys.value = []
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
// 导出功能
|
||||
message.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const formData = await deviceFormRef.value.validate()
|
||||
if (currentDevice.value?.id) {
|
||||
await deviceStore.editDevice(currentDevice.value.id, formData)
|
||||
} else {
|
||||
await deviceStore.addDevice(formData)
|
||||
}
|
||||
modalVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存设备失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
currentDevice.value = null
|
||||
}
|
||||
|
||||
const handleControlOk = async () => {
|
||||
try {
|
||||
await deviceControlRef.value.executeControl()
|
||||
controlModalVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('设备控制失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
warning: 'orange',
|
||||
error: 'red'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
const icons = {
|
||||
online: WifiOutlined,
|
||||
offline: DisconnectOutlined,
|
||||
warning: WarningOutlined,
|
||||
error: CloseCircleOutlined
|
||||
}
|
||||
return icons[status] || WifiOutlined
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
warning: '预警',
|
||||
error: '故障'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
const getDeviceTypeText = (type) => {
|
||||
const texts = {
|
||||
sensor: '传感器',
|
||||
camera: '摄像头',
|
||||
feeder: '投料机',
|
||||
aerator: '增氧机',
|
||||
pump: '水泵'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
const getHeartbeatClass = (lastHeartbeat) => {
|
||||
const now = new Date()
|
||||
const heartbeat = new Date(lastHeartbeat)
|
||||
const diff = now - heartbeat
|
||||
|
||||
if (diff < 5 * 60 * 1000) return 'heartbeat-normal' // 5分钟内
|
||||
if (diff < 30 * 60 * 1000) return 'heartbeat-warning' // 30分钟内
|
||||
return 'heartbeat-danger' // 超过30分钟
|
||||
}
|
||||
|
||||
const filterFarmOption = (input, option) => {
|
||||
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchFarms()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-list {
|
||||
padding: 24px;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.header-content {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.stat-card {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.online {
|
||||
.stat-icon {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
.stat-value {
|
||||
color: #16a34a;
|
||||
}
|
||||
}
|
||||
|
||||
&.offline {
|
||||
.stat-icon {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
.stat-value {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
.stat-icon {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
.stat-value {
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
&.total {
|
||||
.stat-icon {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
.stat-value {
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heartbeat-normal {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.heartbeat-warning {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.heartbeat-danger {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
175
government-admin/src/views/monitoring/EnvironmentMonitor.vue
Normal file
175
government-admin/src/views/monitoring/EnvironmentMonitor.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="environment-monitor">
|
||||
<a-card title="环境监测" :bordered="false">
|
||||
<a-row :gutter="16" style="margin-bottom: 24px;">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="温度"
|
||||
:value="environmentData.temperature"
|
||||
suffix="°C"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="湿度"
|
||||
:value="environmentData.humidity"
|
||||
suffix="%"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="空气质量"
|
||||
:value="environmentData.airQuality"
|
||||
:value-style="{ color: '#cf1322' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="噪音"
|
||||
:value="environmentData.noise"
|
||||
suffix="dB"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-tabs>
|
||||
<a-tab-pane key="realtime" tab="实时监测">
|
||||
<div id="realtime-chart" style="height: 400px;"></div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="history" tab="历史数据">
|
||||
<div class="history-search">
|
||||
<a-form layout="inline" :model="historyForm">
|
||||
<a-form-item label="监测点">
|
||||
<a-select v-model:value="historyForm.station" style="width: 200px">
|
||||
<a-select-option value="station1">监测点1</a-select-option>
|
||||
<a-select-option value="station2">监测点2</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="时间范围">
|
||||
<a-range-picker v-model:value="historyForm.dateRange" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="historyColumns"
|
||||
:data-source="historyData"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="alert" tab="预警设置">
|
||||
<a-form :model="alertForm" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="温度预警阈值">
|
||||
<a-input-number
|
||||
v-model:value="alertForm.temperatureThreshold"
|
||||
:min="0"
|
||||
:max="50"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="湿度预警阈值">
|
||||
<a-input-number
|
||||
v-model:value="alertForm.humidityThreshold"
|
||||
:min="0"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item>
|
||||
<a-button type="primary">保存设置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const historyData = ref([])
|
||||
|
||||
const environmentData = reactive({
|
||||
temperature: 25.6,
|
||||
humidity: 68,
|
||||
airQuality: 'AQI 45',
|
||||
noise: 42
|
||||
})
|
||||
|
||||
const historyForm = reactive({
|
||||
station: undefined,
|
||||
dateRange: null
|
||||
})
|
||||
|
||||
const alertForm = reactive({
|
||||
temperatureThreshold: 30,
|
||||
humidityThreshold: 80
|
||||
})
|
||||
|
||||
const historyColumns = [
|
||||
{ title: '监测时间', dataIndex: 'time', key: 'time' },
|
||||
{ title: '温度(°C)', dataIndex: 'temperature', key: 'temperature' },
|
||||
{ title: '湿度(%)', dataIndex: 'humidity', key: 'humidity' },
|
||||
{ title: '空气质量', dataIndex: 'airQuality', key: 'airQuality' },
|
||||
{ title: '噪音(dB)', dataIndex: 'noise', key: 'noise' }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
historyData.value = [
|
||||
{
|
||||
id: 1,
|
||||
time: '2024-01-15 14:00',
|
||||
temperature: 25.6,
|
||||
humidity: 68,
|
||||
airQuality: 'AQI 45',
|
||||
noise: 42
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
time: '2024-01-15 13:00',
|
||||
temperature: 24.8,
|
||||
humidity: 70,
|
||||
airQuality: 'AQI 48',
|
||||
noise: 40
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.environment-monitor {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.history-search {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
215
government-admin/src/views/monitoring/VideoSurveillance.vue
Normal file
215
government-admin/src/views/monitoring/VideoSurveillance.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="video-surveillance">
|
||||
<a-card title="视频监控" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="18">
|
||||
<div class="video-grid">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="12" v-for="camera in cameras" :key="camera.id">
|
||||
<div class="video-item">
|
||||
<div class="video-header">
|
||||
<span>{{ camera.name }}</span>
|
||||
<a-tag :color="camera.status === 'online' ? 'green' : 'red'">
|
||||
{{ camera.status === 'online' ? '在线' : '离线' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="video-player">
|
||||
<div class="video-placeholder">
|
||||
<video-camera-outlined style="font-size: 48px; color: #ccc;" />
|
||||
<p>{{ camera.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-controls">
|
||||
<a-space>
|
||||
<a-button size="small" type="text">
|
||||
<play-circle-outlined />
|
||||
</a-button>
|
||||
<a-button size="small" type="text">
|
||||
<pause-circle-outlined />
|
||||
</a-button>
|
||||
<a-button size="small" type="text">
|
||||
<fullscreen-outlined />
|
||||
</a-button>
|
||||
<a-button size="small" type="text">
|
||||
<camera-outlined />
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-card title="摄像头列表" size="small">
|
||||
<div class="camera-list">
|
||||
<div
|
||||
v-for="camera in allCameras"
|
||||
:key="camera.id"
|
||||
class="camera-item"
|
||||
:class="{ active: selectedCamera === camera.id }"
|
||||
@click="selectCamera(camera.id)"
|
||||
>
|
||||
<div class="camera-info">
|
||||
<div class="camera-name">{{ camera.name }}</div>
|
||||
<div class="camera-location">{{ camera.location }}</div>
|
||||
</div>
|
||||
<a-tag :color="camera.status === 'online' ? 'green' : 'red'" size="small">
|
||||
{{ camera.status === 'online' ? '在线' : '离线' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card title="录像回放" size="small" style="margin-top: 16px;">
|
||||
<a-form layout="vertical" size="small">
|
||||
<a-form-item label="选择摄像头">
|
||||
<a-select v-model:value="playbackForm.camera" size="small">
|
||||
<a-select-option v-for="camera in allCameras" :key="camera.id" :value="camera.id">
|
||||
{{ camera.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="回放时间">
|
||||
<a-date-picker v-model:value="playbackForm.date" size="small" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="时间段">
|
||||
<a-time-range-picker v-model:value="playbackForm.timeRange" size="small" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" size="small" block>开始回放</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import {
|
||||
VideoCameraOutlined,
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
FullscreenOutlined,
|
||||
CameraOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const selectedCamera = ref(null)
|
||||
const cameras = ref([])
|
||||
const allCameras = ref([])
|
||||
|
||||
const playbackForm = reactive({
|
||||
camera: undefined,
|
||||
date: null,
|
||||
timeRange: null
|
||||
})
|
||||
|
||||
const selectCamera = (cameraId) => {
|
||||
selectedCamera.value = cameraId
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
allCameras.value = [
|
||||
{ id: 1, name: '养殖场入口', location: '大门口', status: 'online' },
|
||||
{ id: 2, name: '饲料仓库', location: '仓库区', status: 'online' },
|
||||
{ id: 3, name: '养殖区域1', location: '1号棚', status: 'online' },
|
||||
{ id: 4, name: '养殖区域2', location: '2号棚', status: 'offline' },
|
||||
{ id: 5, name: '办公区域', location: '办公楼', status: 'online' },
|
||||
{ id: 6, name: '污水处理', location: '处理站', status: 'online' }
|
||||
]
|
||||
|
||||
// 显示前4个摄像头
|
||||
cameras.value = allCameras.value.slice(0, 4)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-surveillance {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
background: #f5f5f5;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.video-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
height: 200px;
|
||||
background: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.camera-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.camera-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.camera-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.camera-item.active {
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
}
|
||||
|
||||
.camera-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.camera-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.camera-location {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div class="device-control">
|
||||
<div class="device-info">
|
||||
<h4>{{ device?.name }}</h4>
|
||||
<p>设备编号: {{ device?.code }}</p>
|
||||
<a-tag :color="getStatusColor(device?.status)">
|
||||
{{ getStatusText(device?.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<!-- 基础控制 -->
|
||||
<div class="control-section">
|
||||
<h5>基础控制</h5>
|
||||
<a-space wrap>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="loading.start"
|
||||
@click="handleControl('start')"
|
||||
:disabled="device?.status === 'online'"
|
||||
>
|
||||
启动设备
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
:loading="loading.stop"
|
||||
@click="handleControl('stop')"
|
||||
:disabled="device?.status === 'offline'"
|
||||
>
|
||||
停止设备
|
||||
</a-button>
|
||||
<a-button
|
||||
:loading="loading.restart"
|
||||
@click="handleControl('restart')"
|
||||
>
|
||||
重启设备
|
||||
</a-button>
|
||||
<a-button
|
||||
:loading="loading.reset"
|
||||
@click="handleControl('reset')"
|
||||
>
|
||||
重置设备
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 参数设置 -->
|
||||
<div class="control-section" v-if="showParameterControl">
|
||||
<h5>参数设置</h5>
|
||||
<a-form layout="vertical" :model="parameterForm">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12" v-if="deviceTypeConfig.temperature">
|
||||
<a-form-item label="目标温度(°C)">
|
||||
<a-input-number
|
||||
v-model:value="parameterForm.targetTemperature"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12" v-if="deviceTypeConfig.humidity">
|
||||
<a-form-item label="目标湿度(%)">
|
||||
<a-input-number
|
||||
v-model:value="parameterForm.targetHumidity"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12" v-if="deviceTypeConfig.speed">
|
||||
<a-form-item label="运行速度(%)">
|
||||
<a-input-number
|
||||
v-model:value="parameterForm.speed"
|
||||
:min="0"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12" v-if="deviceTypeConfig.power">
|
||||
<a-form-item label="功率(%)">
|
||||
<a-input-number
|
||||
v-model:value="parameterForm.power"
|
||||
:min="0"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12" v-if="deviceTypeConfig.interval">
|
||||
<a-form-item label="工作间隔(秒)">
|
||||
<a-input-number
|
||||
v-model:value="parameterForm.workInterval"
|
||||
:min="1"
|
||||
:max="3600"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12" v-if="deviceTypeConfig.duration">
|
||||
<a-form-item label="工作时长(秒)">
|
||||
<a-input-number
|
||||
v-model:value="parameterForm.workDuration"
|
||||
:min="1"
|
||||
:max="3600"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="loading.setParams"
|
||||
@click="handleSetParameters"
|
||||
>
|
||||
设置参数
|
||||
</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 定时任务 -->
|
||||
<div class="control-section">
|
||||
<h5>定时任务</h5>
|
||||
<a-form layout="vertical" :model="scheduleForm">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="任务类型">
|
||||
<a-select v-model:value="scheduleForm.type" placeholder="选择任务类型">
|
||||
<a-select-option value="start">启动</a-select-option>
|
||||
<a-select-option value="stop">停止</a-select-option>
|
||||
<a-select-option value="feed" v-if="device?.type === 'feeder'">投料</a-select-option>
|
||||
<a-select-option value="aerate" v-if="device?.type === 'aerator'">增氧</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="执行时间">
|
||||
<a-time-picker
|
||||
v-model:value="scheduleForm.time"
|
||||
format="HH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="重复">
|
||||
<a-select v-model:value="scheduleForm.repeat" placeholder="选择重复方式">
|
||||
<a-select-option value="once">仅一次</a-select-option>
|
||||
<a-select-option value="daily">每天</a-select-option>
|
||||
<a-select-option value="weekly">每周</a-select-option>
|
||||
<a-select-option value="custom">自定义</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="loading.schedule"
|
||||
@click="handleScheduleTask"
|
||||
>
|
||||
添加定时任务
|
||||
</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 当前定时任务列表 -->
|
||||
<div class="control-section">
|
||||
<h5>当前定时任务</h5>
|
||||
<a-list
|
||||
:data-source="scheduleTasks"
|
||||
size="small"
|
||||
:locale="{ emptyText: '暂无定时任务' }"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="toggleTask(item)"
|
||||
>
|
||||
{{ item.enabled ? '禁用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="deleteTask(item.id)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<span :class="{ 'task-disabled': !item.enabled }">
|
||||
{{ getTaskTypeText(item.type) }} - {{ item.time }}
|
||||
</span>
|
||||
</template>
|
||||
<template #description>
|
||||
重复: {{ getRepeatText(item.repeat) }}
|
||||
<a-tag
|
||||
:color="item.enabled ? 'green' : 'default'"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ item.enabled ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
|
||||
<!-- 实时数据 -->
|
||||
<div class="control-section" v-if="device?.monitorParams?.length">
|
||||
<h5>实时数据</h5>
|
||||
<a-row :gutter="16">
|
||||
<a-col
|
||||
:span="8"
|
||||
v-for="param in device.monitorParams"
|
||||
:key="param"
|
||||
>
|
||||
<a-card size="small" class="data-card">
|
||||
<div class="data-item">
|
||||
<div class="data-label">{{ getParamText(param) }}</div>
|
||||
<div class="data-value">
|
||||
{{ realtimeData[param] || '--' }}
|
||||
<span class="data-unit">{{ getParamUnit(param) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button
|
||||
type="link"
|
||||
:loading="loading.refresh"
|
||||
@click="refreshRealtimeData"
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
刷新数据
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, defineExpose } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const loading = reactive({
|
||||
start: false,
|
||||
stop: false,
|
||||
restart: false,
|
||||
reset: false,
|
||||
setParams: false,
|
||||
schedule: false,
|
||||
refresh: false
|
||||
})
|
||||
|
||||
const parameterForm = reactive({
|
||||
targetTemperature: null,
|
||||
targetHumidity: null,
|
||||
speed: null,
|
||||
power: null,
|
||||
workInterval: null,
|
||||
workDuration: null
|
||||
})
|
||||
|
||||
const scheduleForm = reactive({
|
||||
type: '',
|
||||
time: null,
|
||||
repeat: 'once'
|
||||
})
|
||||
|
||||
const scheduleTasks = ref([])
|
||||
const realtimeData = ref({})
|
||||
|
||||
// 计算属性
|
||||
const deviceTypeConfig = computed(() => {
|
||||
const configs = {
|
||||
sensor: { temperature: true, humidity: true },
|
||||
camera: {},
|
||||
feeder: { speed: true, interval: true, duration: true },
|
||||
aerator: { power: true, interval: true, duration: true },
|
||||
pump: { power: true, speed: true },
|
||||
light: { power: true, interval: true, duration: true },
|
||||
heater: { temperature: true, power: true },
|
||||
cooler: { temperature: true, power: true }
|
||||
}
|
||||
return configs[props.device?.type] || {}
|
||||
})
|
||||
|
||||
const showParameterControl = computed(() => {
|
||||
return Object.keys(deviceTypeConfig.value).length > 0
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleControl = async (action) => {
|
||||
try {
|
||||
loading[action] = true
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
message.success(`${getActionText(action)}成功`)
|
||||
|
||||
// 这里应该调用实际的设备控制API
|
||||
// await deviceControlAPI(props.device.id, action)
|
||||
|
||||
} catch (error) {
|
||||
message.error(`${getActionText(action)}失败`)
|
||||
} finally {
|
||||
loading[action] = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetParameters = async () => {
|
||||
try {
|
||||
loading.setParams = true
|
||||
|
||||
// 过滤空值
|
||||
const params = Object.keys(parameterForm)
|
||||
.filter(key => parameterForm[key] !== null && parameterForm[key] !== '')
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = parameterForm[key]
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
message.warning('请至少设置一个参数')
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
message.success('参数设置成功')
|
||||
|
||||
// 这里应该调用实际的参数设置API
|
||||
// await setDeviceParameters(props.device.id, params)
|
||||
|
||||
} catch (error) {
|
||||
message.error('参数设置失败')
|
||||
} finally {
|
||||
loading.setParams = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleScheduleTask = async () => {
|
||||
try {
|
||||
if (!scheduleForm.type || !scheduleForm.time) {
|
||||
message.warning('请完整填写任务信息')
|
||||
return
|
||||
}
|
||||
|
||||
loading.schedule = true
|
||||
|
||||
const task = {
|
||||
id: Date.now(),
|
||||
type: scheduleForm.type,
|
||||
time: scheduleForm.time.format('HH:mm'),
|
||||
repeat: scheduleForm.repeat,
|
||||
enabled: true,
|
||||
deviceId: props.device.id
|
||||
}
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
scheduleTasks.value.push(task)
|
||||
message.success('定时任务添加成功')
|
||||
|
||||
// 重置表单
|
||||
scheduleForm.type = ''
|
||||
scheduleForm.time = null
|
||||
scheduleForm.repeat = 'once'
|
||||
|
||||
} catch (error) {
|
||||
message.error('添加定时任务失败')
|
||||
} finally {
|
||||
loading.schedule = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTask = async (task) => {
|
||||
try {
|
||||
task.enabled = !task.enabled
|
||||
message.success(`任务已${task.enabled ? '启用' : '禁用'}`)
|
||||
|
||||
// 这里应该调用实际的API
|
||||
// await toggleScheduleTask(task.id, task.enabled)
|
||||
|
||||
} catch (error) {
|
||||
task.enabled = !task.enabled // 回滚状态
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTask = async (taskId) => {
|
||||
try {
|
||||
const index = scheduleTasks.value.findIndex(task => task.id === taskId)
|
||||
if (index !== -1) {
|
||||
scheduleTasks.value.splice(index, 1)
|
||||
message.success('任务删除成功')
|
||||
}
|
||||
|
||||
// 这里应该调用实际的API
|
||||
// await deleteScheduleTask(taskId)
|
||||
|
||||
} catch (error) {
|
||||
message.error('删除任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
const refreshRealtimeData = async () => {
|
||||
try {
|
||||
loading.refresh = true
|
||||
|
||||
// 模拟获取实时数据
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟数据
|
||||
const mockData = {
|
||||
temperature: (Math.random() * 10 + 20).toFixed(1),
|
||||
humidity: (Math.random() * 20 + 60).toFixed(1),
|
||||
ph: (Math.random() * 2 + 6).toFixed(2),
|
||||
oxygen: (Math.random() * 3 + 5).toFixed(2),
|
||||
ammonia: (Math.random() * 0.5).toFixed(3),
|
||||
turbidity: (Math.random() * 10 + 5).toFixed(1),
|
||||
salinity: (Math.random() * 5 + 30).toFixed(1),
|
||||
pressure: (Math.random() * 0.5 + 1).toFixed(2),
|
||||
flow: (Math.random() * 50 + 100).toFixed(1),
|
||||
level: (Math.random() * 2 + 1).toFixed(2)
|
||||
}
|
||||
|
||||
realtimeData.value = mockData
|
||||
|
||||
} catch (error) {
|
||||
message.error('获取实时数据失败')
|
||||
} finally {
|
||||
loading.refresh = false
|
||||
}
|
||||
}
|
||||
|
||||
const executeControl = async () => {
|
||||
// 这个方法由父组件调用,用于执行控制操作
|
||||
return true
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
warning: 'orange',
|
||||
error: 'red'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
warning: '预警',
|
||||
error: '故障'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
const getActionText = (action) => {
|
||||
const texts = {
|
||||
start: '启动设备',
|
||||
stop: '停止设备',
|
||||
restart: '重启设备',
|
||||
reset: '重置设备'
|
||||
}
|
||||
return texts[action] || action
|
||||
}
|
||||
|
||||
const getTaskTypeText = (type) => {
|
||||
const texts = {
|
||||
start: '启动',
|
||||
stop: '停止',
|
||||
feed: '投料',
|
||||
aerate: '增氧'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
const getRepeatText = (repeat) => {
|
||||
const texts = {
|
||||
once: '仅一次',
|
||||
daily: '每天',
|
||||
weekly: '每周',
|
||||
custom: '自定义'
|
||||
}
|
||||
return texts[repeat] || repeat
|
||||
}
|
||||
|
||||
const getParamText = (param) => {
|
||||
const texts = {
|
||||
temperature: '温度',
|
||||
humidity: '湿度',
|
||||
ph: 'pH值',
|
||||
oxygen: '溶氧量',
|
||||
ammonia: '氨氮',
|
||||
turbidity: '浊度',
|
||||
salinity: '盐度',
|
||||
pressure: '压力',
|
||||
flow: '流量',
|
||||
level: '液位'
|
||||
}
|
||||
return texts[param] || param
|
||||
}
|
||||
|
||||
const getParamUnit = (param) => {
|
||||
const units = {
|
||||
temperature: '°C',
|
||||
humidity: '%',
|
||||
ph: '',
|
||||
oxygen: 'mg/L',
|
||||
ammonia: 'mg/L',
|
||||
turbidity: 'NTU',
|
||||
salinity: '‰',
|
||||
pressure: 'MPa',
|
||||
flow: 'L/min',
|
||||
level: 'm'
|
||||
}
|
||||
return units[param] || ''
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 初始化参数表单
|
||||
if (props.device?.parameters) {
|
||||
Object.assign(parameterForm, props.device.parameters)
|
||||
}
|
||||
|
||||
// 获取定时任务列表
|
||||
// fetchScheduleTasks()
|
||||
|
||||
// 获取实时数据
|
||||
if (props.device?.monitorParams?.length) {
|
||||
refreshRealtimeData()
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
executeControl
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-control {
|
||||
.device-info {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 12px 0;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.control-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h5 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
.data-card {
|
||||
.data-item {
|
||||
text-align: center;
|
||||
|
||||
.data-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
|
||||
.data-unit {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-disabled {
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
485
government-admin/src/views/monitoring/components/DeviceForm.vue
Normal file
485
government-admin/src/views/monitoring/components/DeviceForm.vue
Normal file
@@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<div class="device-form">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<!-- 基本信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3>基本信息</h3>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备名称" name="name">
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入设备名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备编号" name="code">
|
||||
<a-input
|
||||
v-model:value="formData.code"
|
||||
placeholder="请输入设备编号"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备类型" name="type">
|
||||
<a-select
|
||||
v-model:value="formData.type"
|
||||
placeholder="请选择设备类型"
|
||||
>
|
||||
<a-select-option value="sensor">传感器</a-select-option>
|
||||
<a-select-option value="camera">摄像头</a-select-option>
|
||||
<a-select-option value="feeder">投料机</a-select-option>
|
||||
<a-select-option value="aerator">增氧机</a-select-option>
|
||||
<a-select-option value="pump">水泵</a-select-option>
|
||||
<a-select-option value="light">照明设备</a-select-option>
|
||||
<a-select-option value="heater">加热设备</a-select-option>
|
||||
<a-select-option value="cooler">制冷设备</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属养殖场" name="farmId">
|
||||
<a-select
|
||||
v-model:value="formData.farmId"
|
||||
placeholder="请选择养殖场"
|
||||
show-search
|
||||
:filter-option="filterFarmOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="farm in farms"
|
||||
:key="farm.id"
|
||||
:value="farm.id"
|
||||
>
|
||||
{{ farm.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备品牌" name="brand">
|
||||
<a-input
|
||||
v-model:value="formData.brand"
|
||||
placeholder="请输入设备品牌"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备型号" name="model">
|
||||
<a-input
|
||||
v-model:value="formData.model"
|
||||
placeholder="请输入设备型号"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 网络配置 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3>网络配置</h3>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="IP地址" name="ipAddress">
|
||||
<a-input
|
||||
v-model:value="formData.ipAddress"
|
||||
placeholder="请输入IP地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="端口号" name="port">
|
||||
<a-input-number
|
||||
v-model:value="formData.port"
|
||||
placeholder="请输入端口号"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="MAC地址" name="macAddress">
|
||||
<a-input
|
||||
v-model:value="formData.macAddress"
|
||||
placeholder="请输入MAC地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="通信协议" name="protocol">
|
||||
<a-select
|
||||
v-model:value="formData.protocol"
|
||||
placeholder="请选择通信协议"
|
||||
>
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="udp">UDP</a-select-option>
|
||||
<a-select-option value="mqtt">MQTT</a-select-option>
|
||||
<a-select-option value="http">HTTP</a-select-option>
|
||||
<a-select-option value="modbus">Modbus</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3>位置信息</h3>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="经度" name="longitude">
|
||||
<a-input-number
|
||||
v-model:value="formData.longitude"
|
||||
placeholder="请输入经度"
|
||||
:precision="6"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="纬度" name="latitude">
|
||||
<a-input-number
|
||||
v-model:value="formData.latitude"
|
||||
placeholder="请输入纬度"
|
||||
:precision="6"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="安装高度(米)" name="altitude">
|
||||
<a-input-number
|
||||
v-model:value="formData.altitude"
|
||||
placeholder="请输入安装高度"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="详细位置" name="location">
|
||||
<a-input
|
||||
v-model:value="formData.location"
|
||||
placeholder="请输入详细位置描述"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 监控参数 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3>监控参数</h3>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="数据采集间隔(秒)" name="dataInterval">
|
||||
<a-input-number
|
||||
v-model:value="formData.dataInterval"
|
||||
placeholder="请输入数据采集间隔"
|
||||
:min="1"
|
||||
:max="3600"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="心跳间隔(秒)" name="heartbeatInterval">
|
||||
<a-input-number
|
||||
v-model:value="formData.heartbeatInterval"
|
||||
placeholder="请输入心跳间隔"
|
||||
:min="10"
|
||||
:max="300"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="监控参数" name="monitorParams">
|
||||
<a-select
|
||||
v-model:value="formData.monitorParams"
|
||||
mode="multiple"
|
||||
placeholder="请选择监控参数"
|
||||
>
|
||||
<a-select-option value="temperature">温度</a-select-option>
|
||||
<a-select-option value="humidity">湿度</a-select-option>
|
||||
<a-select-option value="ph">pH值</a-select-option>
|
||||
<a-select-option value="oxygen">溶氧量</a-select-option>
|
||||
<a-select-option value="ammonia">氨氮</a-select-option>
|
||||
<a-select-option value="turbidity">浊度</a-select-option>
|
||||
<a-select-option value="salinity">盐度</a-select-option>
|
||||
<a-select-option value="pressure">压力</a-select-option>
|
||||
<a-select-option value="flow">流量</a-select-option>
|
||||
<a-select-option value="level">液位</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 预警配置 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3>预警配置</h3>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="启用预警" name="alertEnabled">
|
||||
<a-switch v-model:checked="formData.alertEnabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="离线预警时间(分钟)" name="offlineAlertTime">
|
||||
<a-input-number
|
||||
v-model:value="formData.offlineAlertTime"
|
||||
placeholder="请输入离线预警时间"
|
||||
:min="1"
|
||||
:max="1440"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<a-col :span="24">
|
||||
<div class="form-section">
|
||||
<h3>其他信息</h3>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="安装日期" name="installDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.installDate"
|
||||
placeholder="请选择安装日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="保修期至" name="warrantyDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.warrantyDate"
|
||||
placeholder="请选择保修期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, defineExpose } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
farms: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
type: '',
|
||||
farmId: '',
|
||||
brand: '',
|
||||
model: '',
|
||||
ipAddress: '',
|
||||
port: null,
|
||||
macAddress: '',
|
||||
protocol: '',
|
||||
longitude: null,
|
||||
latitude: null,
|
||||
altitude: null,
|
||||
location: '',
|
||||
dataInterval: 60,
|
||||
heartbeatInterval: 30,
|
||||
monitorParams: [],
|
||||
alertEnabled: true,
|
||||
offlineAlertTime: 5,
|
||||
installDate: null,
|
||||
warrantyDate: null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入设备名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '设备名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入设备编号', trigger: 'blur' },
|
||||
{ pattern: /^[A-Za-z0-9_-]+$/, message: '设备编号只能包含字母、数字、下划线和横线', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择设备类型', trigger: 'change' }
|
||||
],
|
||||
farmId: [
|
||||
{ required: true, message: '请选择所属养殖场', trigger: 'change' }
|
||||
],
|
||||
ipAddress: [
|
||||
{ pattern: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, message: '请输入正确的IP地址格式', trigger: 'blur' }
|
||||
],
|
||||
port: [
|
||||
{ type: 'number', min: 1, max: 65535, message: '端口号范围为 1-65535', trigger: 'blur' }
|
||||
],
|
||||
macAddress: [
|
||||
{ pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/, message: '请输入正确的MAC地址格式', trigger: 'blur' }
|
||||
],
|
||||
dataInterval: [
|
||||
{ required: true, message: '请输入数据采集间隔', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 3600, message: '数据采集间隔范围为 1-3600 秒', trigger: 'blur' }
|
||||
],
|
||||
heartbeatInterval: [
|
||||
{ required: true, message: '请输入心跳间隔', trigger: 'blur' },
|
||||
{ type: 'number', min: 10, max: 300, message: '心跳间隔范围为 10-300 秒', trigger: 'blur' }
|
||||
],
|
||||
offlineAlertTime: [
|
||||
{ type: 'number', min: 1, max: 1440, message: '离线预警时间范围为 1-1440 分钟', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听设备数据变化
|
||||
watch(() => props.device, (newDevice) => {
|
||||
if (newDevice) {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (newDevice[key] !== undefined) {
|
||||
if (key === 'installDate' || key === 'warrantyDate') {
|
||||
formData[key] = newDevice[key] ? dayjs(newDevice[key]) : null
|
||||
} else {
|
||||
formData[key] = newDevice[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key === 'dataInterval') {
|
||||
formData[key] = 60
|
||||
} else if (key === 'heartbeatInterval') {
|
||||
formData[key] = 30
|
||||
} else if (key === 'alertEnabled') {
|
||||
formData[key] = true
|
||||
} else if (key === 'offlineAlertTime') {
|
||||
formData[key] = 5
|
||||
} else if (key === 'monitorParams') {
|
||||
formData[key] = []
|
||||
} else if (key === 'installDate' || key === 'warrantyDate') {
|
||||
formData[key] = null
|
||||
} else {
|
||||
formData[key] = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
const validate = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
const data = { ...formData }
|
||||
|
||||
// 处理日期格式
|
||||
if (data.installDate) {
|
||||
data.installDate = data.installDate.format('YYYY-MM-DD')
|
||||
}
|
||||
if (data.warrantyDate) {
|
||||
data.warrantyDate = data.warrantyDate.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
throw new Error('表单验证失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 养殖场筛选
|
||||
const filterFarmOption = (input, option) => {
|
||||
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
validate,
|
||||
resetForm
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-form {
|
||||
.form-section {
|
||||
margin-bottom: 32px;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
736
government-admin/src/views/personnel/DepartmentManagement.vue
Normal file
736
government-admin/src/views/personnel/DepartmentManagement.vue
Normal file
@@ -0,0 +1,736 @@
|
||||
<template>
|
||||
<div class="department-management">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="部门管理"
|
||||
description="管理系统部门信息,包括部门层级、人员配置、权限分配等"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="addDepartment">
|
||||
<PlusOutlined />
|
||||
新增部门
|
||||
</a-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 统计面板 -->
|
||||
<div class="stats-panel">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总部门数"
|
||||
:value="stats.totalDepartments"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<ApartmentOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="一级部门"
|
||||
:value="stats.topLevelDepartments"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<BankOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总员工数"
|
||||
:value="stats.totalEmployees"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<TeamOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="本月新增"
|
||||
:value="stats.newDepartments"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<PlusCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<a-card :bordered="false">
|
||||
<a-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="部门名称" name="name">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="请输入部门名称"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="部门类型" name="type">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="请选择部门类型"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="management">管理部门</a-select-option>
|
||||
<a-select-option value="business">业务部门</a-select-option>
|
||||
<a-select-option value="support">支持部门</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="inactive">停用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<SearchOutlined />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSearch">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 部门树形结构 -->
|
||||
<div class="department-tree">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-card title="部门结构" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button size="small" @click="expandAll">
|
||||
<NodeExpandOutlined />
|
||||
展开全部
|
||||
</a-button>
|
||||
<a-button size="small" @click="collapseAll">
|
||||
<NodeCollapseOutlined />
|
||||
收起全部
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-tree
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
:tree-data="departmentTree"
|
||||
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
||||
show-line
|
||||
@select="onSelectDepartment"
|
||||
>
|
||||
<template #title="{ name, type, employeeCount }">
|
||||
<span>{{ name }}</span>
|
||||
<a-tag size="small" :color="getTypeColor(type)" style="margin-left: 8px">
|
||||
{{ getTypeName(type) }}
|
||||
</a-tag>
|
||||
<span style="color: #999; font-size: 12px; margin-left: 4px">
|
||||
({{ employeeCount }}人)
|
||||
</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-card title="部门详情" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space v-if="selectedDepartment">
|
||||
<a-button @click="editDepartment(selectedDepartment)">
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button @click="addSubDepartment(selectedDepartment)">
|
||||
<PlusOutlined />
|
||||
添加子部门
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="viewEmployees(selectedDepartment)">
|
||||
<TeamOutlined />
|
||||
查看员工
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="managePerm(selectedDepartment)">
|
||||
<SafetyCertificateOutlined />
|
||||
权限管理
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item @click="deleteDepartment(selectedDepartment)" danger>
|
||||
<DeleteOutlined />
|
||||
删除部门
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button>
|
||||
更多操作
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div v-if="selectedDepartment" class="department-detail">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="部门名称">
|
||||
{{ selectedDepartment.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="部门编码">
|
||||
{{ selectedDepartment.code }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="部门类型">
|
||||
<a-tag :color="getTypeColor(selectedDepartment.type)">
|
||||
{{ getTypeName(selectedDepartment.type) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="selectedDepartment.status === 'active' ? 'green' : 'red'">
|
||||
{{ selectedDepartment.status === 'active' ? '正常' : '停用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="负责人">
|
||||
{{ selectedDepartment.manager || '暂无' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ selectedDepartment.phone || '暂无' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="员工数量">
|
||||
{{ selectedDepartment.employeeCount }}人
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ selectedDepartment.createdAt }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="部门描述" :span="2">
|
||||
{{ selectedDepartment.description || '暂无描述' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 子部门列表 -->
|
||||
<div v-if="selectedDepartment.children && selectedDepartment.children.length > 0" class="sub-departments">
|
||||
<h4>子部门</h4>
|
||||
<a-list
|
||||
:data-source="selectedDepartment.children"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a @click="selectDepartment(item)">查看</a>
|
||||
<a @click="editDepartment(item)">编辑</a>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<span>{{ item.name }}</span>
|
||||
<a-tag size="small" :color="getTypeColor(item.type)" style="margin-left: 8px">
|
||||
{{ getTypeName(item.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #description>
|
||||
负责人:{{ item.manager || '暂无' }} | 员工:{{ item.employeeCount }}人
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<a-empty description="请选择左侧部门查看详情" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑部门弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="600px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="部门名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入部门名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="部门编码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入部门编码" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="上级部门" name="parentId">
|
||||
<a-tree-select
|
||||
v-model:value="formData.parentId"
|
||||
:tree-data="departmentTree"
|
||||
:field-names="{ children: 'children', label: 'name', value: 'id' }"
|
||||
placeholder="请选择上级部门"
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="部门类型" name="type">
|
||||
<a-select v-model:value="formData.type" placeholder="请选择部门类型">
|
||||
<a-select-option value="management">管理部门</a-select-option>
|
||||
<a-select-option value="business">业务部门</a-select-option>
|
||||
<a-select-option value="support">支持部门</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="负责人" name="manager">
|
||||
<a-input v-model:value="formData.manager" placeholder="请输入负责人姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio value="active">正常</a-radio>
|
||||
<a-radio value="inactive">停用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="部门描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入部门描述"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ApartmentOutlined,
|
||||
BankOutlined,
|
||||
TeamOutlined,
|
||||
PlusCircleOutlined,
|
||||
SearchOutlined,
|
||||
NodeExpandOutlined,
|
||||
NodeCollapseOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
SafetyCertificateOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const expandedKeys = ref([])
|
||||
const selectedKeys = ref([])
|
||||
const selectedDepartment = ref(null)
|
||||
const searchFormRef = ref()
|
||||
const formRef = ref()
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalDepartments: 28,
|
||||
topLevelDepartments: 6,
|
||||
totalEmployees: 156,
|
||||
newDepartments: 3
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
parentId: null,
|
||||
type: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
status: 'active',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入部门编码', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择部门类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 部门树形数据
|
||||
const departmentTree = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '总经理办公室',
|
||||
code: 'CEO',
|
||||
type: 'management',
|
||||
status: 'active',
|
||||
manager: '王总',
|
||||
phone: '13800138001',
|
||||
employeeCount: 3,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '公司最高管理层',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '技术部',
|
||||
code: 'TECH',
|
||||
type: 'business',
|
||||
status: 'active',
|
||||
manager: '张三',
|
||||
phone: '13800138002',
|
||||
employeeCount: 25,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责技术研发和系统维护',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
name: '前端开发组',
|
||||
code: 'TECH-FE',
|
||||
type: 'business',
|
||||
status: 'active',
|
||||
manager: '李四',
|
||||
phone: '13800138021',
|
||||
employeeCount: 8,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责前端开发',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: '后端开发组',
|
||||
code: 'TECH-BE',
|
||||
type: 'business',
|
||||
status: 'active',
|
||||
manager: '王五',
|
||||
phone: '13800138022',
|
||||
employeeCount: 12,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责后端开发',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: '测试组',
|
||||
code: 'TECH-QA',
|
||||
type: 'support',
|
||||
status: 'active',
|
||||
manager: '赵六',
|
||||
phone: '13800138023',
|
||||
employeeCount: 5,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责软件测试',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '市场部',
|
||||
code: 'MKT',
|
||||
type: 'business',
|
||||
status: 'active',
|
||||
manager: '孙七',
|
||||
phone: '13800138003',
|
||||
employeeCount: 15,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责市场推广和销售',
|
||||
children: [
|
||||
{
|
||||
id: 31,
|
||||
name: '销售组',
|
||||
code: 'MKT-SALES',
|
||||
type: 'business',
|
||||
status: 'active',
|
||||
manager: '周八',
|
||||
phone: '13800138031',
|
||||
employeeCount: 10,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责产品销售',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
name: '推广组',
|
||||
code: 'MKT-PROMO',
|
||||
type: 'business',
|
||||
status: 'active',
|
||||
manager: '吴九',
|
||||
phone: '13800138032',
|
||||
employeeCount: 5,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责市场推广',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '人事部',
|
||||
code: 'HR',
|
||||
type: 'support',
|
||||
status: 'active',
|
||||
manager: '郑十',
|
||||
phone: '13800138004',
|
||||
employeeCount: 8,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责人力资源管理',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '财务部',
|
||||
code: 'FIN',
|
||||
type: 'support',
|
||||
status: 'active',
|
||||
manager: '钱一',
|
||||
phone: '13800138005',
|
||||
employeeCount: 6,
|
||||
createdAt: '2024-01-01 09:00:00',
|
||||
description: '负责财务管理',
|
||||
children: []
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
management: '#722ed1',
|
||||
business: '#1890ff',
|
||||
support: '#52c41a'
|
||||
}
|
||||
return colors[type] || '#1890ff'
|
||||
}
|
||||
|
||||
const getTypeName = (type) => {
|
||||
const names = {
|
||||
management: '管理',
|
||||
business: '业务',
|
||||
support: '支持'
|
||||
}
|
||||
return names[type] || '未知'
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
message.info('搜索功能开发中')
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
}
|
||||
|
||||
const expandAll = () => {
|
||||
const getAllKeys = (data) => {
|
||||
let keys = []
|
||||
data.forEach(item => {
|
||||
keys.push(item.id)
|
||||
if (item.children && item.children.length > 0) {
|
||||
keys = keys.concat(getAllKeys(item.children))
|
||||
}
|
||||
})
|
||||
return keys
|
||||
}
|
||||
expandedKeys.value = getAllKeys(departmentTree.value)
|
||||
}
|
||||
|
||||
const collapseAll = () => {
|
||||
expandedKeys.value = []
|
||||
}
|
||||
|
||||
const onSelectDepartment = (selectedKeysValue, info) => {
|
||||
if (selectedKeysValue.length > 0) {
|
||||
const findDepartment = (data, id) => {
|
||||
for (const item of data) {
|
||||
if (item.id === id) {
|
||||
return item
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findDepartment(item.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
selectedDepartment.value = findDepartment(departmentTree.value, selectedKeysValue[0])
|
||||
} else {
|
||||
selectedDepartment.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const selectDepartment = (department) => {
|
||||
selectedKeys.value = [department.id]
|
||||
selectedDepartment.value = department
|
||||
}
|
||||
|
||||
const addDepartment = () => {
|
||||
modalTitle.value = '新增部门'
|
||||
modalVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const addSubDepartment = (parent) => {
|
||||
modalTitle.value = '新增子部门'
|
||||
modalVisible.value = true
|
||||
resetForm()
|
||||
formData.parentId = parent.id
|
||||
}
|
||||
|
||||
const editDepartment = (department) => {
|
||||
modalTitle.value = '编辑部门'
|
||||
modalVisible.value = true
|
||||
Object.assign(formData, {
|
||||
...department,
|
||||
parentId: department.parentId || null
|
||||
})
|
||||
}
|
||||
|
||||
const deleteDepartment = (department) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除部门"${department.name}"吗?删除后该部门下的所有子部门也将被删除。`,
|
||||
onOk() {
|
||||
message.success('删除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewEmployees = (department) => {
|
||||
message.info(`查看部门"${department.name}"的员工信息`)
|
||||
}
|
||||
|
||||
const managePerm = (department) => {
|
||||
message.info(`管理部门"${department.name}"的权限`)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
message.success(modalTitle.value.includes('新增') ? '新增成功' : '编辑成功')
|
||||
modalVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
code: '',
|
||||
parentId: null,
|
||||
type: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
status: 'active',
|
||||
description: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 默认展开第一级
|
||||
expandedKeys.value = departmentTree.value.map(item => item.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.department-management {
|
||||
.stats-panel {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.department-tree {
|
||||
margin: 24px 0;
|
||||
|
||||
.department-detail {
|
||||
.sub-departments {
|
||||
margin-top: 24px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tree-title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
664
government-admin/src/views/personnel/FarmerManagement.vue
Normal file
664
government-admin/src/views/personnel/FarmerManagement.vue
Normal file
@@ -0,0 +1,664 @@
|
||||
<template>
|
||||
<div class="farmer-management">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="养殖户管理"
|
||||
description="管理养殖户信息,包括基本信息、养殖场信息、认证状态等"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="addFarmer">
|
||||
<UserAddOutlined />
|
||||
新增养殖户
|
||||
</a-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 统计面板 -->
|
||||
<div class="stats-panel">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总养殖户数"
|
||||
:value="stats.totalFarmers"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="已认证"
|
||||
:value="stats.certifiedFarmers"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<SafetyCertificateOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="待审核"
|
||||
:value="stats.pendingFarmers"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<ClockCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="本月新增"
|
||||
:value="stats.newFarmers"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<a-card :bordered="false">
|
||||
<a-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="养殖户姓名" name="name">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="请输入养殖户姓名"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input
|
||||
v-model:value="searchForm.phone"
|
||||
placeholder="请输入联系电话"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="认证状态" name="status">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="请选择认证状态"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="certified">已认证</a-select-option>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="所在地区" name="region">
|
||||
<a-input
|
||||
v-model:value="searchForm.region"
|
||||
placeholder="请输入所在地区"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<SearchOutlined />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSearch">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 养殖户列表 -->
|
||||
<div class="farmer-table">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<span>养殖户列表</span>
|
||||
<a-tag color="blue" style="margin-left: 8px">
|
||||
共 {{ pagination.total }} 条记录
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="exportFarmers">
|
||||
<ExportOutlined />
|
||||
导出
|
||||
</a-button>
|
||||
<a-button @click="refreshData">
|
||||
<ReloadOutlined />
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="farmerList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :alt="record.name">
|
||||
{{ record.name?.charAt(0) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusName(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="viewFarmer(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="editFarmer(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="certifyFarmer(record)">
|
||||
<SafetyCertificateOutlined />
|
||||
认证管理
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="viewFarms(record)">
|
||||
<HomeOutlined />
|
||||
查看养殖场
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item @click="deleteFarmer(record)" danger>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="link" size="small">
|
||||
更多
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑养殖户弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="800px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖户姓名" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入养殖户姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="身份证号" name="idCard">
|
||||
<a-input v-model:value="formData.idCard" placeholder="请输入身份证号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select v-model:value="formData.gender" placeholder="请选择性别">
|
||||
<a-select-option value="male">男</a-select-option>
|
||||
<a-select-option value="female">女</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="出生日期" name="birthDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.birthDate"
|
||||
placeholder="请选择出生日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="学历" name="education">
|
||||
<a-select v-model:value="formData.education" placeholder="请选择学历">
|
||||
<a-select-option value="primary">小学</a-select-option>
|
||||
<a-select-option value="middle">初中</a-select-option>
|
||||
<a-select-option value="high">高中</a-select-option>
|
||||
<a-select-option value="college">大专</a-select-option>
|
||||
<a-select-option value="bachelor">本科</a-select-option>
|
||||
<a-select-option value="master">硕士</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="详细地址" name="address">
|
||||
<a-textarea
|
||||
v-model:value="formData.address"
|
||||
placeholder="请输入详细地址"
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 养殖户详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="detailModalVisible"
|
||||
title="养殖户详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div v-if="currentFarmer" class="farmer-detail">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="姓名">
|
||||
{{ currentFarmer.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ currentFarmer.phone }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="身份证号">
|
||||
{{ currentFarmer.idCard }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="性别">
|
||||
{{ currentFarmer.gender === 'male' ? '男' : '女' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="出生日期">
|
||||
{{ currentFarmer.birthDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="学历">
|
||||
{{ getEducationName(currentFarmer.education) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="认证状态">
|
||||
<a-tag :color="getStatusColor(currentFarmer.status)">
|
||||
{{ getStatusName(currentFarmer.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="注册时间">
|
||||
{{ currentFarmer.createdAt }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="详细地址" :span="2">
|
||||
{{ currentFarmer.address }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ currentFarmer.remark || '暂无备注' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
UserAddOutlined,
|
||||
UserOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ClockCircleOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
HomeOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const detailModalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const currentFarmer = ref(null)
|
||||
const searchFormRef = ref()
|
||||
const formRef = ref()
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalFarmers: 1256,
|
||||
certifiedFarmers: 1089,
|
||||
pendingFarmers: 125,
|
||||
newFarmers: 42
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
phone: '',
|
||||
status: '',
|
||||
region: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '头像',
|
||||
dataIndex: 'avatar',
|
||||
key: 'avatar',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '身份证号',
|
||||
dataIndex: 'idCard',
|
||||
key: 'idCard',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '所在地区',
|
||||
dataIndex: 'region',
|
||||
key: 'region',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '认证状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
phone: '',
|
||||
idCard: '',
|
||||
gender: '',
|
||||
birthDate: null,
|
||||
education: '',
|
||||
address: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入养殖户姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
],
|
||||
idCard: [
|
||||
{ required: true, message: '请输入身份证号', trigger: 'blur' },
|
||||
{ pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入正确的身份证号', trigger: 'blur' }
|
||||
],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 养殖户列表数据
|
||||
const farmerList = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '张三',
|
||||
phone: '13800138001',
|
||||
idCard: '110101199001011234',
|
||||
gender: 'male',
|
||||
birthDate: '1990-01-01',
|
||||
education: 'high',
|
||||
region: '银川市兴庆区',
|
||||
address: '宁夏银川市兴庆区某某村123号',
|
||||
status: 'certified',
|
||||
createdAt: '2024-01-15 10:30:00',
|
||||
remark: '优秀养殖户'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李四',
|
||||
phone: '13800138002',
|
||||
idCard: '110101199002021234',
|
||||
gender: 'male',
|
||||
birthDate: '1990-02-02',
|
||||
education: 'college',
|
||||
region: '银川市西夏区',
|
||||
address: '宁夏银川市西夏区某某镇456号',
|
||||
status: 'pending',
|
||||
createdAt: '2024-01-14 14:20:00',
|
||||
remark: ''
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
certified: '#52c41a',
|
||||
pending: '#faad14',
|
||||
rejected: '#f5222d'
|
||||
}
|
||||
return colors[status] || '#1890ff'
|
||||
}
|
||||
|
||||
const getStatusName = (status) => {
|
||||
const names = {
|
||||
certified: '已认证',
|
||||
pending: '待审核',
|
||||
rejected: '已拒绝'
|
||||
}
|
||||
return names[status] || '未知状态'
|
||||
}
|
||||
|
||||
const getEducationName = (education) => {
|
||||
const names = {
|
||||
primary: '小学',
|
||||
middle: '初中',
|
||||
high: '高中',
|
||||
college: '大专',
|
||||
bachelor: '本科',
|
||||
master: '硕士'
|
||||
}
|
||||
return names[education] || '未知'
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
phone: '',
|
||||
status: '',
|
||||
region: ''
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
pagination.total = farmerList.value.length
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addFarmer = () => {
|
||||
modalTitle.value = '新增养殖户'
|
||||
modalVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const editFarmer = (farmer) => {
|
||||
modalTitle.value = '编辑养殖户'
|
||||
modalVisible.value = true
|
||||
Object.assign(formData, farmer)
|
||||
}
|
||||
|
||||
const viewFarmer = (farmer) => {
|
||||
currentFarmer.value = farmer
|
||||
detailModalVisible.value = true
|
||||
}
|
||||
|
||||
const deleteFarmer = (farmer) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除养殖户"${farmer.name}"吗?`,
|
||||
onOk() {
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const certifyFarmer = (farmer) => {
|
||||
message.info(`管理养殖户"${farmer.name}"的认证状态`)
|
||||
}
|
||||
|
||||
const viewFarms = (farmer) => {
|
||||
message.info(`查看养殖户"${farmer.name}"的养殖场信息`)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
message.success(modalTitle.value === '新增养殖户' ? '新增成功' : '编辑成功')
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
phone: '',
|
||||
idCard: '',
|
||||
gender: '',
|
||||
birthDate: null,
|
||||
education: '',
|
||||
address: '',
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
const exportFarmers = () => {
|
||||
message.success('导出成功')
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.farmer-management {
|
||||
.stats-panel {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.farmer-table {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.farmer-detail {
|
||||
.ant-descriptions {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
845
government-admin/src/views/personnel/StaffManagement.vue
Normal file
845
government-admin/src/views/personnel/StaffManagement.vue
Normal file
@@ -0,0 +1,845 @@
|
||||
<template>
|
||||
<div class="staff-management">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="员工管理"
|
||||
description="管理系统员工信息,包括基本信息、岗位配置、权限分配等"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="importStaff">
|
||||
<ImportOutlined />
|
||||
批量导入
|
||||
</a-button>
|
||||
<a-button type="primary" @click="addStaff">
|
||||
<UserAddOutlined />
|
||||
新增员工
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 统计面板 -->
|
||||
<div class="stats-panel">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总员工数"
|
||||
:value="stats.totalStaff"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<TeamOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="在职员工"
|
||||
:value="stats.activeStaff"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="离职员工"
|
||||
:value="stats.inactiveStaff"
|
||||
:value-style="{ color: '#f5222d' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserDeleteOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="本月入职"
|
||||
:value="stats.newStaff"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<a-card :bordered="false">
|
||||
<a-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="员工姓名" name="name">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="请输入员工姓名"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="工号" name="employeeId">
|
||||
<a-input
|
||||
v-model:value="searchForm.employeeId"
|
||||
placeholder="请输入工号"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="部门" name="department">
|
||||
<a-select
|
||||
v-model:value="searchForm.department"
|
||||
placeholder="请选择部门"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="tech">技术部</a-select-option>
|
||||
<a-select-option value="market">市场部</a-select-option>
|
||||
<a-select-option value="hr">人事部</a-select-option>
|
||||
<a-select-option value="finance">财务部</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="职位" name="position">
|
||||
<a-input
|
||||
v-model:value="searchForm.position"
|
||||
placeholder="请输入职位"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="active">在职</a-select-option>
|
||||
<a-select-option value="inactive">离职</a-select-option>
|
||||
<a-select-option value="probation">试用期</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<SearchOutlined />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSearch">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 员工列表 -->
|
||||
<div class="staff-table">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<span>员工列表</span>
|
||||
<a-tag color="blue" style="margin-left: 8px">
|
||||
共 {{ pagination.total }} 条记录
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="exportStaff">
|
||||
<ExportOutlined />
|
||||
导出
|
||||
</a-button>
|
||||
<a-button @click="refreshData">
|
||||
<ReloadOutlined />
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="staffList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :alt="record.name">
|
||||
{{ record.name?.charAt(0) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusName(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="viewStaff(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="editStaff(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="resetPassword(record)">
|
||||
<KeyOutlined />
|
||||
重置密码
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="manageRole(record)">
|
||||
<SafetyCertificateOutlined />
|
||||
角色管理
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="viewAttendance(record)">
|
||||
<CalendarOutlined />
|
||||
考勤记录
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item @click="resignStaff(record)" danger>
|
||||
<UserDeleteOutlined />
|
||||
离职处理
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="link" size="small">
|
||||
更多
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑员工弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="800px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<a-tab-pane key="basic" tab="基本信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="员工姓名" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入员工姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="工号" name="employeeId">
|
||||
<a-input v-model:value="formData.employeeId" placeholder="请输入工号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select v-model:value="formData.gender" placeholder="请选择性别">
|
||||
<a-select-option value="male">男</a-select-option>
|
||||
<a-select-option value="female">女</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="出生日期" name="birthDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.birthDate"
|
||||
placeholder="请选择出生日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="身份证号" name="idCard">
|
||||
<a-input v-model:value="formData.idCard" placeholder="请输入身份证号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="学历" name="education">
|
||||
<a-select v-model:value="formData.education" placeholder="请选择学历">
|
||||
<a-select-option value="high">高中</a-select-option>
|
||||
<a-select-option value="college">大专</a-select-option>
|
||||
<a-select-option value="bachelor">本科</a-select-option>
|
||||
<a-select-option value="master">硕士</a-select-option>
|
||||
<a-select-option value="doctor">博士</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="家庭住址" name="address">
|
||||
<a-textarea
|
||||
v-model:value="formData.address"
|
||||
placeholder="请输入家庭住址"
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="work" tab="工作信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属部门" name="department">
|
||||
<a-select v-model:value="formData.department" placeholder="请选择部门">
|
||||
<a-select-option value="tech">技术部</a-select-option>
|
||||
<a-select-option value="market">市场部</a-select-option>
|
||||
<a-select-option value="hr">人事部</a-select-option>
|
||||
<a-select-option value="finance">财务部</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="职位" name="position">
|
||||
<a-input v-model:value="formData.position" placeholder="请输入职位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="入职日期" name="hireDate">
|
||||
<a-date-picker
|
||||
v-model:value="formData.hireDate"
|
||||
placeholder="请选择入职日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="员工状态" name="status">
|
||||
<a-select v-model:value="formData.status" placeholder="请选择状态">
|
||||
<a-select-option value="probation">试用期</a-select-option>
|
||||
<a-select-option value="active">正式员工</a-select-option>
|
||||
<a-select-option value="inactive">离职</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="直属上级" name="supervisor">
|
||||
<a-input v-model:value="formData.supervisor" placeholder="请输入直属上级" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="工作地点" name="workLocation">
|
||||
<a-input v-model:value="formData.workLocation" placeholder="请输入工作地点" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="工作职责" name="jobDescription">
|
||||
<a-textarea
|
||||
v-model:value="formData.jobDescription"
|
||||
placeholder="请输入工作职责"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 员工详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="detailModalVisible"
|
||||
title="员工详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div v-if="currentStaff" class="staff-detail">
|
||||
<a-tabs>
|
||||
<a-tab-pane key="info" tab="基本信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="姓名">
|
||||
{{ currentStaff.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="工号">
|
||||
{{ currentStaff.employeeId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ currentStaff.phone }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="邮箱">
|
||||
{{ currentStaff.email }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="性别">
|
||||
{{ currentStaff.gender === 'male' ? '男' : '女' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="出生日期">
|
||||
{{ currentStaff.birthDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="身份证号">
|
||||
{{ currentStaff.idCard }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="学历">
|
||||
{{ getEducationName(currentStaff.education) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="家庭住址" :span="2">
|
||||
{{ currentStaff.address }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="work" tab="工作信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="所属部门">
|
||||
{{ getDepartmentName(currentStaff.department) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="职位">
|
||||
{{ currentStaff.position }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="入职日期">
|
||||
{{ currentStaff.hireDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="员工状态">
|
||||
<a-tag :color="getStatusColor(currentStaff.status)">
|
||||
{{ getStatusName(currentStaff.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="直属上级">
|
||||
{{ currentStaff.supervisor || '暂无' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="工作地点">
|
||||
{{ currentStaff.workLocation }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="工作职责" :span="2">
|
||||
{{ currentStaff.jobDescription || '暂无' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
ImportOutlined,
|
||||
UserAddOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
UserDeleteOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
KeyOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
CalendarOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const detailModalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const activeTab = ref('basic')
|
||||
const currentStaff = ref(null)
|
||||
const searchFormRef = ref()
|
||||
const formRef = ref()
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalStaff: 156,
|
||||
activeStaff: 142,
|
||||
inactiveStaff: 14,
|
||||
newStaff: 8
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
employeeId: '',
|
||||
department: '',
|
||||
position: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '头像',
|
||||
dataIndex: 'avatar',
|
||||
key: 'avatar',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '工号',
|
||||
dataIndex: 'employeeId',
|
||||
key: 'employeeId',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '部门',
|
||||
dataIndex: 'department',
|
||||
key: 'department',
|
||||
width: 120,
|
||||
customRender: ({ text }) => getDepartmentName(text)
|
||||
},
|
||||
{
|
||||
title: '职位',
|
||||
dataIndex: 'position',
|
||||
key: 'position',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '入职日期',
|
||||
dataIndex: 'hireDate',
|
||||
key: 'hireDate',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
employeeId: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
gender: '',
|
||||
birthDate: null,
|
||||
idCard: '',
|
||||
education: '',
|
||||
address: '',
|
||||
department: '',
|
||||
position: '',
|
||||
hireDate: null,
|
||||
status: 'probation',
|
||||
supervisor: '',
|
||||
workLocation: '',
|
||||
jobDescription: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
|
||||
employeeId: [{ required: true, message: '请输入工号', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
department: [{ required: true, message: '请选择部门', trigger: 'change' }],
|
||||
position: [{ required: true, message: '请输入职位', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 员工列表数据
|
||||
const staffList = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '张三',
|
||||
employeeId: 'EMP001',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
gender: 'male',
|
||||
birthDate: '1990-01-01',
|
||||
idCard: '110101199001011234',
|
||||
education: 'bachelor',
|
||||
address: '北京市朝阳区某某街道123号',
|
||||
department: 'tech',
|
||||
position: '前端开发工程师',
|
||||
hireDate: '2023-01-15',
|
||||
status: 'active',
|
||||
supervisor: '李经理',
|
||||
workLocation: '北京总部',
|
||||
jobDescription: '负责前端页面开发和维护'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李四',
|
||||
employeeId: 'EMP002',
|
||||
phone: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
gender: 'male',
|
||||
birthDate: '1988-05-20',
|
||||
idCard: '110101198805201234',
|
||||
education: 'master',
|
||||
address: '北京市海淀区某某路456号',
|
||||
department: 'tech',
|
||||
position: '后端开发工程师',
|
||||
hireDate: '2022-08-10',
|
||||
status: 'active',
|
||||
supervisor: '王总监',
|
||||
workLocation: '北京总部',
|
||||
jobDescription: '负责后端接口开发和数据库设计'
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
active: '#52c41a',
|
||||
probation: '#faad14',
|
||||
inactive: '#f5222d'
|
||||
}
|
||||
return colors[status] || '#1890ff'
|
||||
}
|
||||
|
||||
const getStatusName = (status) => {
|
||||
const names = {
|
||||
active: '在职',
|
||||
probation: '试用期',
|
||||
inactive: '离职'
|
||||
}
|
||||
return names[status] || '未知状态'
|
||||
}
|
||||
|
||||
const getDepartmentName = (department) => {
|
||||
const names = {
|
||||
tech: '技术部',
|
||||
market: '市场部',
|
||||
hr: '人事部',
|
||||
finance: '财务部'
|
||||
}
|
||||
return names[department] || '未知部门'
|
||||
}
|
||||
|
||||
const getEducationName = (education) => {
|
||||
const names = {
|
||||
high: '高中',
|
||||
college: '大专',
|
||||
bachelor: '本科',
|
||||
master: '硕士',
|
||||
doctor: '博士'
|
||||
}
|
||||
return names[education] || '未知'
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
employeeId: '',
|
||||
department: '',
|
||||
position: '',
|
||||
status: ''
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
pagination.total = staffList.value.length
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addStaff = () => {
|
||||
modalTitle.value = '新增员工'
|
||||
modalVisible.value = true
|
||||
activeTab.value = 'basic'
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const editStaff = (staff) => {
|
||||
modalTitle.value = '编辑员工'
|
||||
modalVisible.value = true
|
||||
activeTab.value = 'basic'
|
||||
Object.assign(formData, staff)
|
||||
}
|
||||
|
||||
const viewStaff = (staff) => {
|
||||
currentStaff.value = staff
|
||||
detailModalVisible.value = true
|
||||
}
|
||||
|
||||
const resignStaff = (staff) => {
|
||||
Modal.confirm({
|
||||
title: '确认离职',
|
||||
content: `确定要处理员工"${staff.name}"的离职手续吗?`,
|
||||
onOk() {
|
||||
message.success('离职处理成功')
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetPassword = (staff) => {
|
||||
Modal.confirm({
|
||||
title: '确认重置密码',
|
||||
content: `确定要重置员工"${staff.name}"的登录密码吗?`,
|
||||
onOk() {
|
||||
message.success('密码重置成功,新密码已发送至员工邮箱')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const manageRole = (staff) => {
|
||||
message.info(`管理员工"${staff.name}"的角色权限`)
|
||||
}
|
||||
|
||||
const viewAttendance = (staff) => {
|
||||
message.info(`查看员工"${staff.name}"的考勤记录`)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
message.success(modalTitle.value === '新增员工' ? '新增成功' : '编辑成功')
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
employeeId: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
gender: '',
|
||||
birthDate: null,
|
||||
idCard: '',
|
||||
education: '',
|
||||
address: '',
|
||||
department: '',
|
||||
position: '',
|
||||
hireDate: null,
|
||||
status: 'probation',
|
||||
supervisor: '',
|
||||
workLocation: '',
|
||||
jobDescription: ''
|
||||
})
|
||||
}
|
||||
|
||||
const importStaff = () => {
|
||||
message.info('批量导入功能开发中')
|
||||
}
|
||||
|
||||
const exportStaff = () => {
|
||||
message.success('导出成功')
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.staff-management {
|
||||
.stats-panel {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.staff-table {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.staff-detail {
|
||||
.ant-descriptions {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1036
government-admin/src/views/personnel/VeterinarianManagement.vue
Normal file
1036
government-admin/src/views/personnel/VeterinarianManagement.vue
Normal file
File diff suppressed because it is too large
Load Diff
1552
government-admin/src/views/policy/PolicyManagement.vue
Normal file
1552
government-admin/src/views/policy/PolicyManagement.vue
Normal file
File diff suppressed because it is too large
Load Diff
1359
government-admin/src/views/reports/ReportCenter.vue
Normal file
1359
government-admin/src/views/reports/ReportCenter.vue
Normal file
File diff suppressed because it is too large
Load Diff
456
government-admin/src/views/reports/ReportList.vue
Normal file
456
government-admin/src/views/reports/ReportList.vue
Normal file
@@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<div class="report-list">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="报告列表"
|
||||
description="查看和管理所有监管报告"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="generateReport">
|
||||
<FileAddOutlined />
|
||||
生成报告
|
||||
</a-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<a-card :bordered="false">
|
||||
<a-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="报告类型" name="type">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="请选择报告类型"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="quarantine">检疫报告</a-select-option>
|
||||
<a-select-option value="slaughter">屠宰报告</a-select-option>
|
||||
<a-select-option value="harmless">无害化报告</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="报告标题" name="title">
|
||||
<a-input
|
||||
v-model:value="searchForm.title"
|
||||
placeholder="请输入报告标题"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="生成时间" name="dateRange">
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.dateRange"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="生成人" name="creator">
|
||||
<a-input
|
||||
v-model:value="searchForm.creator"
|
||||
placeholder="请输入生成人"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<SearchOutlined />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSearch">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 报告列表 -->
|
||||
<div class="report-table">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<span>报告列表</span>
|
||||
<a-tag color="blue" style="margin-left: 8px">
|
||||
共 {{ pagination.total }} 条记录
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="exportReports">
|
||||
<ExportOutlined />
|
||||
导出
|
||||
</a-button>
|
||||
<a-button @click="refreshData">
|
||||
<ReloadOutlined />
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="reportList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getReportTypeColor(record.type)">
|
||||
{{ getReportTypeName(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusName(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="viewReport(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="downloadReport(record)">
|
||||
下载
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="editReport(record)">
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="shareReport(record)">
|
||||
<ShareAltOutlined />
|
||||
分享
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item @click="deleteReport(record)" danger>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="link" size="small">
|
||||
更多
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 报告详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="detailModalVisible"
|
||||
title="报告详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div v-if="currentReport" class="report-detail">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="报告标题">
|
||||
{{ currentReport.title }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="报告类型">
|
||||
<a-tag :color="getReportTypeColor(currentReport.type)">
|
||||
{{ getReportTypeName(currentReport.type) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="生成时间">
|
||||
{{ currentReport.createdAt }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="生成人">
|
||||
{{ currentReport.creator }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(currentReport.status)">
|
||||
{{ getStatusName(currentReport.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="文件大小">
|
||||
{{ currentReport.fileSize }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="报告描述" :span="2">
|
||||
{{ currentReport.description || '暂无描述' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="report-actions" style="margin-top: 24px; text-align: center;">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="downloadReport(currentReport)">
|
||||
<DownloadOutlined />
|
||||
下载报告
|
||||
</a-button>
|
||||
<a-button @click="shareReport(currentReport)">
|
||||
<ShareAltOutlined />
|
||||
分享报告
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
FileAddOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
ShareAltOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const detailModalVisible = ref(false)
|
||||
const currentReport = ref(null)
|
||||
const searchFormRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
type: '',
|
||||
title: '',
|
||||
dateRange: [],
|
||||
creator: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '报告标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '报告类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '生成时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '生成人',
|
||||
dataIndex: 'creator',
|
||||
key: 'creator',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
dataIndex: 'fileSize',
|
||||
key: 'fileSize',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 报告列表数据
|
||||
const reportList = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '2024年1月检疫报告',
|
||||
type: 'quarantine',
|
||||
createdAt: '2024-01-15 10:30:00',
|
||||
creator: '张三',
|
||||
status: 'completed',
|
||||
fileSize: '2.5MB',
|
||||
description: '2024年1月份动物检疫工作总结报告'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '屠宰场监管月报',
|
||||
type: 'slaughter',
|
||||
createdAt: '2024-01-14 14:20:00',
|
||||
creator: '李四',
|
||||
status: 'completed',
|
||||
fileSize: '1.8MB',
|
||||
description: '屠宰场日常监管工作月度报告'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '无害化处理统计报告',
|
||||
type: 'harmless',
|
||||
createdAt: '2024-01-13 09:15:00',
|
||||
creator: '王五',
|
||||
status: 'generating',
|
||||
fileSize: '3.2MB',
|
||||
description: '病死动物无害化处理统计分析报告'
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const getReportTypeColor = (type) => {
|
||||
const colors = {
|
||||
quarantine: '#52c41a',
|
||||
slaughter: '#faad14',
|
||||
harmless: '#f5222d'
|
||||
}
|
||||
return colors[type] || '#1890ff'
|
||||
}
|
||||
|
||||
const getReportTypeName = (type) => {
|
||||
const names = {
|
||||
quarantine: '检疫报告',
|
||||
slaughter: '屠宰报告',
|
||||
harmless: '无害化报告'
|
||||
}
|
||||
return names[type] || '未知类型'
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
completed: '#52c41a',
|
||||
generating: '#faad14',
|
||||
failed: '#f5222d'
|
||||
}
|
||||
return colors[status] || '#1890ff'
|
||||
}
|
||||
|
||||
const getStatusName = (status) => {
|
||||
const names = {
|
||||
completed: '已完成',
|
||||
generating: '生成中',
|
||||
failed: '生成失败'
|
||||
}
|
||||
return names[status] || '未知状态'
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
type: '',
|
||||
title: '',
|
||||
dateRange: [],
|
||||
creator: ''
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
pagination.total = reportList.value.length
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const generateReport = () => {
|
||||
message.info('跳转到报告生成页面')
|
||||
}
|
||||
|
||||
const viewReport = (report) => {
|
||||
currentReport.value = report
|
||||
detailModalVisible.value = true
|
||||
}
|
||||
|
||||
const downloadReport = (report) => {
|
||||
message.success(`下载报告:${report.title}`)
|
||||
}
|
||||
|
||||
const editReport = (report) => {
|
||||
message.info(`编辑报告:${report.title}`)
|
||||
}
|
||||
|
||||
const shareReport = (report) => {
|
||||
message.success(`分享报告:${report.title}`)
|
||||
}
|
||||
|
||||
const deleteReport = (report) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除报告"${report.title}"吗?`,
|
||||
onOk() {
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const exportReports = () => {
|
||||
message.success('导出成功')
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.report-list {
|
||||
.search-area {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.report-table {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.report-detail {
|
||||
.report-actions {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
659
government-admin/src/views/services/CattleEducation.vue
Normal file
659
government-admin/src/views/services/CattleEducation.vue
Normal file
@@ -0,0 +1,659 @@
|
||||
<template>
|
||||
<div class="cattle-education">
|
||||
<PageHeader title="牛只教育服务" />
|
||||
|
||||
<div class="content-wrapper">
|
||||
<!-- 课程分类 -->
|
||||
<a-row :gutter="16" class="category-section">
|
||||
<a-col :span="6" v-for="category in categories" :key="category.id">
|
||||
<a-card
|
||||
hoverable
|
||||
class="category-card"
|
||||
@click="selectCategory(category)"
|
||||
:class="{ active: selectedCategory?.id === category.id }"
|
||||
>
|
||||
<template #cover>
|
||||
<div class="category-icon">
|
||||
<component :is="category.icon" style="font-size: 32px; color: #1890ff;" />
|
||||
</div>
|
||||
</template>
|
||||
<a-card-meta :title="category.name" :description="category.description" />
|
||||
<div class="category-stats">
|
||||
<a-statistic :value="category.courseCount" suffix="门课程" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 课程列表 -->
|
||||
<a-card class="course-section">
|
||||
<template #title>
|
||||
<div class="section-title">
|
||||
<span>{{ selectedCategory?.name || '全部课程' }}</span>
|
||||
<a-tag color="blue">{{ filteredCourses.length }} 门课程</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索课程"
|
||||
style="width: 200px"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-select v-model:value="sortBy" style="width: 120px" @change="handleSort">
|
||||
<a-select-option value="latest">最新</a-select-option>
|
||||
<a-select-option value="popular">最热门</a-select-option>
|
||||
<a-select-option value="rating">评分最高</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :span="8" v-for="course in paginatedCourses" :key="course.id">
|
||||
<a-card hoverable class="course-card">
|
||||
<template #cover>
|
||||
<div class="course-cover">
|
||||
<img :src="course.cover" :alt="course.title" />
|
||||
<div class="course-duration">{{ course.duration }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-card-meta :title="course.title" :description="course.description" />
|
||||
|
||||
<div class="course-info">
|
||||
<div class="course-stats">
|
||||
<a-space>
|
||||
<span><UserOutlined /> {{ course.students }}</span>
|
||||
<span><StarOutlined /> {{ course.rating }}</span>
|
||||
<span><ClockCircleOutlined /> {{ course.duration }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="course-level">
|
||||
<a-tag :color="getLevelColor(course.level)">{{ course.level }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="course-actions">
|
||||
<a-button type="primary" block @click="startCourse(course)">
|
||||
开始学习
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<a-pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="filteredCourses.length"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
:show-total="(total) => `共 ${total} 门课程`"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 学习进度 -->
|
||||
<a-card title="我的学习进度" class="progress-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8" v-for="progress in learningProgress" :key="progress.courseId">
|
||||
<div class="progress-item">
|
||||
<div class="progress-header">
|
||||
<h4>{{ progress.courseTitle }}</h4>
|
||||
<span class="progress-percent">{{ progress.progress }}%</span>
|
||||
</div>
|
||||
<a-progress :percent="progress.progress" :status="getProgressStatus(progress.progress)" />
|
||||
<div class="progress-info">
|
||||
<span>已学习 {{ progress.completedLessons }}/{{ progress.totalLessons }} 课时</span>
|
||||
<a-button type="link" size="small" @click="continueLearning(progress)">
|
||||
继续学习
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 课程详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="courseModalVisible"
|
||||
:title="selectedCourse?.title"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div v-if="selectedCourse" class="course-detail">
|
||||
<div class="course-header">
|
||||
<img :src="selectedCourse.cover" :alt="selectedCourse.title" class="course-image" />
|
||||
<div class="course-meta">
|
||||
<h3>{{ selectedCourse.title }}</h3>
|
||||
<p>{{ selectedCourse.description }}</p>
|
||||
<div class="course-tags">
|
||||
<a-tag v-for="tag in selectedCourse.tags" :key="tag" color="blue">{{ tag }}</a-tag>
|
||||
</div>
|
||||
<div class="course-stats-detail">
|
||||
<a-space size="large">
|
||||
<span><UserOutlined /> {{ selectedCourse.students }} 学员</span>
|
||||
<span><StarOutlined /> {{ selectedCourse.rating }} 评分</span>
|
||||
<span><ClockCircleOutlined /> {{ selectedCourse.duration }}</span>
|
||||
<span><BookOutlined /> {{ selectedCourse.lessons }} 课时</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tabs>
|
||||
<a-tab-pane key="outline" tab="课程大纲">
|
||||
<div class="course-outline">
|
||||
<div v-for="(chapter, index) in selectedCourse.outline" :key="index" class="chapter">
|
||||
<h4>第{{ index + 1 }}章 {{ chapter.title }}</h4>
|
||||
<ul>
|
||||
<li v-for="lesson in chapter.lessons" :key="lesson.id">
|
||||
<PlayCircleOutlined />
|
||||
{{ lesson.title }}
|
||||
<span class="lesson-duration">{{ lesson.duration }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="reviews" tab="学员评价">
|
||||
<div class="course-reviews">
|
||||
<div v-for="review in selectedCourse.reviews" :key="review.id" class="review-item">
|
||||
<div class="review-header">
|
||||
<a-avatar :src="review.avatar">{{ review.username.charAt(0) }}</a-avatar>
|
||||
<div class="review-meta">
|
||||
<span class="username">{{ review.username }}</span>
|
||||
<a-rate :value="review.rating" disabled />
|
||||
</div>
|
||||
<span class="review-date">{{ review.date }}</span>
|
||||
</div>
|
||||
<p class="review-content">{{ review.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<div class="course-actions-detail">
|
||||
<a-button type="primary" size="large" @click="startCourse(selectedCourse)">
|
||||
开始学习
|
||||
</a-button>
|
||||
<a-button size="large" @click="addToFavorites(selectedCourse)">
|
||||
<HeartOutlined /> 收藏
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
BookOutlined,
|
||||
ExperimentOutlined,
|
||||
MedicineBoxOutlined,
|
||||
EnvironmentOutlined,
|
||||
UserOutlined,
|
||||
StarOutlined,
|
||||
ClockCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
HeartOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const selectedCategory = ref(null)
|
||||
const searchKeyword = ref('')
|
||||
const sortBy = ref('latest')
|
||||
const courseModalVisible = ref(false)
|
||||
const selectedCourse = ref(null)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 9
|
||||
})
|
||||
|
||||
// 课程分类
|
||||
const categories = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '基础知识',
|
||||
description: '牛只基本生理知识',
|
||||
icon: BookOutlined,
|
||||
courseCount: 15
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '疾病防治',
|
||||
description: '常见疾病预防与治疗',
|
||||
icon: MedicineBoxOutlined,
|
||||
courseCount: 12
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '营养管理',
|
||||
description: '饲料配比与营养搭配',
|
||||
icon: ExperimentOutlined,
|
||||
courseCount: 8
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '环境管理',
|
||||
description: '养殖环境优化管理',
|
||||
icon: EnvironmentOutlined,
|
||||
courseCount: 10
|
||||
}
|
||||
])
|
||||
|
||||
// 课程数据
|
||||
const courses = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '牛只基础生理学',
|
||||
description: '了解牛只的基本生理结构和功能',
|
||||
cover: '/images/course1.jpg',
|
||||
categoryId: 1,
|
||||
level: '初级',
|
||||
duration: '2小时',
|
||||
students: 1234,
|
||||
rating: 4.8,
|
||||
lessons: 12,
|
||||
tags: ['基础', '生理学', '入门'],
|
||||
outline: [
|
||||
{
|
||||
title: '牛只解剖结构',
|
||||
lessons: [
|
||||
{ id: 1, title: '消化系统', duration: '15分钟' },
|
||||
{ id: 2, title: '呼吸系统', duration: '12分钟' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 1,
|
||||
username: '张三',
|
||||
avatar: '',
|
||||
rating: 5,
|
||||
date: '2024-01-15',
|
||||
content: '课程内容很详细,讲解清晰易懂。'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '常见疾病预防',
|
||||
description: '学习牛只常见疾病的预防措施',
|
||||
cover: '/images/course2.jpg',
|
||||
categoryId: 2,
|
||||
level: '中级',
|
||||
duration: '3小时',
|
||||
students: 856,
|
||||
rating: 4.6,
|
||||
lessons: 18,
|
||||
tags: ['疾病', '预防', '健康'],
|
||||
outline: [
|
||||
{
|
||||
title: '传染病预防',
|
||||
lessons: [
|
||||
{ id: 1, title: '口蹄疫预防', duration: '20分钟' },
|
||||
{ id: 2, title: '结核病防控', duration: '18分钟' }
|
||||
]
|
||||
}
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 1,
|
||||
username: '李四',
|
||||
avatar: '',
|
||||
rating: 4,
|
||||
date: '2024-01-10',
|
||||
content: '实用性很强,对实际工作帮助很大。'
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 学习进度
|
||||
const learningProgress = ref([
|
||||
{
|
||||
courseId: 1,
|
||||
courseTitle: '牛只基础生理学',
|
||||
progress: 75,
|
||||
completedLessons: 9,
|
||||
totalLessons: 12
|
||||
},
|
||||
{
|
||||
courseId: 2,
|
||||
courseTitle: '常见疾病预防',
|
||||
progress: 30,
|
||||
completedLessons: 5,
|
||||
totalLessons: 18
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const filteredCourses = computed(() => {
|
||||
let result = courses.value
|
||||
|
||||
// 按分类筛选
|
||||
if (selectedCategory.value) {
|
||||
result = result.filter(course => course.categoryId === selectedCategory.value.id)
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
result = result.filter(course =>
|
||||
course.title.includes(searchKeyword.value) ||
|
||||
course.description.includes(searchKeyword.value)
|
||||
)
|
||||
}
|
||||
|
||||
// 排序
|
||||
switch (sortBy.value) {
|
||||
case 'popular':
|
||||
result.sort((a, b) => b.students - a.students)
|
||||
break
|
||||
case 'rating':
|
||||
result.sort((a, b) => b.rating - a.rating)
|
||||
break
|
||||
case 'latest':
|
||||
default:
|
||||
// 默认按最新排序
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const paginatedCourses = computed(() => {
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
return filteredCourses.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const selectCategory = (category) => {
|
||||
selectedCategory.value = selectedCategory.value?.id === category.id ? null : category
|
||||
pagination.current = 1
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
}
|
||||
|
||||
const handleSort = () => {
|
||||
pagination.current = 1
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
// 分页变化时的处理
|
||||
}
|
||||
|
||||
const getLevelColor = (level) => {
|
||||
const colorMap = {
|
||||
'初级': 'green',
|
||||
'中级': 'orange',
|
||||
'高级': 'red'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
const getProgressStatus = (progress) => {
|
||||
if (progress === 100) return 'success'
|
||||
if (progress >= 50) return 'active'
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
const startCourse = (course) => {
|
||||
message.success(`开始学习:${course.title}`)
|
||||
courseModalVisible.value = false
|
||||
// 这里可以跳转到学习页面
|
||||
}
|
||||
|
||||
const continueLearning = (progress) => {
|
||||
message.success(`继续学习:${progress.courseTitle}`)
|
||||
// 这里可以跳转到学习页面的具体位置
|
||||
}
|
||||
|
||||
const addToFavorites = (course) => {
|
||||
message.success(`已收藏:${course.title}`)
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cattle-education {
|
||||
.content-wrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.category-card {
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover, &.active {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.category-stats {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.course-card {
|
||||
height: 100%;
|
||||
|
||||
.course-cover {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.course-duration {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.course-info {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.course-stats {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.course-actions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
.progress-item {
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-detail {
|
||||
.course-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.course-image {
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.course-tags {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.course-stats-detail {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-outline {
|
||||
.chapter {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h4 {
|
||||
color: #1890ff;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.lesson-duration {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-reviews {
|
||||
.review-item {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.review-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.review-meta {
|
||||
flex: 1;
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.review-date {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.review-content {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-actions-detail {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
|
||||
.ant-btn {
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
869
government-admin/src/views/services/CommunityManagement.vue
Normal file
869
government-admin/src/views/services/CommunityManagement.vue
Normal file
@@ -0,0 +1,869 @@
|
||||
<template>
|
||||
<div class="community-management">
|
||||
<PageHeader title="社区管理服务" />
|
||||
|
||||
<div class="content-wrapper">
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧社区列表 -->
|
||||
<a-col :span="6">
|
||||
<a-card title="社区列表" class="community-list-card">
|
||||
<template #extra>
|
||||
<a-button type="primary" size="small" @click="showCreateCommunity">
|
||||
<PlusOutlined /> 创建社区
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div class="community-search">
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索社区"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="community-list">
|
||||
<div
|
||||
v-for="community in filteredCommunities"
|
||||
:key="community.id"
|
||||
class="community-item"
|
||||
:class="{ active: selectedCommunity?.id === community.id }"
|
||||
@click="selectCommunity(community)"
|
||||
>
|
||||
<div class="community-avatar">
|
||||
<a-avatar :src="community.avatar" :size="40">
|
||||
{{ community.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
</div>
|
||||
<div class="community-info">
|
||||
<div class="community-name">{{ community.name }}</div>
|
||||
<div class="community-desc">{{ community.description }}</div>
|
||||
<div class="community-stats">
|
||||
<span>{{ community.memberCount }} 成员</span>
|
||||
<span>{{ community.postCount }} 帖子</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="community-status">
|
||||
<a-badge
|
||||
:status="community.status === 'active' ? 'success' : 'default'"
|
||||
:text="community.status === 'active' ? '活跃' : '普通'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 中间内容区域 -->
|
||||
<a-col :span="12">
|
||||
<a-card v-if="!selectedCommunity" class="empty-content">
|
||||
<a-empty description="请选择社区查看详情" />
|
||||
</a-card>
|
||||
|
||||
<div v-else class="community-content">
|
||||
<!-- 社区信息 -->
|
||||
<a-card class="community-info-card">
|
||||
<template #title>
|
||||
<div class="community-header">
|
||||
<a-avatar :src="selectedCommunity.avatar" :size="50">
|
||||
{{ selectedCommunity.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
<div class="community-meta">
|
||||
<h3>{{ selectedCommunity.name }}</h3>
|
||||
<p>{{ selectedCommunity.description }}</p>
|
||||
<div class="community-tags">
|
||||
<a-tag v-for="tag in selectedCommunity.tags" :key="tag" color="blue">
|
||||
{{ tag }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="editCommunity(selectedCommunity)">
|
||||
<EditOutlined /> 编辑
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<a-button>
|
||||
<MoreOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="manageMember">成员管理</a-menu-item>
|
||||
<a-menu-item @click="communitySettings">社区设置</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item danger @click="deleteCommunity">删除社区</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="成员数量" :value="selectedCommunity.memberCount" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="帖子数量" :value="selectedCommunity.postCount" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="今日活跃" :value="selectedCommunity.todayActive" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="创建时间" :value="formatDate(selectedCommunity.createTime)" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 帖子管理 -->
|
||||
<a-card title="帖子管理" class="posts-card">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-select v-model:value="postFilter" style="width: 120px">
|
||||
<a-select-option value="all">全部帖子</a-select-option>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="reported">被举报</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="createPost">
|
||||
<PlusOutlined /> 发布公告
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-list
|
||||
:data-source="filteredPosts"
|
||||
:pagination="postPagination"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a @click="viewPost(item)">查看</a>
|
||||
<a @click="editPost(item)">编辑</a>
|
||||
<a-dropdown>
|
||||
<a>更多</a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="pinPost(item)">
|
||||
{{ item.pinned ? '取消置顶' : '置顶' }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="lockPost(item)">
|
||||
{{ item.locked ? '解锁' : '锁定' }}
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item danger @click="deletePost(item)">删除</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-avatar :src="item.author.avatar">
|
||||
{{ item.author.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="post-title">
|
||||
<span>{{ item.title }}</span>
|
||||
<a-space class="post-badges">
|
||||
<a-tag v-if="item.pinned" color="red">置顶</a-tag>
|
||||
<a-tag v-if="item.locked" color="orange">锁定</a-tag>
|
||||
<a-tag v-if="item.status === 'pending'" color="yellow">待审核</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="post-meta">
|
||||
<span>{{ item.author.name }}</span>
|
||||
<span>{{ formatTime(item.createTime) }}</span>
|
||||
<span>{{ item.viewCount }} 浏览</span>
|
||||
<span>{{ item.replyCount }} 回复</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧管理面板 -->
|
||||
<a-col :span="6">
|
||||
<a-card title="管理工具" class="management-tools">
|
||||
<a-space direction="vertical" style="width: 100%;">
|
||||
<a-button block @click="showMemberManagement">
|
||||
<UserOutlined /> 成员管理
|
||||
</a-button>
|
||||
<a-button block @click="showContentModeration">
|
||||
<SafetyOutlined /> 内容审核
|
||||
</a-button>
|
||||
<a-button block @click="showReportManagement">
|
||||
<ExclamationCircleOutlined /> 举报处理
|
||||
</a-button>
|
||||
<a-button block @click="showAnalytics">
|
||||
<BarChartOutlined /> 数据分析
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
|
||||
<a-card title="最新动态" class="recent-activities">
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
:color="getActivityColor(activity.type)"
|
||||
>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">{{ activity.content }}</div>
|
||||
<div class="activity-time">{{ formatTime(activity.time) }}</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</a-card>
|
||||
|
||||
<a-card title="社区统计" class="community-stats">
|
||||
<div class="stats-chart">
|
||||
<div ref="memberChart" style="height: 200px;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑社区弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="communityModalVisible"
|
||||
:title="editingCommunity ? '编辑社区' : '创建社区'"
|
||||
@ok="handleCommunitySubmit"
|
||||
@cancel="handleCommunityCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="communityFormRef"
|
||||
:model="communityForm"
|
||||
:rules="communityRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="社区名称" name="name">
|
||||
<a-input v-model:value="communityForm.name" placeholder="请输入社区名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="社区描述" name="description">
|
||||
<a-textarea v-model:value="communityForm.description" :rows="3" placeholder="请输入社区描述" />
|
||||
</a-form-item>
|
||||
<a-form-item label="社区标签" name="tags">
|
||||
<a-select
|
||||
v-model:value="communityForm.tags"
|
||||
mode="tags"
|
||||
placeholder="请选择或输入标签"
|
||||
:options="tagOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="社区类型" name="type">
|
||||
<a-radio-group v-model:value="communityForm.type">
|
||||
<a-radio value="public">公开社区</a-radio>
|
||||
<a-radio value="private">私密社区</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 成员管理弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="memberModalVisible"
|
||||
title="成员管理"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="member-management">
|
||||
<div class="member-search">
|
||||
<a-input-search
|
||||
v-model:value="memberSearchKeyword"
|
||||
placeholder="搜索成员"
|
||||
style="width: 300px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="memberColumns"
|
||||
:data-source="communityMembers"
|
||||
:pagination="memberPagination"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar">{{ record.name.charAt(0) }}</a-avatar>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'role'">
|
||||
<a-tag :color="getRoleColor(record.role)">{{ getRoleText(record.role) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge
|
||||
:status="record.status === 'active' ? 'success' : 'default'"
|
||||
:text="record.status === 'active' ? '正常' : '禁用'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a @click="changeMemberRole(record)">角色</a>
|
||||
<a @click="toggleMemberStatus(record)">
|
||||
{{ record.status === 'active' ? '禁用' : '启用' }}
|
||||
</a>
|
||||
<a @click="removeMember(record)" style="color: red;">移除</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
MoreOutlined,
|
||||
UserOutlined,
|
||||
SafetyOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
BarChartOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const selectedCommunity = ref(null)
|
||||
const searchKeyword = ref('')
|
||||
const postFilter = ref('all')
|
||||
const communityModalVisible = ref(false)
|
||||
const memberModalVisible = ref(false)
|
||||
const editingCommunity = ref(null)
|
||||
const memberSearchKeyword = ref('')
|
||||
const memberChart = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
// 表单数据
|
||||
const communityForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
tags: [],
|
||||
type: 'public'
|
||||
})
|
||||
|
||||
const communityRules = {
|
||||
name: [{ required: true, message: '请输入社区名称' }],
|
||||
description: [{ required: true, message: '请输入社区描述' }]
|
||||
}
|
||||
|
||||
// 分页配置
|
||||
const postPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const memberPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 社区数据
|
||||
const communities = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '养牛技术交流',
|
||||
description: '专业的养牛技术讨论社区',
|
||||
avatar: '/images/community1.jpg',
|
||||
memberCount: 1234,
|
||||
postCount: 567,
|
||||
todayActive: 89,
|
||||
status: 'active',
|
||||
tags: ['技术', '交流', '养牛'],
|
||||
createTime: new Date('2023-01-15')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '牛只健康管理',
|
||||
description: '关注牛只健康,分享管理经验',
|
||||
avatar: '/images/community2.jpg',
|
||||
memberCount: 856,
|
||||
postCount: 342,
|
||||
todayActive: 45,
|
||||
status: 'active',
|
||||
tags: ['健康', '管理', '经验'],
|
||||
createTime: new Date('2023-03-20')
|
||||
}
|
||||
])
|
||||
|
||||
// 帖子数据
|
||||
const posts = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '春季牛只疫苗接种注意事项',
|
||||
author: { name: '张三', avatar: '/images/user1.jpg' },
|
||||
createTime: new Date(),
|
||||
viewCount: 234,
|
||||
replyCount: 12,
|
||||
pinned: true,
|
||||
locked: false,
|
||||
status: 'approved',
|
||||
communityId: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '如何提高牛奶产量的几个技巧',
|
||||
author: { name: '李四', avatar: '/images/user2.jpg' },
|
||||
createTime: new Date(Date.now() - 3600000),
|
||||
viewCount: 156,
|
||||
replyCount: 8,
|
||||
pinned: false,
|
||||
locked: false,
|
||||
status: 'pending',
|
||||
communityId: 1
|
||||
}
|
||||
])
|
||||
|
||||
// 成员数据
|
||||
const communityMembers = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '张三',
|
||||
avatar: '/images/user1.jpg',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
joinTime: new Date('2023-01-20'),
|
||||
postCount: 45,
|
||||
lastActive: new Date()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李四',
|
||||
avatar: '/images/user2.jpg',
|
||||
role: 'moderator',
|
||||
status: 'active',
|
||||
joinTime: new Date('2023-02-15'),
|
||||
postCount: 23,
|
||||
lastActive: new Date(Date.now() - 3600000)
|
||||
}
|
||||
])
|
||||
|
||||
// 最新动态
|
||||
const recentActivities = ref([
|
||||
{
|
||||
id: 1,
|
||||
type: 'post',
|
||||
content: '张三发布了新帖子《春季牛只疫苗接种注意事项》',
|
||||
time: new Date()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'member',
|
||||
content: '新成员王五加入了社区',
|
||||
time: new Date(Date.now() - 1800000)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'report',
|
||||
content: '收到一条举报信息,需要处理',
|
||||
time: new Date(Date.now() - 3600000)
|
||||
}
|
||||
])
|
||||
|
||||
// 标签选项
|
||||
const tagOptions = ref([
|
||||
{ label: '技术', value: '技术' },
|
||||
{ label: '交流', value: '交流' },
|
||||
{ label: '养牛', value: '养牛' },
|
||||
{ label: '健康', value: '健康' },
|
||||
{ label: '管理', value: '管理' }
|
||||
])
|
||||
|
||||
// 成员表格列
|
||||
const memberColumns = [
|
||||
{ title: '头像', key: 'avatar', width: 80 },
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '角色', key: 'role', width: 100 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '帖子数', dataIndex: 'postCount', key: 'postCount', width: 100 },
|
||||
{ title: '加入时间', dataIndex: 'joinTime', key: 'joinTime', width: 120 },
|
||||
{ title: '操作', key: 'actions', width: 150 }
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const filteredCommunities = computed(() => {
|
||||
if (!searchKeyword.value) return communities.value
|
||||
return communities.value.filter(community =>
|
||||
community.name.includes(searchKeyword.value) ||
|
||||
community.description.includes(searchKeyword.value)
|
||||
)
|
||||
})
|
||||
|
||||
const filteredPosts = computed(() => {
|
||||
if (!selectedCommunity.value) return []
|
||||
|
||||
let result = posts.value.filter(post => post.communityId === selectedCommunity.value.id)
|
||||
|
||||
if (postFilter.value !== 'all') {
|
||||
result = result.filter(post => post.status === postFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 方法
|
||||
const selectCommunity = (community) => {
|
||||
selectedCommunity.value = community
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索处理
|
||||
}
|
||||
|
||||
const showCreateCommunity = () => {
|
||||
editingCommunity.value = null
|
||||
Object.assign(communityForm, {
|
||||
name: '',
|
||||
description: '',
|
||||
tags: [],
|
||||
type: 'public'
|
||||
})
|
||||
communityModalVisible.value = true
|
||||
}
|
||||
|
||||
const editCommunity = (community) => {
|
||||
editingCommunity.value = community
|
||||
Object.assign(communityForm, {
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
tags: community.tags,
|
||||
type: community.type || 'public'
|
||||
})
|
||||
communityModalVisible.value = true
|
||||
}
|
||||
|
||||
const handleCommunitySubmit = () => {
|
||||
if (editingCommunity.value) {
|
||||
message.success('社区信息更新成功')
|
||||
} else {
|
||||
message.success('社区创建成功')
|
||||
}
|
||||
communityModalVisible.value = false
|
||||
}
|
||||
|
||||
const handleCommunityCancel = () => {
|
||||
communityModalVisible.value = false
|
||||
}
|
||||
|
||||
const manageMember = () => {
|
||||
memberModalVisible.value = true
|
||||
}
|
||||
|
||||
const communitySettings = () => {
|
||||
message.info('社区设置功能开发中')
|
||||
}
|
||||
|
||||
const deleteCommunity = () => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个社区吗?此操作不可恢复。',
|
||||
onOk() {
|
||||
message.success('社区删除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createPost = () => {
|
||||
message.info('发布公告功能开发中')
|
||||
}
|
||||
|
||||
const viewPost = (post) => {
|
||||
message.info(`查看帖子:${post.title}`)
|
||||
}
|
||||
|
||||
const editPost = (post) => {
|
||||
message.info(`编辑帖子:${post.title}`)
|
||||
}
|
||||
|
||||
const pinPost = (post) => {
|
||||
post.pinned = !post.pinned
|
||||
message.success(post.pinned ? '帖子已置顶' : '帖子已取消置顶')
|
||||
}
|
||||
|
||||
const lockPost = (post) => {
|
||||
post.locked = !post.locked
|
||||
message.success(post.locked ? '帖子已锁定' : '帖子已解锁')
|
||||
}
|
||||
|
||||
const deletePost = (post) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个帖子吗?',
|
||||
onOk() {
|
||||
message.success('帖子删除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showMemberManagement = () => {
|
||||
memberModalVisible.value = true
|
||||
}
|
||||
|
||||
const showContentModeration = () => {
|
||||
message.info('内容审核功能开发中')
|
||||
}
|
||||
|
||||
const showReportManagement = () => {
|
||||
message.info('举报处理功能开发中')
|
||||
}
|
||||
|
||||
const showAnalytics = () => {
|
||||
message.info('数据分析功能开发中')
|
||||
}
|
||||
|
||||
const changeMemberRole = (member) => {
|
||||
message.info(`修改 ${member.name} 的角色`)
|
||||
}
|
||||
|
||||
const toggleMemberStatus = (member) => {
|
||||
member.status = member.status === 'active' ? 'inactive' : 'active'
|
||||
message.success(`${member.name} 状态已更新`)
|
||||
}
|
||||
|
||||
const removeMember = (member) => {
|
||||
Modal.confirm({
|
||||
title: '确认移除',
|
||||
content: `确定要移除成员 ${member.name} 吗?`,
|
||||
onOk() {
|
||||
message.success('成员移除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getActivityColor = (type) => {
|
||||
const colorMap = {
|
||||
post: 'blue',
|
||||
member: 'green',
|
||||
report: 'red'
|
||||
}
|
||||
return colorMap[type] || 'gray'
|
||||
}
|
||||
|
||||
const getRoleColor = (role) => {
|
||||
const colorMap = {
|
||||
admin: 'red',
|
||||
moderator: 'orange',
|
||||
member: 'blue'
|
||||
}
|
||||
return colorMap[role] || 'default'
|
||||
}
|
||||
|
||||
const getRoleText = (role) => {
|
||||
const textMap = {
|
||||
admin: '管理员',
|
||||
moderator: '版主',
|
||||
member: '成员'
|
||||
}
|
||||
return textMap[role] || '成员'
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
const initChart = () => {
|
||||
if (memberChart.value && !chartInstance) {
|
||||
chartInstance = echarts.init(memberChart.value)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '成员增长趋势',
|
||||
textStyle: { fontSize: 14 }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: [120, 200, 150, 80, 70, 110],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#1890ff'
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载和卸载
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initChart()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.community-management {
|
||||
.content-wrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.community-list-card {
|
||||
.community-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.community-list {
|
||||
.community-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover, &.active {
|
||||
background: #f0f2ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.community-avatar {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.community-info {
|
||||
flex: 1;
|
||||
|
||||
.community-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.community-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.community-stats {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
|
||||
span {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.community-status {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.community-content {
|
||||
.community-info-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.community-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.community-meta {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.community-tags {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.posts-card {
|
||||
.post-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.post-badges {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.management-tools, .recent-activities, .community-stats {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.recent-activities {
|
||||
.activity-content {
|
||||
.activity-text {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-management {
|
||||
.member-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
744
government-admin/src/views/services/OnlineConsultation.vue
Normal file
744
government-admin/src/views/services/OnlineConsultation.vue
Normal file
@@ -0,0 +1,744 @@
|
||||
<template>
|
||||
<div class="online-consultation">
|
||||
<PageHeader title="在线咨询服务" />
|
||||
|
||||
<div class="content-wrapper">
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧咨询列表 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="咨询分类" class="category-card">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedCategory"
|
||||
mode="inline"
|
||||
@click="handleCategoryChange"
|
||||
>
|
||||
<a-menu-item v-for="category in categories" :key="category.key">
|
||||
<template #icon>
|
||||
<component :is="category.icon" />
|
||||
</template>
|
||||
{{ category.label }}
|
||||
<a-badge :count="category.count" style="margin-left: 8px;" />
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-card>
|
||||
|
||||
<a-card title="专家列表" class="expert-card">
|
||||
<div class="expert-list">
|
||||
<div
|
||||
v-for="expert in filteredExperts"
|
||||
:key="expert.id"
|
||||
class="expert-item"
|
||||
:class="{ active: selectedExpert?.id === expert.id }"
|
||||
@click="selectExpert(expert)"
|
||||
>
|
||||
<a-avatar :src="expert.avatar" :size="40">
|
||||
{{ expert.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
<div class="expert-info">
|
||||
<div class="expert-name">{{ expert.name }}</div>
|
||||
<div class="expert-title">{{ expert.title }}</div>
|
||||
<div class="expert-status">
|
||||
<a-badge
|
||||
:status="expert.online ? 'success' : 'default'"
|
||||
:text="expert.online ? '在线' : '离线'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="expert-rating">
|
||||
<a-rate :value="expert.rating" disabled />
|
||||
<div class="rating-text">{{ expert.rating }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 中间聊天区域 -->
|
||||
<a-col :span="10">
|
||||
<a-card class="chat-card">
|
||||
<template #title>
|
||||
<div v-if="selectedExpert" class="chat-header">
|
||||
<a-avatar :src="selectedExpert.avatar">
|
||||
{{ selectedExpert.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
<div class="expert-info">
|
||||
<span class="expert-name">{{ selectedExpert.name }}</span>
|
||||
<span class="expert-title">{{ selectedExpert.title }}</span>
|
||||
</div>
|
||||
<a-badge
|
||||
:status="selectedExpert.online ? 'success' : 'default'"
|
||||
:text="selectedExpert.online ? '在线' : '离线'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="no-expert">
|
||||
请选择专家开始咨询
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="chat-content" ref="chatContent">
|
||||
<div v-if="!selectedExpert" class="empty-chat">
|
||||
<a-empty description="选择专家开始咨询" />
|
||||
</div>
|
||||
<div v-else class="message-list">
|
||||
<div
|
||||
v-for="message in currentMessages"
|
||||
:key="message.id"
|
||||
class="message-item"
|
||||
:class="{ 'own-message': message.sender === 'user' }"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
<a-avatar
|
||||
:src="message.sender === 'user' ? userAvatar : selectedExpert.avatar"
|
||||
:size="32"
|
||||
>
|
||||
{{ message.sender === 'user' ? 'U' : selectedExpert.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-info">
|
||||
<span class="sender-name">
|
||||
{{ message.sender === 'user' ? '我' : selectedExpert.name }}
|
||||
</span>
|
||||
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="message-text">{{ message.content }}</div>
|
||||
<div v-if="message.images" class="message-images">
|
||||
<img
|
||||
v-for="(image, index) in message.images"
|
||||
:key="index"
|
||||
:src="image"
|
||||
class="message-image"
|
||||
@click="previewImage(image)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedExpert" class="chat-input">
|
||||
<div class="input-toolbar">
|
||||
<a-space>
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:before-upload="handleImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<a-button type="text" size="small">
|
||||
<PictureOutlined />
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-button type="text" size="small">
|
||||
<FileOutlined />
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<a-textarea
|
||||
v-model:value="inputMessage"
|
||||
:rows="3"
|
||||
placeholder="输入您的问题..."
|
||||
@keydown.enter.prevent="handleSendMessage"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleSendMessage"
|
||||
:disabled="!inputMessage.trim()"
|
||||
class="send-button"
|
||||
>
|
||||
发送
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧信息面板 -->
|
||||
<a-col :span="6">
|
||||
<a-card title="咨询统计" class="stats-card">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-statistic title="今日咨询" :value="todayConsultations" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-statistic title="总咨询数" :value="totalConsultations" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" style="margin-top: 16px;">
|
||||
<a-col :span="12">
|
||||
<a-statistic title="在线专家" :value="onlineExperts" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-statistic title="平均响应" :value="avgResponseTime" suffix="分钟" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<a-card title="常见问题" class="faq-card">
|
||||
<a-collapse ghost>
|
||||
<a-collapse-panel
|
||||
v-for="faq in faqs"
|
||||
:key="faq.id"
|
||||
:header="faq.question"
|
||||
>
|
||||
<p>{{ faq.answer }}</p>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-card>
|
||||
|
||||
<a-card title="咨询记录" class="history-card">
|
||||
<a-list
|
||||
:data-source="consultationHistory"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<span>{{ item.expertName }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>{{ item.topic }}</div>
|
||||
<div class="history-time">{{ formatDate(item.date) }}</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a @click="viewHistory(item)">查看</a>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<a-modal
|
||||
v-model:open="imagePreviewVisible"
|
||||
:footer="null"
|
||||
:width="600"
|
||||
>
|
||||
<img :src="previewImageUrl" style="width: 100%;" />
|
||||
</a-modal>
|
||||
|
||||
<!-- 咨询历史详情 -->
|
||||
<a-modal
|
||||
v-model:open="historyModalVisible"
|
||||
title="咨询记录详情"
|
||||
:width="800"
|
||||
:footer="null"
|
||||
>
|
||||
<div v-if="selectedHistory" class="history-detail">
|
||||
<div class="history-header">
|
||||
<h3>{{ selectedHistory.topic }}</h3>
|
||||
<div class="history-meta">
|
||||
<span>专家:{{ selectedHistory.expertName }}</span>
|
||||
<span>时间:{{ formatDate(selectedHistory.date) }}</span>
|
||||
<span>状态:{{ selectedHistory.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-messages">
|
||||
<div
|
||||
v-for="message in selectedHistory.messages"
|
||||
:key="message.id"
|
||||
class="history-message"
|
||||
:class="{ 'own-message': message.sender === 'user' }"
|
||||
>
|
||||
<div class="message-sender">{{ message.sender === 'user' ? '我' : selectedHistory.expertName }}</div>
|
||||
<div class="message-content">{{ message.content }}</div>
|
||||
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
MedicineBoxOutlined,
|
||||
ExperimentOutlined,
|
||||
EnvironmentOutlined,
|
||||
HeartOutlined,
|
||||
PictureOutlined,
|
||||
FileOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const selectedCategory = ref(['all'])
|
||||
const selectedExpert = ref(null)
|
||||
const inputMessage = ref('')
|
||||
const chatContent = ref(null)
|
||||
const imagePreviewVisible = ref(false)
|
||||
const previewImageUrl = ref('')
|
||||
const historyModalVisible = ref(false)
|
||||
const selectedHistory = ref(null)
|
||||
const userAvatar = ref('/images/user-avatar.jpg')
|
||||
|
||||
// 咨询分类
|
||||
const categories = ref([
|
||||
{ key: 'all', label: '全部咨询', icon: HeartOutlined, count: 25 },
|
||||
{ key: 'disease', label: '疾病诊断', icon: MedicineBoxOutlined, count: 12 },
|
||||
{ key: 'nutrition', label: '营养管理', icon: ExperimentOutlined, count: 8 },
|
||||
{ key: 'environment', label: '环境管理', icon: EnvironmentOutlined, count: 5 }
|
||||
])
|
||||
|
||||
// 专家列表
|
||||
const experts = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '张教授',
|
||||
title: '兽医学专家',
|
||||
avatar: '/images/expert1.jpg',
|
||||
online: true,
|
||||
rating: 4.9,
|
||||
specialty: ['disease', 'nutrition'],
|
||||
responseTime: 5
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李博士',
|
||||
title: '动物营养学专家',
|
||||
avatar: '/images/expert2.jpg',
|
||||
online: true,
|
||||
rating: 4.8,
|
||||
specialty: ['nutrition', 'environment'],
|
||||
responseTime: 8
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '王医生',
|
||||
title: '畜牧兽医师',
|
||||
avatar: '/images/expert3.jpg',
|
||||
online: false,
|
||||
rating: 4.7,
|
||||
specialty: ['disease'],
|
||||
responseTime: 12
|
||||
}
|
||||
])
|
||||
|
||||
// 消息数据
|
||||
const messages = ref({
|
||||
1: [
|
||||
{
|
||||
id: 1,
|
||||
sender: 'expert',
|
||||
content: '您好!我是张教授,很高兴为您服务。请问有什么问题需要咨询?',
|
||||
timestamp: new Date(Date.now() - 3600000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sender: 'user',
|
||||
content: '我家的牛最近食欲不振,请问可能是什么原因?',
|
||||
timestamp: new Date(Date.now() - 3500000)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sender: 'expert',
|
||||
content: '食欲不振可能有多种原因,比如消化系统问题、环境应激、疾病等。您能详细描述一下牛的具体症状吗?比如精神状态、体温、排便情况等。',
|
||||
timestamp: new Date(Date.now() - 3400000)
|
||||
}
|
||||
],
|
||||
2: [],
|
||||
3: []
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const todayConsultations = ref(15)
|
||||
const totalConsultations = ref(1248)
|
||||
const onlineExperts = ref(2)
|
||||
const avgResponseTime = ref(6)
|
||||
|
||||
// 常见问题
|
||||
const faqs = ref([
|
||||
{
|
||||
id: 1,
|
||||
question: '如何预防牛只感冒?',
|
||||
answer: '保持牛舍通风良好,避免温差过大,定期消毒,加强营养管理。'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: '牛只不吃草怎么办?',
|
||||
answer: '检查草料质量,观察牛只健康状况,必要时咨询兽医。'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: '如何提高牛奶产量?',
|
||||
answer: '优化饲料配比,保证充足饮水,创造舒适环境,定期健康检查。'
|
||||
}
|
||||
])
|
||||
|
||||
// 咨询历史
|
||||
const consultationHistory = ref([
|
||||
{
|
||||
id: 1,
|
||||
expertName: '张教授',
|
||||
topic: '牛只消化不良问题',
|
||||
date: new Date(Date.now() - 86400000),
|
||||
status: '已解决',
|
||||
messages: [
|
||||
{
|
||||
id: 1,
|
||||
sender: 'user',
|
||||
content: '我的牛出现消化不良症状',
|
||||
timestamp: new Date(Date.now() - 86400000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sender: 'expert',
|
||||
content: '建议调整饲料配比,增加益生菌',
|
||||
timestamp: new Date(Date.now() - 86300000)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
expertName: '李博士',
|
||||
topic: '饲料营养搭配咨询',
|
||||
date: new Date(Date.now() - 172800000),
|
||||
status: '已解决',
|
||||
messages: []
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const filteredExperts = computed(() => {
|
||||
if (selectedCategory.value.includes('all')) {
|
||||
return experts.value
|
||||
}
|
||||
return experts.value.filter(expert =>
|
||||
expert.specialty.some(spec => selectedCategory.value.includes(spec))
|
||||
)
|
||||
})
|
||||
|
||||
const currentMessages = computed(() => {
|
||||
return selectedExpert.value ? messages.value[selectedExpert.value.id] || [] : []
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleCategoryChange = ({ key }) => {
|
||||
selectedCategory.value = [key]
|
||||
selectedExpert.value = null
|
||||
}
|
||||
|
||||
const selectExpert = (expert) => {
|
||||
selectedExpert.value = expert
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!inputMessage.value.trim() || !selectedExpert.value) return
|
||||
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
sender: 'user',
|
||||
content: inputMessage.value.trim(),
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
if (!messages.value[selectedExpert.value.id]) {
|
||||
messages.value[selectedExpert.value.id] = []
|
||||
}
|
||||
|
||||
messages.value[selectedExpert.value.id].push(newMessage)
|
||||
inputMessage.value = ''
|
||||
|
||||
// 模拟专家回复
|
||||
setTimeout(() => {
|
||||
const expertReply = {
|
||||
id: Date.now() + 1,
|
||||
sender: 'expert',
|
||||
content: '感谢您的咨询,我正在分析您的问题,请稍等...',
|
||||
timestamp: new Date()
|
||||
}
|
||||
messages.value[selectedExpert.value.id].push(expertReply)
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
const handleImageUpload = (file) => {
|
||||
// 处理图片上传
|
||||
message.success('图片上传成功')
|
||||
return false // 阻止默认上传行为
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (chatContent.value) {
|
||||
chatContent.value.scrollTop = chatContent.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
const previewImage = (imageUrl) => {
|
||||
previewImageUrl.value = imageUrl
|
||||
imagePreviewVisible.value = true
|
||||
}
|
||||
|
||||
const viewHistory = (history) => {
|
||||
selectedHistory.value = history
|
||||
historyModalVisible.value = true
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.online-consultation {
|
||||
.content-wrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.category-card, .expert-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.expert-list {
|
||||
.expert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:hover, &.active {
|
||||
background: #f0f2ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.expert-info {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
|
||||
.expert-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.expert-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.expert-status {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.expert-rating {
|
||||
text-align: center;
|
||||
|
||||
.rating-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
height: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.expert-info {
|
||||
flex: 1;
|
||||
|
||||
.expert-name {
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.expert-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-expert {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 16px 0;
|
||||
|
||||
.empty-chat {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.own-message {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
background: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
|
||||
.message-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-images {
|
||||
margin-top: 8px;
|
||||
|
||||
.message-image {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 16px;
|
||||
|
||||
.input-toolbar {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.ant-textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-card, .faq-card, .history-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.history-card {
|
||||
.history-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.history-detail {
|
||||
.history-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.history-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.history-messages {
|
||||
.history-message {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
|
||||
&.own-message {
|
||||
background: #e6f7ff;
|
||||
margin-left: 20%;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1588
government-admin/src/views/settings/SystemSettings.vue
Normal file
1588
government-admin/src/views/settings/SystemSettings.vue
Normal file
File diff suppressed because it is too large
Load Diff
945
government-admin/src/views/statistics/DataStatistics.vue
Normal file
945
government-admin/src/views/statistics/DataStatistics.vue
Normal file
@@ -0,0 +1,945 @@
|
||||
<template>
|
||||
<div class="data-statistics">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader
|
||||
title="数据统计"
|
||||
description="数据可视化分析,生成统计报表,监控业务趋势"
|
||||
icon="bar-chart"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
@change="handleDateRangeChange"
|
||||
/>
|
||||
<a-button type="primary" @click="handleGenerateReport">
|
||||
<template #icon>
|
||||
<FileTextOutlined />
|
||||
</template>
|
||||
生成报表
|
||||
</a-button>
|
||||
<a-button @click="handleExportData">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button @click="handleRefreshData">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新数据
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 核心指标卡片 -->
|
||||
<a-row :gutter="16" class="stats-cards">
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card farms">
|
||||
<a-statistic
|
||||
title="养殖场总数"
|
||||
:value="coreStats.totalFarms"
|
||||
suffix="家"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<HomeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span class="trend-text" :class="{ positive: coreStats.farmsTrend > 0, negative: coreStats.farmsTrend < 0 }">
|
||||
<ArrowUpOutlined v-if="coreStats.farmsTrend > 0" />
|
||||
<ArrowDownOutlined v-if="coreStats.farmsTrend < 0" />
|
||||
{{ Math.abs(coreStats.farmsTrend) }}%
|
||||
</span>
|
||||
<span class="trend-label">较上月</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card animals">
|
||||
<a-statistic
|
||||
title="动物总数"
|
||||
:value="coreStats.totalAnimals"
|
||||
suffix="头"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<BugOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span class="trend-text" :class="{ positive: coreStats.animalsTrend > 0, negative: coreStats.animalsTrend < 0 }">
|
||||
<ArrowUpOutlined v-if="coreStats.animalsTrend > 0" />
|
||||
<ArrowDownOutlined v-if="coreStats.animalsTrend < 0" />
|
||||
{{ Math.abs(coreStats.animalsTrend) }}%
|
||||
</span>
|
||||
<span class="trend-label">较上月</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card inspections">
|
||||
<a-statistic
|
||||
title="检查次数"
|
||||
:value="coreStats.totalInspections"
|
||||
suffix="次"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span class="trend-text" :class="{ positive: coreStats.inspectionsTrend > 0, negative: coreStats.inspectionsTrend < 0 }">
|
||||
<ArrowUpOutlined v-if="coreStats.inspectionsTrend > 0" />
|
||||
<ArrowDownOutlined v-if="coreStats.inspectionsTrend < 0" />
|
||||
{{ Math.abs(coreStats.inspectionsTrend) }}%
|
||||
</span>
|
||||
<span class="trend-label">较上月</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-card class="stat-card compliance">
|
||||
<a-statistic
|
||||
title="合规率"
|
||||
:value="coreStats.complianceRate"
|
||||
suffix="%"
|
||||
:precision="1"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<CheckCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="stat-trend">
|
||||
<span class="trend-text" :class="{ positive: coreStats.complianceTrend > 0, negative: coreStats.complianceTrend < 0 }">
|
||||
<ArrowUpOutlined v-if="coreStats.complianceTrend > 0" />
|
||||
<ArrowDownOutlined v-if="coreStats.complianceTrend < 0" />
|
||||
{{ Math.abs(coreStats.complianceTrend) }}%
|
||||
</span>
|
||||
<span class="trend-label">较上月</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-section">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="养殖场数量趋势" class="chart-card">
|
||||
<template #extra>
|
||||
<a-radio-group v-model:value="farmTrendPeriod" size="small" @change="handleFarmTrendChange">
|
||||
<a-radio-button value="week">周</a-radio-button>
|
||||
<a-radio-button value="month">月</a-radio-button>
|
||||
<a-radio-button value="year">年</a-radio-button>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<div ref="farmTrendChart" class="chart-container"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="动物健康状况" class="chart-card">
|
||||
<div ref="healthStatusChart" class="chart-container"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" class="chart-section">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="检查结果分布" class="chart-card">
|
||||
<div ref="inspectionResultChart" class="chart-container"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="区域分布统计" class="chart-card">
|
||||
<div ref="regionDistributionChart" class="chart-container"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 详细统计表格 -->
|
||||
<a-card title="详细统计数据" class="table-card">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="statisticsType"
|
||||
style="width: 120px"
|
||||
@change="handleStatisticsTypeChange"
|
||||
>
|
||||
<a-select-option value="farm">养殖场</a-select-option>
|
||||
<a-select-option value="animal">动物</a-select-option>
|
||||
<a-select-option value="inspection">检查</a-select-option>
|
||||
<a-select-option value="region">区域</a-select-option>
|
||||
</a-select>
|
||||
<a-button size="small" @click="handleTableExport">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出表格
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<DataTable
|
||||
:columns="currentTableColumns"
|
||||
:data-source="currentTableData"
|
||||
:loading="tableLoading"
|
||||
:pagination="tablePagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'healthStatus'">
|
||||
<a-tag :color="getHealthStatusColor(record.healthStatus)">
|
||||
{{ getHealthStatusText(record.healthStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'result'">
|
||||
<a-tag :color="getResultColor(record.result)">
|
||||
{{ getResultText(record.result) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'compliance'">
|
||||
<a-progress
|
||||
:percent="record.compliance"
|
||||
size="small"
|
||||
:status="record.compliance >= 80 ? 'success' : record.compliance >= 60 ? 'active' : 'exception'"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</DataTable>
|
||||
</a-card>
|
||||
|
||||
<!-- 报表生成弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="reportModalVisible"
|
||||
title="生成统计报表"
|
||||
width="600px"
|
||||
@ok="handleReportSubmit"
|
||||
@cancel="handleReportCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="reportFormRef"
|
||||
:model="reportFormData"
|
||||
:rules="reportFormRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="报表名称" name="name">
|
||||
<a-input v-model:value="reportFormData.name" placeholder="请输入报表名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="报表类型" name="type">
|
||||
<a-select v-model:value="reportFormData.type" placeholder="请选择报表类型">
|
||||
<a-select-option value="summary">综合统计</a-select-option>
|
||||
<a-select-option value="farm">养殖场统计</a-select-option>
|
||||
<a-select-option value="animal">动物统计</a-select-option>
|
||||
<a-select-option value="inspection">检查统计</a-select-option>
|
||||
<a-select-option value="health">健康统计</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="统计周期" name="period">
|
||||
<a-select v-model:value="reportFormData.period" placeholder="请选择统计周期">
|
||||
<a-select-option value="daily">日报</a-select-option>
|
||||
<a-select-option value="weekly">周报</a-select-option>
|
||||
<a-select-option value="monthly">月报</a-select-option>
|
||||
<a-select-option value="quarterly">季报</a-select-option>
|
||||
<a-select-option value="yearly">年报</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" name="dateRange">
|
||||
<a-range-picker
|
||||
v-model:value="reportFormData.dateRange"
|
||||
style="width: 100%"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="包含内容" name="content">
|
||||
<a-checkbox-group v-model:value="reportFormData.content">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-checkbox value="overview">概览统计</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-checkbox value="trend">趋势分析</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-checkbox value="distribution">分布统计</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-checkbox value="comparison">对比分析</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-checkbox value="charts">图表展示</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-checkbox value="details">详细数据</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="输出格式" name="format">
|
||||
<a-radio-group v-model:value="reportFormData.format">
|
||||
<a-radio value="pdf">PDF</a-radio>
|
||||
<a-radio value="excel">Excel</a-radio>
|
||||
<a-radio value="word">Word</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="reportFormData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
FileTextOutlined,
|
||||
ExportOutlined,
|
||||
ReloadOutlined,
|
||||
HomeOutlined,
|
||||
BugOutlined,
|
||||
SearchOutlined,
|
||||
CheckCircleOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
import DataTable from '@/components/common/DataTable.vue'
|
||||
import { useStatisticsStore } from '@/stores/statistics'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
const statisticsStore = useStatisticsStore()
|
||||
|
||||
// 响应式数据
|
||||
const dateRange = ref([dayjs().subtract(30, 'day'), dayjs()])
|
||||
const farmTrendPeriod = ref('month')
|
||||
const statisticsType = ref('farm')
|
||||
const reportModalVisible = ref(false)
|
||||
const reportFormRef = ref()
|
||||
const tableLoading = ref(false)
|
||||
|
||||
// 图表引用
|
||||
const farmTrendChart = ref()
|
||||
const healthStatusChart = ref()
|
||||
const inspectionResultChart = ref()
|
||||
const regionDistributionChart = ref()
|
||||
|
||||
// 核心统计数据
|
||||
const coreStats = reactive({
|
||||
totalFarms: 0,
|
||||
farmsTrend: 0,
|
||||
totalAnimals: 0,
|
||||
animalsTrend: 0,
|
||||
totalInspections: 0,
|
||||
inspectionsTrend: 0,
|
||||
complianceRate: 0,
|
||||
complianceTrend: 0
|
||||
})
|
||||
|
||||
// 报表表单数据
|
||||
const reportFormData = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
period: '',
|
||||
dateRange: null,
|
||||
content: [],
|
||||
format: 'pdf',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表格分页
|
||||
const tablePagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const farmColumns = [
|
||||
{
|
||||
title: '养殖场名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所在区域',
|
||||
dataIndex: 'region',
|
||||
key: 'region',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '养殖类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '动物数量',
|
||||
dataIndex: 'animalCount',
|
||||
key: 'animalCount',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '合规率',
|
||||
dataIndex: 'compliance',
|
||||
key: 'compliance',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '最后检查',
|
||||
dataIndex: 'lastInspection',
|
||||
key: 'lastInspection',
|
||||
width: 120,
|
||||
customRender: ({ text }) => formatDate(text)
|
||||
}
|
||||
]
|
||||
|
||||
const animalColumns = [
|
||||
{
|
||||
title: '动物类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '健康状况',
|
||||
dataIndex: 'healthStatus',
|
||||
key: 'healthStatus',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '疫苗接种率',
|
||||
dataIndex: 'vaccinationRate',
|
||||
key: 'vaccinationRate',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
customRender: ({ text }) => `${text}%`
|
||||
},
|
||||
{
|
||||
title: '死亡率',
|
||||
dataIndex: 'mortalityRate',
|
||||
key: 'mortalityRate',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
customRender: ({ text }) => `${text}%`
|
||||
},
|
||||
{
|
||||
title: '所属养殖场',
|
||||
dataIndex: 'farmName',
|
||||
key: 'farmName',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
}
|
||||
]
|
||||
|
||||
const inspectionColumns = [
|
||||
{
|
||||
title: '检查日期',
|
||||
dataIndex: 'date',
|
||||
key: 'date',
|
||||
width: 120,
|
||||
customRender: ({ text }) => formatDate(text)
|
||||
},
|
||||
{
|
||||
title: '检查类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '检查对象',
|
||||
dataIndex: 'target',
|
||||
key: 'target',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '检查结果',
|
||||
dataIndex: 'result',
|
||||
key: 'result',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '检查人员',
|
||||
dataIndex: 'inspector',
|
||||
key: 'inspector',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '问题数量',
|
||||
dataIndex: 'issueCount',
|
||||
key: 'issueCount',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
const regionColumns = [
|
||||
{
|
||||
title: '区域名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '养殖场数量',
|
||||
dataIndex: 'farmCount',
|
||||
key: 'farmCount',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '动物总数',
|
||||
dataIndex: 'animalCount',
|
||||
key: 'animalCount',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '检查次数',
|
||||
dataIndex: 'inspectionCount',
|
||||
key: 'inspectionCount',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '合规率',
|
||||
dataIndex: 'compliance',
|
||||
key: 'compliance',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '风险等级',
|
||||
dataIndex: 'riskLevel',
|
||||
key: 'riskLevel',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 报表表单验证规则
|
||||
const reportFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入报表名称', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择报表类型', trigger: 'change' }
|
||||
],
|
||||
period: [
|
||||
{ required: true, message: '请选择统计周期', trigger: 'change' }
|
||||
],
|
||||
dateRange: [
|
||||
{ required: true, message: '请选择时间范围', trigger: 'change' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请选择包含内容', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const currentTableColumns = computed(() => {
|
||||
const columnMap = {
|
||||
farm: farmColumns,
|
||||
animal: animalColumns,
|
||||
inspection: inspectionColumns,
|
||||
region: regionColumns
|
||||
}
|
||||
return columnMap[statisticsType.value] || farmColumns
|
||||
})
|
||||
|
||||
const currentTableData = computed(() => {
|
||||
const dataMap = {
|
||||
farm: statisticsStore.farmStatistics,
|
||||
animal: statisticsStore.animalStatistics,
|
||||
inspection: statisticsStore.inspectionStatistics,
|
||||
region: statisticsStore.regionStatistics
|
||||
}
|
||||
return dataMap[statisticsType.value] || []
|
||||
})
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
active: 'success',
|
||||
inactive: 'default',
|
||||
suspended: 'warning',
|
||||
closed: 'error'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
active: '正常',
|
||||
inactive: '停用',
|
||||
suspended: '暂停',
|
||||
closed: '关闭'
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取健康状况颜色
|
||||
const getHealthStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
healthy: 'success',
|
||||
sick: 'error',
|
||||
quarantine: 'warning',
|
||||
dead: 'default'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取健康状况文本
|
||||
const getHealthStatusText = (status) => {
|
||||
const textMap = {
|
||||
healthy: '健康',
|
||||
sick: '患病',
|
||||
quarantine: '隔离',
|
||||
dead: '死亡'
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取检查结果颜色
|
||||
const getResultColor = (result) => {
|
||||
const colorMap = {
|
||||
pass: 'success',
|
||||
fail: 'error',
|
||||
warning: 'warning'
|
||||
}
|
||||
return colorMap[result] || 'default'
|
||||
}
|
||||
|
||||
// 获取检查结果文本
|
||||
const getResultText = (result) => {
|
||||
const textMap = {
|
||||
pass: '合格',
|
||||
fail: '不合格',
|
||||
warning: '警告'
|
||||
}
|
||||
return textMap[result] || '未知'
|
||||
}
|
||||
|
||||
// 加载核心统计数据
|
||||
const loadCoreStats = async () => {
|
||||
try {
|
||||
const params = {
|
||||
startDate: dateRange.value[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange.value[1]?.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const stats = await statisticsStore.fetchCoreStats(params)
|
||||
Object.assign(coreStats, stats)
|
||||
} catch (error) {
|
||||
message.error('加载核心统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载表格数据
|
||||
const loadTableData = async () => {
|
||||
try {
|
||||
tableLoading.value = true
|
||||
const params = {
|
||||
type: statisticsType.value,
|
||||
page: tablePagination.current,
|
||||
pageSize: tablePagination.pageSize,
|
||||
startDate: dateRange.value[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange.value[1]?.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const result = await statisticsStore.fetchTableData(params)
|
||||
tablePagination.total = result.total
|
||||
} catch (error) {
|
||||
message.error('加载表格数据失败')
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 这里应该初始化ECharts图表
|
||||
// 暂时用占位符代替
|
||||
if (farmTrendChart.value) {
|
||||
farmTrendChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">养殖场数量趋势图</div>'
|
||||
}
|
||||
|
||||
if (healthStatusChart.value) {
|
||||
healthStatusChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">动物健康状况图</div>'
|
||||
}
|
||||
|
||||
if (inspectionResultChart.value) {
|
||||
inspectionResultChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">检查结果分布图</div>'
|
||||
}
|
||||
|
||||
if (regionDistributionChart.value) {
|
||||
regionDistributionChart.value.innerHTML = '<div style="height: 300px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">区域分布统计图</div>'
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
const updateCharts = async () => {
|
||||
try {
|
||||
const params = {
|
||||
startDate: dateRange.value[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange.value[1]?.format('YYYY-MM-DD'),
|
||||
period: farmTrendPeriod.value
|
||||
}
|
||||
|
||||
await statisticsStore.fetchChartData(params)
|
||||
// 这里应该更新图表数据
|
||||
// 暂时跳过实际的图表更新
|
||||
} catch (error) {
|
||||
message.error('更新图表数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 日期范围变化处理
|
||||
const handleDateRangeChange = (dates) => {
|
||||
if (dates && dates.length === 2) {
|
||||
loadCoreStats()
|
||||
loadTableData()
|
||||
updateCharts()
|
||||
}
|
||||
}
|
||||
|
||||
// 养殖场趋势周期变化处理
|
||||
const handleFarmTrendChange = () => {
|
||||
updateCharts()
|
||||
}
|
||||
|
||||
// 统计类型变化处理
|
||||
const handleStatisticsTypeChange = () => {
|
||||
tablePagination.current = 1
|
||||
loadTableData()
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag, filters, sorter) => {
|
||||
tablePagination.current = pag.current
|
||||
tablePagination.pageSize = pag.pageSize
|
||||
loadTableData()
|
||||
}
|
||||
|
||||
// 生成报表处理
|
||||
const handleGenerateReport = () => {
|
||||
reportFormData.name = `统计报表_${dayjs().format('YYYY-MM-DD')}`
|
||||
reportFormData.dateRange = dateRange.value
|
||||
reportModalVisible.value = true
|
||||
}
|
||||
|
||||
// 导出数据处理
|
||||
const handleExportData = async () => {
|
||||
try {
|
||||
const params = {
|
||||
type: statisticsType.value,
|
||||
startDate: dateRange.value[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange.value[1]?.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
await statisticsStore.exportData(params)
|
||||
message.success('导出成功')
|
||||
} catch (error) {
|
||||
message.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据处理
|
||||
const handleRefreshData = () => {
|
||||
loadCoreStats()
|
||||
loadTableData()
|
||||
updateCharts()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 表格导出处理
|
||||
const handleTableExport = async () => {
|
||||
try {
|
||||
const params = {
|
||||
type: statisticsType.value,
|
||||
startDate: dateRange.value[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange.value[1]?.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
await statisticsStore.exportTableData(params)
|
||||
message.success('导出成功')
|
||||
} catch (error) {
|
||||
message.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交报表表单
|
||||
const handleReportSubmit = async () => {
|
||||
try {
|
||||
await reportFormRef.value.validate()
|
||||
|
||||
await statisticsStore.generateReport(reportFormData)
|
||||
message.success('报表生成成功')
|
||||
reportModalVisible.value = false
|
||||
resetReportForm()
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
message.error('请检查表单输入')
|
||||
} else {
|
||||
message.error('生成报表失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消报表表单
|
||||
const handleReportCancel = () => {
|
||||
reportModalVisible.value = false
|
||||
resetReportForm()
|
||||
}
|
||||
|
||||
// 重置报表表单
|
||||
const resetReportForm = () => {
|
||||
Object.assign(reportFormData, {
|
||||
name: '',
|
||||
type: '',
|
||||
period: '',
|
||||
dateRange: null,
|
||||
content: [],
|
||||
format: 'pdf',
|
||||
remark: ''
|
||||
})
|
||||
reportFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 监听日期范围变化
|
||||
watch(dateRange, (newRange) => {
|
||||
if (newRange && newRange.length === 2) {
|
||||
handleDateRangeChange(newRange)
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
loadCoreStats()
|
||||
loadTableData()
|
||||
initCharts()
|
||||
updateCharts()
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
// 清理图表资源
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-statistics {
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
&.farms {
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
|
||||
&.animals {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
|
||||
&.inspections {
|
||||
border-left: 4px solid #faad14;
|
||||
}
|
||||
|
||||
&.compliance {
|
||||
border-left: 4px solid #722ed1;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
font-size: 12px;
|
||||
|
||||
.trend-text {
|
||||
margin-right: 4px;
|
||||
|
||||
&.positive {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-label {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.chart-card {
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
.ant-table-wrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1219
government-admin/src/views/traceability/TraceabilitySystem.vue
Normal file
1219
government-admin/src/views/traceability/TraceabilitySystem.vue
Normal file
File diff suppressed because it is too large
Load Diff
604
government-admin/src/views/users/UserList.vue
Normal file
604
government-admin/src/views/users/UserList.vue
Normal file
@@ -0,0 +1,604 @@
|
||||
<template>
|
||||
<div class="user-list">
|
||||
<PageHeader title="用户管理" />
|
||||
|
||||
<div class="content-wrapper">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>用户列表</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加用户
|
||||
</a-button>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-section">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="用户名">
|
||||
<a-input
|
||||
v-model:value="searchForm.username"
|
||||
placeholder="请输入用户名"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱">
|
||||
<a-input
|
||||
v-model:value="searchForm.email"
|
||||
placeholder="请输入邮箱"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="角色">
|
||||
<a-select
|
||||
v-model:value="searchForm.role"
|
||||
placeholder="请选择角色"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-select-option value="user">普通用户</a-select-option>
|
||||
<a-select-option value="viewer">查看者</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="active">启用</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset" style="margin-left: 8px">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :alt="record.username">
|
||||
{{ record.username.charAt(0).toUpperCase() }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'role'">
|
||||
<a-tag :color="getRoleColor(record.role)">
|
||||
{{ getRoleText(record.role) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-switch
|
||||
:checked="record.status === 'active'"
|
||||
@change="(checked) => handleStatusChange(record, checked)"
|
||||
:loading="record.statusLoading"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'lastLogin'">
|
||||
<span v-if="record.lastLogin">{{ formatDate(record.lastLogin) }}</span>
|
||||
<span v-else class="text-muted">从未登录</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleResetPassword(record)">
|
||||
重置密码
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个用户吗?"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="600px"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="formData.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="真实姓名" name="realName">
|
||||
<a-input v-model:value="formData.realName" placeholder="请输入真实姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="角色" name="role">
|
||||
<a-select v-model:value="formData.role" placeholder="请选择角色">
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-select-option value="user">普通用户</a-select-option>
|
||||
<a-select-option value="viewer">查看者</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formData.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">启用</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item v-if="!formData.id" label="密码" name="password">
|
||||
<a-input-password v-model:value="formData.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="部门" name="department">
|
||||
<a-input v-model:value="formData.department" placeholder="请输入部门" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
role: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
username: '',
|
||||
email: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
password: '',
|
||||
department: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
realName: [
|
||||
{ required: true, message: '请输入真实姓名', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '头像',
|
||||
dataIndex: 'avatar',
|
||||
key: 'avatar',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '真实姓名',
|
||||
dataIndex: 'realName',
|
||||
key: 'realName',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '部门',
|
||||
dataIndex: 'department',
|
||||
key: 'department',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
dataIndex: 'lastLogin',
|
||||
key: 'lastLogin',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取角色颜色
|
||||
const getRoleColor = (role) => {
|
||||
const colorMap = {
|
||||
admin: 'red',
|
||||
user: 'blue',
|
||||
viewer: 'green'
|
||||
}
|
||||
return colorMap[role] || 'default'
|
||||
}
|
||||
|
||||
// 获取角色文本
|
||||
const getRoleText = (role) => {
|
||||
const textMap = {
|
||||
admin: '管理员',
|
||||
user: '普通用户',
|
||||
viewer: '查看者'
|
||||
}
|
||||
return textMap[role] || '未知'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
realName: '系统管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
role: 'admin',
|
||||
department: '技术部',
|
||||
status: 'active',
|
||||
lastLogin: '2024-01-15 10:30:00',
|
||||
createTime: '2024-01-01 09:00:00',
|
||||
avatar: '',
|
||||
remark: '系统管理员账户',
|
||||
statusLoading: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user001',
|
||||
realName: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
phone: '13800138001',
|
||||
role: 'user',
|
||||
department: '业务部',
|
||||
status: 'active',
|
||||
lastLogin: '2024-01-14 16:20:00',
|
||||
createTime: '2024-01-02 14:30:00',
|
||||
avatar: '',
|
||||
remark: '普通用户',
|
||||
statusLoading: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'viewer001',
|
||||
realName: '李四',
|
||||
email: 'lisi@example.com',
|
||||
phone: '13800138002',
|
||||
role: 'viewer',
|
||||
department: '运营部',
|
||||
status: 'inactive',
|
||||
lastLogin: null,
|
||||
createTime: '2024-01-03 11:15:00',
|
||||
avatar: '',
|
||||
remark: '查看者账户',
|
||||
statusLoading: false
|
||||
}
|
||||
]
|
||||
|
||||
dataSource.value = mockData
|
||||
pagination.total = mockData.length
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
username: '',
|
||||
email: '',
|
||||
role: undefined,
|
||||
status: undefined
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '添加用户'
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
username: '',
|
||||
email: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
password: '',
|
||||
department: '',
|
||||
remark: ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record) => {
|
||||
modalTitle.value = '编辑用户'
|
||||
Object.assign(formData, { ...record, password: '' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = (record) => {
|
||||
message.info(`查看用户: ${record.username}`)
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
const handleStatusChange = async (record, checked) => {
|
||||
record.statusLoading = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
record.status = checked ? 'active' : 'inactive'
|
||||
message.success(`用户状态已${checked ? '启用' : '禁用'}`)
|
||||
} catch (error) {
|
||||
message.error('状态更新失败')
|
||||
} finally {
|
||||
record.statusLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = async (record) => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
message.success(`用户 ${record.username} 的密码已重置`)
|
||||
} catch (error) {
|
||||
message.error('密码重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (record) => {
|
||||
try {
|
||||
// 模拟删除API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 弹窗确认
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
// 模拟保存API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
message.success(formData.id ? '更新成功' : '添加成功')
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗取消
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-list {
|
||||
.content-wrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
government-admin/vite.config.js
Normal file
60
government-admin/vite.config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
import { webcrypto } from 'crypto'
|
||||
|
||||
globalThis.crypto = webcrypto
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 加载环境变量
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
plugins: [vue({
|
||||
style: {
|
||||
postcss: {}, // 仅使用PostCSS处理CSS
|
||||
css: {
|
||||
postcss: {},
|
||||
modules: false
|
||||
}
|
||||
}
|
||||
})],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5400,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api/auth': {
|
||||
target: env.VITE_API_FULL_URL || 'http://localhost:5352',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
},
|
||||
'/api': {
|
||||
target: env.VITE_API_FULL_URL || 'http://localhost:5352',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
entryFileNames: 'assets/[name]-[hash].js'
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
// 将环境变量注入到应用中
|
||||
__APP_ENV__: JSON.stringify(env)
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user