修改保险后端代码,政府前端代码
This commit is contained in:
@@ -1,71 +1,47 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<a-config-provider :locale="zhCN">
|
||||
<router-view />
|
||||
</div>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(async () => {
|
||||
// 应用初始化时检查用户登录状态
|
||||
const isLoggedIn = await authStore.checkAuthStatus()
|
||||
// 检查登录状态
|
||||
onMounted(() => {
|
||||
const token = userStore.token
|
||||
|
||||
// 如果用户已登录,初始化权限
|
||||
if (isLoggedIn && authStore.userInfo) {
|
||||
// 初始化用户权限
|
||||
await permissionStore.initPermissions(authStore.userInfo)
|
||||
|
||||
// 获取菜单列表
|
||||
await permissionStore.fetchMenuList()
|
||||
// 如果没有token且当前不是登录页,则跳转到登录页
|
||||
if (!token && router.currentRoute.value.path !== '/login') {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 如果有token且当前是登录页,则跳转到首页
|
||||
if (token && router.currentRoute.value.path === '/login') {
|
||||
router.push('/')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
// 全局样式重置
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
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 }
|
||||
})
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
/**
|
||||
* 政府业务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
|
||||
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 217 B |
7
government-admin/src/assets/logo.svg
Normal file
7
government-admin/src/assets/logo.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
<path d="M2 12h20" />
|
||||
<path d="M6 8v8" />
|
||||
<path d="M18 8v8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 394 B |
362
government-admin/src/components/Layout.vue
Normal file
362
government-admin/src/components/Layout.vue
Normal file
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<a-layout style="min-height: 100vh">
|
||||
<!-- 侧边栏 -->
|
||||
<a-layout-sider v-model:collapsed="collapsed" collapsible>
|
||||
<div class="logo">
|
||||
<h2 v-if="!collapsed">政府管理系统</h2>
|
||||
<h2 v-else>政府</h2>
|
||||
</div>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
:items="menus"
|
||||
@click="handleMenuClick"
|
||||
/>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<a-layout>
|
||||
<!-- 头部 -->
|
||||
<a-layout-header style="background: #fff; padding: 0 16px; display: flex; justify-content: space-between; align-items: center">
|
||||
<div>
|
||||
<menu-unfold-outlined
|
||||
v-if="collapsed"
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
/>
|
||||
<menu-fold-outlined
|
||||
v-else
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<a-dropdown>
|
||||
<a class="ant-dropdown-link" @click.prevent>
|
||||
<user-outlined />
|
||||
{{ userStore.userInfo.real_name || '管理员' }}
|
||||
<down-outlined />
|
||||
</a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="profile">
|
||||
<user-outlined />
|
||||
个人中心
|
||||
</a-menu-item>
|
||||
<a-menu-item key="logout" @click="handleLogout">
|
||||
<logout-outlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<a-layout-content style="margin: 16px">
|
||||
<div :style="{ padding: '24px', background: '#fff', minHeight: '360px' }">
|
||||
<router-view />
|
||||
</div>
|
||||
</a-layout-content>
|
||||
|
||||
<!-- 底部 -->
|
||||
<a-layout-footer style="text-align: center">
|
||||
政府端后台管理系统 ©2024
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, h } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
UserOutlined,
|
||||
DownOutlined,
|
||||
LogoutOutlined,
|
||||
DashboardOutlined,
|
||||
UserAddOutlined,
|
||||
EyeOutlined,
|
||||
CheckCircleOutlined,
|
||||
LineChartOutlined,
|
||||
FileOutlined,
|
||||
TeamOutlined,
|
||||
SettingOutlined,
|
||||
MedicineBoxOutlined,
|
||||
ShoppingOutlined,
|
||||
FolderOutlined,
|
||||
BarChartOutlined,
|
||||
PieChartOutlined,
|
||||
ShoppingCartOutlined,
|
||||
FileTextOutlined,
|
||||
DatabaseOutlined,
|
||||
HomeOutlined,
|
||||
ShopOutlined,
|
||||
MessageOutlined,
|
||||
BookOutlined,
|
||||
VideoCameraOutlined,
|
||||
EnvironmentOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const collapsed = ref(false)
|
||||
const selectedKeys = ref([route.name])
|
||||
const menus = ref([])
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
DashboardOutlined: () => h(DashboardOutlined),
|
||||
UserAddOutlined: () => h(UserAddOutlined),
|
||||
EyeOutlined: () => h(EyeOutlined),
|
||||
CheckCircleOutlined: () => h(CheckCircleOutlined),
|
||||
LineChartOutlined: () => h(LineChartOutlined),
|
||||
FileOutlined: () => h(FileOutlined),
|
||||
TeamOutlined: () => h(TeamOutlined),
|
||||
SettingOutlined: () => h(SettingOutlined),
|
||||
MedicineBoxOutlined: () => h(MedicineBoxOutlined),
|
||||
ShoppingOutlined: () => h(ShoppingOutlined),
|
||||
FolderOutlined: () => h(FolderOutlined),
|
||||
BarChartOutlined: () => h(BarChartOutlined),
|
||||
PieChartOutlined: () => h(PieChartOutlined),
|
||||
ShoppingCartOutlined: () => h(ShoppingCartOutlined),
|
||||
FileTextOutlined: () => h(FileTextOutlined),
|
||||
DatabaseOutlined: () => h(DatabaseOutlined),
|
||||
HomeOutlined: () => h(HomeOutlined),
|
||||
ShopOutlined: () => h(ShopOutlined),
|
||||
MessageOutlined: () => h(MessageOutlined),
|
||||
BookOutlined: () => h(BookOutlined),
|
||||
VideoCameraOutlined: () => h(VideoCameraOutlined),
|
||||
ShopOutlined: () => h(ShopOutlined),
|
||||
EnvironmentOutlined: () => h(EnvironmentOutlined)
|
||||
};
|
||||
|
||||
// 格式化菜单数据
|
||||
const formatMenuItems = (menuList) => {
|
||||
return menuList.map(menu => {
|
||||
const menuItem = {
|
||||
key: menu.key,
|
||||
label: menu.label,
|
||||
path: menu.path
|
||||
};
|
||||
|
||||
// 添加图标
|
||||
if (menu.icon && iconMap[menu.icon]) {
|
||||
menuItem.icon = iconMap[menu.icon];
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menuItem.children = formatMenuItems(menu.children);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取菜单数据
|
||||
const fetchMenus = async () => {
|
||||
try {
|
||||
// 这里可以根据实际情况从API获取菜单数据
|
||||
// 由于没有实际的API,这里提供默认菜单作为备用
|
||||
menus.value = [
|
||||
{
|
||||
key: 'DataCenter',
|
||||
icon: 'DatabaseOutlined',
|
||||
label: '数据览仓',
|
||||
path: '/index/data_center'
|
||||
},
|
||||
{
|
||||
key: 'MarketPrice',
|
||||
icon: 'BarChartOutlined',
|
||||
label: '市场行情',
|
||||
path: '/price/price_list'
|
||||
},
|
||||
{
|
||||
key: 'PersonnelManagement',
|
||||
icon: 'TeamOutlined',
|
||||
label: '人员管理',
|
||||
path: '/personnel'
|
||||
},
|
||||
{
|
||||
key: 'FarmerManagement',
|
||||
icon: 'UserAddOutlined',
|
||||
label: '养殖户管理',
|
||||
path: '/farmer'
|
||||
},
|
||||
{
|
||||
key: 'SmartWarehouse',
|
||||
icon: 'FolderOutlined',
|
||||
label: '智能仓库',
|
||||
path: '/smart-warehouse'
|
||||
},
|
||||
{
|
||||
key: 'BreedImprovement',
|
||||
icon: 'SettingOutlined',
|
||||
label: '品种改良管理',
|
||||
path: '/breed-improvement'
|
||||
},
|
||||
{
|
||||
key: 'PaperlessService',
|
||||
icon: 'FileTextOutlined',
|
||||
label: '无纸化服务',
|
||||
path: '/paperless'
|
||||
},
|
||||
{
|
||||
key: 'SlaughterHarmless',
|
||||
icon: 'EnvironmentOutlined',
|
||||
label: '屠宰无害化',
|
||||
path: '/slaughter'
|
||||
},
|
||||
{
|
||||
key: 'FinanceInsurance',
|
||||
icon: 'ShoppingOutlined',
|
||||
label: '金融保险',
|
||||
path: '/finance'
|
||||
},
|
||||
{
|
||||
key: 'ProductCertification',
|
||||
icon: 'CheckCircleOutlined',
|
||||
label: '生资认证',
|
||||
path: '/examine/index'
|
||||
},
|
||||
{
|
||||
key: 'ProductTrade',
|
||||
icon: 'ShoppingCartOutlined',
|
||||
label: '生资交易',
|
||||
path: '/shengzijiaoyi'
|
||||
},
|
||||
{
|
||||
key: 'CommunicationCommunity',
|
||||
icon: 'MessageOutlined',
|
||||
label: '交流社区',
|
||||
path: '/community'
|
||||
},
|
||||
{
|
||||
key: 'OnlineConsultation',
|
||||
icon: 'EyeOutlined',
|
||||
label: '线上问诊',
|
||||
path: '/consultation'
|
||||
},
|
||||
{
|
||||
key: 'CattleAcademy',
|
||||
icon: 'BookOutlined',
|
||||
label: '养牛学院',
|
||||
path: '/academy'
|
||||
},
|
||||
{
|
||||
key: 'MessageNotification',
|
||||
icon: 'VideoCameraOutlined',
|
||||
label: '消息通知',
|
||||
path: '/notification'
|
||||
},
|
||||
{
|
||||
key: 'UserManagement',
|
||||
icon: 'UserAddOutlined',
|
||||
label: '用户管理',
|
||||
path: '/users'
|
||||
},
|
||||
{
|
||||
key: 'WarehouseManagement',
|
||||
icon: 'MedicineBoxOutlined',
|
||||
label: '仓库管理',
|
||||
path: '/warehouse'
|
||||
},
|
||||
{
|
||||
key: 'FileManagement',
|
||||
icon: 'FolderOutlined',
|
||||
label: '文件管理',
|
||||
path: '/files'
|
||||
},
|
||||
{
|
||||
key: 'ServiceManagement',
|
||||
icon: 'SettingOutlined',
|
||||
label: '服务管理',
|
||||
path: '/service'
|
||||
},
|
||||
{
|
||||
key: 'ApprovalProcess',
|
||||
icon: 'CheckCircleOutlined',
|
||||
label: '审批流程',
|
||||
path: '/approval'
|
||||
},
|
||||
{
|
||||
key: 'EpidemicManagement',
|
||||
icon: 'LineChartOutlined',
|
||||
label: '防疫管理',
|
||||
path: '/epidemic'
|
||||
},
|
||||
{
|
||||
key: 'SupervisionDashboard',
|
||||
icon: 'EyeOutlined',
|
||||
label: '监管大屏',
|
||||
path: '/supervision'
|
||||
},
|
||||
{
|
||||
key: 'VisualAnalysis',
|
||||
icon: 'PieChartOutlined',
|
||||
label: '数据分析',
|
||||
path: '/visualization'
|
||||
},
|
||||
{
|
||||
key: 'LogManagement',
|
||||
icon: 'FileOutlined',
|
||||
label: '日志管理',
|
||||
path: '/log'
|
||||
}
|
||||
];
|
||||
|
||||
// 应用图标映射
|
||||
menus.value = formatMenuItems(menus.value);
|
||||
} catch (error) {
|
||||
console.error('获取菜单失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 菜单点击处理
|
||||
const handleMenuClick = (e) => {
|
||||
const menuItem = menus.value.find(item => item.key === e.key);
|
||||
if (menuItem && menuItem.path) {
|
||||
router.push(menuItem.path);
|
||||
}
|
||||
};
|
||||
|
||||
// 退出登录处理
|
||||
const handleLogout = () => {
|
||||
userStore.logout();
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
// 组件挂载时获取菜单
|
||||
onMounted(() => {
|
||||
fetchMenus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 32px;
|
||||
margin: 16px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
line-height: 64px;
|
||||
padding: 0 24px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.trigger:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
@@ -1,76 +0,0 @@
|
||||
<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>
|
||||
@@ -1,89 +0,0 @@
|
||||
<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>
|
||||
@@ -1,196 +0,0 @@
|
||||
<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>
|
||||
@@ -1,205 +0,0 @@
|
||||
<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>
|
||||
@@ -1,200 +0,0 @@
|
||||
<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>
|
||||
@@ -1,185 +0,0 @@
|
||||
<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>
|
||||
@@ -1,179 +0,0 @@
|
||||
<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>
|
||||
@@ -1,22 +0,0 @@
|
||||
// 图表组件统一导出
|
||||
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
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
<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>
|
||||
@@ -1,80 +0,0 @@
|
||||
<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>
|
||||
@@ -1,83 +0,0 @@
|
||||
<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>
|
||||
@@ -1,106 +0,0 @@
|
||||
<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>
|
||||
@@ -1,210 +0,0 @@
|
||||
<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>
|
||||
@@ -1,551 +0,0 @@
|
||||
<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>
|
||||
@@ -1,312 +0,0 @@
|
||||
<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>
|
||||
@@ -1,271 +0,0 @@
|
||||
<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>
|
||||
103
government-admin/src/components/modal/SupervisionEntityModal.vue
Normal file
103
government-admin/src/components/modal/SupervisionEntityModal.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="title"
|
||||
width={600}
|
||||
@cancel="handleClose"
|
||||
centered
|
||||
>
|
||||
<supervision-entity-form
|
||||
ref="formRef"
|
||||
:entity-id="entityId"
|
||||
:entity-types="entityTypes"
|
||||
@success="handleFormSuccess"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="js" setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import SupervisionEntityForm from '@/views/supervision-entities/Form.vue'
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
entityId: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
entityTypes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create'
|
||||
}
|
||||
})
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits([
|
||||
'cancel',
|
||||
'submit'
|
||||
])
|
||||
|
||||
// 表单组件引用
|
||||
const formRef = ref<any>(null)
|
||||
|
||||
// 计算弹窗标题
|
||||
const title = computed(() => {
|
||||
return props.mode === 'create' ? '新增监管实体' : '编辑监管实体'
|
||||
})
|
||||
|
||||
// 监听 visible 变化,当弹窗显示时重置表单
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
if (newVisible && formRef.value) {
|
||||
// 短暂延迟确保表单组件已更新 entityId
|
||||
setTimeout(() => {
|
||||
// 如果是创建模式,重置表单
|
||||
if (props.mode === 'create' && !props.entityId) {
|
||||
formRef.value.resetForm()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 处理关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理表单成功提交
|
||||
const handleFormSuccess = () => {
|
||||
message.success(props.mode === 'create' ? '创建成功' : '更新成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// 处理取消
|
||||
const handleCancel = () => {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({
|
||||
resetForm: () => {
|
||||
if (formRef.value) {
|
||||
formRef.value.resetForm()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 弹窗样式可以根据需要调整 */
|
||||
</style>
|
||||
@@ -1,899 +0,0 @@
|
||||
<template>
|
||||
<div class="government-layout">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="layout-header">
|
||||
<div class="header-left">
|
||||
<div class="logo">
|
||||
<img src="/favicon.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="authStore.userInfo.avatar" :size="32">
|
||||
{{ authStore.userInfo.name?.charAt(0) }}
|
||||
</a-avatar>
|
||||
<span class="user-name">{{ authStore.userName }}</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-container">
|
||||
<!-- 侧边栏 -->
|
||||
<aside :class="['layout-sidebar', { 'collapsed': siderCollapsed }]">
|
||||
<div class="sider-trigger" @click="toggleSider">
|
||||
<MenuUnfoldOutlined v-if="siderCollapsed" />
|
||||
<MenuFoldOutlined v-else />
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedSiderKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
:inline-collapsed="siderCollapsed"
|
||||
:items="siderMenuItems"
|
||||
@click="handleSiderMenuClick"
|
||||
/>
|
||||
</div>
|
||||
</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,
|
||||
DashboardOutlined,
|
||||
EyeOutlined,
|
||||
AuditOutlined,
|
||||
TeamOutlined,
|
||||
InboxOutlined,
|
||||
SafetyOutlined,
|
||||
CustomerServiceOutlined,
|
||||
BarChartOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import TabsView from '@/components/layout/TabsView.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// Store
|
||||
const appStore = useAppStore()
|
||||
const tabsStore = useTabsStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const authStore = useAuthStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 响应式数据
|
||||
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: () => h(DashboardOutlined),
|
||||
label: '工作台',
|
||||
permission: 'dashboard:view'
|
||||
},
|
||||
{
|
||||
key: '/supervision',
|
||||
icon: () => h(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: () => h(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: () => h(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: () => h(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: () => h(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: () => h(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: () => h(BarChartOutlined),
|
||||
label: '可视化大屏',
|
||||
permission: 'visualization:view'
|
||||
},
|
||||
{
|
||||
key: '/system',
|
||||
icon: () => h(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 authStore.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 => {
|
||||
// 检查当前菜单项权限,使用utils中的hasPermission函数
|
||||
// 该函数已实现管理员角色拥有所有权限的逻辑
|
||||
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(async () => {
|
||||
// 从store恢复状态
|
||||
siderCollapsed.value = appStore.sidebarCollapsed
|
||||
|
||||
// 初始化通知
|
||||
try {
|
||||
await notificationStore.fetchNotifications()
|
||||
} catch (error) {
|
||||
console.error('获取通知失败:', error)
|
||||
}
|
||||
|
||||
// 设置当前选中的菜单
|
||||
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;
|
||||
|
||||
:deep(.ant-menu) {
|
||||
border: none;
|
||||
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-title {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-item-selected {
|
||||
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;
|
||||
|
||||
.breadcrumb-container {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:deep(.ant-breadcrumb) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.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>
|
||||
389
government-admin/src/layout/Header.vue
Normal file
389
government-admin/src/layout/Header.vue
Normal file
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="header-right">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-container" v-if="showSearch">
|
||||
<a-input
|
||||
placeholder="搜索..."
|
||||
allowClear
|
||||
@change="handleSearch"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 通知 -->
|
||||
<div class="header-item" v-if="showNotifications">
|
||||
<a-popover
|
||||
v-model:open="notificationOpen"
|
||||
placement="bottomRight"
|
||||
trigger="click"
|
||||
:mouseEnterDelay="0.1"
|
||||
:mouseLeaveDelay="0.3"
|
||||
@openChange="handleNotificationOpenChange"
|
||||
>
|
||||
<template #content>
|
||||
<div class="notification-panel" v-if="notificationOpen">
|
||||
<div class="notification-header">
|
||||
<span>通知中心</span>
|
||||
<a-button type="link" size="small" @click="markAllAsRead">全部已读</a-button>
|
||||
</div>
|
||||
<div class="notification-list">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="notification-item"
|
||||
:class="{ 'unread': !notification.read }"
|
||||
@click="handleNotificationClick(notification)"
|
||||
>
|
||||
<div class="notification-icon">
|
||||
<a-icon :type="notification.type === 'error' ? 'exclamation-circle' : 'bell'" />
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<div class="notification-title">{{ notification.title }}</div>
|
||||
<div class="notification-message">{{ notification.message }}</div>
|
||||
<div class="notification-time">{{ formatTime(notification.time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-empty" v-if="notifications.length === 0">
|
||||
暂无通知
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-footer">
|
||||
<a-button type="link" size="small" @click="viewAllNotifications">查看全部</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-badge count="5" :offset="[-4, 4]">
|
||||
<a-icon type="bell" class="header-icon" />
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="header-item">
|
||||
<a-dropdown
|
||||
v-model:open="userMenuOpen"
|
||||
placement="bottomRight"
|
||||
trigger="click"
|
||||
:mouseEnterDelay="0.1"
|
||||
:mouseLeaveDelay="0.3"
|
||||
>
|
||||
<div class="user-info" @click="e => e.preventDefault()">
|
||||
<a-avatar class="user-avatar" :src="userInfo.avatar" :icon="userInfo.name ? undefined : UserOutlined" />
|
||||
<span class="user-name" v-if="userInfo.name">{{ userInfo.name }}</span>
|
||||
<a-icon type="down" class="user-arrow" />
|
||||
</div>
|
||||
<template #content>
|
||||
<a-menu>
|
||||
<a-menu-item key="profile" @click="viewProfile">
|
||||
<a-icon type="user" />
|
||||
<span>个人信息</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings" @click="openSettings">
|
||||
<a-icon type="setting" />
|
||||
<span>个人设置</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="help" @click="openHelp">
|
||||
<a-icon type="question-circle" />
|
||||
<span>帮助中心</span>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout" danger @click="handleLogout">
|
||||
<a-icon type="logout" />
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { UserOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 显示搜索框
|
||||
const showSearch = ref(true)
|
||||
|
||||
// 显示通知
|
||||
const showNotifications = ref(true)
|
||||
|
||||
// 通知面板状态
|
||||
const notificationOpen = ref(false)
|
||||
|
||||
// 用户菜单状态
|
||||
const userMenuOpen = ref(false)
|
||||
|
||||
// 用户信息
|
||||
const userInfo = computed(() => {
|
||||
return authStore.userInfo || { name: '', avatar: '' }
|
||||
})
|
||||
|
||||
// 模拟通知数据
|
||||
const notifications = ref([
|
||||
{
|
||||
id: '1',
|
||||
title: '新的审批请求',
|
||||
message: '有一个新的养殖场备案申请等待您的审批',
|
||||
time: Date.now() - 3600000,
|
||||
type: 'info',
|
||||
read: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '检查计划提醒',
|
||||
message: '您有一项检查计划将于明天开始,请做好准备',
|
||||
time: Date.now() - 7200000,
|
||||
type: 'info',
|
||||
read: false
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '系统更新提醒',
|
||||
message: '系统将于今晚23:00-次日01:00进行维护更新',
|
||||
time: Date.now() - 86400000,
|
||||
type: 'info',
|
||||
read: true
|
||||
}
|
||||
])
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (createTime) => {
|
||||
const now = Date.now()
|
||||
const diff = now - time
|
||||
|
||||
if (diff < 60000) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600000) {
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
} else if (diff < 86400000) {
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
} else if (diff < 2592000000) {
|
||||
return `${Math.floor(diff / 86400000)}天前`
|
||||
} else {
|
||||
return new Date(time).toLocaleDateString('zh-CN')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (e) => {
|
||||
const value = e.target.value
|
||||
if (value) {
|
||||
// 这里可以实现搜索逻辑
|
||||
message.info(`搜索: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理通知面板开关
|
||||
const handleNotificationOpenChange = (open) => {
|
||||
notificationOpen.value = open
|
||||
if (open) {
|
||||
// 这里可以加载最新通知
|
||||
loadNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载通知
|
||||
const loadNotifications = async () => {
|
||||
// 实际项目中这里应该调用API获取通知列表
|
||||
// 模拟API请求
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
}
|
||||
|
||||
// 处理通知点击
|
||||
const handleNotificationClick = (notification) => {
|
||||
// 标记为已读
|
||||
notification.read = true
|
||||
// 跳转到相关页面
|
||||
if (notification.title.includes('审批')) {
|
||||
router.push('/approval/pending')
|
||||
} else if (notification.title.includes('检查')) {
|
||||
router.push('/inspection/list')
|
||||
}
|
||||
notificationOpen.value = false
|
||||
}
|
||||
|
||||
// 标记所有通知为已读
|
||||
const markAllAsRead = async () => {
|
||||
// 实际项目中这里应该调用API
|
||||
notifications.value.forEach(n => n.read = true)
|
||||
message.success('已将所有通知标记为已读')
|
||||
}
|
||||
|
||||
// 查看所有通知
|
||||
const viewAllNotifications = () => {
|
||||
router.push('/notification/list')
|
||||
notificationOpen.value = false
|
||||
}
|
||||
|
||||
// 查看个人信息
|
||||
const viewProfile = () => {
|
||||
router.push('/user/profile')
|
||||
userMenuOpen.value = false
|
||||
}
|
||||
|
||||
// 打开设置
|
||||
const openSettings = () => {
|
||||
router.push('/user/settings')
|
||||
userMenuOpen.value = false
|
||||
}
|
||||
|
||||
// 打开帮助中心
|
||||
const openHelp = () => {
|
||||
router.push('/help')
|
||||
userMenuOpen.value = false
|
||||
}
|
||||
|
||||
// 处理退出登录
|
||||
const handleLogout = () => {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
userMenuOpen.value = false
|
||||
message.success('已成功退出登录')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-item {
|
||||
margin-left: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.header-icon:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin: 0 8px;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.user-arrow {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.notification-panel {
|
||||
width: 400px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
margin-right: 12px;
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin-bottom: 4px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 11px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.notification-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.notification-footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
91
government-admin/src/layout/MainContent.vue
Normal file
91
government-admin/src/layout/MainContent.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<!-- 面包屑导航 -->
|
||||
<Breadcrumb v-if="showBreadcrumb" />
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<PageHeader v-if="showPageHeader" />
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="content-wrapper">
|
||||
<!-- 内容区域使用 router-view 来渲染路由匹配的组件 -->
|
||||
<router-view />
|
||||
</div>
|
||||
|
||||
<!-- 返回顶部按钮 -->
|
||||
<BackToTop v-if="showBackToTop" />
|
||||
|
||||
<!-- 全局加载状态 -->
|
||||
<GlobalLoading v-if="isGlobalLoading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Breadcrumb from './Breadcrumb.vue'
|
||||
import PageHeader from './PageHeader.vue'
|
||||
import BackToTop from './BackToTop.vue'
|
||||
import GlobalLoading from './GlobalLoading.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 显示面包屑
|
||||
const showBreadcrumb = ref(true)
|
||||
|
||||
// 显示页面标题
|
||||
const showPageHeader = ref(true)
|
||||
|
||||
// 显示返回顶部按钮
|
||||
const showBackToTop = ref(true)
|
||||
|
||||
// 全局加载状态
|
||||
const isGlobalLoading = ref(false)
|
||||
|
||||
// 监听路由变化,处理页面加载状态
|
||||
const handleRouteChange = () => {
|
||||
// 页面切换时显示加载状态
|
||||
isGlobalLoading.value = true
|
||||
|
||||
// 模拟页面加载完成
|
||||
setTimeout(() => {
|
||||
isGlobalLoading.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 页面滚动处理
|
||||
const handleScroll = () => {
|
||||
// 可以在这里处理滚动相关的逻辑
|
||||
}
|
||||
|
||||
// 组件挂载时添加事件监听
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
handleRouteChange()
|
||||
})
|
||||
|
||||
// 组件卸载时移除事件监听
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-content {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.content-wrapper {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
214
government-admin/src/layout/PageHeader.vue
Normal file
214
government-admin/src/layout/PageHeader.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<a-page-header
|
||||
v-if="pageHeaderConfig.title"
|
||||
:title="pageHeaderConfig.title"
|
||||
:subtitle="pageHeaderConfig.subtitle"
|
||||
:ghost="pageHeaderConfig.ghost"
|
||||
class="page-header"
|
||||
>
|
||||
<template #extra>
|
||||
<slot name="extra"></slot>
|
||||
<div v-if="hasDefaultActions" class="header-actions">
|
||||
<a-button
|
||||
v-if="showBackButton"
|
||||
type="default"
|
||||
@click="handleBack"
|
||||
icon="back"
|
||||
>
|
||||
返回
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="showRefreshButton"
|
||||
type="default"
|
||||
@click="handleRefresh"
|
||||
icon="reload"
|
||||
>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="showExportButton"
|
||||
type="default"
|
||||
@click="handleExport"
|
||||
icon="download"
|
||||
>
|
||||
导出
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="showAddButton && hasPermission('create')"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
icon="plus"
|
||||
>
|
||||
新增
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #tags>
|
||||
<slot name="tags"></slot>
|
||||
<a-tag
|
||||
v-for="tag in pageHeaderConfig.tags"
|
||||
:key="tag.key"
|
||||
:color="tag.color || 'blue'"
|
||||
:closable="tag.closable || false"
|
||||
@close="handleTagClose(tag.key)"
|
||||
>
|
||||
{{ tag.text }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<slot name="avatar"></slot>
|
||||
<a-avatar v-if="pageHeaderConfig.avatar" :src="pageHeaderConfig.avatar"></a-avatar>
|
||||
</template>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
<div v-if="pageHeaderConfig.footer" class="header-footer">
|
||||
{{ pageHeaderConfig.footer }}
|
||||
</div>
|
||||
</template>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 组件属性
|
||||
const props = defineProps({
|
||||
showBackButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showRefreshButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showExportButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showAddButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// 页面头部配置
|
||||
const pageHeaderConfig = computed(() => {
|
||||
// 从路由元数据中获取页面标题配置
|
||||
const meta = route.meta
|
||||
|
||||
return {
|
||||
title: meta.title || '',
|
||||
subtitle: meta.subtitle || '',
|
||||
ghost: meta.ghost || false,
|
||||
tags: meta.tags || [],
|
||||
avatar: meta.avatar || '',
|
||||
footer: meta.footer || ''
|
||||
}
|
||||
})
|
||||
|
||||
// 是否有默认操作按钮
|
||||
const hasDefaultActions = computed(() => {
|
||||
return props.showBackButton || props.showRefreshButton || props.showExportButton || props.showAddButton
|
||||
})
|
||||
|
||||
// 检查是否有权限
|
||||
const hasPermission = (permission) => {
|
||||
return authStore.hasPermission([permission])
|
||||
}
|
||||
|
||||
// 处理返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = () => {
|
||||
// 触发父组件的刷新事件
|
||||
const event = new CustomEvent('refresh-page')
|
||||
window.dispatchEvent(event)
|
||||
message.success('页面已刷新')
|
||||
}
|
||||
|
||||
// 处理导出
|
||||
const handleExport = () => {
|
||||
// 触发父组件的导出事件
|
||||
const event = new CustomEvent('export-data')
|
||||
window.dispatchEvent(event)
|
||||
message.info('开始导出数据...')
|
||||
}
|
||||
|
||||
// 处理新增
|
||||
const handleAdd = () => {
|
||||
// 获取新增路由
|
||||
const addRoute = getAddRoute()
|
||||
if (addRoute) {
|
||||
router.push(addRoute)
|
||||
} else {
|
||||
// 触发父组件的新增事件
|
||||
const event = new CustomEvent('add-data')
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签关闭
|
||||
const handleTagClose = (key) => {
|
||||
// 触发父组件的标签关闭事件
|
||||
const event = new CustomEvent('tag-close', { detail: { key } })
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
// 获取新增路由
|
||||
const getAddRoute = () => {
|
||||
const currentPath = route.path
|
||||
const routeMap = {
|
||||
'/supervision/list': '/supervision/add',
|
||||
'/inspection/list': '/inspection/add',
|
||||
'/violation/list': '/violation/add',
|
||||
'/epidemic/list': '/epidemic/add',
|
||||
'/approval/list': '/approval/add',
|
||||
'/personnel/list': '/personnel/add'
|
||||
}
|
||||
|
||||
return routeMap[currentPath] || ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-actions .ant-btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
290
government-admin/src/layout/Sidebar.vue
Normal file
290
government-admin/src/layout/Sidebar.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
:root-sub-menu-open-close="false"
|
||||
@select="handleMenuSelect"
|
||||
@openChange="handleOpenChange"
|
||||
class="sidebar-menu"
|
||||
>
|
||||
<!-- 首页 -->
|
||||
<a-menu-item key="/" :icon="DashboardOutlined">
|
||||
<span>首页</span>
|
||||
</a-menu-item>
|
||||
|
||||
<!-- 监管实体管理 -->
|
||||
<a-sub-menu
|
||||
v-if="hasPermission('supervision')"
|
||||
key="supervision"
|
||||
:icon="FileOutlined"
|
||||
>
|
||||
<template #title>
|
||||
<span>监管实体管理</span>
|
||||
</template>
|
||||
<a-menu-item key="/supervision/list">
|
||||
<span>实体列表</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/supervision/add">
|
||||
<span>新增实体</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/supervision/stats">
|
||||
<span>统计分析</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 检查记录 -->
|
||||
<a-sub-menu
|
||||
v-if="hasPermission('inspection')"
|
||||
key="inspection"
|
||||
:icon="CheckCircleOutlined"
|
||||
>
|
||||
<template #title>
|
||||
<span>检查记录</span>
|
||||
</template>
|
||||
<a-menu-item key="/inspection/list">
|
||||
<span>记录列表</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/inspection/add">
|
||||
<span>新增记录</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/inspection/stats">
|
||||
<span>统计分析</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 违规处理 -->
|
||||
<a-sub-menu
|
||||
v-if="hasPermission('violation')"
|
||||
key="violation"
|
||||
:icon="ExclamationCircleOutlined"
|
||||
>
|
||||
<template #title>
|
||||
<span>违规处理</span>
|
||||
</template>
|
||||
<a-menu-item key="/violation/list">
|
||||
<span>违规记录</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/violation/add">
|
||||
<span>新增违规</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/violation/process">
|
||||
<span>处理流程</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 防疫管理 -->
|
||||
<a-sub-menu
|
||||
v-if="hasPermission('epidemic')"
|
||||
key="epidemic"
|
||||
:icon="AlertOutlined"
|
||||
>
|
||||
<template #title>
|
||||
<span>防疫管理</span>
|
||||
</template>
|
||||
<a-menu-item key="/epidemic/list">
|
||||
<span>防疫记录</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/epidemic/add">
|
||||
<span>新增记录</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/epidemic/stats">
|
||||
<span>疫情统计</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 审批管理 -->
|
||||
<a-sub-menu
|
||||
v-if="hasPermission('approval')"
|
||||
key="approval"
|
||||
:icon="FormOutlined"
|
||||
>
|
||||
<template #title>
|
||||
<span>审批管理</span>
|
||||
</template>
|
||||
<a-menu-item key="/approval/list">
|
||||
<span>审批列表</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/approval/pending">
|
||||
<span>待我审批</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/approval/history">
|
||||
<span>审批历史</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 人员管理 -->
|
||||
<a-sub-menu
|
||||
v-if="hasPermission('personnel')"
|
||||
key="personnel"
|
||||
:icon="UserOutlined"
|
||||
>
|
||||
<template #title>
|
||||
<span>人员管理</span>
|
||||
</template>
|
||||
<a-menu-item key="/personnel/list">
|
||||
<span>人员列表</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/personnel/add">
|
||||
<span>新增人员</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/personnel/role">
|
||||
<span>角色管理</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 系统设置 -->
|
||||
<a-sub-menu
|
||||
v-if="hasPermission('system')"
|
||||
key="system"
|
||||
:icon="SettingOutlined"
|
||||
>
|
||||
<template #title>
|
||||
<span>系统设置</span>
|
||||
</template>
|
||||
<a-menu-item key="/system/config">
|
||||
<span>系统配置</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/system/logs">
|
||||
<span>系统日志</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/system/backup">
|
||||
<span>数据备份</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 帮助中心 -->
|
||||
<a-menu-item key="/help" :icon="QuestionCircleOutlined">
|
||||
<span>帮助中心</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
FileOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
AlertOutlined,
|
||||
FormOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 当前选中的菜单项
|
||||
const selectedKeys = ref([])
|
||||
|
||||
// 展开的菜单项
|
||||
const openKeys = ref([])
|
||||
|
||||
// 检查是否有权限
|
||||
const hasPermission = (permission) => {
|
||||
return authStore.hasPermission([permission])
|
||||
}
|
||||
|
||||
// 处理菜单选择
|
||||
const handleMenuSelect = ({ key }) => {
|
||||
router.push(key)
|
||||
}
|
||||
|
||||
// 处理展开/收起
|
||||
const handleOpenChange = (keys) => {
|
||||
openKeys.value = keys
|
||||
}
|
||||
|
||||
// 获取父级菜单key
|
||||
const getParentMenuKey = (path) => {
|
||||
const menuMap= {
|
||||
'/supervision': 'supervision',
|
||||
'/inspection': 'inspection',
|
||||
'/violation': 'violation',
|
||||
'/epidemic': 'epidemic',
|
||||
'/approval': 'approval',
|
||||
'/personnel': 'personnel',
|
||||
'/system': 'system'
|
||||
}
|
||||
|
||||
for (const [prefix, key] of Object.entries(menuMap)) {
|
||||
if (path.startsWith(prefix)) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
const updateSelectedState = () => {
|
||||
const currentPath = route.path
|
||||
selectedKeys.value = [currentPath]
|
||||
|
||||
const parentKey = getParentMenuKey(currentPath)
|
||||
if (parentKey) {
|
||||
openKeys.value = [parentKey]
|
||||
} else {
|
||||
openKeys.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
updateSelectedState()
|
||||
}
|
||||
)
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
updateSelectedState()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-menu {
|
||||
height: 100%;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
/* 自定义菜单项样式 */
|
||||
:deep(.ant-menu-item) {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item:hover) {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item-selected) {
|
||||
background-color: #1890ff !important;
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
:deep(.ant-menu-submenu-title) {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-submenu-title:hover) {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
:deep(.anticon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 菜单项文本样式 */
|
||||
:deep(.ant-menu-item .ant-menu-title-content),
|
||||
:deep(.ant-menu-submenu-title .ant-menu-title-content) {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,388 +0,0 @@
|
||||
<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>
|
||||
@@ -1,20 +1,29 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './stores'
|
||||
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'
|
||||
import dayjs from 'dayjs'
|
||||
import './mock' // 导入mock服务
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
|
||||
// 配置 dayjs
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(duration)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 为 Ant Design Vue 配置日期库
|
||||
globalThis.dayjs = dayjs
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(Antd)
|
||||
app.use(router)
|
||||
|
||||
// 注册权限指令
|
||||
app.directive('permission', permissionDirective)
|
||||
app.use(store)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
340
government-admin/src/mock/index.js
Normal file
340
government-admin/src/mock/index.js
Normal file
@@ -0,0 +1,340 @@
|
||||
// 模拟数据服务,用于在开发环境中提供数据
|
||||
|
||||
// 模拟的后端延迟
|
||||
const mockDelay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
// 模拟用户数据
|
||||
const mockUsers = [
|
||||
{ id: 1, username: 'admin', real_name: '管理员', phone: '13800138000', email: 'admin@example.com', role: 'admin', status: 1, created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 2, username: 'user1', real_name: '用户一', phone: '13800138001', email: 'user1@example.com', role: 'user', status: 1, created_at: '2024-01-02 11:00:00' },
|
||||
{ id: 3, username: 'user2', real_name: '用户二', phone: '13800138002', email: 'user2@example.com', role: 'user', status: 0, created_at: '2024-01-03 12:00:00' },
|
||||
{ id: 4, username: 'user3', real_name: '用户三', phone: '13800138003', email: 'user3@example.com', role: 'guest', status: 1, created_at: '2024-01-04 13:00:00' },
|
||||
{ id: 5, username: 'user4', real_name: '用户四', phone: '13800138004', email: 'user4@example.com', role: 'user', status: 1, created_at: '2024-01-05 14:00:00' }
|
||||
]
|
||||
|
||||
// 模拟监管统计数据
|
||||
const mockSupervisionStats = {
|
||||
entityCount: 150,
|
||||
inspectionCount: 78
|
||||
}
|
||||
|
||||
// 模拟疫情统计数据
|
||||
const mockEpidemicStats = {
|
||||
vaccinated: 12500,
|
||||
tested: 89000
|
||||
}
|
||||
|
||||
// 模拟可视化数据
|
||||
const mockVisualizationData = {
|
||||
charts: [
|
||||
{
|
||||
id: 1,
|
||||
title: '监管趋势图',
|
||||
type: 'line',
|
||||
data: {
|
||||
xAxis: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
series: [
|
||||
{
|
||||
name: '检查次数',
|
||||
data: [12, 19, 3, 5, 2, 3]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '数据分布图',
|
||||
type: 'pie',
|
||||
data: {
|
||||
series: [
|
||||
{
|
||||
name: '数据分布',
|
||||
data: [
|
||||
{ value: 30, name: '类型A' },
|
||||
{ value: 25, name: '类型B' },
|
||||
{ value: 20, name: '类型C' },
|
||||
{ value: 15, name: '类型D' },
|
||||
{ value: 10, name: '其他' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
indicators: [
|
||||
{ name: '总实体数', value: 150, unit: '个' },
|
||||
{ name: '检查次数', value: 78, unit: '次' },
|
||||
{ name: '疫苗接种数', value: 12500, unit: '人' },
|
||||
{ name: '检测人数', value: 89000, unit: '人' }
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟审批流程数据
|
||||
const mockApprovals = [
|
||||
{ id: 1, name: '企业注册审批', status: 'pending', create_time: '2024-01-10 09:00:00' },
|
||||
{ id: 2, name: '资质认证申请', status: 'approved', create_time: '2024-01-09 14:30:00' },
|
||||
{ id: 3, name: '数据变更申请', status: 'rejected', create_time: '2024-01-08 11:20:00' },
|
||||
{ id: 4, name: '权限申请', status: 'pending', create_time: '2024-01-07 16:40:00' },
|
||||
{ id: 5, name: '系统配置变更', status: 'approved', create_time: '2024-01-06 10:15:00' }
|
||||
]
|
||||
|
||||
// 模拟登录验证
|
||||
export const login = async (username, password) => {
|
||||
await mockDelay()
|
||||
|
||||
// 简单的模拟验证
|
||||
if (username === 'admin' && password === 'admin123') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
token: 'mock-jwt-token-' + Date.now(),
|
||||
userInfo: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
real_name: '管理员',
|
||||
phone: '13800138000',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin'
|
||||
},
|
||||
permissions: ['admin', 'user', 'guest']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟普通用户登录
|
||||
if (username === 'user' && password === 'user123') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
token: 'mock-jwt-token-' + Date.now(),
|
||||
userInfo: {
|
||||
id: 2,
|
||||
username: 'user',
|
||||
real_name: '普通用户',
|
||||
phone: '13800138001',
|
||||
email: 'user@example.com',
|
||||
role: 'user'
|
||||
},
|
||||
permissions: ['user']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 400,
|
||||
message: '用户名或密码错误'
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟获取用户信息
|
||||
export const getUserInfo = async () => {
|
||||
await mockDelay()
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
real_name: '管理员',
|
||||
phone: '13800138000',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin',
|
||||
permissions: ['admin', 'user', 'guest']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟获取用户列表
|
||||
export const getUsers = async (page = 1, pageSize = 10, keyword = '') => {
|
||||
await mockDelay()
|
||||
|
||||
// 模拟搜索
|
||||
let filteredUsers = [...mockUsers]
|
||||
if (keyword) {
|
||||
filteredUsers = mockUsers.filter(user =>
|
||||
user.username.includes(keyword) ||
|
||||
user.real_name.includes(keyword) ||
|
||||
user.id.toString().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 模拟分页
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const paginatedUsers = filteredUsers.slice(start, end)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
list: paginatedUsers,
|
||||
total: filteredUsers.length,
|
||||
page,
|
||||
pageSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟添加用户
|
||||
export const createUser = async (userData) => {
|
||||
await mockDelay()
|
||||
|
||||
const newUser = {
|
||||
id: mockUsers.length + 1,
|
||||
...userData,
|
||||
created_at: new Date().toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
mockUsers.push(newUser)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '用户创建成功',
|
||||
data: newUser
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟更新用户
|
||||
export const updateUser = async (id, userData) => {
|
||||
await mockDelay()
|
||||
|
||||
const index = mockUsers.findIndex(user => user.id === id)
|
||||
if (index === -1) {
|
||||
return {
|
||||
code: 404,
|
||||
message: '用户不存在'
|
||||
}
|
||||
}
|
||||
|
||||
mockUsers[index] = { ...mockUsers[index], ...userData }
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '用户更新成功',
|
||||
data: mockUsers[index]
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟删除用户
|
||||
export const deleteUser = async (id) => {
|
||||
await mockDelay()
|
||||
|
||||
const index = mockUsers.findIndex(user => user.id === id)
|
||||
if (index === -1) {
|
||||
return {
|
||||
code: 404,
|
||||
message: '用户不存在'
|
||||
}
|
||||
}
|
||||
|
||||
mockUsers.splice(index, 1)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '用户删除成功'
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟获取监管统计数据
|
||||
export const getSupervisionStats = async () => {
|
||||
await mockDelay()
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: mockSupervisionStats
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟获取疫情统计数据
|
||||
export const getEpidemicStats = async () => {
|
||||
await mockDelay()
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: mockEpidemicStats
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟获取可视化数据
|
||||
export const getVisualizationData = async () => {
|
||||
await mockDelay()
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: mockVisualizationData
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟获取审批列表
|
||||
export const getApprovals = async (page = 1, pageSize = 10, keyword = '') => {
|
||||
await mockDelay()
|
||||
|
||||
// 模拟搜索
|
||||
let filteredApprovals = [...mockApprovals]
|
||||
if (keyword) {
|
||||
filteredApprovals = mockApprovals.filter(approval =>
|
||||
approval.name.includes(keyword) ||
|
||||
approval.status.includes(keyword) ||
|
||||
approval.id.toString().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 模拟分页
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const paginatedApprovals = filteredApprovals.slice(start, end)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
list: paginatedApprovals,
|
||||
total: filteredApprovals.length,
|
||||
page,
|
||||
pageSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟创建审批流程
|
||||
export const createApproval = async (approvalData) => {
|
||||
await mockDelay()
|
||||
|
||||
const newApproval = {
|
||||
id: mockApprovals.length + 1,
|
||||
...approvalData,
|
||||
status: 'pending',
|
||||
create_time: new Date().toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
mockApprovals.push(newApproval)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '审批流程创建成功',
|
||||
data: newApproval
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟更新审批状态
|
||||
export const updateApprovalStatus = async (id, status) => {
|
||||
await mockDelay()
|
||||
|
||||
const index = mockApprovals.findIndex(approval => approval.id === id)
|
||||
if (index === -1) {
|
||||
return {
|
||||
code: 404,
|
||||
message: '审批流程不存在'
|
||||
}
|
||||
}
|
||||
|
||||
mockApprovals[index].status = status
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '审批状态更新成功',
|
||||
data: mockApprovals[index]
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
/**
|
||||
* 路由守卫配置
|
||||
*/
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
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 authStore = useAuthStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 获取用户token
|
||||
const token = authStore.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 (!authStore.userInfo || !authStore.userInfo.id) {
|
||||
try {
|
||||
// 获取用户信息
|
||||
await authStore.fetchUserInfo()
|
||||
// 初始化权限
|
||||
await permissionStore.initPermissions(authStore.userInfo)
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
message.error('获取用户信息失败,请重新登录')
|
||||
authStore.logout()
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为公共路由
|
||||
if (publicRoutes.includes(to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查路由权限,使用utils中的checkRoutePermission函数
|
||||
if (!checkRoutePermission(to)) {
|
||||
message.error('您没有访问该页面的权限')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查动态路由是否已生成
|
||||
if (!permissionStore.routesGenerated) {
|
||||
try {
|
||||
// 生成动态路由
|
||||
const accessRoutes = await permissionStore.generateRoutes(authStore.userInfo)
|
||||
|
||||
// 动态添加路由
|
||||
accessRoutes.forEach(route => {
|
||||
// Note: router is not available in this scope,
|
||||
// this would need to be handled differently in a real implementation
|
||||
console.log('Adding route:', 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 authStore = useAuthStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (!authStore.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 authStore = useAuthStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
if (!authStore.token) {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
|
||||
if (role && !permissionStore.hasRole(role)) {
|
||||
message.error('角色权限不足')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员权限验证
|
||||
*/
|
||||
export function requireAdmin(to, from, next) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (!authStore.token) {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
|
||||
const userRole = authStore.userInfo?.role
|
||||
if (!['super_admin', 'admin'].includes(userRole)) {
|
||||
message.error('需要管理员权限')
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* 超级管理员权限验证
|
||||
*/
|
||||
export function requireSuperAdmin(to, from, next) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (!authStore.token) {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
|
||||
if (authStore.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()
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,36 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { beforeEach, afterEach, onError } from './guards'
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
import Layout from '@/components/Layout.vue'
|
||||
import Login from '@/views/Login.vue'
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import UserManagement from '@/views/UserManagement.vue'
|
||||
import SupervisionDashboard from '@/views/SupervisionDashboard.vue'
|
||||
import ApprovalProcess from '@/views/ApprovalProcess.vue'
|
||||
import EpidemicManagement from '@/views/EpidemicManagement.vue'
|
||||
import VisualAnalysis from '@/views/VisualAnalysis.vue'
|
||||
import FileManagement from '@/views/FileManagement.vue'
|
||||
import PersonnelManagement from '@/views/PersonnelManagement.vue'
|
||||
import ServiceManagement from '@/views/ServiceManagement.vue'
|
||||
import WarehouseManagement from '@/views/WarehouseManagement.vue'
|
||||
import LogManagement from '@/views/LogManagement.vue'
|
||||
// 新增页面组件
|
||||
import MarketPrice from '@/views/MarketPrice.vue'
|
||||
import DataCenter from '@/views/DataCenter.vue'
|
||||
import FarmerManagement from '@/views/FarmerManagement.vue'
|
||||
import SmartWarehouse from '@/views/SmartWarehouse.vue'
|
||||
import BreedImprovement from '@/views/BreedImprovement.vue'
|
||||
import PaperlessService from '@/views/PaperlessService.vue'
|
||||
import SlaughterHarmless from '@/views/SlaughterHarmless.vue'
|
||||
import FinanceInsurance from '@/views/FinanceInsurance.vue'
|
||||
import CommunicationCommunity from '@/views/CommunicationCommunity.vue'
|
||||
import OnlineConsultation from '@/views/OnlineConsultation.vue'
|
||||
import CattleAcademy from '@/views/CattleAcademy.vue'
|
||||
import MessageNotification from '@/views/MessageNotification.vue'
|
||||
|
||||
// 配置 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
|
||||
}
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
@@ -55,444 +40,182 @@ const routes = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/Dashboard.vue'),
|
||||
meta: {
|
||||
title: '仪表盘',
|
||||
icon: 'dashboard',
|
||||
affix: true,
|
||||
permission: 'dashboard:view'
|
||||
}
|
||||
component: Dashboard,
|
||||
meta: { title: '仪表板' }
|
||||
},
|
||||
{
|
||||
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: 'index/data_center',
|
||||
name: 'DataCenter',
|
||||
component: DataCenter,
|
||||
meta: { title: '数据览仓' }
|
||||
},
|
||||
{
|
||||
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: 'price/price_list',
|
||||
name: 'MarketPrice',
|
||||
component: MarketPrice,
|
||||
meta: { title: '市场行情' }
|
||||
},
|
||||
{
|
||||
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: 'personnel',
|
||||
name: 'PersonnelManagement',
|
||||
component: PersonnelManagement,
|
||||
meta: { title: '人员管理' }
|
||||
},
|
||||
{
|
||||
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: 'farmer',
|
||||
name: 'FarmerManagement',
|
||||
component: FarmerManagement,
|
||||
meta: { title: '养殖户管理' }
|
||||
},
|
||||
{
|
||||
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: 'smart-warehouse',
|
||||
name: 'SmartWarehouse',
|
||||
component: SmartWarehouse,
|
||||
meta: { title: '智能仓库' }
|
||||
},
|
||||
{
|
||||
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: 'breed-improvement',
|
||||
name: 'BreedImprovement',
|
||||
component: BreedImprovement,
|
||||
meta: { title: '品种改良管理' }
|
||||
},
|
||||
{
|
||||
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: 'paperless',
|
||||
name: 'PaperlessService',
|
||||
component: PaperlessService,
|
||||
meta: { title: '无纸化服务' }
|
||||
},
|
||||
{
|
||||
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: 'slaughter',
|
||||
name: 'SlaughterHarmless',
|
||||
component: SlaughterHarmless,
|
||||
meta: { title: '屠宰无害化' }
|
||||
},
|
||||
{
|
||||
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: 'finance',
|
||||
name: 'FinanceInsurance',
|
||||
component: FinanceInsurance,
|
||||
meta: { title: '金融保险' }
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('@/views/monitor/MonitorDashboard.vue'),
|
||||
meta: {
|
||||
title: '实时监控',
|
||||
icon: 'eye',
|
||||
requiresAuth: true,
|
||||
permission: 'monitor:view'
|
||||
}
|
||||
path: 'examine/index',
|
||||
name: 'ProductCertification',
|
||||
component: ApprovalProcess,
|
||||
meta: { title: '生资认证' }
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
name: 'Reports',
|
||||
component: () => import('@/views/reports/ReportList.vue'),
|
||||
meta: {
|
||||
title: '报表管理',
|
||||
icon: 'file-text',
|
||||
requiresAuth: true,
|
||||
permission: 'monitor:report'
|
||||
}
|
||||
path: 'shengzijiaoyi',
|
||||
name: 'ProductTrade',
|
||||
component: ServiceManagement,
|
||||
meta: { title: '生资交易' }
|
||||
},
|
||||
{
|
||||
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: 'community',
|
||||
name: 'CommunicationCommunity',
|
||||
component: CommunicationCommunity,
|
||||
meta: { title: '交流社区' }
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: () => import('@/views/users/UserList.vue'),
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'user',
|
||||
requiresAuth: true,
|
||||
permission: 'user:view'
|
||||
}
|
||||
path: 'consultation',
|
||||
name: 'OnlineConsultation',
|
||||
component: OnlineConsultation,
|
||||
meta: { title: '线上问诊' }
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/settings/SystemSettings.vue'),
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
icon: 'setting',
|
||||
requiresAuth: true,
|
||||
permission: 'system:config'
|
||||
}
|
||||
path: 'academy',
|
||||
name: 'CattleAcademy',
|
||||
component: CattleAcademy,
|
||||
meta: { title: '养牛学院' }
|
||||
},
|
||||
{
|
||||
path: 'notification',
|
||||
name: 'MessageNotification',
|
||||
component: MessageNotification,
|
||||
meta: { title: '消息通知' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: UserManagement,
|
||||
meta: { title: '用户管理' }
|
||||
},
|
||||
{
|
||||
path: 'supervision',
|
||||
name: 'SupervisionDashboard',
|
||||
component: SupervisionDashboard,
|
||||
meta: { title: '监管仪表板' }
|
||||
},
|
||||
{
|
||||
path: 'approval',
|
||||
name: 'ApprovalProcess',
|
||||
component: ApprovalProcess,
|
||||
meta: { title: '审批流程' }
|
||||
},
|
||||
{
|
||||
path: 'file',
|
||||
name: 'FileManagement',
|
||||
component: FileManagement,
|
||||
meta: { title: '文件管理' }
|
||||
},
|
||||
{
|
||||
path: 'service',
|
||||
name: 'ServiceManagement',
|
||||
component: ServiceManagement,
|
||||
meta: { title: '服务管理' }
|
||||
},
|
||||
{
|
||||
path: 'warehouse',
|
||||
name: 'WarehouseManagement',
|
||||
component: WarehouseManagement,
|
||||
meta: { title: '仓库管理' }
|
||||
},
|
||||
{
|
||||
path: 'log',
|
||||
name: 'LogManagement',
|
||||
component: LogManagement,
|
||||
meta: { title: '日志管理' }
|
||||
},
|
||||
{
|
||||
path: 'epidemic',
|
||||
name: 'EpidemicManagement',
|
||||
component: EpidemicManagement,
|
||||
meta: { title: '疫情管理' }
|
||||
},
|
||||
{
|
||||
path: 'visualization',
|
||||
name: 'VisualAnalysis',
|
||||
component: VisualAnalysis,
|
||||
meta: { title: '可视化分析' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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 }
|
||||
}
|
||||
}
|
||||
routes
|
||||
})
|
||||
|
||||
// 注册路由守卫
|
||||
router.beforeEach(beforeEach)
|
||||
router.afterEach(afterEach)
|
||||
router.onError(onError)
|
||||
// 路由拦截器 - 检查登录状态
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title ? `${to.meta.title} - 政府端管理系统` : '政府端管理系统'
|
||||
|
||||
// 不需要登录的页面直接通过
|
||||
if (to.path === '/login') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
// 未登录,重定向到登录页面
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,51 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// 状态
|
||||
const sidebarCollapsed = ref(false)
|
||||
const theme = ref('light')
|
||||
const language = ref('zh-CN')
|
||||
const loading = ref(false)
|
||||
|
||||
// 方法
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
const setSidebarCollapsed = (collapsed) => {
|
||||
sidebarCollapsed.value = collapsed
|
||||
}
|
||||
|
||||
const setSiderCollapsed = (collapsed) => {
|
||||
sidebarCollapsed.value = collapsed
|
||||
}
|
||||
|
||||
const setTheme = (newTheme) => {
|
||||
theme.value = newTheme
|
||||
}
|
||||
|
||||
const setLanguage = (newLanguage) => {
|
||||
language.value = newLanguage
|
||||
}
|
||||
|
||||
const setLoading = (isLoading) => {
|
||||
loading.value = isLoading
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
sidebarCollapsed,
|
||||
theme,
|
||||
language,
|
||||
loading,
|
||||
|
||||
// 方法
|
||||
toggleSidebar,
|
||||
setSidebarCollapsed,
|
||||
setSiderCollapsed,
|
||||
setTheme,
|
||||
setLanguage,
|
||||
setLoading
|
||||
}
|
||||
})
|
||||
@@ -1,312 +1,109 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { request } from '@/utils/api'
|
||||
import router from '@/router'
|
||||
import { getRolePermissions } from '@/utils/permission'
|
||||
|
||||
// 配置常量
|
||||
const TOKEN_KEY = 'token'
|
||||
const USER_KEY = 'userInfo'
|
||||
const PERMISSIONS_KEY = 'permissions'
|
||||
const REFRESH_TOKEN_KEY = 'refresh_token'
|
||||
// 认证状态管理
|
||||
// 管理用户的登录、登出和认证信息
|
||||
// 提供认证相关的API调用和状态管理
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const token = ref(localStorage.getItem(TOKEN_KEY) || '')
|
||||
const refreshToken = ref(localStorage.getItem(REFRESH_TOKEN_KEY) || '')
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem(USER_KEY) || 'null'))
|
||||
const permissions = ref(JSON.parse(localStorage.getItem(PERMISSIONS_KEY) || '[]'))
|
||||
const isRefreshing = ref(false)
|
||||
const refreshCallbacks = ref([])
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => !!token.value && !!userInfo.value)
|
||||
const userName = computed(() => userInfo.value?.name || '')
|
||||
const userRole = computed(() => userInfo.value?.role || 'viewer') // 默认返回最低权限角色
|
||||
const avatar = computed(() => userInfo.value?.avatar || '')
|
||||
const isSuperAdmin = computed(() => userRole.value === 'super_admin')
|
||||
const isAdmin = computed(() => ['super_admin', 'admin'].includes(userRole.value))
|
||||
|
||||
// 方法
|
||||
// 用户认证令牌
|
||||
const token = ref(localStorage.getItem('token'))
|
||||
// 用户信息
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
|
||||
// 用户权限列表
|
||||
const permissions = ref(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||||
|
||||
// 设置用户令牌
|
||||
const setToken = (newToken) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
const setUserInfo = (info) => {
|
||||
userInfo.value = info
|
||||
localStorage.setItem('userInfo', JSON.stringify(info))
|
||||
}
|
||||
|
||||
// 设置用户权限
|
||||
const setPermissions = (perms) => {
|
||||
permissions.value = perms
|
||||
localStorage.setItem('permissions', JSON.stringify(perms))
|
||||
}
|
||||
|
||||
// 登录方法
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
const response = await request.post('/auth/login', credentials)
|
||||
const { code, message: msg, data } = response.data
|
||||
// 在实际应用中,这里应该调用后端API进行登录验证
|
||||
// 现在使用模拟数据模拟登录成功
|
||||
|
||||
if (code === 200 && data) {
|
||||
const { token: newToken, refreshToken: newRefreshToken } = data
|
||||
|
||||
// 保存认证信息
|
||||
token.value = newToken
|
||||
refreshToken.value = newRefreshToken
|
||||
|
||||
// 获取并保存用户信息
|
||||
await fetchUserInfo()
|
||||
|
||||
message.success('登录成功')
|
||||
return { success: true, user: userInfo.value }
|
||||
} else {
|
||||
message.error(msg || '登录失败')
|
||||
return { success: false, message: msg || '登录失败' }
|
||||
// 模拟API调用延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 模拟登录成功数据
|
||||
const mockToken = 'mock-jwt-token-' + Date.now()
|
||||
const mockUserInfo = {
|
||||
id: '1',
|
||||
username: credentials.username,
|
||||
name: '管理员',
|
||||
avatar: '',
|
||||
role: 'admin',
|
||||
department: '信息管理处'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录请求失败:', error)
|
||||
const errorMsg = error.response?.data?.message || '登录失败'
|
||||
message.error(errorMsg)
|
||||
return { success: false, message: errorMsg }
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await request.post('/auth/logout')
|
||||
} catch (error) {
|
||||
console.error('退出登录请求失败:', error)
|
||||
} finally {
|
||||
// 清除认证信息
|
||||
token.value = ''
|
||||
refreshToken.value = ''
|
||||
userInfo.value = null
|
||||
permissions.value = []
|
||||
const mockPermissions = ['view', 'add', 'edit', 'delete', 'export']
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
localStorage.removeItem(PERMISSIONS_KEY)
|
||||
// 保存登录信息
|
||||
setToken(mockToken)
|
||||
setUserInfo(mockUserInfo)
|
||||
setPermissions(mockPermissions)
|
||||
|
||||
message.success('已退出登录')
|
||||
|
||||
// 跳转到登录页
|
||||
router.replace('/login')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查认证状态
|
||||
const checkAuthStatus = async () => {
|
||||
if (!token.value) return false
|
||||
|
||||
try {
|
||||
// 尝试验证token有效性
|
||||
const isValid = await validateToken()
|
||||
if (isValid) {
|
||||
// 如果用户信息不存在或已过期,重新获取
|
||||
if (!userInfo.value) {
|
||||
await fetchUserInfo()
|
||||
}
|
||||
return true
|
||||
// 如果勾选了记住我,保存更长时间
|
||||
if (credentials.remember) {
|
||||
// 在实际应用中,这里可以设置更长的过期时间
|
||||
// 这里简化处理
|
||||
}
|
||||
|
||||
// token无效,尝试刷新
|
||||
if (refreshToken.value) {
|
||||
const refreshed = await refreshAccessToken()
|
||||
if (refreshed) {
|
||||
await fetchUserInfo()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新失败,退出登录
|
||||
logout()
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('验证用户认证状态失败:', error)
|
||||
|
||||
// 开发环境中,如果已有本地存储的用户信息,则使用它
|
||||
if (import.meta.env.DEV && userInfo.value) {
|
||||
console.warn('开发环境:使用本地存储的用户信息')
|
||||
return true
|
||||
}
|
||||
|
||||
// 生产环境中认证失败,清除本地数据
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 验证token有效性
|
||||
const validateToken = async () => {
|
||||
try {
|
||||
const response = await request.get('/auth/validate')
|
||||
return response.data.code === 200
|
||||
} catch (error) {
|
||||
console.error('验证token失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新access token
|
||||
const refreshAccessToken = async () => {
|
||||
if (isRefreshing.value) {
|
||||
// 如果正在刷新中,将请求放入队列
|
||||
return new Promise((resolve) => {
|
||||
refreshCallbacks.value.push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
isRefreshing.value = true
|
||||
const response = await request.post('/auth/refresh', {
|
||||
refreshToken: refreshToken.value
|
||||
})
|
||||
|
||||
if (response.data.code === 200 && response.data.data) {
|
||||
const { token: newToken, refreshToken: newRefreshToken } = response.data.data
|
||||
|
||||
token.value = newToken
|
||||
refreshToken.value = newRefreshToken
|
||||
|
||||
// 持久化存储
|
||||
localStorage.setItem(TOKEN_KEY, newToken)
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken)
|
||||
|
||||
// 执行所有等待的请求回调
|
||||
refreshCallbacks.value.forEach((callback) => callback(true))
|
||||
refreshCallbacks.value = []
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('刷新token失败:', error)
|
||||
|
||||
// 执行所有等待的请求回调
|
||||
refreshCallbacks.value.forEach((callback) => callback(false))
|
||||
refreshCallbacks.value = []
|
||||
|
||||
return false
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const response = await request.get('/auth/userinfo')
|
||||
|
||||
if (response.data.code === 200 && response.data.data) {
|
||||
userInfo.value = response.data.data
|
||||
|
||||
// 获取并设置用户权限
|
||||
if (response.data.data.permissions) {
|
||||
permissions.value = response.data.data.permissions
|
||||
} else {
|
||||
// 如果后端没有返回权限,则根据角色设置默认权限
|
||||
permissions.value = getRolePermissions(userInfo.value.role)
|
||||
}
|
||||
|
||||
// 持久化存储
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(userInfo.value))
|
||||
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify(permissions.value))
|
||||
|
||||
return userInfo.value
|
||||
} else {
|
||||
throw new Error('获取用户信息失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const hasPermission = (permission) => {
|
||||
// 超级管理员和管理员拥有所有权限
|
||||
if (isAdmin.value) {
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
message.error(error.message || '登录失败,请重试')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!permission) return true
|
||||
|
||||
if (Array.isArray(permission)) {
|
||||
return permission.some(p => permissions.value.includes(p))
|
||||
}
|
||||
|
||||
return permissions.value.includes(permission)
|
||||
}
|
||||
|
||||
// 角色检查
|
||||
const hasRole = (roles) => {
|
||||
if (!roles) return true
|
||||
|
||||
if (Array.isArray(roles)) {
|
||||
return roles.includes(userRole.value)
|
||||
}
|
||||
|
||||
return userRole.value === roles
|
||||
|
||||
// 退出登录
|
||||
const logout = () => {
|
||||
token.value = null
|
||||
userInfo.value = {}
|
||||
permissions.value = []
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('permissions')
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 所有权限检查
|
||||
const hasAllPermissions = (permissionList) => {
|
||||
if (!permissionList || !Array.isArray(permissionList)) return true
|
||||
return permissionList.every(permission => hasPermission(permission))
|
||||
|
||||
// 检查用户是否有特定权限
|
||||
const hasPermission = (perm) => {
|
||||
return permissions.value.includes(perm)
|
||||
}
|
||||
|
||||
// 任一权限检查
|
||||
const hasAnyPermission = (permissionList) => {
|
||||
if (!permissionList || !Array.isArray(permissionList)) return true
|
||||
return permissionList.some(permission => hasPermission(permission))
|
||||
|
||||
// 检查用户是否已登录
|
||||
const isLoggedIn = () => {
|
||||
return !!token.value
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
const updateUserInfo = (newUserInfo) => {
|
||||
userInfo.value = { ...userInfo.value, ...newUserInfo }
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(userInfo.value))
|
||||
}
|
||||
|
||||
// 设置权限列表
|
||||
const setPermissions = (newPermissions) => {
|
||||
permissions.value = newPermissions || []
|
||||
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify(permissions.value))
|
||||
}
|
||||
|
||||
// 检查路由权限
|
||||
const checkRoutePermission = (route) => {
|
||||
// 检查路由元信息中的权限
|
||||
if (route.meta?.permission) {
|
||||
return hasPermission(route.meta.permission)
|
||||
}
|
||||
|
||||
// 检查路由元信息中的角色
|
||||
if (route.meta?.roles && route.meta.roles.length > 0) {
|
||||
return hasRole(route.meta.roles)
|
||||
}
|
||||
|
||||
// 默认允许访问
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
refreshToken,
|
||||
userInfo,
|
||||
permissions,
|
||||
|
||||
// 计算属性
|
||||
isAuthenticated,
|
||||
userName,
|
||||
userRole,
|
||||
avatar,
|
||||
isSuperAdmin,
|
||||
isAdmin,
|
||||
|
||||
// 方法
|
||||
setToken,
|
||||
setUserInfo,
|
||||
setPermissions,
|
||||
login,
|
||||
logout,
|
||||
checkAuthStatus,
|
||||
validateToken,
|
||||
refreshAccessToken,
|
||||
fetchUserInfo,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasAllPermissions,
|
||||
hasAnyPermission,
|
||||
updateUserInfo,
|
||||
setPermissions,
|
||||
checkRoutePermission
|
||||
isLoggedIn
|
||||
}
|
||||
})
|
||||
@@ -1,130 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useDashboardStore = defineStore('dashboard', () => {
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const dashboardData = ref({
|
||||
totalFarms: 0,
|
||||
totalAnimals: 0,
|
||||
totalDevices: 0,
|
||||
totalAlerts: 0,
|
||||
farmGrowth: 0,
|
||||
animalGrowth: 0,
|
||||
deviceGrowth: 0,
|
||||
alertGrowth: 0
|
||||
})
|
||||
|
||||
const chartData = ref({
|
||||
farmTrend: [],
|
||||
animalTrend: [],
|
||||
deviceStatus: [],
|
||||
alertDistribution: [],
|
||||
regionDistribution: []
|
||||
})
|
||||
|
||||
const timeRange = ref('month')
|
||||
|
||||
// 计算属性
|
||||
const isLoading = computed(() => loading.value)
|
||||
|
||||
// 方法
|
||||
const fetchDashboardData = async (range = 'month') => {
|
||||
try {
|
||||
loading.value = true
|
||||
timeRange.value = range
|
||||
|
||||
// 模拟数据获取
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟数据
|
||||
dashboardData.value = {
|
||||
totalFarms: 156,
|
||||
totalAnimals: 12847,
|
||||
totalDevices: 892,
|
||||
totalAlerts: 23,
|
||||
farmGrowth: 12.5,
|
||||
animalGrowth: 8.3,
|
||||
deviceGrowth: 15.2,
|
||||
alertGrowth: -5.1
|
||||
}
|
||||
|
||||
chartData.value = {
|
||||
farmTrend: [
|
||||
{ date: '2024-01', value: 120 },
|
||||
{ date: '2024-02', value: 125 },
|
||||
{ date: '2024-03', value: 130 },
|
||||
{ date: '2024-04', value: 135 },
|
||||
{ date: '2024-05', value: 140 },
|
||||
{ date: '2024-06', value: 145 },
|
||||
{ date: '2024-07', value: 150 },
|
||||
{ date: '2024-08', value: 156 }
|
||||
],
|
||||
animalTrend: [
|
||||
{ date: '2024-01', value: 10000 },
|
||||
{ date: '2024-02', value: 10500 },
|
||||
{ date: '2024-03', value: 11000 },
|
||||
{ date: '2024-04', value: 11500 },
|
||||
{ date: '2024-05', value: 12000 },
|
||||
{ date: '2024-06', value: 12200 },
|
||||
{ date: '2024-07', value: 12500 },
|
||||
{ date: '2024-08', value: 12847 }
|
||||
],
|
||||
deviceStatus: [
|
||||
{ name: '正常', value: 756, color: '#52c41a' },
|
||||
{ name: '离线', value: 89, color: '#ff4d4f' },
|
||||
{ name: '故障', value: 47, color: '#faad14' }
|
||||
],
|
||||
alertDistribution: [
|
||||
{ name: '温度异常', value: 8 },
|
||||
{ name: '湿度异常', value: 6 },
|
||||
{ name: '设备离线', value: 5 },
|
||||
{ name: '其他', value: 4 }
|
||||
],
|
||||
regionDistribution: [
|
||||
{ name: '银川市', value: 45 },
|
||||
{ name: '石嘴山市', value: 32 },
|
||||
{ name: '吴忠市', value: 28 },
|
||||
{ name: '固原市', value: 25 },
|
||||
{ name: '中卫市', value: 26 }
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取仪表盘数据失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
await fetchDashboardData(timeRange.value)
|
||||
}
|
||||
|
||||
const exportReport = async () => {
|
||||
try {
|
||||
// 模拟导出报表
|
||||
console.log('导出报表...')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('导出报表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
dashboardData,
|
||||
chartData,
|
||||
timeRange,
|
||||
|
||||
// 计算属性
|
||||
isLoading,
|
||||
|
||||
// 方法
|
||||
fetchDashboardData,
|
||||
refreshData,
|
||||
exportReport
|
||||
}
|
||||
})
|
||||
@@ -1,292 +0,0 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
@@ -1,522 +0,0 @@
|
||||
/**
|
||||
* 政府业务状态管理
|
||||
*/
|
||||
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']
|
||||
}
|
||||
})
|
||||
@@ -1,14 +1,3 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
export default pinia
|
||||
|
||||
// 导出所有store
|
||||
export { useAuthStore } from './auth'
|
||||
export { useAppStore } from './app'
|
||||
export { useTabsStore } from './tabs'
|
||||
export { useNotificationStore } from './notification'
|
||||
export { useGovernmentStore } from './government'
|
||||
export default createPinia()
|
||||
@@ -1,425 +0,0 @@
|
||||
/**
|
||||
* 通知状态管理
|
||||
*/
|
||||
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} options - 查询选项
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async fetchNotifications(options = {}) {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 返回当前通知列表(实际项目中应该从API获取)
|
||||
return this.notifications
|
||||
} catch (error) {
|
||||
console.error('获取通知失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加通知
|
||||
* @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']
|
||||
}
|
||||
})
|
||||
@@ -1,359 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/utils/api'
|
||||
import { getRolePermissions } from '@/utils/permission'
|
||||
|
||||
export const usePermissionStore = defineStore('permission', () => {
|
||||
// 状态
|
||||
const permissions = ref([])
|
||||
const roles = ref([])
|
||||
const userRole = ref('')
|
||||
const menuList = ref([])
|
||||
const loading = ref(false)
|
||||
const routesGenerated = 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 {
|
||||
// 模拟菜单数据
|
||||
const mockMenus = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
meta: { title: '工作台', icon: 'dashboard' }
|
||||
},
|
||||
{
|
||||
path: '/farm',
|
||||
name: 'FarmManagement',
|
||||
meta: { title: '养殖场管理', icon: 'farm' },
|
||||
children: [
|
||||
{ path: 'list', name: 'FarmList', meta: { title: '养殖场列表' }},
|
||||
{ path: 'monitor', name: 'FarmMonitor', meta: { title: '实时监控' }}
|
||||
]
|
||||
}
|
||||
]
|
||||
setMenuList(mockMenus)
|
||||
return mockMenus
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单列表失败,使用模拟数据:', error)
|
||||
const mockMenus = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
meta: { title: '工作台', icon: 'dashboard' }
|
||||
},
|
||||
{
|
||||
path: '/farm',
|
||||
name: 'FarmManagement',
|
||||
meta: { title: '养殖场管理', icon: 'farm' },
|
||||
children: [
|
||||
{ path: 'list', name: 'FarmList', meta: { title: '养殖场列表' }},
|
||||
{ path: 'monitor', name: 'FarmMonitor', meta: { title: '实时监控' }}
|
||||
]
|
||||
}
|
||||
]
|
||||
setMenuList(mockMenus)
|
||||
return mockMenus
|
||||
}
|
||||
}
|
||||
|
||||
// 根据权限过滤菜单
|
||||
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 generateRoutes = async (userInfo) => {
|
||||
try {
|
||||
// 这里应该根据用户权限生成动态路由
|
||||
// 暂时返回空数组,实际实现需要根据业务需求
|
||||
routesGenerated.value = true
|
||||
return []
|
||||
} catch (error) {
|
||||
console.error('生成动态路由失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化权限
|
||||
const initPermissions = async (userInfo) => {
|
||||
try {
|
||||
// 根据用户信息设置权限
|
||||
if (userInfo && userInfo.role) {
|
||||
setUserRole(userInfo.role)
|
||||
|
||||
// 根据角色设置权限(这里可以扩展为从后端获取)
|
||||
const rolePermissions = getRolePermissions(userInfo.role)
|
||||
setPermissions(rolePermissions)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化权限失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重置权限数据
|
||||
const resetPermissions = () => {
|
||||
permissions.value = []
|
||||
roles.value = []
|
||||
userRole.value = ''
|
||||
menuList.value = []
|
||||
routesGenerated.value = false
|
||||
}
|
||||
|
||||
// 权限常量定义
|
||||
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,
|
||||
routesGenerated,
|
||||
|
||||
// 计算属性
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
filteredMenus,
|
||||
|
||||
// 方法
|
||||
setPermissions,
|
||||
setRoles,
|
||||
setUserRole,
|
||||
setMenuList,
|
||||
fetchUserPermissions,
|
||||
fetchMenuList,
|
||||
filterMenusByPermission,
|
||||
checkRoutePermission,
|
||||
generateRoutes,
|
||||
initPermissions,
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
/**
|
||||
* 标签页状态管理
|
||||
*/
|
||||
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']
|
||||
}
|
||||
})
|
||||
@@ -1,97 +1,59 @@
|
||||
/**
|
||||
* 用户状态管理适配器
|
||||
* 注意:该文件是为了兼容旧代码而保留的适配器
|
||||
* 请使用新的 auth.js 代替此文件
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
// 用户状态管理
|
||||
// 管理用户的认证信息、权限和个人信息
|
||||
// 提供登录、登出和用户信息更新等功能
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 获取新的认证store实例
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 状态(通过代理到authStore)
|
||||
const token = authStore.token
|
||||
const userInfo = authStore.userInfo
|
||||
const permissions = authStore.permissions
|
||||
const roles = authStore.permissions // 兼容旧代码,将permissions也作为roles返回
|
||||
|
||||
// 计算属性(通过代理到authStore)
|
||||
const isLoggedIn = authStore.isAuthenticated
|
||||
const userName = authStore.userName
|
||||
const userRole = authStore.userRole
|
||||
|
||||
// 方法(通过代理到authStore)
|
||||
const checkLoginStatus = async () => {
|
||||
console.warn('useUserStore已废弃,请使用useAuthStore.checkAuthStatus')
|
||||
return authStore.checkAuthStatus()
|
||||
// 用户认证令牌
|
||||
const token = ref(localStorage.getItem('token'))
|
||||
// 用户信息
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
|
||||
// 用户权限列表
|
||||
const permissions = ref(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||||
|
||||
// 设置用户令牌
|
||||
const setToken = (newToken) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
const login = async (credentials) => {
|
||||
console.warn('useUserStore已废弃,请使用useAuthStore.login')
|
||||
const result = await authStore.login(credentials)
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: result.user
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: result.message
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
const setUserInfo = (info) => {
|
||||
userInfo.value = info
|
||||
localStorage.setItem('userInfo', JSON.stringify(info))
|
||||
}
|
||||
|
||||
|
||||
// 设置用户权限
|
||||
const setPermissions = (perms) => {
|
||||
permissions.value = perms
|
||||
localStorage.setItem('permissions', JSON.stringify(perms))
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const logout = () => {
|
||||
console.warn('useUserStore已废弃,请使用useAuthStore.logout')
|
||||
authStore.logout()
|
||||
token.value = null
|
||||
userInfo.value = {}
|
||||
permissions.value = []
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('permissions')
|
||||
}
|
||||
|
||||
const updateUserInfo = (newUserInfo) => {
|
||||
console.warn('useUserStore已废弃,请使用useAuthStore.updateUserInfo')
|
||||
authStore.updateUserInfo(newUserInfo)
|
||||
|
||||
// 检查用户是否有特定权限
|
||||
const hasPermission = (perm) => {
|
||||
return permissions.value.includes(perm)
|
||||
}
|
||||
|
||||
const getUserInfo = async () => {
|
||||
console.warn('useUserStore已废弃,请使用useAuthStore.fetchUserInfo')
|
||||
try {
|
||||
return await authStore.fetchUserInfo()
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const hasPermission = (permission) => {
|
||||
console.warn('useUserStore已废弃,请使用useAuthStore.hasPermission')
|
||||
return authStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
const hasRole = (role) => {
|
||||
console.warn('useUserStore已废弃,请使用useAuthStore.hasRole')
|
||||
return authStore.hasRole(role)
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
userInfo,
|
||||
permissions,
|
||||
roles,
|
||||
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
userName,
|
||||
userRole,
|
||||
|
||||
// 方法
|
||||
checkLoginStatus,
|
||||
login,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
setPermissions,
|
||||
logout,
|
||||
updateUserInfo,
|
||||
getUserInfo,
|
||||
hasPermission,
|
||||
hasRole
|
||||
hasPermission
|
||||
}
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
/* 全局基础样式 */
|
||||
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;
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
// 全局样式文件
|
||||
|
||||
// 变量定义
|
||||
: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;
|
||||
}
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import axios from 'axios'
|
||||
import { message } from 'ant-design-vue'
|
||||
import router from '@/router'
|
||||
import { message } from 'antd'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
const instance = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -12,122 +12,203 @@ const api = axios.create({
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
instance.interceptors.request.use(
|
||||
config => {
|
||||
// 获取用户store
|
||||
const userStore = useUserStore()
|
||||
// 如果有token,添加到请求头
|
||||
if (userStore.token) {
|
||||
config.headers['Authorization'] = `Bearer ${userStore.token}`
|
||||
}
|
||||
|
||||
// 添加时间戳防止缓存
|
||||
if (config.method === 'get') {
|
||||
config.params = {
|
||||
...config.params,
|
||||
_t: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
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
|
||||
instance.interceptors.response.use(
|
||||
response => {
|
||||
// 处理响应数据
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
const { response } = error
|
||||
|
||||
if (response) {
|
||||
const { status, data } = response
|
||||
|
||||
switch (status) {
|
||||
error => {
|
||||
// 处理响应错误
|
||||
if (error.response) {
|
||||
// 根据不同的状态码处理错误
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,清除token并跳转到登录页
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('permissions')
|
||||
router.push('/login')
|
||||
// 未授权,跳转到登录页面
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
window.location.href = '/login'
|
||||
message.error('登录已过期,请重新登录')
|
||||
break
|
||||
|
||||
case 403:
|
||||
message.error('没有权限访问该资源')
|
||||
message.error('没有权限执行此操作')
|
||||
break
|
||||
|
||||
case 404:
|
||||
message.error('请求的资源不存在')
|
||||
break
|
||||
|
||||
case 500:
|
||||
message.error('服务器内部错误')
|
||||
break
|
||||
|
||||
default:
|
||||
message.error(data?.message || `请求失败 (${status})`)
|
||||
message.error(error.response.data.message || '请求失败')
|
||||
}
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
message.error('请求超时,请稍后重试')
|
||||
} else {
|
||||
} else if (error.request) {
|
||||
// 请求发出但没有收到响应
|
||||
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'
|
||||
// API接口定义
|
||||
const api = {
|
||||
// 认证相关API
|
||||
auth: {
|
||||
// 登录
|
||||
login: (data) => instance.post('/auth/login', data),
|
||||
// 获取用户信息
|
||||
getUserInfo: () => instance.get('/auth/userinfo'),
|
||||
// 退出登录
|
||||
logout: () => instance.post('/auth/logout'),
|
||||
// 重置密码
|
||||
resetPassword: (data) => instance.post('/auth/reset-password', data)
|
||||
},
|
||||
|
||||
// 用户管理相关API
|
||||
user: {
|
||||
// 获取用户列表
|
||||
getList: (params) => instance.get('/users', { params }),
|
||||
// 获取单个用户信息
|
||||
getDetail: (id) => instance.get(`/users/${id}`),
|
||||
// 创建用户
|
||||
create: (data) => instance.post('/users', data),
|
||||
// 更新用户
|
||||
update: (id, data) => instance.put(`/users/${id}`, data),
|
||||
// 删除用户
|
||||
delete: (id) => instance.delete(`/users/${id}`),
|
||||
// 批量删除用户
|
||||
batchDelete: (ids) => instance.post('/users/batch-delete', { ids }),
|
||||
// 更新用户状态
|
||||
updateStatus: (id, status) => instance.put(`/users/${id}/status`, { status })
|
||||
},
|
||||
|
||||
// 监管相关API
|
||||
supervision: {
|
||||
// 获取监管统计数据
|
||||
getStats: () => instance.get('/supervision/stats'),
|
||||
// 获取监管任务列表
|
||||
getTasks: (params) => instance.get('/supervision/tasks', { params }),
|
||||
// 获取监管任务详情
|
||||
getTaskDetail: (id) => instance.get(`/supervision/tasks/${id}`),
|
||||
// 创建监管任务
|
||||
createTask: (data) => instance.post('/supervision/tasks', data),
|
||||
// 更新监管任务
|
||||
updateTask: (id, data) => instance.put(`/supervision/tasks/${id}`, data),
|
||||
// 删除监管任务
|
||||
deleteTask: (id) => instance.delete(`/supervision/tasks/${id}`)
|
||||
},
|
||||
|
||||
// 审批相关API
|
||||
approval: {
|
||||
// 获取审批流程列表
|
||||
getList: (params) => instance.get('/approval', { params }),
|
||||
// 创建审批流程
|
||||
create: (data) => instance.post('/approval', data),
|
||||
// 获取审批详情
|
||||
getDetail: (id) => instance.get(`/approval/${id}`),
|
||||
// 更新审批状态
|
||||
updateStatus: (id, status) => instance.put(`/approval/${id}/status`, { status })
|
||||
},
|
||||
|
||||
// 疫情监控相关API
|
||||
epidemic: {
|
||||
// 获取疫情统计数据
|
||||
getStats: () => instance.get('/epidemic/stats'),
|
||||
// 获取疫苗接种数据
|
||||
getVaccinationData: (params) => instance.get('/epidemic/vaccination', { params }),
|
||||
// 获取检测数据
|
||||
getTestData: (params) => instance.get('/epidemic/test', { params })
|
||||
},
|
||||
|
||||
// 数据可视化相关API
|
||||
visualization: {
|
||||
// 获取可视化数据
|
||||
getData: (params) => instance.get('/visualization/data', { params })
|
||||
},
|
||||
|
||||
// 文件管理相关API
|
||||
file: {
|
||||
// 获取文件列表
|
||||
getList: (params) => instance.get('/files', { params }),
|
||||
// 上传文件
|
||||
upload: (file, onUploadProgress) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return instance.post('/files/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress
|
||||
})
|
||||
},
|
||||
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
|
||||
// 下载文件
|
||||
download: (id) => instance.get(`/files/${id}/download`, { responseType: 'blob' }),
|
||||
// 删除文件
|
||||
delete: (id) => instance.delete(`/files/${id}`)
|
||||
},
|
||||
|
||||
// 人员管理相关API
|
||||
personnel: {
|
||||
// 获取人员列表
|
||||
getList: (params) => instance.get('/personnel', { params }),
|
||||
// 创建人员
|
||||
create: (data) => instance.post('/personnel', data),
|
||||
// 更新人员
|
||||
update: (id, data) => instance.put(`/personnel/${id}`, data),
|
||||
// 删除人员
|
||||
delete: (id) => instance.delete(`/personnel/${id}`)
|
||||
},
|
||||
|
||||
// 服务管理相关API
|
||||
service: {
|
||||
// 获取服务列表
|
||||
getList: (params) => instance.get('/service', { params }),
|
||||
// 创建服务
|
||||
create: (data) => instance.post('/service', data),
|
||||
// 更新服务
|
||||
update: (id, data) => instance.put(`/service/${id}`, data),
|
||||
// 删除服务
|
||||
delete: (id) => instance.delete(`/service/${id}`)
|
||||
},
|
||||
|
||||
// 仓库管理相关API
|
||||
warehouse: {
|
||||
// 获取仓库列表
|
||||
getList: (params) => instance.get('/warehouse', { params }),
|
||||
// 创建仓库
|
||||
create: (data) => instance.post('/warehouse', data),
|
||||
// 更新仓库
|
||||
update: (id, data) => instance.put(`/warehouse/${id}`, data),
|
||||
// 删除仓库
|
||||
delete: (id) => instance.delete(`/warehouse/${id}`)
|
||||
},
|
||||
|
||||
// 系统设置相关API
|
||||
system: {
|
||||
// 获取系统设置
|
||||
getSettings: () => instance.get('/system/settings'),
|
||||
// 更新系统设置
|
||||
updateSettings: (data) => instance.put('/system/settings', data),
|
||||
// 获取日志列表
|
||||
getLogs: (params) => instance.get('/system/logs', { params })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* 格式化工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param {Date|string|number} date - 日期
|
||||
* @param {string} format - 格式字符串
|
||||
* @returns {string} 格式化后的日期字符串
|
||||
*/
|
||||
export function formatDateTime(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) return ''
|
||||
|
||||
const d = new Date(date)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {Date|string|number} date - 日期
|
||||
* @returns {string} 格式化后的日期字符串
|
||||
*/
|
||||
export function formatDate(date) {
|
||||
return formatDateTime(date, 'YYYY-MM-DD')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date|string|number} date - 日期
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
export function formatTime(date) {
|
||||
return formatDateTime(date, 'HH:mm:ss')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param {number} num - 数字
|
||||
* @param {number} decimals - 小数位数
|
||||
* @returns {string} 格式化后的数字字符串
|
||||
*/
|
||||
export function formatNumber(num, decimals = 0) {
|
||||
if (typeof num !== 'number' || isNaN(num)) return '0'
|
||||
|
||||
return num.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} bytes - 字节数
|
||||
* @returns {string} 格式化后的文件大小字符串
|
||||
*/
|
||||
export function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param {number} value - 数值
|
||||
* @param {number} total - 总数
|
||||
* @param {number} decimals - 小数位数
|
||||
* @returns {string} 格式化后的百分比字符串
|
||||
*/
|
||||
export function formatPercentage(value, total, decimals = 1) {
|
||||
if (!total || total === 0) return '0%'
|
||||
|
||||
const percentage = (value / total) * 100
|
||||
return percentage.toFixed(decimals) + '%'
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化货币
|
||||
* @param {number} amount - 金额
|
||||
* @param {string} currency - 货币符号
|
||||
* @returns {string} 格式化后的货币字符串
|
||||
*/
|
||||
export function formatCurrency(amount, currency = '¥') {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) return currency + '0.00'
|
||||
|
||||
return currency + amount.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {Date|string|number} date - 日期
|
||||
* @returns {string} 相对时间字符串
|
||||
*/
|
||||
export function formatRelativeTime(date) {
|
||||
if (!date) return ''
|
||||
|
||||
const now = new Date()
|
||||
const target = new Date(date)
|
||||
const diff = now - target
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
const week = 7 * day
|
||||
const month = 30 * day
|
||||
const year = 365 * day
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚'
|
||||
} else if (diff < hour) {
|
||||
return Math.floor(diff / minute) + '分钟前'
|
||||
} else if (diff < day) {
|
||||
return Math.floor(diff / hour) + '小时前'
|
||||
} else if (diff < week) {
|
||||
return Math.floor(diff / day) + '天前'
|
||||
} else if (diff < month) {
|
||||
return Math.floor(diff / week) + '周前'
|
||||
} else if (diff < year) {
|
||||
return Math.floor(diff / month) + '个月前'
|
||||
} else {
|
||||
return Math.floor(diff / year) + '年前'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化手机号
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {string} 格式化后的手机号
|
||||
*/
|
||||
export function formatPhone(phone) {
|
||||
if (!phone) return ''
|
||||
|
||||
const cleaned = phone.replace(/\D/g, '')
|
||||
if (cleaned.length === 11) {
|
||||
return cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3')
|
||||
}
|
||||
|
||||
return phone
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化身份证号
|
||||
* @param {string} idCard - 身份证号
|
||||
* @returns {string} 格式化后的身份证号
|
||||
*/
|
||||
export function formatIdCard(idCard) {
|
||||
if (!idCard) return ''
|
||||
|
||||
const cleaned = idCard.replace(/\D/g, '')
|
||||
if (cleaned.length === 18) {
|
||||
return cleaned.replace(/(\d{6})(\d{8})(\d{4})/, '$1-$2-$3')
|
||||
}
|
||||
|
||||
return idCard
|
||||
}
|
||||
@@ -1,527 +0,0 @@
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
/**
|
||||
* 权限检查工具函数
|
||||
*/
|
||||
|
||||
// 检查单个权限
|
||||
export function hasPermission(permission) {
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 超级管理员和管理员拥有所有权限
|
||||
if (permissionStore.hasRole('super_admin') || permissionStore.hasRole('admin')) {
|
||||
return true
|
||||
}
|
||||
|
||||
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 = {
|
||||
// 工作台
|
||||
DASHBOARD_VIEW: 'dashboard:view',
|
||||
|
||||
// 养殖场管理
|
||||
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 = {
|
||||
// 工作台权限组
|
||||
DASHBOARD_MANAGEMENT: [
|
||||
PERMISSIONS.DASHBOARD_VIEW
|
||||
],
|
||||
|
||||
// 养殖场管理权限组
|
||||
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.DASHBOARD_MANAGEMENT,
|
||||
...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.DASHBOARD_MANAGEMENT,
|
||||
...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 = {
|
||||
// 工作台
|
||||
'dashboard:view': '查看工作台',
|
||||
|
||||
// 养殖场管理
|
||||
'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
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/**
|
||||
* HTTP请求工具
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
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 authStore = useAuthStore()
|
||||
|
||||
// 添加认证token
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.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 authStore = useAuthStore()
|
||||
|
||||
Modal.confirm({
|
||||
title: '登录已过期',
|
||||
content: '您的登录状态已过期,请重新登录',
|
||||
okText: '重新登录',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
authStore.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
|
||||
418
government-admin/src/views/ApprovalProcess.vue
Normal file
418
government-admin/src/views/ApprovalProcess.vue
Normal file
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>审批流程管理</h1>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-button type="primary" @click="showCreateModal">新建审批流程</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="exportApprovalList">导出列表</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 过滤器和搜索 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-row gutter={16}>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filters.status" placeholder="审批状态" style="width: 100%;">
|
||||
<a-select-option value="all">全部状态</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="processing">处理中</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filters.type" placeholder="审批类型" style="width: 100%;">
|
||||
<a-select-option value="all">全部类型</a-select-option>
|
||||
<a-select-option value="enterprise">企业资质</a-select-option>
|
||||
<a-select-option value="license">许可证</a-select-option>
|
||||
<a-select-option value="project">项目审批</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-range-picker v-model:value="filters.dateRange" style="width: 100%;" />
|
||||
</a-col>
|
||||
<a-col :span="6" style="text-align: right;">
|
||||
<a-input-search placeholder="搜索审批编号或申请人" @search="searchApproval" style="width: 100%;" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 审批流程表格 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="approvalColumns"
|
||||
:data-source="approvalList"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #bodyCell:type="{ record }">
|
||||
{{ getTypeText(record.type) }}
|
||||
</template>
|
||||
<template #bodyCell:action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" @click="viewApprovalDetail(record.id)">查看</a-button>
|
||||
<a-button type="link" @click="editApproval(record.id)" v-if="record.status === 'pending'">编辑</a-button>
|
||||
<a-button type="link" @click="deleteApproval(record.id)" danger>删除</a-button>
|
||||
<a-button type="primary" size="small" @click="processApproval(record.id)" v-if="record.status === 'pending'">审批</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新建审批流程弹窗 -->
|
||||
<a-modal
|
||||
title="新建审批流程"
|
||||
v-model:open="createModalVisible"
|
||||
:footer="null"
|
||||
@cancel="closeCreateModal"
|
||||
>
|
||||
<a-form
|
||||
ref="createFormRef"
|
||||
:model="createFormData"
|
||||
layout="vertical"
|
||||
:rules="createFormRules"
|
||||
>
|
||||
<a-form-item name="title" label="审批标题">
|
||||
<a-input v-model:value="createFormData.title" placeholder="请输入审批标题" />
|
||||
</a-form-item>
|
||||
<a-form-item name="type" label="审批类型">
|
||||
<a-select v-model:value="createFormData.type" placeholder="请选择审批类型">
|
||||
<a-select-option value="enterprise">企业资质</a-select-option>
|
||||
<a-select-option value="license">许可证</a-select-option>
|
||||
<a-select-option value="project">项目审批</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item name="applicant" label="申请人">
|
||||
<a-input v-model:value="createFormData.applicant" placeholder="请输入申请人姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item name="description" label="审批说明">
|
||||
<a-textarea v-model:value="createFormData.description" placeholder="请输入审批说明" rows={4} />
|
||||
</a-form-item>
|
||||
<a-form-item name="files" label="附件上传">
|
||||
<a-upload
|
||||
name="file"
|
||||
:multiple="true"
|
||||
:file-list="createFormData.files"
|
||||
:before-upload="beforeUpload"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
<a-button>
|
||||
<upload-outlined /> 点击上传
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
<div style="text-align: right;">
|
||||
<a-button @click="closeCreateModal">取消</a-button>
|
||||
<a-button type="primary" @click="submitCreateForm">提交</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 审批弹窗 -->
|
||||
<a-modal
|
||||
title="审批操作"
|
||||
v-model:open="processModalVisible"
|
||||
:footer="null"
|
||||
@cancel="closeProcessModal"
|
||||
>
|
||||
<div v-if="currentApproval">
|
||||
<h3>{{ currentApproval.title }}</h3>
|
||||
<p>申请人: {{ currentApproval.applicant }}</p>
|
||||
<p>申请时间: {{ currentApproval.create_time }}</p>
|
||||
<p>审批类型: {{ getTypeText(currentApproval.type) }}</p>
|
||||
<p>审批说明: {{ currentApproval.description }}</p>
|
||||
|
||||
<a-form-item label="审批意见">
|
||||
<a-textarea v-model:value="approvalComment" placeholder="请输入审批意见" rows={4} />
|
||||
</a-form-item>
|
||||
|
||||
<div style="text-align: right;">
|
||||
<a-button @click="closeProcessModal">取消</a-button>
|
||||
<a-button danger style="margin-left: 8px;" @click="rejectApproval">拒绝</a-button>
|
||||
<a-button type="primary" style="margin-left: 8px;" @click="approveApproval">通过</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'antd'
|
||||
import axios from 'axios'
|
||||
import { UploadOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const approvalList = ref([])
|
||||
const filters = ref({
|
||||
status: 'all',
|
||||
type: 'all',
|
||||
dateRange: []
|
||||
})
|
||||
const createModalVisible = ref(false)
|
||||
const processModalVisible = ref(false)
|
||||
const currentApproval = ref(null)
|
||||
const approvalComment = ref('')
|
||||
const createFormRef = ref(null)
|
||||
const createFormData = ref({
|
||||
title: '',
|
||||
type: '',
|
||||
applicant: '',
|
||||
description: '',
|
||||
files: []
|
||||
})
|
||||
const createFormRules = ref({
|
||||
title: [{ required: true, message: '请输入审批标题', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择审批类型', trigger: 'change' }],
|
||||
applicant: [{ required: true, message: '请输入申请人姓名', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '请输入审批说明', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 审批流程表格列定义
|
||||
const approvalColumns = [
|
||||
{
|
||||
title: '审批编号',
|
||||
dataIndex: 'id',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '审批标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title'
|
||||
},
|
||||
{
|
||||
title: '审批类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
slots: { customRender: 'type' }
|
||||
},
|
||||
{
|
||||
title: '申请人',
|
||||
dataIndex: 'applicant',
|
||||
key: 'applicant'
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
slots: { customRender: 'status' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
slots: { customRender: 'action' }
|
||||
}
|
||||
]
|
||||
|
||||
// 根据状态获取标签颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
processing: 'blue'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 根据状态获取显示文本
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
pending: '待审批',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
processing: '处理中'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 根据类型获取显示文本
|
||||
const getTypeText = (type) => {
|
||||
const textMap = {
|
||||
enterprise: '企业资质',
|
||||
license: '许可证',
|
||||
project: '项目审批',
|
||||
other: '其他'
|
||||
}
|
||||
return textMap[type] || type
|
||||
}
|
||||
|
||||
// 获取审批流程列表
|
||||
const fetchApprovalList = async () => {
|
||||
try {
|
||||
// 这里应该从API获取数据
|
||||
// 由于没有实际API,使用模拟数据
|
||||
approvalList.value = [
|
||||
{ id: 1, title: '食品经营许可证申请', type: 'license', applicant: '张三', create_time: '2024-01-10 09:00:00', status: 'pending', description: '申请食品经营许可证' },
|
||||
{ id: 2, title: '企业资质年审', type: 'enterprise', applicant: '李四', create_time: '2024-01-09 14:30:00', status: 'approved', description: '企业资质年度审核' },
|
||||
{ id: 3, title: '新药品研发项目', type: 'project', applicant: '王五', create_time: '2024-01-08 11:20:00', status: 'processing', description: '新型药品研发项目审批' },
|
||||
{ id: 4, title: '环保设施改造', type: 'project', applicant: '赵六', create_time: '2024-01-07 16:40:00', status: 'rejected', description: '工厂环保设施改造审批' },
|
||||
{ id: 5, title: '特殊行业许可证', type: 'license', applicant: '钱七', create_time: '2024-01-06 10:15:00', status: 'pending', description: '申请特殊行业经营许可证' },
|
||||
{ id: 6, title: '企业扩大经营规模', type: 'enterprise', applicant: '孙八', create_time: '2024-01-05 13:30:00', status: 'approved', description: '企业扩大生产经营规模审批' },
|
||||
{ id: 7, title: '新产品上市审批', type: 'other', applicant: '周九', create_time: '2024-01-04 09:45:00', status: 'pending', description: '新产品上市销售审批' },
|
||||
{ id: 8, title: '消防设施验收', type: 'project', applicant: '吴十', create_time: '2024-01-03 15:20:00', status: 'processing', description: '新建建筑消防设施验收' },
|
||||
{ id: 9, title: '卫生许可证换证', type: 'license', applicant: '郑一', create_time: '2024-01-02 11:00:00', status: 'approved', description: '卫生许可证到期换证' },
|
||||
{ id: 10, title: '临时占道经营', type: 'other', applicant: '王二', create_time: '2024-01-01 09:30:00', status: 'rejected', description: '临时占道经营活动审批' }
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('获取审批流程列表失败:', error)
|
||||
message.error('获取审批流程列表失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索审批
|
||||
const searchApproval = (value) => {
|
||||
message.info(`搜索关键词: ${value}`)
|
||||
// 这里应该根据搜索关键词过滤数据
|
||||
// 目前使用模拟数据,实际项目中需要调用API
|
||||
}
|
||||
|
||||
// 查看审批详情
|
||||
const viewApprovalDetail = (id) => {
|
||||
message.info(`查看审批ID: ${id} 的详情`)
|
||||
// 这里可以跳转到详情页面
|
||||
// router.push(`/approval/detail/${id}`)
|
||||
}
|
||||
|
||||
// 编辑审批
|
||||
const editApproval = (id) => {
|
||||
message.info(`编辑审批ID: ${id}`)
|
||||
// 这里应该打开编辑弹窗并加载数据
|
||||
}
|
||||
|
||||
// 删除审批
|
||||
const deleteApproval = (id) => {
|
||||
message.info(`删除审批ID: ${id}`)
|
||||
// 这里应该弹出确认框并调用删除API
|
||||
}
|
||||
|
||||
// 处理审批
|
||||
const processApproval = (id) => {
|
||||
// 找到当前审批项
|
||||
const approval = approvalList.value.find(item => item.id === id)
|
||||
if (approval) {
|
||||
currentApproval.value = { ...approval }
|
||||
approvalComment.value = ''
|
||||
processModalVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 通过审批
|
||||
const approveApproval = () => {
|
||||
if (!currentApproval.value) return
|
||||
|
||||
message.success(`已通过审批: ${currentApproval.value.title}`)
|
||||
// 这里应该调用审批通过API
|
||||
|
||||
// 更新本地数据
|
||||
const index = approvalList.value.findIndex(item => item.id === currentApproval.value.id)
|
||||
if (index !== -1) {
|
||||
approvalList.value[index].status = 'approved'
|
||||
}
|
||||
|
||||
closeProcessModal()
|
||||
}
|
||||
|
||||
// 拒绝审批
|
||||
const rejectApproval = () => {
|
||||
if (!currentApproval.value) return
|
||||
|
||||
if (!approvalComment.value.trim()) {
|
||||
message.warning('请输入拒绝理由')
|
||||
return
|
||||
}
|
||||
|
||||
message.success(`已拒绝审批: ${currentApproval.value.title}`)
|
||||
// 这里应该调用审批拒绝API
|
||||
|
||||
// 更新本地数据
|
||||
const index = approvalList.value.findIndex(item => item.id === currentApproval.value.id)
|
||||
if (index !== -1) {
|
||||
approvalList.value[index].status = 'rejected'
|
||||
}
|
||||
|
||||
closeProcessModal()
|
||||
}
|
||||
|
||||
// 显示新建弹窗
|
||||
const showCreateModal = () => {
|
||||
createFormData.value = {
|
||||
title: '',
|
||||
type: '',
|
||||
applicant: '',
|
||||
description: '',
|
||||
files: []
|
||||
}
|
||||
if (createFormRef.value) {
|
||||
createFormRef.value.resetFields()
|
||||
}
|
||||
createModalVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭新建弹窗
|
||||
const closeCreateModal = () => {
|
||||
createModalVisible.value = false
|
||||
}
|
||||
|
||||
// 关闭审批弹窗
|
||||
const closeProcessModal = () => {
|
||||
processModalVisible.value = false
|
||||
currentApproval.value = null
|
||||
approvalComment.value = ''
|
||||
}
|
||||
|
||||
// 提交新建表单
|
||||
const submitCreateForm = async () => {
|
||||
if (!createFormRef.value) return
|
||||
|
||||
try {
|
||||
await createFormRef.value.validate()
|
||||
// 这里应该调用创建审批API
|
||||
|
||||
message.success('新建审批流程成功')
|
||||
closeCreateModal()
|
||||
|
||||
// 重新加载数据
|
||||
fetchApprovalList()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传前处理
|
||||
const beforeUpload = (file) => {
|
||||
// 这里可以添加文件类型和大小的校验
|
||||
return false; // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = ({ fileList }) => {
|
||||
createFormData.value.files = fileList
|
||||
}
|
||||
|
||||
// 导出列表
|
||||
const exportApprovalList = () => {
|
||||
message.info('导出审批流程列表')
|
||||
// 这里应该调用导出API
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
fetchApprovalList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式可以根据需要进行调整 */
|
||||
</style>
|
||||
844
government-admin/src/views/BreedImprovement.vue
Normal file
844
government-admin/src/views/BreedImprovement.vue
Normal file
@@ -0,0 +1,844 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>品种改良管理</h1>
|
||||
|
||||
<!-- 搜索和操作栏 -->
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: center;">
|
||||
<a-input v-model:value="searchKeyword" placeholder="输入品种名称或编号" style="width: 250px;">
|
||||
<template #prefix>
|
||||
<span class="iconfont icon-sousuo"></span>
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-select v-model:value="breedTypeFilter" placeholder="品种类型" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="local">地方品种</a-select-option>
|
||||
<a-select-option value="improved">培育品种</a-select-option>
|
||||
<a-select-option value="imported">引进品种</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select v-model:value="statusFilter" placeholder="推广状态" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="developing">开发中</a-select-option>
|
||||
<a-select-option value="promoting">推广中</a-select-option>
|
||||
<a-select-option value="promoted">已推广</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-button type="primary" @click="handleSearch" style="margin-left: auto;">
|
||||
<span class="iconfont icon-sousuo"></span> 搜索
|
||||
</a-button>
|
||||
|
||||
<a-button type="default" @click="handleReset">重置</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleImport">
|
||||
<span class="iconfont icon-daoru"></span> 导入
|
||||
</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleExport">
|
||||
<span class="iconfont icon-daochu"></span> 导出
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" danger @click="handleAddBreed">
|
||||
<span class="iconfont icon-tianjia"></span> 新增改良品种
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="改良品种总数" :value="totalBreeds" suffix="个" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="推广中品种" :value="promotingBreeds" suffix="个" :valueStyle="{ color: '#1890ff' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="已推广品种" :value="promotedBreeds" suffix="个" :valueStyle="{ color: '#52c41a' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="开发中品种" :value="developingBreeds" suffix="个" :valueStyle="{ color: '#faad14' }" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 品种分布图表 -->
|
||||
<a-card title="品种类型分布" style="margin-bottom: 16px;">
|
||||
<div style="height: 300px;" ref="breedChartRef"></div>
|
||||
</a-card>
|
||||
|
||||
<!-- 品种列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="breedsData"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #bodyCell:action="{ record }">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a-button size="small" @click="handleView(record)">查看</a-button>
|
||||
<a-button size="small" type="primary" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button size="small" danger @click="handleDelete(record.id)">删除</a-button>
|
||||
<a-button size="small" @click="handleUpload(record.id)">上传资料</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑品种模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isAddEditModalOpen"
|
||||
title="新增/编辑改良品种"
|
||||
:footer="null"
|
||||
width={800}
|
||||
>
|
||||
<a-form
|
||||
:model="currentBreed"
|
||||
layout="vertical"
|
||||
style="max-width: 600px; margin: 0 auto;"
|
||||
>
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="品种编号" name="code" :rules="[{ required: true, message: '请输入品种编号' }]">
|
||||
<a-input v-model:value="currentBreed.code" placeholder="请输入品种编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="品种名称" name="name" :rules="[{ required: true, message: '请输入品种名称' }]">
|
||||
<a-input v-model:value="currentBreed.name" placeholder="请输入品种名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="品种类型" name="breedType" :rules="[{ required: true, message: '请选择品种类型' }]">
|
||||
<a-select v-model:value="currentBreed.breedType" placeholder="请选择品种类型">
|
||||
<a-select-option value="local">地方品种</a-select-option>
|
||||
<a-select-option value="improved">培育品种</a-select-option>
|
||||
<a-select-option value="imported">引进品种</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="推广状态" name="status" :rules="[{ required: true, message: '请选择推广状态' }]">
|
||||
<a-select v-model:value="currentBreed.status" placeholder="请选择推广状态">
|
||||
<a-select-option value="developing">开发中</a-select-option>
|
||||
<a-select-option value="promoting">推广中</a-select-option>
|
||||
<a-select-option value="promoted">已推广</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="培育单位" name="breedingUnit" :rules="[{ required: true, message: '请输入培育单位' }]">
|
||||
<a-input v-model:value="currentBreed.breedingUnit" placeholder="请输入培育单位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="培育负责人" name="breedingManager" :rules="[{ required: true, message: '请输入培育负责人' }]">
|
||||
<a-input v-model:value="currentBreed.breedingManager" placeholder="请输入培育负责人" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="培育开始时间" name="startTime" :rules="[{ required: true, message: '请选择培育开始时间' }]">
|
||||
<a-date-picker v-model:value="currentBreed.startTime" style="width: 100%;" placeholder="请选择培育开始时间" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="预期完成时间" name="expectedEndTime">
|
||||
<a-date-picker v-model:value="currentBreed.expectedEndTime" style="width: 100%;" placeholder="请选择预期完成时间" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="父本品种" name="maleParent">
|
||||
<a-input v-model:value="currentBreed.maleParent" placeholder="请输入父本品种" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="母本品种" name="femaleParent">
|
||||
<a-input v-model:value="currentBreed.femaleParent" placeholder="请输入母本品种" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="品种特性" name="characteristics">
|
||||
<a-input.TextArea v-model:value="currentBreed.characteristics" placeholder="请输入品种特性" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="推广情况" name="promotionStatus">
|
||||
<a-input.TextArea v-model:value="currentBreed.promotionStatus" placeholder="请输入推广情况" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注信息" name="remark">
|
||||
<a-input.TextArea v-model:value="currentBreed.remark" placeholder="请输入备注信息" :rows="2" />
|
||||
</a-form-item>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 16px;">
|
||||
<a-button @click="isAddEditModalOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看品种详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isViewModalOpen"
|
||||
title="品种改良详情"
|
||||
:footer="null"
|
||||
width={900}
|
||||
>
|
||||
<div v-if="viewBreed" class="breed-detail">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">品种编号:</div>
|
||||
<div class="detail-value">{{ viewBreed.code }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">品种名称:</div>
|
||||
<div class="detail-value">{{ viewBreed.name }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">品种类型:</div>
|
||||
<div class="detail-value">{{ getBreedTypeText(viewBreed.breedType) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">推广状态:</div>
|
||||
<div class="detail-value">
|
||||
<a-tag :color="getStatusColor(viewBreed.status)">{{ getStatusText(viewBreed.status) }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">培育单位:</div>
|
||||
<div class="detail-value">{{ viewBreed.breedingUnit }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">培育负责人:</div>
|
||||
<div class="detail-value">{{ viewBreed.breedingManager }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">培育开始时间:</div>
|
||||
<div class="detail-value">{{ viewBreed.startTime }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">预期完成时间:</div>
|
||||
<div class="detail-value">{{ viewBreed.expectedEndTime || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">父本品种:</div>
|
||||
<div class="detail-value">{{ viewBreed.maleParent || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">母本品种:</div>
|
||||
<div class="detail-value">{{ viewBreed.femaleParent || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">品种特性:</div>
|
||||
<div class="detail-value">{{ viewBreed.characteristics || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">推广情况:</div>
|
||||
<div class="detail-value">{{ viewBreed.promotionStatus || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">备注信息:</div>
|
||||
<div class="detail-value">{{ viewBreed.remark || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row" v-if="viewBreed.relatedFiles && viewBreed.relatedFiles.length > 0">
|
||||
<div class="detail-label">相关资料:</div>
|
||||
<div class="detail-value">
|
||||
<div v-for="file in viewBreed.relatedFiles" :key="file.id" class="file-item">
|
||||
<a :href="file.url" target="_blank">{{ file.name }}</a>
|
||||
<span class="file-size">({{ file.size }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 24px;">
|
||||
<a-button @click="isViewModalOpen = false">关闭</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 上传资料模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isUploadModalOpen"
|
||||
title="上传品种改良资料"
|
||||
:footer="null"
|
||||
width={600}
|
||||
>
|
||||
<div style="padding: 16px;">
|
||||
<div v-if="uploadingBreedName">
|
||||
<p style="margin-bottom: 16px;">正在为 <strong>{{ uploadingBreedName }}</strong> 上传资料</p>
|
||||
</div>
|
||||
<a-upload
|
||||
name="file"
|
||||
:multiple="true"
|
||||
:fileList="fileList"
|
||||
@change="handleFileChange"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<a-button>
|
||||
<span class="iconfont icon-upload"></span> 选择文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<p style="margin-top: 16px; color: #8c8c8c; font-size: 12px;">支持上传 Word、PDF、Excel、图片等格式文件</p>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 16px; margin-top: 24px;">
|
||||
<a-button @click="isUploadModalOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleConfirmUpload">确认上传</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 搜索条件
|
||||
const searchKeyword = ref('')
|
||||
const breedTypeFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
|
||||
// 统计数据
|
||||
const totalBreeds = ref(28)
|
||||
const promotingBreeds = ref(12)
|
||||
const promotedBreeds = ref(10)
|
||||
const developingBreeds = ref(6)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 选中的行
|
||||
const selectedRowKeys = ref([])
|
||||
const onSelectChange = (newSelectedRowKeys) => {
|
||||
selectedRowKeys.value = newSelectedRowKeys
|
||||
}
|
||||
|
||||
// 图表引用
|
||||
const breedChartRef = ref(null)
|
||||
|
||||
// 模态框状态
|
||||
const isAddEditModalOpen = ref(false)
|
||||
const isViewModalOpen = ref(false)
|
||||
const isUploadModalOpen = ref(false)
|
||||
const currentUploadBreedId = ref('')
|
||||
const uploadingBreedName = ref('')
|
||||
|
||||
// 当前编辑的品种
|
||||
const currentBreed = reactive({
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
breedType: '',
|
||||
status: 'developing',
|
||||
breedingUnit: '',
|
||||
breedingManager: '',
|
||||
startTime: '',
|
||||
expectedEndTime: '',
|
||||
maleParent: '',
|
||||
femaleParent: '',
|
||||
characteristics: '',
|
||||
promotionStatus: '',
|
||||
remark: '',
|
||||
relatedFiles: []
|
||||
})
|
||||
|
||||
// 查看的品种
|
||||
const viewBreed = ref(null)
|
||||
|
||||
// 上传文件列表
|
||||
const fileList = ref([])
|
||||
|
||||
// 品种列表数据
|
||||
const breedsData = ref([
|
||||
{
|
||||
id: '1',
|
||||
code: 'B001',
|
||||
name: '中国荷斯坦牛',
|
||||
breedType: 'improved',
|
||||
status: 'promoted',
|
||||
breedingUnit: '中国农业科学院畜牧研究所',
|
||||
breedingManager: '张明',
|
||||
startTime: '2010-01-01',
|
||||
expectedEndTime: '2015-12-31',
|
||||
maleParent: '荷斯坦牛',
|
||||
femaleParent: '本地黄牛',
|
||||
characteristics: '产奶量高,适应力强,抗病性好',
|
||||
promotionStatus: '已在全国28个省市推广,存栏量超过100万头',
|
||||
remark: '国家级重点推广品种',
|
||||
relatedFiles: [
|
||||
{ id: 'f1', name: '品种标准.pdf', size: '2.5MB', url: '#' },
|
||||
{ id: 'f2', name: '饲养管理指南.docx', size: '1.8MB', url: '#' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
code: 'B002',
|
||||
name: '秦川牛(改良型)',
|
||||
breedType: 'improved',
|
||||
status: 'promoting',
|
||||
breedingUnit: '陕西省农业科学院',
|
||||
breedingManager: '李华',
|
||||
startTime: '2012-03-15',
|
||||
expectedEndTime: '2018-03-15',
|
||||
maleParent: '日本和牛',
|
||||
femaleParent: '秦川牛',
|
||||
characteristics: '肉质优良,生长速度快,饲料转化率高',
|
||||
promotionStatus: '已在陕西、甘肃、河南等省推广,效果良好',
|
||||
remark: '优质肉牛品种',
|
||||
relatedFiles: [
|
||||
{ id: 'f3', name: '秦川牛改良报告.pdf', size: '3.2MB', url: '#' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
code: 'B003',
|
||||
name: '西门塔尔牛',
|
||||
breedType: 'imported',
|
||||
status: 'promoted',
|
||||
breedingUnit: '中国农业大学',
|
||||
breedingManager: '王强',
|
||||
startTime: '2008-05-20',
|
||||
expectedEndTime: '2013-05-20',
|
||||
maleParent: '瑞士西门塔尔牛',
|
||||
femaleParent: '本地黄牛',
|
||||
characteristics: '肉乳兼用,适应性强,耐粗饲',
|
||||
promotionStatus: '已在全国广泛推广,是我国主要的肉牛品种之一',
|
||||
remark: '引进后本土化改良品种',
|
||||
relatedFiles: []
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
code: 'B004',
|
||||
name: '草原红牛',
|
||||
breedType: 'local',
|
||||
status: 'promoted',
|
||||
breedingUnit: '内蒙古农业大学',
|
||||
breedingManager: '赵芳',
|
||||
startTime: '2005-09-10',
|
||||
expectedEndTime: '2010-09-10',
|
||||
maleParent: '蒙古牛',
|
||||
femaleParent: '本地黄牛',
|
||||
characteristics: '抗寒能力强,适应草原环境,肉质鲜美',
|
||||
promotionStatus: '主要在内蒙古、黑龙江等地区推广',
|
||||
remark: '地方特色品种',
|
||||
relatedFiles: [
|
||||
{ id: 'f4', name: '草原红牛品种志.pdf', size: '4.1MB', url: '#' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
code: 'B005',
|
||||
name: '南阳牛(新品系)',
|
||||
breedType: 'improved',
|
||||
status: 'promoting',
|
||||
breedingUnit: '河南省农业科学院',
|
||||
breedingManager: '孙建国',
|
||||
startTime: '2015-02-20',
|
||||
expectedEndTime: '2020-02-20',
|
||||
maleParent: '皮埃蒙特牛',
|
||||
femaleParent: '南阳牛',
|
||||
characteristics: '体型大,生长快,瘦肉率高',
|
||||
promotionStatus: '正在河南省及周边地区推广试验',
|
||||
remark: '传统品种改良项目',
|
||||
relatedFiles: []
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
code: 'B006',
|
||||
name: '鲁西黄牛(高产品系)',
|
||||
breedType: 'improved',
|
||||
status: 'developing',
|
||||
breedingUnit: '山东省农业科学院',
|
||||
breedingManager: '周小丽',
|
||||
startTime: '2018-07-01',
|
||||
expectedEndTime: '2023-07-01',
|
||||
maleParent: '夏洛莱牛',
|
||||
femaleParent: '鲁西黄牛',
|
||||
characteristics: '生长速度快,饲料转化率高,肉质好',
|
||||
promotionStatus: '处于培育阶段,已完成第一阶段选育',
|
||||
remark: '重点科研项目',
|
||||
relatedFiles: [
|
||||
{ id: 'f5', name: '选育方案.docx', size: '1.5MB', url: '#' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
code: 'B007',
|
||||
name: '延边牛',
|
||||
breedType: 'local',
|
||||
status: 'promoted',
|
||||
breedingUnit: '吉林省农业科学院',
|
||||
breedingManager: '吴大山',
|
||||
startTime: '2006-04-15',
|
||||
expectedEndTime: '2011-04-15',
|
||||
maleParent: '朝鲜牛',
|
||||
femaleParent: '本地黄牛',
|
||||
characteristics: '抗寒、耐粗饲,肉质优良',
|
||||
promotionStatus: '主要在东北地区推广,存栏量稳定',
|
||||
remark: '东北地区特色品种',
|
||||
relatedFiles: []
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
code: 'B008',
|
||||
name: '安格斯牛(本土化)',
|
||||
breedType: 'imported',
|
||||
status: 'promoting',
|
||||
breedingUnit: '中国农业科学院',
|
||||
breedingManager: '郑小华',
|
||||
startTime: '2016-09-30',
|
||||
expectedEndTime: '2021-09-30',
|
||||
maleParent: '苏格兰安格斯牛',
|
||||
femaleParent: '本地黄牛',
|
||||
characteristics: '肉质大理石花纹明显,适应性逐渐增强',
|
||||
promotionStatus: '在多个省份进行适应性试验,效果良好',
|
||||
remark: '引进品种本土化改良',
|
||||
relatedFiles: [
|
||||
{ id: 'f6', name: '安格斯牛适应性研究.pdf', size: '2.8MB', url: '#' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
code: 'B009',
|
||||
name: '三河牛',
|
||||
breedType: 'local',
|
||||
status: 'promoted',
|
||||
breedingUnit: '内蒙古呼伦贝尔市畜牧研究所',
|
||||
breedingManager: '钱小红',
|
||||
startTime: '2003-06-10',
|
||||
expectedEndTime: '2008-06-10',
|
||||
maleParent: '俄罗斯牛',
|
||||
femaleParent: '蒙古牛',
|
||||
characteristics: '乳肉兼用,适应草原环境',
|
||||
promotionStatus: '主要在内蒙古呼伦贝尔地区推广',
|
||||
remark: '草原特色品种',
|
||||
relatedFiles: []
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
code: 'B010',
|
||||
name: '蜀宣花牛',
|
||||
breedType: 'improved',
|
||||
status: 'developing',
|
||||
breedingUnit: '四川省畜牧科学研究院',
|
||||
breedingManager: '陈明亮',
|
||||
startTime: '2019-03-01',
|
||||
expectedEndTime: '2024-03-01',
|
||||
maleParent: '西门塔尔牛',
|
||||
femaleParent: '宣汉黄牛',
|
||||
characteristics: '适应南方气候,生长快,肉质好',
|
||||
promotionStatus: '处于培育阶段,已完成基础种群建立',
|
||||
remark: '南方地区特色品种培育',
|
||||
relatedFiles: []
|
||||
}
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '品种编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '品种名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '品种类型',
|
||||
dataIndex: 'breedType',
|
||||
key: 'breedType',
|
||||
width: 100,
|
||||
customRender: ({ text }) => getBreedTypeText(text)
|
||||
},
|
||||
{
|
||||
title: '培育单位',
|
||||
dataIndex: 'breedingUnit',
|
||||
key: 'breedingUnit',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '推广状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '培育开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '资料数量',
|
||||
dataIndex: 'relatedFiles',
|
||||
key: 'fileCount',
|
||||
width: 100,
|
||||
customRender: ({ text }) => text && text.length ? text.length : 0
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'developing': return 'orange'
|
||||
case 'promoting': return 'blue'
|
||||
case 'promoted': return 'green'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'developing': return '开发中'
|
||||
case 'promoting': return '推广中'
|
||||
case 'promoted': return '已推广'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
// 获取品种类型文本
|
||||
const getBreedTypeText = (breedType) => {
|
||||
switch (breedType) {
|
||||
case 'local': return '地方品种'
|
||||
case 'improved': return '培育品种'
|
||||
case 'imported': return '引进品种'
|
||||
default: return breedType
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索条件:', {
|
||||
keyword: searchKeyword.value,
|
||||
breedType: breedTypeFilter.value,
|
||||
status: statusFilter.value
|
||||
})
|
||||
// 这里应该有实际的搜索逻辑
|
||||
// 模拟搜索后的总数
|
||||
pagination.total = breedsData.value.length
|
||||
}
|
||||
|
||||
// 重置处理
|
||||
const handleReset = () => {
|
||||
searchKeyword.value = ''
|
||||
breedTypeFilter.value = ''
|
||||
statusFilter.value = ''
|
||||
selectedRowKeys.value = []
|
||||
}
|
||||
|
||||
// 导入处理
|
||||
const handleImport = () => {
|
||||
console.log('导入品种数据')
|
||||
// 这里应该有实际的导入逻辑
|
||||
}
|
||||
|
||||
// 导出处理
|
||||
const handleExport = () => {
|
||||
console.log('导出品种数据')
|
||||
// 这里应该有实际的导出逻辑
|
||||
}
|
||||
|
||||
// 新增品种
|
||||
const handleAddBreed = () => {
|
||||
// 重置当前品种数据
|
||||
Object.assign(currentBreed, {
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
breedType: '',
|
||||
status: 'developing',
|
||||
breedingUnit: '',
|
||||
breedingManager: '',
|
||||
startTime: '',
|
||||
expectedEndTime: '',
|
||||
maleParent: '',
|
||||
femaleParent: '',
|
||||
characteristics: '',
|
||||
promotionStatus: '',
|
||||
remark: '',
|
||||
relatedFiles: []
|
||||
})
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 编辑品种
|
||||
const handleEdit = (record) => {
|
||||
// 复制记录数据到当前品种
|
||||
Object.assign(currentBreed, JSON.parse(JSON.stringify(record)))
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 查看品种
|
||||
const handleView = (record) => {
|
||||
viewBreed.value = JSON.parse(JSON.stringify(record))
|
||||
isViewModalOpen.value = true
|
||||
}
|
||||
|
||||
// 删除品种
|
||||
const handleDelete = (id) => {
|
||||
console.log('删除品种:', id)
|
||||
// 这里应该有实际的删除逻辑和确认提示
|
||||
// 模拟删除成功
|
||||
alert(`成功删除品种ID: ${id}`)
|
||||
}
|
||||
|
||||
// 保存品种
|
||||
const handleSave = () => {
|
||||
console.log('保存品种:', currentBreed)
|
||||
// 这里应该有实际的保存逻辑
|
||||
// 模拟保存成功
|
||||
isAddEditModalOpen.value = false
|
||||
alert('保存成功')
|
||||
}
|
||||
|
||||
// 上传资料
|
||||
const handleUpload = (id) => {
|
||||
const breed = breedsData.value.find(item => item.id === id)
|
||||
if (breed) {
|
||||
currentUploadBreedId.value = id
|
||||
uploadingBreedName.value = breed.name
|
||||
fileList.value = []
|
||||
isUploadModalOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = ({ fileList: newFileList }) => {
|
||||
fileList.value = newFileList
|
||||
}
|
||||
|
||||
// 拖放处理
|
||||
const handleDrop = (e) => {
|
||||
console.log('Dropped files', e.dataTransfer.files)
|
||||
}
|
||||
|
||||
// 确认上传
|
||||
const handleConfirmUpload = () => {
|
||||
console.log('上传文件:', fileList.value)
|
||||
console.log('品种ID:', currentUploadBreedId.value)
|
||||
// 这里应该有实际的文件上传逻辑
|
||||
// 模拟上传成功
|
||||
isUploadModalOpen.value = false
|
||||
alert('文件上传成功')
|
||||
}
|
||||
|
||||
// 初始化品种分布图表
|
||||
const initBreedChart = () => {
|
||||
if (!breedChartRef.value) return
|
||||
|
||||
const chart = echarts.init(breedChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: ['地方品种', '培育品种', '引进品种']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '品种类型分布',
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
center: ['50%', '50%'],
|
||||
data: [
|
||||
{ value: 8, name: '地方品种' },
|
||||
{ value: 12, name: '培育品种' },
|
||||
{ value: 8, name: '引进品种' }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initBreedChart()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
pagination.total = breedsData.value.length
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.breed-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
margin-left: 8px;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
1600
government-admin/src/views/CattleAcademy.vue
Normal file
1600
government-admin/src/views/CattleAcademy.vue
Normal file
File diff suppressed because it is too large
Load Diff
1081
government-admin/src/views/CommunicationCommunity.vue
Normal file
1081
government-admin/src/views/CommunicationCommunity.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,681 +1,334 @@
|
||||
<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>
|
||||
<h1>仪表板</h1>
|
||||
<a-row gutter={24}>
|
||||
<!-- 数据卡片区域 -->
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.totalEntities }}</div>
|
||||
<div class="stat-label">总实体数</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</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>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.inspectionCount }}</div>
|
||||
<div class="stat-label">检查次数</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</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>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.vaccinated }}</div>
|
||||
<div class="stat-label">疫苗接种数</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</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>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.tested }}</div>
|
||||
<div class="stat-label">检测人数</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</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>
|
||||
<a-row gutter={24} style="margin-top: 24px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="监管趋势图" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<!-- 这里将放置图表 -->
|
||||
<div id="trend-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
<div class="map-container" ref="mapRef">
|
||||
<!-- 地图将在这里渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</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>
|
||||
<a-col :span="12">
|
||||
<a-card title="饼图数据" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<!-- 这里将放置饼图 -->
|
||||
<div id="pie-chart" style="width: 100%; height: 100%;"></div>
|
||||
</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-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 最近活动区域 -->
|
||||
<a-card title="最近活动" style="margin-top: 24px;">
|
||||
<a-table :columns="activityColumns" :data-source="recentActivities" pagination={false}>
|
||||
<template #bodyCell:time="{ record }">
|
||||
<span>{{ formatTime(record.time) }}</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import dayjs from 'dayjs'
|
||||
import axios from 'axios'
|
||||
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 userStore = useUserStore()
|
||||
const stats = ref({
|
||||
totalEntities: 0,
|
||||
inspectionCount: 0,
|
||||
vaccinated: 0,
|
||||
tested: 0
|
||||
})
|
||||
const recentActivities = ref([])
|
||||
let trendChart = null
|
||||
let pieChart = null
|
||||
|
||||
// 预警统计
|
||||
const alertStats = reactive({
|
||||
high: 3,
|
||||
medium: 7,
|
||||
low: 15
|
||||
})
|
||||
|
||||
// 最新动态
|
||||
const recentNews = ref([
|
||||
// 活动表格列定义
|
||||
const activityColumns = [
|
||||
{
|
||||
title: '新增5家养殖场接入监管平台',
|
||||
description: '本月新增5家大型养殖场接入智慧监管平台,覆盖范围进一步扩大',
|
||||
time: '2小时前'
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type'
|
||||
},
|
||||
{
|
||||
title: '设备维护通知',
|
||||
description: '计划于本周末对部分监控设备进行例行维护,请提前做好准备',
|
||||
time: '5小时前'
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
title: '月度数据报表已生成',
|
||||
description: '12月份养殖监管数据报表已生成完成,可在报表管理中查看',
|
||||
time: '1天前'
|
||||
},
|
||||
{
|
||||
title: '系统升级公告',
|
||||
description: '系统将于下周进行功能升级,新增多项智能分析功能',
|
||||
time: '2天前'
|
||||
title: '时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
slots: { customRender: 'time' }
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
// 图表实例
|
||||
let deviceChart = null
|
||||
let healthChart = null
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 初始化设备状态图表
|
||||
const initDeviceChart = () => {
|
||||
if (!deviceChartRef.value) return
|
||||
|
||||
deviceChart = echarts.init(deviceChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
// 获取统计数据
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
// 获取监管统计数据
|
||||
const supervisionResponse = await axios.get('/api/supervision/stats')
|
||||
if (supervisionResponse.data.code === 200) {
|
||||
const supervisionData = supervisionResponse.data.data
|
||||
stats.value.totalEntities = supervisionData.entityCount || 0
|
||||
stats.value.inspectionCount = supervisionData.inspectionCount || 0
|
||||
}
|
||||
|
||||
// 获取疫情统计数据
|
||||
const epidemicResponse = await axios.get('/api/epidemic/stats')
|
||||
if (epidemicResponse.data.code === 200) {
|
||||
const epidemicData = epidemicResponse.data.data
|
||||
stats.value.vaccinated = epidemicData.vaccinated || 0
|
||||
stats.value.tested = epidemicData.tested || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
// 使用模拟数据
|
||||
stats.value = {
|
||||
totalEntities: 150,
|
||||
inspectionCount: 78,
|
||||
vaccinated: 12500,
|
||||
tested: 89000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最近活动数据
|
||||
const fetchRecentActivities = async () => {
|
||||
try {
|
||||
// 这里应该从API获取数据
|
||||
// 由于没有实际API,使用模拟数据
|
||||
recentActivities.value = [
|
||||
{
|
||||
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)'
|
||||
}
|
||||
key: '1',
|
||||
type: '登录',
|
||||
description: `${userStore.userInfo.real_name || '管理员'} 登录系统`,
|
||||
time: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
type: '操作',
|
||||
description: '系统配置已更新',
|
||||
time: new Date(Date.now() - 3600000).toISOString()
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
type: '提醒',
|
||||
description: '有5个待审批的申请',
|
||||
time: new Date(Date.now() - 7200000).toISOString()
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
type: '警告',
|
||||
description: '检测到2个异常数据',
|
||||
time: new Date(Date.now() - 10800000).toISOString()
|
||||
}
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('获取活动数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 趋势图
|
||||
const trendChartDom = document.getElementById('trend-chart')
|
||||
if (trendChartDom) {
|
||||
trendChart = echarts.init(trendChartDom)
|
||||
const trendOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '检查次数',
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
type: 'line'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
trendChart.setOption(trendOption)
|
||||
}
|
||||
|
||||
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]
|
||||
// 饼图
|
||||
const pieChartDom = document.getElementById('pie-chart')
|
||||
if (pieChartDom) {
|
||||
pieChart = echarts.init(pieChartDom)
|
||||
const pieOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
{
|
||||
name: '亚健康',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [220, 182, 191, 234, 290, 330, 310]
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
{
|
||||
name: '异常',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [150, 232, 201, 154, 190, 330, 410]
|
||||
}
|
||||
]
|
||||
series: [
|
||||
{
|
||||
name: '数据分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 30,
|
||||
name: '类型A'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '类型B'
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
name: '类型C'
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
name: '类型D'
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
name: '其他'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
pieChart.setOption(pieOption)
|
||||
}
|
||||
|
||||
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 (trendChart) {
|
||||
trendChart.resize()
|
||||
}
|
||||
if (healthChart) {
|
||||
healthChart.resize()
|
||||
if (pieChart) {
|
||||
pieChart.resize()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
// 初始化图表
|
||||
initDeviceChart()
|
||||
initHealthChart()
|
||||
|
||||
// 监听窗口大小变化
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchRecentActivities()
|
||||
setTimeout(() => {
|
||||
initCharts()
|
||||
}, 100)
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
// 销毁图表实例
|
||||
if (deviceChart) {
|
||||
deviceChart.dispose()
|
||||
}
|
||||
if (healthChart) {
|
||||
healthChart.dispose()
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (trendChart) {
|
||||
trendChart.dispose()
|
||||
}
|
||||
if (pieChart) {
|
||||
pieChart.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stats-row {
|
||||
margin-bottom: 24px;
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-bottom: 24px;
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.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;
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
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;
|
||||
}
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
490
government-admin/src/views/DataCenter.vue
Normal file
490
government-admin/src/views/DataCenter.vue
Normal file
@@ -0,0 +1,490 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>数据览仓</h1>
|
||||
|
||||
<!-- 数据概览卡片 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-card hoverable>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-primary">
|
||||
<span class="iconfont icon-renyuanguanli" style="font-size: 24px;"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">养殖户总数</div>
|
||||
<div class="stat-value">{{养殖户总数}}</div>
|
||||
<div class="stat-change">
|
||||
<span class="text-success">+5.2%</span> 较上月
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card hoverable>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-success">
|
||||
<span class="iconfont icon-niu" style="font-size: 24px;"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">存栏总量</div>
|
||||
<div class="stat-value">{{存栏总量}}</div>
|
||||
<div class="stat-change">
|
||||
<span class="text-success">+2.8%</span> 较上月
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card hoverable>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-warning">
|
||||
<span class="iconfont icon-shengzijiaoyi" style="font-size: 24px;"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">交易量</div>
|
||||
<div class="stat-value">{{交易量}}</div>
|
||||
<div class="stat-change">
|
||||
<span class="text-danger">-1.5%</span> 较上月
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card hoverable>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-danger">
|
||||
<span class="iconfont icon-yonghu" style="font-size: 24px;"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">活跃用户</div>
|
||||
<div class="stat-value">{{活跃用户}}</div>
|
||||
<div class="stat-change">
|
||||
<span class="text-success">+8.3%</span> 较上月
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 主要图表区域 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="养殖区域分布" style="height: 400px;">
|
||||
<div style="height: 340px;" ref="mapChartRef"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="养殖规模分布" style="height: 400px;">
|
||||
<div style="height: 340px;" ref="scaleChartRef"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 第二行图表 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="月度交易量趋势" style="height: 350px;">
|
||||
<div style="height: 290px;" ref="transactionChartRef"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="品类占比分析" style="height: 350px;">
|
||||
<div style="height: 290px;" ref="categoryChartRef"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 最近数据更新信息 -->
|
||||
<a-card title="数据更新信息">
|
||||
<a-timeline mode="alternate">
|
||||
<a-timeline-item color="blue">
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-title">市场行情数据更新</div>
|
||||
<div class="timeline-time">2024-04-10 15:30:00</div>
|
||||
<div class="timeline-desc">更新全国主要地区牛肉、牛奶、饲料价格数据</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
<a-timeline-item color="green">
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-title">养殖户数据同步</div>
|
||||
<div class="timeline-time">2024-04-10 10:15:00</div>
|
||||
<div class="timeline-desc">新增56家养殖户信息,更新32家养殖户状态</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
<a-timeline-item color="orange">
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-title">产品认证数据导入</div>
|
||||
<div class="timeline-time">2024-04-09 16:45:00</div>
|
||||
<div class="timeline-desc">导入120条生资产品认证信息</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
<a-timeline-item color="red">
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-title">疫病监测数据更新</div>
|
||||
<div class="timeline-time">2024-04-09 09:20:00</div>
|
||||
<div class="timeline-desc">更新本周疫病监测数据,暂无异常情况</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据导出功能 -->
|
||||
<a-card title="数据导出" style="margin-top: 16px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 16px 0;">
|
||||
<div>
|
||||
<a-select v-model:value="exportDataType" style="width: 200px; margin-right: 16px;">
|
||||
<a-select-option value="market_price">市场行情数据</a-select-option>
|
||||
<a-select-option value="farmer_info">养殖户信息</a-select-option>
|
||||
<a-select-option value="transaction_data">交易数据</a-select-option>
|
||||
<a-select-option value="epidemic_data">疫病监测数据</a-select-option>
|
||||
</a-select>
|
||||
<a-input v-model:value="exportDateRange" style="width: 250px;" placeholder="选择日期范围 (YYYY-MM-DD ~ YYYY-MM-DD)"></a-input>
|
||||
</div>
|
||||
<a-button type="primary" danger @click="handleExportData">导出数据</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 数据统计
|
||||
const 养殖户总数 = ref('8,256')
|
||||
const 存栏总量 = ref('426,831头')
|
||||
const 交易量 = ref('¥3.26亿')
|
||||
const 活跃用户 = ref('12,548')
|
||||
|
||||
// 图表引用
|
||||
const mapChartRef = ref(null)
|
||||
const scaleChartRef = ref(null)
|
||||
const transactionChartRef = ref(null)
|
||||
const categoryChartRef = ref(null)
|
||||
|
||||
// 导出数据类型
|
||||
const exportDataType = ref('market_price')
|
||||
const exportDateRange = ref('2024-03-10 ~ 2024-04-10')
|
||||
|
||||
// 初始化养殖区域分布图表
|
||||
const initMapChart = () => {
|
||||
if (!mapChartRef.value) return
|
||||
|
||||
const chart = echarts.init(mapChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: ['东北地区', '华北地区', '华东地区', '华南地区', '西南地区', '西北地区']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '养殖区域分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{ value: 28, name: '东北地区' },
|
||||
{ value: 22, name: '华北地区' },
|
||||
{ value: 18, name: '华东地区' },
|
||||
{ value: 12, name: '华南地区' },
|
||||
{ value: 15, name: '西南地区' },
|
||||
{ value: 5, name: '西北地区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化养殖规模分布图表
|
||||
const initScaleChart = () => {
|
||||
if (!scaleChartRef.value) return
|
||||
|
||||
const chart = echarts.init(scaleChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['小型养殖场(<50头)', '中型养殖场(50-200头)', '大型养殖场(>200头)']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '养殖场数量',
|
||||
type: 'bar',
|
||||
barWidth: '60%',
|
||||
data: [6850, 1250, 156],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#83bff6' },
|
||||
{ offset: 0.5, color: '#188df0' },
|
||||
{ offset: 1, color: '#188df0' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化交易量趋势图表
|
||||
const initTransactionChart = () => {
|
||||
if (!transactionChartRef.value) return
|
||||
|
||||
const chart = echarts.init(transactionChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '交易额(亿元)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '交易量',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [2.8, 3.1, 3.5, 3.26, null, null],
|
||||
itemStyle: {
|
||||
color: '#52c41a'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3
|
||||
},
|
||||
symbol: 'circle',
|
||||
symbolSize: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化品类占比图表
|
||||
const initCategoryChart = () => {
|
||||
if (!categoryChartRef.value) return
|
||||
|
||||
const chart = echarts.init(categoryChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '品类占比',
|
||||
type: 'pie',
|
||||
radius: '65%',
|
||||
center: ['50%', '40%'],
|
||||
data: [
|
||||
{ value: 45, name: '肉牛养殖' },
|
||||
{ value: 30, name: '奶牛养殖' },
|
||||
{ value: 15, name: '犊牛养殖' },
|
||||
{ value: 10, name: '其他养殖' }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 导出数据处理函数
|
||||
const handleExportData = () => {
|
||||
console.log('导出数据类型:', exportDataType.value)
|
||||
console.log('导出日期范围:', exportDateRange.value)
|
||||
// 这里应该有实际的导出逻辑
|
||||
|
||||
// 显示导出成功提示
|
||||
const message = `成功导出${exportDataType.value === 'market_price' ? '市场行情' :
|
||||
exportDataType.value === 'farmer_info' ? '养殖户信息' :
|
||||
exportDataType.value === 'transaction_data' ? '交易数据' : '疫病监测数据'}数据`
|
||||
|
||||
// 在实际项目中,这里应该使用Ant Design的message组件
|
||||
alert(message)
|
||||
}
|
||||
|
||||
// 组件挂载时初始化所有图表
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initMapChart()
|
||||
initScaleChart()
|
||||
initTransactionChart()
|
||||
initCategoryChart()
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-icon-primary {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
}
|
||||
|
||||
.stat-icon-success {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
}
|
||||
|
||||
.stat-icon-warning {
|
||||
background: linear-gradient(135deg, #fa8c16 0%, #ffa940 100%);
|
||||
}
|
||||
|
||||
.stat-icon-danger {
|
||||
background: linear-gradient(135deg, #f5222d 0%, #ff4d4f 100%);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.timeline-desc {
|
||||
font-size: 13px;
|
||||
color: #595959;
|
||||
}
|
||||
</style>
|
||||
436
government-admin/src/views/EpidemicManagement.vue
Normal file
436
government-admin/src/views/EpidemicManagement.vue
Normal file
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>疫情管理</h1>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ epidemicStats.vaccinated }}</div>
|
||||
<div class="stat-label">累计接种人数</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ epidemicStats.tested }}</div>
|
||||
<div class="stat-label">累计检测次数</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ currentCases.confirmed }}</div>
|
||||
<div class="stat-label">当前确诊</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ currentCases.observed }}</div>
|
||||
<div class="stat-label">隔离观察</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row gutter={24} style="margin-top: 24px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="疫情趋势" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="epidemic-trend-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="区域分布" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="area-distribution-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 数据过滤器和表格 -->
|
||||
<a-card title="疫情数据记录" style="margin-top: 24px;">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-row gutter={16}>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filters.region" placeholder="选择地区" style="width: 100%;">
|
||||
<a-select-option value="all">全部地区</a-select-option>
|
||||
<a-select-option value="east">东区</a-select-option>
|
||||
<a-select-option value="west">西区</a-select-option>
|
||||
<a-select-option value="south">南区</a-select-option>
|
||||
<a-select-option value="north">北区</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filters.type" placeholder="选择数据类型" style="width: 100%;">
|
||||
<a-select-option value="all">全部类型</a-select-option>
|
||||
<a-select-option value="confirmed">确诊病例</a-select-option>
|
||||
<a-select-option value="suspected">疑似病例</a-select-option>
|
||||
<a-select-option value="observed">隔离观察</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-range-picker v-model:value="filters.dateRange" style="width: 100%;" />
|
||||
</a-col>
|
||||
<a-col :span="6" style="text-align: right;">
|
||||
<a-button type="primary" @click="searchData">查询</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="resetFilters">重置</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<a-table :columns="epidemicColumns" :data-source="epidemicData" :pagination="{ pageSize: 10 }">
|
||||
<template #bodyCell:type="{ record }">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #bodyCell:action="{ record }">
|
||||
<a-button type="link" @click="viewEpidemicDetail(record.id)">查看详情</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'antd'
|
||||
import axios from 'axios'
|
||||
import * as echarts from 'echarts'
|
||||
import { getEpidemicStats } from '@/mock'
|
||||
|
||||
const epidemicStats = ref({
|
||||
vaccinated: 0,
|
||||
tested: 0
|
||||
})
|
||||
const currentCases = ref({
|
||||
confirmed: 0,
|
||||
observed: 0,
|
||||
suspected: 0
|
||||
})
|
||||
const epidemicData = ref([])
|
||||
const filters = ref({
|
||||
region: 'all',
|
||||
type: 'all',
|
||||
dateRange: []
|
||||
})
|
||||
let trendChart = null
|
||||
let distributionChart = null
|
||||
|
||||
// 疫情数据表格列定义
|
||||
const epidemicColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '地区',
|
||||
dataIndex: 'region',
|
||||
key: 'region'
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
slots: { customRender: 'type' }
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'count',
|
||||
key: 'count'
|
||||
},
|
||||
{
|
||||
title: '报告时间',
|
||||
dataIndex: 'report_time',
|
||||
key: 'report_time'
|
||||
},
|
||||
{
|
||||
title: '报告人',
|
||||
dataIndex: 'reporter',
|
||||
key: 'reporter'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
slots: { customRender: 'action' }
|
||||
}
|
||||
]
|
||||
|
||||
// 根据类型获取标签颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
confirmed: 'red',
|
||||
suspected: 'orange',
|
||||
observed: 'blue'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
// 根据类型获取显示文本
|
||||
const getTypeText = (type) => {
|
||||
const textMap = {
|
||||
confirmed: '确诊病例',
|
||||
suspected: '疑似病例',
|
||||
observed: '隔离观察'
|
||||
}
|
||||
return textMap[type] || type
|
||||
}
|
||||
|
||||
// 获取疫情统计数据
|
||||
const fetchEpidemicStats = async () => {
|
||||
try {
|
||||
// 尝试从API获取数据
|
||||
const response = await axios.get('/api/epidemic/stats')
|
||||
if (response.data.code === 200) {
|
||||
const data = response.data.data
|
||||
epidemicStats.value.vaccinated = data.vaccinated || 0
|
||||
epidemicStats.value.tested = data.tested || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取疫情统计数据失败,使用模拟数据:', error)
|
||||
// 使用模拟数据
|
||||
const mockResponse = await getEpidemicStats()
|
||||
if (mockResponse.code === 200) {
|
||||
const data = mockResponse.data
|
||||
epidemicStats.value.vaccinated = data.vaccinated || 0
|
||||
epidemicStats.value.tested = data.tested || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 设置其他模拟数据
|
||||
currentCases.value.confirmed = Math.floor(Math.random() * 50)
|
||||
currentCases.value.observed = Math.floor(Math.random() * 200) + 50
|
||||
currentCases.value.suspected = Math.floor(Math.random() * 30)
|
||||
}
|
||||
|
||||
// 获取疫情记录数据
|
||||
const fetchEpidemicData = async () => {
|
||||
try {
|
||||
// 这里应该从API获取数据
|
||||
// 由于没有实际API,使用模拟数据
|
||||
epidemicData.value = [
|
||||
{ id: 1, region: '东区', type: 'confirmed', count: 15, report_time: '2024-01-10 09:00:00', reporter: '张三' },
|
||||
{ id: 2, region: '西区', type: 'confirmed', count: 8, report_time: '2024-01-10 10:30:00', reporter: '李四' },
|
||||
{ id: 3, region: '南区', type: 'suspected', count: 12, report_time: '2024-01-09 14:20:00', reporter: '王五' },
|
||||
{ id: 4, region: '北区', type: 'observed', count: 60, report_time: '2024-01-09 16:45:00', reporter: '赵六' },
|
||||
{ id: 5, region: '东区', type: 'observed', count: 45, report_time: '2024-01-08 08:30:00', reporter: '钱七' },
|
||||
{ id: 6, region: '西区', type: 'suspected', count: 7, report_time: '2024-01-08 11:15:00', reporter: '孙八' },
|
||||
{ id: 7, region: '南区', type: 'confirmed', count: 10, report_time: '2024-01-07 14:50:00', reporter: '周九' },
|
||||
{ id: 8, region: '北区', type: 'observed', count: 52, report_time: '2024-01-07 17:20:00', reporter: '吴十' },
|
||||
{ id: 9, region: '东区', type: 'suspected', count: 9, report_time: '2024-01-06 09:40:00', reporter: '郑一' },
|
||||
{ id: 10, region: '西区', type: 'confirmed', count: 6, report_time: '2024-01-06 13:30:00', reporter: '王二' }
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('获取疫情记录数据失败:', error)
|
||||
message.error('获取疫情记录数据失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 疫情趋势图
|
||||
const trendChartDom = document.getElementById('epidemic-trend-chart')
|
||||
if (trendChartDom) {
|
||||
trendChart = echarts.init(trendChartDom)
|
||||
const trendOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['确诊病例', '疑似病例', '隔离观察']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '确诊病例',
|
||||
type: 'line',
|
||||
data: [12, 19, 15, 10, 8, 12],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#ff4d4f'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '疑似病例',
|
||||
type: 'line',
|
||||
data: [8, 15, 10, 7, 5, 8],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#fa8c16'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '隔离观察',
|
||||
type: 'line',
|
||||
data: [45, 60, 50, 35, 40, 45],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#1890ff'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
trendChart.setOption(trendOption)
|
||||
}
|
||||
|
||||
// 区域分布图
|
||||
const distributionChartDom = document.getElementById('area-distribution-chart')
|
||||
if (distributionChartDom) {
|
||||
distributionChart = echarts.init(distributionChartDom)
|
||||
const distributionOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '区域分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 30,
|
||||
name: '东区'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '西区'
|
||||
},
|
||||
{
|
||||
value: 22,
|
||||
name: '南区'
|
||||
},
|
||||
{
|
||||
value: 23,
|
||||
name: '北区'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
distributionChart.setOption(distributionOption)
|
||||
}
|
||||
}
|
||||
|
||||
// 响应窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (trendChart) {
|
||||
trendChart.resize()
|
||||
}
|
||||
if (distributionChart) {
|
||||
distributionChart.resize()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索数据
|
||||
const searchData = () => {
|
||||
message.info('根据筛选条件查询数据')
|
||||
// 这里应该根据筛选条件重新请求数据
|
||||
// 目前使用模拟数据,实际项目中需要调用API
|
||||
}
|
||||
|
||||
// 重置筛选条件
|
||||
const resetFilters = () => {
|
||||
filters.value = {
|
||||
region: 'all',
|
||||
type: 'all',
|
||||
dateRange: []
|
||||
}
|
||||
}
|
||||
|
||||
// 查看疫情详情
|
||||
const viewEpidemicDetail = (id) => {
|
||||
message.info(`查看ID: ${id} 的疫情详情`)
|
||||
// 这里可以跳转到详情页面
|
||||
// router.push(`/epidemic/detail/${id}`)
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
fetchEpidemicStats()
|
||||
fetchEpidemicData()
|
||||
setTimeout(() => {
|
||||
initCharts()
|
||||
}, 100)
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (trendChart) {
|
||||
trendChart.dispose()
|
||||
}
|
||||
if (distributionChart) {
|
||||
distributionChart.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
708
government-admin/src/views/FarmerManagement.vue
Normal file
708
government-admin/src/views/FarmerManagement.vue
Normal file
@@ -0,0 +1,708 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>养殖户管理</h1>
|
||||
|
||||
<!-- 搜索和操作栏 -->
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: center;">
|
||||
<a-input v-model:value="searchKeyword" placeholder="输入养殖户名称或负责人姓名" style="width: 250px;">
|
||||
<template #prefix>
|
||||
<span class="iconfont icon-sousuo"></span>
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-select v-model:value="statusFilter" placeholder="选择状态" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="active">正常经营</a-select-option>
|
||||
<a-select-option value="inactive">暂停经营</a-select-option>
|
||||
<a-select-option value="closed">已关闭</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select v-model:value="scaleFilter" placeholder="养殖规模" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="small">小型(<50头)</a-select-option>
|
||||
<a-select-option value="medium">中型(50-200头)</a-select-option>
|
||||
<a-select-option value="large">大型(>200头)</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select v-model:value="regionFilter" placeholder="所在地区" style="width: 150px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="northeast">东北地区</a-select-option>
|
||||
<a-select-option value="north">华北地区</a-select-option>
|
||||
<a-select-option value="east">华东地区</a-select-option>
|
||||
<a-select-option value="south">华南地区</a-select-option>
|
||||
<a-select-option value="southwest">西南地区</a-select-option>
|
||||
<a-select-option value="northwest">西北地区</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-button type="primary" @click="handleSearch" style="margin-left: auto;">
|
||||
<span class="iconfont icon-sousuo"></span> 搜索
|
||||
</a-button>
|
||||
|
||||
<a-button type="default" @click="handleReset">重置</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleImport">
|
||||
<span class="iconfont icon-daoru"></span> 导入
|
||||
</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleExport">
|
||||
<span class="iconfont icon-daochu"></span> 导出
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" danger @click="handleAddFarmer">
|
||||
<span class="iconfont icon-tianjia"></span> 新增养殖户
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="总养殖户数" :value="totalFarmers" suffix="家" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="正常经营" :value="activeFarmers" suffix="家" :valueStyle="{ color: '#3f8600' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="暂停经营" :value="inactiveFarmers" suffix="家" :valueStyle="{ color: '#faad14' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="已关闭" :value="closedFarmers" suffix="家" :valueStyle="{ color: '#cf1322' }" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 养殖户列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="farmersData"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 养殖规模列 -->
|
||||
<template #bodyCell:scale="{ record }">
|
||||
<span>{{ getScaleText(record.scale) }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #bodyCell:action="{ record }">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a-button size="small" @click="handleView(record)">查看</a-button>
|
||||
<a-button size="small" type="primary" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button size="small" danger @click="handleDelete(record.id)">删除</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑养殖户模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isAddEditModalOpen"
|
||||
title="新增/编辑养殖户"
|
||||
:footer="null"
|
||||
width={800}
|
||||
>
|
||||
<a-form
|
||||
:model="currentFarmer"
|
||||
layout="vertical"
|
||||
style="max-width: 600px; margin: 0 auto;"
|
||||
>
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖户名称" name="name" :rules="[{ required: true, message: '请输入养殖户名称' }]">
|
||||
<a-input v-model:value="currentFarmer.name" placeholder="请输入养殖户名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="统一社会信用代码" name="creditCode" :rules="[{ required: true, message: '请输入统一社会信用代码' }]">
|
||||
<a-input v-model:value="currentFarmer.creditCode" placeholder="请输入统一社会信用代码" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="负责人姓名" name="manager" :rules="[{ required: true, message: '请输入负责人姓名' }]">
|
||||
<a-input v-model:value="currentFarmer.manager" placeholder="请输入负责人姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="phone" :rules="[{ required: true, message: '请输入联系电话' }]">
|
||||
<a-input v-model:value="currentFarmer.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所在地区" name="region" :rules="[{ required: true, message: '请选择所在地区' }]">
|
||||
<a-select v-model:value="currentFarmer.region" placeholder="请选择所在地区">
|
||||
<a-select-option value="northeast">东北地区</a-select-option>
|
||||
<a-select-option value="north">华北地区</a-select-option>
|
||||
<a-select-option value="east">华东地区</a-select-option>
|
||||
<a-select-option value="south">华南地区</a-select-option>
|
||||
<a-select-option value="southwest">西南地区</a-select-option>
|
||||
<a-select-option value="northwest">西北地区</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="详细地址" name="address" :rules="[{ required: true, message: '请输入详细地址' }]">
|
||||
<a-input v-model:value="currentFarmer.address" placeholder="请输入详细地址" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖规模" name="scale" :rules="[{ required: true, message: '请选择养殖规模' }]">
|
||||
<a-select v-model:value="currentFarmer.scale" placeholder="请选择养殖规模">
|
||||
<a-select-option value="small">小型(<50头)</a-select-option>
|
||||
<a-select-option value="medium">中型(50-200头)</a-select-option>
|
||||
<a-select-option value="large">大型(>200头)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="存栏数量" name="stockQuantity" :rules="[{ required: true, message: '请输入存栏数量' }]">
|
||||
<a-input-number v-model:value="currentFarmer.stockQuantity" :min="0" placeholder="请输入存栏数量" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="养殖类型" name="farmType">
|
||||
<a-checkbox-group v-model:value="currentFarmer.farmType">
|
||||
<a-checkbox value="beef">肉牛养殖</a-checkbox>
|
||||
<a-checkbox value="milk">奶牛养殖</a-checkbox>
|
||||
<a-checkbox value="calf">犊牛养殖</a-checkbox>
|
||||
<a-checkbox value="other">其他养殖</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="经营状态" name="status" :rules="[{ required: true, message: '请选择经营状态' }]">
|
||||
<a-radio-group v-model:value="currentFarmer.status">
|
||||
<a-radio value="active">正常经营</a-radio>
|
||||
<a-radio value="inactive">暂停经营</a-radio>
|
||||
<a-radio value="closed">已关闭</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注信息" name="remark">
|
||||
<a-input.TextArea v-model:value="currentFarmer.remark" placeholder="请输入备注信息" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 16px;">
|
||||
<a-button @click="isAddEditModalOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看养殖户详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isViewModalOpen"
|
||||
title="养殖户详情"
|
||||
:footer="null"
|
||||
width={800}
|
||||
>
|
||||
<div v-if="viewFarmer" class="farmer-detail">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">养殖户名称:</div>
|
||||
<div class="detail-value">{{ viewFarmer.name }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">统一社会信用代码:</div>
|
||||
<div class="detail-value">{{ viewFarmer.creditCode }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">负责人姓名:</div>
|
||||
<div class="detail-value">{{ viewFarmer.manager }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">联系电话:</div>
|
||||
<div class="detail-value">{{ viewFarmer.phone }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">所在地区:</div>
|
||||
<div class="detail-value">{{ getRegionText(viewFarmer.region) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">详细地址:</div>
|
||||
<div class="detail-value">{{ viewFarmer.address }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">养殖规模:</div>
|
||||
<div class="detail-value">{{ getScaleText(viewFarmer.scale) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">存栏数量:</div>
|
||||
<div class="detail-value">{{ viewFarmer.stockQuantity }}头</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">养殖类型:</div>
|
||||
<div class="detail-value">{{ getFarmTypeText(viewFarmer.farmType) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">经营状态:</div>
|
||||
<div class="detail-value">
|
||||
<a-tag :color="getStatusColor(viewFarmer.status)">{{ getStatusText(viewFarmer.status) }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">创建时间:</div>
|
||||
<div class="detail-value">{{ viewFarmer.createTime }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">备注信息:</div>
|
||||
<div class="detail-value">{{ viewFarmer.remark || '无' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 24px;">
|
||||
<a-button @click="isViewModalOpen = false">关闭</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
// 搜索条件
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref('')
|
||||
const scaleFilter = ref('')
|
||||
const regionFilter = ref('')
|
||||
|
||||
// 统计数据
|
||||
const totalFarmers = ref(1256)
|
||||
const activeFarmers = ref(986)
|
||||
const inactiveFarmers = ref(158)
|
||||
const closedFarmers = ref(112)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 选中的行
|
||||
const selectedRowKeys = ref([])
|
||||
const onSelectChange = (newSelectedRowKeys) => {
|
||||
selectedRowKeys.value = newSelectedRowKeys
|
||||
}
|
||||
|
||||
// 模态框状态
|
||||
const isAddEditModalOpen = ref(false)
|
||||
const isViewModalOpen = ref(false)
|
||||
|
||||
// 当前编辑的养殖户
|
||||
const currentFarmer = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
creditCode: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
region: '',
|
||||
address: '',
|
||||
scale: '',
|
||||
stockQuantity: 0,
|
||||
farmType: [],
|
||||
status: 'active',
|
||||
remark: '',
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
// 查看的养殖户
|
||||
const viewFarmer = ref(null)
|
||||
|
||||
// 养殖户列表数据
|
||||
const farmersData = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: '瑞丰养殖场',
|
||||
creditCode: '91370100MA3C8Y6D6X',
|
||||
manager: '张明',
|
||||
phone: '13800138001',
|
||||
region: 'north',
|
||||
address: '北京市海淀区中关村南大街5号',
|
||||
scale: 'large',
|
||||
stockQuantity: 580,
|
||||
farmType: ['beef', 'milk'],
|
||||
status: 'active',
|
||||
remark: '市级重点养殖企业',
|
||||
createTime: '2020-01-15 10:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '绿源生态养殖合作社',
|
||||
creditCode: '93370100MA3C8Y6D6X',
|
||||
manager: '李华',
|
||||
phone: '13900139001',
|
||||
region: 'east',
|
||||
address: '上海市浦东新区张江高科技园区博云路2号',
|
||||
scale: 'medium',
|
||||
stockQuantity: 150,
|
||||
farmType: ['beef'],
|
||||
status: 'active',
|
||||
remark: '生态养殖示范基地',
|
||||
createTime: '2020-02-20 14:20:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '金牛养殖场',
|
||||
creditCode: '91370100MA3C8Y6D6Y',
|
||||
manager: '王强',
|
||||
phone: '13700137001',
|
||||
region: 'south',
|
||||
address: '广州市天河区天河路385号',
|
||||
scale: 'small',
|
||||
stockQuantity: 30,
|
||||
farmType: ['milk'],
|
||||
status: 'inactive',
|
||||
remark: '暂时休业整顿',
|
||||
createTime: '2020-03-05 09:15:00'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '草原牧歌养殖公司',
|
||||
creditCode: '91370100MA3C8Y6D6Z',
|
||||
manager: '赵芳',
|
||||
phone: '13600136001',
|
||||
region: 'northwest',
|
||||
address: '西安市雁塔区科技路25号',
|
||||
scale: 'large',
|
||||
stockQuantity: 850,
|
||||
farmType: ['beef', 'calf'],
|
||||
status: 'active',
|
||||
remark: '省级农业产业化龙头企业',
|
||||
createTime: '2020-04-10 11:45:00'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '祥和养殖场',
|
||||
creditCode: '91370100MA3C8Y6D6A',
|
||||
manager: '孙建国',
|
||||
phone: '13500135001',
|
||||
region: 'southwest',
|
||||
address: '成都市武侯区一环路南四段1号',
|
||||
scale: 'medium',
|
||||
stockQuantity: 120,
|
||||
farmType: ['beef'],
|
||||
status: 'closed',
|
||||
remark: '经营不善已关闭',
|
||||
createTime: '2020-05-15 16:00:00'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: '福满家养殖园',
|
||||
creditCode: '91370100MA3C8Y6D6B',
|
||||
manager: '周小丽',
|
||||
phone: '13400134001',
|
||||
region: 'northeast',
|
||||
address: '沈阳市沈河区青年大街100号',
|
||||
scale: 'small',
|
||||
stockQuantity: 40,
|
||||
farmType: ['milk', 'other'],
|
||||
status: 'active',
|
||||
remark: '家庭农场示范户',
|
||||
createTime: '2020-06-20 13:30:00'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: '康源养殖有限公司',
|
||||
creditCode: '91370100MA3C8Y6D6C',
|
||||
manager: '吴大山',
|
||||
phone: '13300133001',
|
||||
region: 'east',
|
||||
address: '南京市鼓楼区中山北路1号',
|
||||
scale: 'large',
|
||||
stockQuantity: 620,
|
||||
farmType: ['beef', 'milk', 'calf'],
|
||||
status: 'active',
|
||||
remark: '现代化养殖企业',
|
||||
createTime: '2020-07-25 10:15:00'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: '田园牧歌生态养殖',
|
||||
creditCode: '91370100MA3C8Y6D6D',
|
||||
manager: '郑小华',
|
||||
phone: '13200132001',
|
||||
region: 'north',
|
||||
address: '石家庄市桥西区自强路22号',
|
||||
scale: 'medium',
|
||||
stockQuantity: 95,
|
||||
farmType: ['beef'],
|
||||
status: 'inactive',
|
||||
remark: '设备升级中',
|
||||
createTime: '2020-08-30 15:45:00'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: '龙凤养殖场',
|
||||
creditCode: '91370100MA3C8Y6D6E',
|
||||
manager: '钱小红',
|
||||
phone: '13100131001',
|
||||
region: 'south',
|
||||
address: '深圳市南山区科技园南区',
|
||||
scale: 'small',
|
||||
stockQuantity: 25,
|
||||
farmType: ['milk'],
|
||||
status: 'active',
|
||||
remark: '特色奶制品供应商',
|
||||
createTime: '2020-09-05 09:30:00'
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
name: '远大养殖集团',
|
||||
creditCode: '91370100MA3C8Y6D6F',
|
||||
manager: '陈明亮',
|
||||
phone: '13000130001',
|
||||
region: 'east',
|
||||
address: '杭州市西湖区文三路478号',
|
||||
scale: 'large',
|
||||
stockQuantity: 980,
|
||||
farmType: ['beef', 'calf'],
|
||||
status: 'active',
|
||||
remark: '国家级农业龙头企业',
|
||||
createTime: '2020-10-10 14:20:00'
|
||||
}
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '养殖户名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'manager',
|
||||
key: 'manager',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '所在地区',
|
||||
dataIndex: 'region',
|
||||
key: 'region',
|
||||
width: 100,
|
||||
customRender: ({ text }) => getRegionText(text)
|
||||
},
|
||||
{
|
||||
title: '养殖规模',
|
||||
dataIndex: 'scale',
|
||||
key: 'scale',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '存栏数量',
|
||||
dataIndex: 'stockQuantity',
|
||||
key: 'stockQuantity',
|
||||
width: 100,
|
||||
customRender: ({ text }) => `${text}头`
|
||||
},
|
||||
{
|
||||
title: '经营状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return 'green'
|
||||
case 'inactive': return 'orange'
|
||||
case 'closed': return 'red'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return '正常经营'
|
||||
case 'inactive': return '暂停经营'
|
||||
case 'closed': return '已关闭'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
// 获取规模文本
|
||||
const getScaleText = (scale) => {
|
||||
switch (scale) {
|
||||
case 'small': return '小型(<50头)'
|
||||
case 'medium': return '中型(50-200头)'
|
||||
case 'large': return '大型(>200头)'
|
||||
default: return scale
|
||||
}
|
||||
}
|
||||
|
||||
// 获取地区文本
|
||||
const getRegionText = (region) => {
|
||||
switch (region) {
|
||||
case 'northeast': return '东北地区'
|
||||
case 'north': return '华北地区'
|
||||
case 'east': return '华东地区'
|
||||
case 'south': return '华南地区'
|
||||
case 'southwest': return '西南地区'
|
||||
case 'northwest': return '西北地区'
|
||||
default: return region
|
||||
}
|
||||
}
|
||||
|
||||
// 获取养殖类型文本
|
||||
const getFarmTypeText = (farmTypes) => {
|
||||
if (!farmTypes || farmTypes.length === 0) return '无'
|
||||
const typeMap = {
|
||||
beef: '肉牛养殖',
|
||||
milk: '奶牛养殖',
|
||||
calf: '犊牛养殖',
|
||||
other: '其他养殖'
|
||||
}
|
||||
return farmTypes.map(type => typeMap[type] || type).join('、')
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索条件:', {
|
||||
keyword: searchKeyword.value,
|
||||
status: statusFilter.value,
|
||||
scale: scaleFilter.value,
|
||||
region: regionFilter.value
|
||||
})
|
||||
// 这里应该有实际的搜索逻辑
|
||||
// 模拟搜索后的总数
|
||||
pagination.total = farmersData.value.length
|
||||
}
|
||||
|
||||
// 重置处理
|
||||
const handleReset = () => {
|
||||
searchKeyword.value = ''
|
||||
statusFilter.value = ''
|
||||
scaleFilter.value = ''
|
||||
regionFilter.value = ''
|
||||
selectedRowKeys.value = []
|
||||
}
|
||||
|
||||
// 导入处理
|
||||
const handleImport = () => {
|
||||
console.log('导入养殖户数据')
|
||||
// 这里应该有实际的导入逻辑
|
||||
}
|
||||
|
||||
// 导出处理
|
||||
const handleExport = () => {
|
||||
console.log('导出养殖户数据')
|
||||
// 这里应该有实际的导出逻辑
|
||||
}
|
||||
|
||||
// 新增养殖户
|
||||
const handleAddFarmer = () => {
|
||||
// 重置当前养殖户数据
|
||||
Object.assign(currentFarmer, {
|
||||
id: '',
|
||||
name: '',
|
||||
creditCode: '',
|
||||
manager: '',
|
||||
phone: '',
|
||||
region: '',
|
||||
address: '',
|
||||
scale: '',
|
||||
stockQuantity: 0,
|
||||
farmType: [],
|
||||
status: 'active',
|
||||
remark: '',
|
||||
createTime: ''
|
||||
})
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 编辑养殖户
|
||||
const handleEdit = (record) => {
|
||||
// 复制记录数据到当前养殖户
|
||||
Object.assign(currentFarmer, JSON.parse(JSON.stringify(record)))
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 查看养殖户
|
||||
const handleView = (record) => {
|
||||
viewFarmer.value = JSON.parse(JSON.stringify(record))
|
||||
isViewModalOpen.value = true
|
||||
}
|
||||
|
||||
// 删除养殖户
|
||||
const handleDelete = (id) => {
|
||||
console.log('删除养殖户:', id)
|
||||
// 这里应该有实际的删除逻辑和确认提示
|
||||
// 模拟删除成功
|
||||
alert(`成功删除养殖户ID: ${id}`)
|
||||
}
|
||||
|
||||
// 保存养殖户
|
||||
const handleSave = () => {
|
||||
console.log('保存养殖户:', currentFarmer)
|
||||
// 这里应该有实际的保存逻辑
|
||||
// 模拟保存成功
|
||||
isAddEditModalOpen.value = false
|
||||
alert('保存成功')
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
pagination.total = farmersData.value.length
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.farmer-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
270
government-admin/src/views/FileManagement.vue
Normal file
270
government-admin/src/views/FileManagement.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>文件管理</h1>
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-upload-dragger
|
||||
name="file"
|
||||
:multiple="true"
|
||||
:action="uploadUrl"
|
||||
@change="handleUploadChange"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||
<p class="ant-upload-hint">
|
||||
支持单个或批量上传,文件大小不超过100MB
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="上传设置" :body-style="{ padding: '20px' }">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="文件分类">
|
||||
<a-select v-model:value="fileCategory">
|
||||
<a-select-option value="policy">政策文件</a-select-option>
|
||||
<a-select-option value="report">报表文件</a-select-option>
|
||||
<a-select-option value="notice">通知文件</a-select-option>
|
||||
<a-select-option value="other">其他文件</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="文件描述">
|
||||
<a-input v-model:value="fileDescription" placeholder="请输入文件描述" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleUploadConfirm">确认上传</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<a-card title="文件列表">
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-input v-model:value="searchKeyword" placeholder="搜索文件名" allow-clear />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filterCategory" placeholder="筛选分类" allow-clear>
|
||||
<a-select-option value="policy">政策文件</a-select-option>
|
||||
<a-select-option value="report">报表文件</a-select-option>
|
||||
<a-select-option value="notice">通知文件</a-select-option>
|
||||
<a-select-option value="other">其他文件</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-date-picker v-model:value="uploadDateRange" style="width: 100%;" />
|
||||
</a-col>
|
||||
<a-col :span="6" style="text-align: right;">
|
||||
<a-button type="primary" @click="handleSearch">搜索</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleReset">重置</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table :columns="fileColumns" :data-source="fileList" :pagination="pagination" row-key="id">
|
||||
<template #bodyCell:name="{ record }">
|
||||
<a @click="handleFileDownload(record)">{{ record.name }}</a>
|
||||
</template>
|
||||
<template #bodyCell:size="{ record }">
|
||||
<span>{{ formatFileSize(record.size) }}</span>
|
||||
</template>
|
||||
<template #bodyCell:action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleFilePreview(record)">预览</a-button>
|
||||
<a-button type="link" @click="handleFileDownload(record)">下载</a-button>
|
||||
<a-button type="link" danger @click="handleFileDelete(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = '/api/files/upload'
|
||||
const fileCategory = ref('')
|
||||
const fileDescription = ref('')
|
||||
|
||||
// 搜索和筛选
|
||||
const searchKeyword = ref('')
|
||||
const filterCategory = ref('')
|
||||
const uploadDateRange = ref(null)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 文件列表数据
|
||||
const fileList = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: '2024年第一季度工作报告.pdf',
|
||||
size: 2097152,
|
||||
type: 'pdf',
|
||||
category: 'report',
|
||||
uploadTime: '2024-04-01T08:30:00Z',
|
||||
uploader: '张三',
|
||||
downloadCount: 12
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '关于加强养殖场监管的通知.docx',
|
||||
size: 1048576,
|
||||
type: 'docx',
|
||||
category: 'notice',
|
||||
uploadTime: '2024-03-28T14:15:00Z',
|
||||
uploader: '李四',
|
||||
downloadCount: 35
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '动物防疫政策解读.pdf',
|
||||
size: 3145728,
|
||||
type: 'pdf',
|
||||
category: 'policy',
|
||||
uploadTime: '2024-03-15T10:45:00Z',
|
||||
uploader: '王五',
|
||||
downloadCount: 89
|
||||
}
|
||||
])
|
||||
|
||||
// 文件表格列定义
|
||||
const fileColumns = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '文件类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type'
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
customRender: (text) => {
|
||||
const categoryMap = {
|
||||
policy: '政策文件',
|
||||
report: '报表文件',
|
||||
notice: '通知文件',
|
||||
other: '其他文件'
|
||||
}
|
||||
return categoryMap[text] || text
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
customRender: (text) => formatFileSize(text)
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'uploadTime',
|
||||
key: 'uploadTime',
|
||||
customRender: (text) => dayjs(text).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '上传人',
|
||||
dataIndex: 'uploader',
|
||||
key: 'uploader'
|
||||
},
|
||||
{
|
||||
title: '下载次数',
|
||||
dataIndex: 'downloadCount',
|
||||
key: 'downloadCount'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 上传文件变化处理
|
||||
const handleUploadChange = (info) => {
|
||||
console.log('文件上传变化:', info)
|
||||
}
|
||||
|
||||
// 确认上传
|
||||
const handleUploadConfirm = () => {
|
||||
// 这里应该有实际的上传逻辑
|
||||
console.log('确认上传', { fileCategory: fileCategory.value, fileDescription: fileDescription.value })
|
||||
}
|
||||
|
||||
// 文件搜索
|
||||
const handleSearch = () => {
|
||||
// 这里应该有实际的搜索逻辑
|
||||
console.log('搜索文件', { searchKeyword: searchKeyword.value, filterCategory: filterCategory.value, uploadDateRange: uploadDateRange.value })
|
||||
}
|
||||
|
||||
// 重置搜索条件
|
||||
const handleReset = () => {
|
||||
searchKeyword.value = ''
|
||||
filterCategory.value = ''
|
||||
uploadDateRange.value = null
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
const handleFilePreview = (file) => {
|
||||
console.log('预览文件:', file)
|
||||
}
|
||||
|
||||
// 文件下载
|
||||
const handleFileDownload = (file) => {
|
||||
console.log('下载文件:', file)
|
||||
}
|
||||
|
||||
// 文件删除
|
||||
const handleFileDelete = (id) => {
|
||||
console.log('删除文件:', id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
1089
government-admin/src/views/FinanceInsurance.vue
Normal file
1089
government-admin/src/views/FinanceInsurance.vue
Normal file
File diff suppressed because it is too large
Load Diff
386
government-admin/src/views/LogManagement.vue
Normal file
386
government-admin/src/views/LogManagement.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>日志管理</h1>
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="16">
|
||||
<a-input-search
|
||||
placeholder="搜索操作内容或操作人"
|
||||
allow-clear
|
||||
enter-button="搜索"
|
||||
size="large"
|
||||
style="width: 300px; margin-right: 16px;"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-select v-model:value="filterOperationType" placeholder="筛选操作类型" allow-clear style="width: 150px; margin-right: 16px;">
|
||||
<a-select-option value="login">登录</a-select-option>
|
||||
<a-select-option value="logout">登出</a-select-option>
|
||||
<a-select-option value="create">创建</a-select-option>
|
||||
<a-select-option value="update">更新</a-select-option>
|
||||
<a-select-option value="delete">删除</a-select-option>
|
||||
<a-select-option value="query">查询</a-select-option>
|
||||
<a-select-option value="export">导出</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterModule" placeholder="筛选模块" allow-clear style="width: 150px; margin-right: 16px;">
|
||||
<a-select-option value="user">用户管理</a-select-option>
|
||||
<a-select-option value="epidemic">疫情管理</a-select-option>
|
||||
<a-select-option value="supervision">监管管理</a-select-option>
|
||||
<a-select-option value="file">文件管理</a-select-option>
|
||||
<a-select-option value="warehouse">仓库管理</a-select-option>
|
||||
<a-select-option value="service">服务管理</a-select-option>
|
||||
<a-select-option value="personnel">人员管理</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterStatus" placeholder="筛选状态" allow-clear style="width: 120px;">
|
||||
<a-select-option value="success">成功</a-select-option>
|
||||
<a-select-option value="fail">失败</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="8" style="text-align: right;">
|
||||
<a-range-picker v-model:value="dateRange" format="YYYY-MM-DD" @change="handleDateChange" style="width: 280px; margin-right: 16px;" />
|
||||
<a-button type="primary" @click="handleExportLogs">导出日志</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 日志统计图表 -->
|
||||
<a-card title="操作统计" style="margin-bottom: 16px;">
|
||||
<div style="height: 300px;" ref="chartRef"></div>
|
||||
</a-card>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<a-card title="日志列表">
|
||||
<a-table :columns="logColumns" :data-source="logList" :pagination="pagination" row-key="id">
|
||||
<template #bodyCell:operationType="{ record }">
|
||||
<a-tag :color="getOperationTypeColor(record.operationType)">{{ getOperationTypeLabel(record.operationType) }}</a-tag>
|
||||
</template>
|
||||
<template #bodyCell:module="{ record }">
|
||||
<a-tag color="blue">{{ getModuleLabel(record.module) }}</a-tag>
|
||||
</template>
|
||||
<template #bodyCell:status="{ record }">
|
||||
<span :class="getStatusClass(record.status)">{{ record.status === 'success' ? '成功' : '失败' }}</span>
|
||||
</template>
|
||||
<template #bodyCell:time="{ record }">
|
||||
{{ dayjs(record.time).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 日期范围
|
||||
const dateRange = ref([dayjs().subtract(7, 'day'), dayjs()])
|
||||
|
||||
// 筛选条件
|
||||
const filterOperationType = ref('')
|
||||
const filterModule = ref('')
|
||||
const filterStatus = ref('')
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 日志列表数据
|
||||
const logList = ref([
|
||||
{
|
||||
id: '1',
|
||||
operator: 'admin',
|
||||
operationType: 'login',
|
||||
module: 'system',
|
||||
operationContent: '用户登录系统',
|
||||
ip: '192.168.1.100',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
|
||||
status: 'success',
|
||||
time: '2024-04-10T09:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
operator: 'admin',
|
||||
operationType: 'update',
|
||||
module: 'user',
|
||||
operationContent: '更新用户信息,用户ID: 1001',
|
||||
ip: '192.168.1.100',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
|
||||
status: 'success',
|
||||
time: '2024-04-10T10:15:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
operator: 'admin',
|
||||
operationType: 'create',
|
||||
module: 'epidemic',
|
||||
operationContent: '新增疫情记录,记录ID: 2001',
|
||||
ip: '192.168.1.100',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
|
||||
status: 'success',
|
||||
time: '2024-04-10T11:30:00Z'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
operator: 'user001',
|
||||
operationType: 'query',
|
||||
module: 'supervision',
|
||||
operationContent: '查询监管数据',
|
||||
ip: '192.168.1.101',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/111.0 Safari/537.36',
|
||||
status: 'success',
|
||||
time: '2024-04-10T13:45:00Z'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
operator: 'user002',
|
||||
operationType: 'export',
|
||||
module: 'file',
|
||||
operationContent: '导出文件,文件ID: 3001',
|
||||
ip: '192.168.1.102',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/112.0.0.0 Safari/537.36',
|
||||
status: 'success',
|
||||
time: '2024-04-10T14:20:00Z'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
operator: 'user003',
|
||||
operationType: 'delete',
|
||||
module: 'warehouse',
|
||||
operationContent: '删除物资,物资ID: 4001',
|
||||
ip: '192.168.1.103',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
|
||||
status: 'fail',
|
||||
errorMsg: '权限不足',
|
||||
time: '2024-04-10T15:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
operator: 'admin',
|
||||
operationType: 'logout',
|
||||
module: 'system',
|
||||
operationContent: '用户登出系统',
|
||||
ip: '192.168.1.100',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
|
||||
status: 'success',
|
||||
time: '2024-04-10T17:30:00Z'
|
||||
}
|
||||
])
|
||||
|
||||
// 日志表格列定义
|
||||
const logColumns = [
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作类型',
|
||||
dataIndex: 'operationType',
|
||||
key: 'operationType',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '所属模块',
|
||||
dataIndex: 'module',
|
||||
key: 'module',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作内容',
|
||||
dataIndex: 'operationContent',
|
||||
key: 'operationContent'
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip',
|
||||
key: 'ip',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '错误信息',
|
||||
dataIndex: 'errorMsg',
|
||||
key: 'errorMsg',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
width: 180
|
||||
}
|
||||
]
|
||||
|
||||
// 图表引用
|
||||
const chartRef = ref(null)
|
||||
|
||||
// 操作类型标签映射
|
||||
const getOperationTypeLabel = (type) => {
|
||||
const map = {
|
||||
login: '登录',
|
||||
logout: '登出',
|
||||
create: '创建',
|
||||
update: '更新',
|
||||
delete: '删除',
|
||||
query: '查询',
|
||||
export: '导出'
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
// 操作类型颜色映射
|
||||
const getOperationTypeColor = (type) => {
|
||||
const map = {
|
||||
login: 'blue',
|
||||
logout: 'purple',
|
||||
create: 'green',
|
||||
update: 'orange',
|
||||
delete: 'red',
|
||||
query: 'default',
|
||||
export: 'cyan'
|
||||
}
|
||||
return map[type] || 'default'
|
||||
}
|
||||
|
||||
// 模块标签映射
|
||||
const getModuleLabel = (module) => {
|
||||
const map = {
|
||||
system: '系统',
|
||||
user: '用户管理',
|
||||
epidemic: '疫情管理',
|
||||
supervision: '监管管理',
|
||||
file: '文件管理',
|
||||
warehouse: '仓库管理',
|
||||
service: '服务管理',
|
||||
personnel: '人员管理'
|
||||
}
|
||||
return map[module] || module
|
||||
}
|
||||
|
||||
// 状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
return status === 'success' ? 'text-success' : 'text-danger'
|
||||
}
|
||||
|
||||
// 搜索日志
|
||||
const handleSearch = (keyword) => {
|
||||
console.log('搜索日志:', keyword)
|
||||
// 这里应该有实际的搜索逻辑
|
||||
}
|
||||
|
||||
// 日期范围改变
|
||||
const handleDateChange = (dates) => {
|
||||
console.log('日期范围改变:', dates)
|
||||
// 这里应该有实际的日期筛选逻辑
|
||||
}
|
||||
|
||||
// 导出日志
|
||||
const handleExportLogs = () => {
|
||||
console.log('导出日志')
|
||||
// 这里应该有实际的导出逻辑
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
const chart = echarts.init(chartRef.value)
|
||||
|
||||
// 统计数据
|
||||
const operationStats = {
|
||||
login: 15,
|
||||
logout: 12,
|
||||
create: 8,
|
||||
update: 23,
|
||||
delete: 5,
|
||||
query: 45,
|
||||
export: 10
|
||||
}
|
||||
|
||||
const moduleStats = {
|
||||
system: 27,
|
||||
user: 18,
|
||||
epidemic: 15,
|
||||
supervision: 20,
|
||||
file: 12,
|
||||
warehouse: 10,
|
||||
service: 8,
|
||||
personnel: 6
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['操作类型统计', '模块统计']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: Object.keys(operationStats).map(key => getOperationTypeLabel(key)),
|
||||
axisTick: {
|
||||
alignWithLabel: true
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '操作类型统计',
|
||||
type: 'bar',
|
||||
barWidth: '30%',
|
||||
data: Object.values(operationStats),
|
||||
itemStyle: {
|
||||
color: '#1890ff'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f5222d;
|
||||
}
|
||||
</style>
|
||||
@@ -8,8 +8,8 @@
|
||||
<div class="login-form-container">
|
||||
<!-- Logo和标题 -->
|
||||
<div class="login-header">
|
||||
<img src="@/assets/images/favicon.svg" alt="Logo" class="logo" />
|
||||
<h1 class="title">宁夏养殖政府管理平台</h1>
|
||||
<img src="@/assets/logo.svg" alt="Logo" class="logo" />
|
||||
<h1 class="title">宁夏智慧养殖监管平台</h1>
|
||||
<p class="subtitle">政府端管理后台</p>
|
||||
</div>
|
||||
|
||||
@@ -149,31 +149,22 @@ const handleLogin = async (values) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const result = await authStore.login({
|
||||
const success = await authStore.login({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
captcha: values.captcha,
|
||||
remember: values.remember
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
if (success) {
|
||||
message.success('登录成功')
|
||||
|
||||
// 跳转到首页或之前访问的页面
|
||||
const redirect = router.currentRoute.value.query.redirect || '/dashboard'
|
||||
const redirect = router.currentRoute.value.query.redirect || '/'
|
||||
router.push(redirect)
|
||||
} else {
|
||||
message.error(result.message || '登录失败')
|
||||
|
||||
// 登录失败后显示验证码
|
||||
if (!showCaptcha.value) {
|
||||
showCaptcha.value = true
|
||||
refreshCaptcha()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
message.error('登录失败')
|
||||
|
||||
// 登录失败后显示验证码
|
||||
if (!showCaptcha.value) {
|
||||
@@ -193,8 +184,8 @@ const refreshCaptcha = () => {
|
||||
// 组件挂载时的处理
|
||||
onMounted(() => {
|
||||
// 如果已经登录,直接跳转到首页
|
||||
if (authStore.isLoggedIn) {
|
||||
router.push('/dashboard')
|
||||
if (authStore.isAuthenticated) {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 初始化验证码(如果需要)
|
||||
@@ -221,7 +212,7 @@ onMounted(() => {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f0f2f5; /* 使用纯色背景替代渐变色 */
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
@@ -231,9 +222,9 @@ onMounted(() => {
|
||||
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%);
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 119, 198, 0.2) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
@@ -242,7 +233,7 @@ onMounted(() => {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,11 +347,11 @@ onMounted(() => {
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: #1890ff; /* 使用纯色背景替代渐变色 */
|
||||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: #40a9ff; /* 使用纯色背景替代渐变色 */
|
||||
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
396
government-admin/src/views/MarketPrice.vue
Normal file
396
government-admin/src/views/MarketPrice.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>市场行情</h1>
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="8">
|
||||
<a-select v-model:value="selectedProduct" placeholder="选择产品类型" allow-clear style="width: 200px;">
|
||||
<a-select-option value="beef">牛肉</a-select-option>
|
||||
<a-select-option value="milk">牛奶</a-select-option>
|
||||
<a-select-option value="calf">牛犊</a-select-option>
|
||||
<a-select-option value="feed">饲料</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-select v-model:value="selectedRegion" placeholder="选择地区" allow-clear style="width: 200px;">
|
||||
<a-select-option value="national">全国</a-select-option>
|
||||
<a-select-option value="north">北方地区</a-select-option>
|
||||
<a-select-option value="south">南方地区</a-select-option>
|
||||
<a-select-option value="east">东部地区</a-select-option>
|
||||
<a-select-option value="west">西部地区</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-button type="primary" @click="handleSearch">查询</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleExport">导出数据</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 价格走势图表 -->
|
||||
<a-card title="价格走势图" style="margin-bottom: 16px;">
|
||||
<div style="height: 400px;" ref="chartRef"></div>
|
||||
</a-card>
|
||||
|
||||
<!-- 最新市场价格表 -->
|
||||
<a-card title="最新市场价格表">
|
||||
<a-table :columns="priceColumns" :data-source="priceData" :pagination="pagination" row-key="id">
|
||||
<template #bodyCell:price="{ record }">
|
||||
<span style="font-weight: bold;">{{ record.price }}</span>
|
||||
</template>
|
||||
<template #bodyCell:change="{ record }">
|
||||
<span :class="record.change > 0 ? 'text-danger' : 'text-success'">
|
||||
{{ record.change > 0 ? '+' : '' }}{{ record.change }}%
|
||||
</span>
|
||||
</template>
|
||||
<template #bodyCell:date="{ record }">
|
||||
{{ dayjs(record.date).format('YYYY-MM-DD') }}
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 市场分析报告 -->
|
||||
<a-card title="市场分析报告" style="margin-top: 16px;">
|
||||
<div class="report-content">
|
||||
<h3>近期市场行情分析</h3>
|
||||
<p>根据最新数据显示,近期牛肉价格呈现{{ overallTrend }}趋势,主要受以下因素影响:</p>
|
||||
<ol>
|
||||
<li>市场供需关系变化</li>
|
||||
<li>养殖成本波动</li>
|
||||
<li>季节性需求变化</li>
|
||||
<li>政策因素影响</li>
|
||||
</ol>
|
||||
<p>预计未来一段时间内,价格将保持{{ futureTrend }}态势。建议养殖户和相关企业密切关注市场动态,合理安排生产和销售计划。</p>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 选择的产品和地区
|
||||
const selectedProduct = ref('beef')
|
||||
const selectedRegion = ref('national')
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 图表引用
|
||||
const chartRef = ref(null)
|
||||
|
||||
// 整体趋势和未来趋势
|
||||
const overallTrend = ref('稳中有升')
|
||||
const futureTrend = ref('相对稳定')
|
||||
|
||||
// 价格数据
|
||||
const priceData = ref([
|
||||
{
|
||||
id: '1',
|
||||
productType: '牛肉',
|
||||
spec: '一等品',
|
||||
unit: '元/公斤',
|
||||
price: 78.50,
|
||||
change: 1.2,
|
||||
region: '全国均价',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
productType: '牛肉',
|
||||
spec: '二等品',
|
||||
unit: '元/公斤',
|
||||
price: 68.20,
|
||||
change: 0.8,
|
||||
region: '全国均价',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
productType: '牛奶',
|
||||
spec: '生鲜乳',
|
||||
unit: '元/公斤',
|
||||
price: 4.30,
|
||||
change: -0.5,
|
||||
region: '全国均价',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
productType: '牛犊',
|
||||
spec: '优良品种',
|
||||
unit: '元/头',
|
||||
price: 5800,
|
||||
change: 2.5,
|
||||
region: '全国均价',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
productType: '饲料',
|
||||
spec: '精饲料',
|
||||
unit: '元/吨',
|
||||
price: 3200,
|
||||
change: 1.8,
|
||||
region: '全国均价',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
productType: '牛肉',
|
||||
spec: '一等品',
|
||||
unit: '元/公斤',
|
||||
price: 82.30,
|
||||
change: 1.5,
|
||||
region: '北方地区',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
productType: '牛肉',
|
||||
spec: '一等品',
|
||||
unit: '元/公斤',
|
||||
price: 76.80,
|
||||
change: 1.0,
|
||||
region: '南方地区',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
productType: '牛肉',
|
||||
spec: '一等品',
|
||||
unit: '元/公斤',
|
||||
price: 85.50,
|
||||
change: 2.0,
|
||||
region: '东部地区',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
productType: '牛肉',
|
||||
spec: '一等品',
|
||||
unit: '元/公斤',
|
||||
price: 72.10,
|
||||
change: 0.5,
|
||||
region: '西部地区',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
productType: '饲料',
|
||||
spec: '粗饲料',
|
||||
unit: '元/吨',
|
||||
price: 1800,
|
||||
change: 0.2,
|
||||
region: '全国均价',
|
||||
date: '2024-04-10T00:00:00Z'
|
||||
}
|
||||
])
|
||||
|
||||
// 价格表格列定义
|
||||
const priceColumns = [
|
||||
{
|
||||
title: '产品类型',
|
||||
dataIndex: 'productType',
|
||||
key: 'productType',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '规格',
|
||||
dataIndex: 'spec',
|
||||
key: 'spec',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '单位',
|
||||
dataIndex: 'unit',
|
||||
key: 'unit',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '涨跌幅',
|
||||
dataIndex: 'change',
|
||||
key: 'change',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '地区',
|
||||
dataIndex: 'region',
|
||||
key: 'region',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '数据日期',
|
||||
dataIndex: 'date',
|
||||
key: 'date',
|
||||
width: 150
|
||||
}
|
||||
]
|
||||
|
||||
// 查询按钮点击事件
|
||||
const handleSearch = () => {
|
||||
console.log('查询条件:', { selectedProduct: selectedProduct.value, selectedRegion: selectedRegion.value })
|
||||
// 这里应该有实际的查询逻辑
|
||||
}
|
||||
|
||||
// 导出按钮点击事件
|
||||
const handleExport = () => {
|
||||
console.log('导出数据')
|
||||
// 这里应该有实际的导出逻辑
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
const chart = echarts.init(chartRef.value)
|
||||
|
||||
// 模拟历史价格数据
|
||||
const historicalData = [
|
||||
{ date: '2024-03-11', beef: 75.2, milk: 4.4, feed: 3150 },
|
||||
{ date: '2024-03-18', beef: 76.5, milk: 4.4, feed: 3160 },
|
||||
{ date: '2024-03-25', beef: 77.1, milk: 4.3, feed: 3180 },
|
||||
{ date: '2024-04-01', beef: 77.8, milk: 4.3, feed: 3190 },
|
||||
{ date: '2024-04-08', beef: 78.2, milk: 4.3, feed: 3200 },
|
||||
{ date: '2024-04-15', beef: 78.5, milk: 4.3, feed: 3200 }
|
||||
]
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '产品价格走势(最近6周)',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['牛肉(元/公斤)', '牛奶(元/公斤)', '饲料(元/吨)'],
|
||||
bottom: 10
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: historicalData.map(item => item.date)
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '价格',
|
||||
position: 'left'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '牛肉(元/公斤)',
|
||||
type: 'line',
|
||||
data: historicalData.map(item => item.beef),
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
itemStyle: {
|
||||
color: '#1890ff'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '牛奶(元/公斤)',
|
||||
type: 'line',
|
||||
data: historicalData.map(item => item.milk),
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
itemStyle: {
|
||||
color: '#52c41a'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '饲料(元/吨)',
|
||||
type: 'line',
|
||||
data: historicalData.map(item => item.feed),
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
itemStyle: {
|
||||
color: '#fa8c16'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-danger {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.report-content h3 {
|
||||
margin-bottom: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.report-content p {
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.report-content ol {
|
||||
margin-left: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.report-content li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
1083
government-admin/src/views/MessageNotification.vue
Normal file
1083
government-admin/src/views/MessageNotification.vue
Normal file
File diff suppressed because it is too large
Load Diff
1169
government-admin/src/views/OnlineConsultation.vue
Normal file
1169
government-admin/src/views/OnlineConsultation.vue
Normal file
File diff suppressed because it is too large
Load Diff
764
government-admin/src/views/PaperlessService.vue
Normal file
764
government-admin/src/views/PaperlessService.vue
Normal file
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>无纸化服务</h1>
|
||||
|
||||
<!-- 搜索和操作栏 -->
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: center;">
|
||||
<a-input v-model:value="searchKeyword" placeholder="输入服务名称或编号" style="width: 250px;">
|
||||
<template #prefix>
|
||||
<span class="iconfont icon-sousuo"></span>
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-select v-model:value="serviceTypeFilter" placeholder="服务类型" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="approval">审批服务</a-select-option>
|
||||
<a-select-option value="certificate">证件办理</a-select-option>
|
||||
<a-select-option value="report">报告申请</a-select-option>
|
||||
<a-select-option value="consultation">咨询服务</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select v-model:value="statusFilter" placeholder="服务状态" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="active">启用</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
<a-select-option value="developing">开发中</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-button type="primary" @click="handleSearch" style="margin-left: auto;">
|
||||
<span class="iconfont icon-sousuo"></span> 搜索
|
||||
</a-button>
|
||||
|
||||
<a-button type="default" @click="handleReset">重置</a-button>
|
||||
|
||||
<a-button type="primary" danger @click="handleAddService">
|
||||
<span class="iconfont icon-tianjia"></span> 新增服务
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="服务总数" :value="totalServices" suffix="项" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="已启用服务" :value="activeServices" suffix="项" :valueStyle="{ color: '#52c41a' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="本月服务量" :value="monthlyVolume" suffix="次" :valueStyle="{ color: '#1890ff' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="平均办理时长" :value="avgProcessingTime" suffix="天" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 服务量统计图表 -->
|
||||
<a-card title="服务量趋势统计" style="margin-bottom: 16px;">
|
||||
<div style="height: 300px;" ref="serviceChartRef"></div>
|
||||
</a-card>
|
||||
|
||||
<!-- 服务列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="servicesData"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #bodyCell:action="{ record }">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a-button size="small" @click="handleView(record)">查看</a-button>
|
||||
<a-button size="small" type="primary" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button size="small" danger @click="handleDelete(record.id)">删除</a-button>
|
||||
<a-button size="small" @click="handleToggleStatus(record)">
|
||||
{{ record.status === 'active' ? '禁用' : '启用' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑服务模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isAddEditModalOpen"
|
||||
title="新增/编辑无纸化服务"
|
||||
:footer="null"
|
||||
width={700}
|
||||
>
|
||||
<a-form
|
||||
:model="currentService"
|
||||
layout="vertical"
|
||||
style="max-width: 500px; margin: 0 auto;"
|
||||
>
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="服务编号" name="code" :rules="[{ required: true, message: '请输入服务编号' }]">
|
||||
<a-input v-model:value="currentService.code" placeholder="请输入服务编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="服务名称" name="name" :rules="[{ required: true, message: '请输入服务名称' }]">
|
||||
<a-input v-model:value="currentService.name" placeholder="请输入服务名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="服务类型" name="serviceType" :rules="[{ required: true, message: '请选择服务类型' }]">
|
||||
<a-select v-model:value="currentService.serviceType" placeholder="请选择服务类型">
|
||||
<a-select-option value="approval">审批服务</a-select-option>
|
||||
<a-select-option value="certificate">证件办理</a-select-option>
|
||||
<a-select-option value="report">报告申请</a-select-option>
|
||||
<a-select-option value="consultation">咨询服务</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="服务状态" name="status" :rules="[{ required: true, message: '请选择服务状态' }]">
|
||||
<a-select v-model:value="currentService.status" placeholder="请选择服务状态">
|
||||
<a-select-option value="active">启用</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
<a-select-option value="developing">开发中</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="预计办理时长" name="estimatedTime" :rules="[{ required: true, message: '请输入预计办理时长' }]">
|
||||
<a-input-number v-model:value="currentService.estimatedTime" min={1} style="width: 100%;" placeholder="预计办理时长" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="收费标准" name="feeStandard">
|
||||
<a-input v-model:value="currentService.feeStandard" placeholder="请输入收费标准" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="服务简介" name="description" :rules="[{ required: true, message: '请输入服务简介' }]">
|
||||
<a-input.TextArea v-model:value="currentService.description" placeholder="请输入服务简介" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="办理流程" name="process" :rules="[{ required: true, message: '请输入办理流程' }]">
|
||||
<a-input.TextArea v-model:value="currentService.process" placeholder="请输入办理流程" :rows="4" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="所需材料" name="requiredMaterials" :rules="[{ required: true, message: '请输入所需材料' }]">
|
||||
<a-input.TextArea v-model:value="currentService.requiredMaterials" placeholder="请输入所需材料" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="注意事项" name="notes">
|
||||
<a-input.TextArea v-model:value="currentService.notes" placeholder="请输入注意事项" :rows="2" />
|
||||
</a-form-item>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 16px;">
|
||||
<a-button @click="isAddEditModalOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看服务详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isViewModalOpen"
|
||||
title="服务详情"
|
||||
:footer="null"
|
||||
width={800}
|
||||
>
|
||||
<div v-if="viewService" class="service-detail">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">服务编号:</div>
|
||||
<div class="detail-value">{{ viewService.code }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">服务名称:</div>
|
||||
<div class="detail-value">{{ viewService.name }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">服务类型:</div>
|
||||
<div class="detail-value">{{ getServiceTypeText(viewService.serviceType) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">服务状态:</div>
|
||||
<div class="detail-value">
|
||||
<a-tag :color="getStatusColor(viewService.status)">{{ getStatusText(viewService.status) }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">预计办理时长:</div>
|
||||
<div class="detail-value">{{ viewService.estimatedTime }} 天</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">收费标准:</div>
|
||||
<div class="detail-value">{{ viewService.feeStandard || '免费' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">服务简介:</div>
|
||||
<div class="detail-value">{{ viewService.description }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">办理流程:</div>
|
||||
<div class="detail-value">{{ viewService.process }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">所需材料:</div>
|
||||
<div class="detail-value">{{ viewService.requiredMaterials }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">注意事项:</div>
|
||||
<div class="detail-value">{{ viewService.notes || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">累计办理次数:</div>
|
||||
<div class="detail-value">{{ viewService.processedCount || 0 }} 次</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">平均办理时长:</div>
|
||||
<div class="detail-value">{{ viewService.averageTime || 0 }} 天</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 24px;">
|
||||
<a-button @click="isViewModalOpen = false">关闭</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<a-popconfirm
|
||||
v-model:open="isConfirmModalOpen"
|
||||
title="确认操作"
|
||||
:description="confirmMessage"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmAction"
|
||||
>
|
||||
<template #reference>
|
||||
<!-- 这里是空的,通过其他按钮触发 -->
|
||||
</template>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 搜索条件
|
||||
const searchKeyword = ref('')
|
||||
const serviceTypeFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
|
||||
// 统计数据
|
||||
const totalServices = ref(32)
|
||||
const activeServices = ref(26)
|
||||
const monthlyVolume = ref(856)
|
||||
const avgProcessingTime = ref(2.5)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 选中的行
|
||||
const selectedRowKeys = ref([])
|
||||
const onSelectChange = (newSelectedRowKeys) => {
|
||||
selectedRowKeys.value = newSelectedRowKeys
|
||||
}
|
||||
|
||||
// 图表引用
|
||||
const serviceChartRef = ref(null)
|
||||
|
||||
// 模态框状态
|
||||
const isAddEditModalOpen = ref(false)
|
||||
const isViewModalOpen = ref(false)
|
||||
const isConfirmModalOpen = ref(false)
|
||||
const confirmMessage = ref('')
|
||||
const confirmAction = ref('')
|
||||
const confirmTargetId = ref('')
|
||||
|
||||
// 当前编辑的服务
|
||||
const currentService = reactive({
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
serviceType: '',
|
||||
status: 'active',
|
||||
estimatedTime: 3,
|
||||
feeStandard: '',
|
||||
description: '',
|
||||
process: '',
|
||||
requiredMaterials: '',
|
||||
notes: '',
|
||||
processedCount: 0,
|
||||
averageTime: 0
|
||||
})
|
||||
|
||||
// 查看的服务
|
||||
const viewService = ref(null)
|
||||
|
||||
// 服务列表数据
|
||||
const servicesData = ref([
|
||||
{
|
||||
id: '1',
|
||||
code: 'S001',
|
||||
name: '动物防疫条件合格证办理',
|
||||
serviceType: 'certificate',
|
||||
status: 'active',
|
||||
estimatedTime: 5,
|
||||
feeStandard: '免费',
|
||||
description: '为养殖场提供动物防疫条件合格证的申请、审核和发放服务',
|
||||
process: '1. 提交申请;2. 资料审核;3. 现场勘查;4. 审核通过;5. 发放证件',
|
||||
requiredMaterials: '1. 申请表;2. 养殖场平面图;3. 防疫制度;4. 负责人身份证明;5. 营业执照',
|
||||
notes: '现场勘查需提前3天预约',
|
||||
processedCount: 256,
|
||||
averageTime: 4.2
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
code: 'S002',
|
||||
name: '种畜禽生产经营许可证办理',
|
||||
serviceType: 'certificate',
|
||||
status: 'active',
|
||||
estimatedTime: 7,
|
||||
feeStandard: '免费',
|
||||
description: '为种畜禽养殖场提供生产经营许可证的申请、审核和发放服务',
|
||||
process: '1. 提交申请;2. 资料审核;3. 现场评审;4. 审核通过;5. 发放证件',
|
||||
requiredMaterials: '1. 申请表;2. 养殖场资质证明;3. 种畜禽来源证明;4. 技术人员资质;5. 防疫条件证明',
|
||||
notes: '需提供种畜禽质量检测报告',
|
||||
processedCount: 128,
|
||||
averageTime: 6.8
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
code: 'S003',
|
||||
name: '养殖场建设项目备案',
|
||||
serviceType: 'approval',
|
||||
status: 'active',
|
||||
estimatedTime: 10,
|
||||
feeStandard: '免费',
|
||||
description: '为新建、扩建、改建养殖场提供项目备案服务',
|
||||
process: '1. 提交备案申请;2. 资料审核;3. 现场核查;4. 备案登记;5. 公示',
|
||||
requiredMaterials: '1. 备案申请表;2. 项目可行性研究报告;3. 选址意见书;4. 环境影响评价文件;5. 土地使用证明',
|
||||
notes: '大型养殖场需提供环评批复',
|
||||
processedCount: 95,
|
||||
averageTime: 9.3
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
code: 'S004',
|
||||
name: '畜禽标识代码申请',
|
||||
serviceType: 'approval',
|
||||
status: 'active',
|
||||
estimatedTime: 3,
|
||||
feeStandard: '免费',
|
||||
description: '为养殖场提供畜禽标识代码的申请和发放服务',
|
||||
process: '1. 提交申请;2. 资料审核;3. 发放标识代码',
|
||||
requiredMaterials: '1. 申请表;2. 养殖场资质证明;3. 负责人身份证明',
|
||||
notes: '无',
|
||||
processedCount: 487,
|
||||
averageTime: 2.1
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
code: 'S005',
|
||||
name: '动物检疫申报',
|
||||
serviceType: 'approval',
|
||||
status: 'active',
|
||||
estimatedTime: 1,
|
||||
feeStandard: '按规定收费',
|
||||
description: '为畜禽出栏、运输提供检疫申报和检疫合格证明发放服务',
|
||||
process: '1. 网上申报;2. 现场检疫;3. 合格发证',
|
||||
requiredMaterials: '1. 检疫申报单;2. 免疫记录;3. 养殖档案;4. 运输工具消毒证明',
|
||||
notes: '需提前24小时申报',
|
||||
processedCount: 1245,
|
||||
averageTime: 0.8
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
code: 'S006',
|
||||
name: '养殖污染防治技术指导',
|
||||
serviceType: 'consultation',
|
||||
status: 'active',
|
||||
estimatedTime: 2,
|
||||
feeStandard: '免费',
|
||||
description: '为养殖场提供养殖污染防治技术咨询和指导服务',
|
||||
process: '1. 提交咨询申请;2. 技术专家评估;3. 提供技术方案;4. 现场指导',
|
||||
requiredMaterials: '1. 咨询申请表;2. 养殖场基本情况;3. 现有污染处理设施说明',
|
||||
notes: '可预约上门服务',
|
||||
processedCount: 76,
|
||||
averageTime: 3.5
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
code: 'S007',
|
||||
name: '畜禽产品质量安全检测报告申请',
|
||||
serviceType: 'report',
|
||||
status: 'active',
|
||||
estimatedTime: 5,
|
||||
feeStandard: '按检测项目收费',
|
||||
description: '为养殖户提供畜禽产品质量安全检测和报告出具服务',
|
||||
process: '1. 提交申请;2. 样品采集;3. 实验室检测;4. 出具报告',
|
||||
requiredMaterials: '1. 检测申请表;2. 样品标识;3. 样品来源说明',
|
||||
notes: '检测报告有效期为6个月',
|
||||
processedCount: 168,
|
||||
averageTime: 4.7
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
code: 'S008',
|
||||
name: '养殖技术培训申请',
|
||||
serviceType: 'consultation',
|
||||
status: 'active',
|
||||
estimatedTime: 7,
|
||||
feeStandard: '免费',
|
||||
description: '为养殖户提供养殖技术培训服务',
|
||||
process: '1. 提交培训申请;2. 制定培训计划;3. 开展培训;4. 培训考核',
|
||||
requiredMaterials: '1. 培训申请表;2. 参训人员名单;3. 培训需求说明',
|
||||
notes: '可根据需求定制培训内容',
|
||||
processedCount: 324,
|
||||
averageTime: 6.2
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
code: 'S009',
|
||||
name: '畜牧业扶持政策咨询',
|
||||
serviceType: 'consultation',
|
||||
status: 'active',
|
||||
estimatedTime: 1,
|
||||
feeStandard: '免费',
|
||||
description: '为养殖户提供畜牧业相关扶持政策的咨询服务',
|
||||
process: '1. 提交咨询;2. 政策解读;3. 提供建议',
|
||||
requiredMaterials: '无',
|
||||
notes: '可通过线上或电话咨询',
|
||||
processedCount: 986,
|
||||
averageTime: 0.5
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
code: 'S010',
|
||||
name: '畜禽养殖保险理赔申请',
|
||||
serviceType: 'approval',
|
||||
status: 'inactive',
|
||||
estimatedTime: 15,
|
||||
feeStandard: '免费',
|
||||
description: '为参保养殖户提供养殖保险理赔申请和处理服务',
|
||||
process: '1. 提交理赔申请;2. 现场勘查;3. 损失评估;4. 理赔核算;5. 赔付',
|
||||
requiredMaterials: '1. 理赔申请表;2. 保险单;3. 损失证明材料;4. 其他相关证明',
|
||||
notes: '需在事故发生后48小时内报案',
|
||||
processedCount: 0,
|
||||
averageTime: 0
|
||||
}
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '服务编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '服务类型',
|
||||
dataIndex: 'serviceType',
|
||||
key: 'serviceType',
|
||||
width: 100,
|
||||
customRender: ({ text }) => getServiceTypeText(text)
|
||||
},
|
||||
{
|
||||
title: '预计办理时长',
|
||||
dataIndex: 'estimatedTime',
|
||||
key: 'estimatedTime',
|
||||
width: 100,
|
||||
customRender: ({ text }) => `${text} 天`
|
||||
},
|
||||
{
|
||||
title: '收费标准',
|
||||
dataIndex: 'feeStandard',
|
||||
key: 'feeStandard',
|
||||
width: 100,
|
||||
customRender: ({ text }) => text || '免费'
|
||||
},
|
||||
{
|
||||
title: '服务状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '累计办理次数',
|
||||
dataIndex: 'processedCount',
|
||||
key: 'processedCount',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return 'green'
|
||||
case 'inactive': return 'default'
|
||||
case 'developing': return 'orange'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return '启用'
|
||||
case 'inactive': return '禁用'
|
||||
case 'developing': return '开发中'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
// 获取服务类型文本
|
||||
const getServiceTypeText = (serviceType) => {
|
||||
switch (serviceType) {
|
||||
case 'approval': return '审批服务'
|
||||
case 'certificate': return '证件办理'
|
||||
case 'report': return '报告申请'
|
||||
case 'consultation': return '咨询服务'
|
||||
default: return serviceType
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索条件:', {
|
||||
keyword: searchKeyword.value,
|
||||
serviceType: serviceTypeFilter.value,
|
||||
status: statusFilter.value
|
||||
})
|
||||
// 这里应该有实际的搜索逻辑
|
||||
// 模拟搜索后的总数
|
||||
pagination.total = servicesData.value.length
|
||||
}
|
||||
|
||||
// 重置处理
|
||||
const handleReset = () => {
|
||||
searchKeyword.value = ''
|
||||
serviceTypeFilter.value = ''
|
||||
statusFilter.value = ''
|
||||
selectedRowKeys.value = []
|
||||
}
|
||||
|
||||
// 新增服务
|
||||
const handleAddService = () => {
|
||||
// 重置当前服务数据
|
||||
Object.assign(currentService, {
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
serviceType: '',
|
||||
status: 'active',
|
||||
estimatedTime: 3,
|
||||
feeStandard: '',
|
||||
description: '',
|
||||
process: '',
|
||||
requiredMaterials: '',
|
||||
notes: '',
|
||||
processedCount: 0,
|
||||
averageTime: 0
|
||||
})
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 编辑服务
|
||||
const handleEdit = (record) => {
|
||||
// 复制记录数据到当前服务
|
||||
Object.assign(currentService, JSON.parse(JSON.stringify(record)))
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 查看服务
|
||||
const handleView = (record) => {
|
||||
viewService.value = JSON.parse(JSON.stringify(record))
|
||||
isViewModalOpen.value = true
|
||||
}
|
||||
|
||||
// 删除服务 - 弹出确认对话框
|
||||
const handleDelete = (id) => {
|
||||
confirmMessage.value = '确定要删除该服务吗?'
|
||||
confirmAction.value = 'delete'
|
||||
confirmTargetId.value = id
|
||||
isConfirmModalOpen.value = true
|
||||
}
|
||||
|
||||
// 切换服务状态 - 弹出确认对话框
|
||||
const handleToggleStatus = (record) => {
|
||||
const newStatus = record.status === 'active' ? 'inactive' : 'active'
|
||||
confirmMessage.value = `确定要${newStatus === 'active' ? '启用' : '禁用'}该服务吗?`
|
||||
confirmAction.value = 'toggleStatus'
|
||||
confirmTargetId.value = record.id
|
||||
isConfirmModalOpen.value = true
|
||||
}
|
||||
|
||||
// 确认操作
|
||||
const handleConfirmAction = () => {
|
||||
if (confirmAction.value === 'delete') {
|
||||
console.log('删除服务:', confirmTargetId.value)
|
||||
// 这里应该有实际的删除逻辑
|
||||
// 模拟删除成功
|
||||
alert(`成功删除服务ID: ${confirmTargetId.value}`)
|
||||
} else if (confirmAction.value === 'toggleStatus') {
|
||||
console.log('切换服务状态:', confirmTargetId.value)
|
||||
// 这里应该有实际的状态切换逻辑
|
||||
// 模拟切换成功
|
||||
alert(`成功切换服务状态`)
|
||||
}
|
||||
isConfirmModalOpen.value = false
|
||||
}
|
||||
|
||||
// 保存服务
|
||||
const handleSave = () => {
|
||||
console.log('保存服务:', currentService)
|
||||
// 这里应该有实际的保存逻辑
|
||||
// 模拟保存成功
|
||||
isAddEditModalOpen.value = false
|
||||
alert('保存成功')
|
||||
}
|
||||
|
||||
// 初始化服务量趋势图表
|
||||
const initServiceChart = () => {
|
||||
if (!serviceChartRef.value) return
|
||||
|
||||
const chart = echarts.init(serviceChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['审批服务', '证件办理', '报告申请', '咨询服务']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '审批服务',
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [120, 132, 101, 134, 90, 230, 210, 230, 180, 230, 210, 120]
|
||||
},
|
||||
{
|
||||
name: '证件办理',
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [220, 182, 191, 234, 290, 330, 310, 210, 230, 280, 270, 220]
|
||||
},
|
||||
{
|
||||
name: '报告申请',
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [150, 232, 201, 154, 190, 330, 410, 310, 280, 320, 340, 300]
|
||||
},
|
||||
{
|
||||
name: '咨询服务',
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [320, 332, 301, 334, 390, 330, 320, 350, 320, 340, 380, 390]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initServiceChart()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
pagination.total = servicesData.value.length
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.service-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
321
government-admin/src/views/PersonnelManagement.vue
Normal file
321
government-admin/src/views/PersonnelManagement.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>人员管理</h1>
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="8">
|
||||
<a-button type="primary" @click="handleAddUser">添加人员</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleImportUsers">导入人员</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleExportUsers">导出人员</a-button>
|
||||
</a-col>
|
||||
<a-col :span="16" style="text-align: right;">
|
||||
<a-input-search
|
||||
placeholder="搜索人员姓名或ID"
|
||||
allow-clear
|
||||
enter-button="搜索"
|
||||
size="large"
|
||||
style="width: 300px;"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 人员列表 -->
|
||||
<a-card title="人员列表">
|
||||
<a-table :columns="userColumns" :data-source="userList" :pagination="pagination" row-key="id">
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-badge :status="record.status === 'active' ? 'success' : 'default'" :text="record.status === 'active' ? '在职' : '离职'" />
|
||||
</template>
|
||||
<template #bodyCell:role="{ record }">
|
||||
<a-tag>{{ record.role }}</a-tag>
|
||||
</template>
|
||||
<template #bodyCell:action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleViewUser(record)">查看</a-button>
|
||||
<a-button type="link" @click="handleEditUser(record)">编辑</a-button>
|
||||
<a-button type="link" danger @click="handleDeleteUser(record.id)">删除</a-button>
|
||||
<a-button type="link" @click="handleResetPassword(record.id)">重置密码</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑人员对话框 -->
|
||||
<a-modal
|
||||
v-model:open="userModalVisible"
|
||||
:title="userModalTitle"
|
||||
@ok="handleUserModalOk"
|
||||
@cancel="handleUserModalCancel"
|
||||
>
|
||||
<a-form :model="userForm" layout="vertical">
|
||||
<a-form-item label="姓名" name="name" :rules="[{ required: true, message: '请输入姓名' }]">
|
||||
<a-input v-model:value="userForm.name" placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="工号" name="employeeId" :rules="[{ required: true, message: '请输入工号' }]">
|
||||
<a-input v-model:value="userForm.employeeId" placeholder="请输入工号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone" :rules="[{ required: true, message: '请输入手机号' }]">
|
||||
<a-input v-model:value="userForm.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email" :rules="[{ type: 'email', message: '请输入正确的邮箱地址' }]">
|
||||
<a-input v-model:value="userForm.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色" name="role" :rules="[{ required: true, message: '请选择角色' }]">
|
||||
<a-select v-model:value="userForm.role" placeholder="请选择角色">
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-select-option value="supervisor">监管人员</a-select-option>
|
||||
<a-select-option value="auditor">审核人员</a-select-option>
|
||||
<a-select-option value="analyst">分析人员</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status" :rules="[{ required: true, message: '请选择状态' }]">
|
||||
<a-select v-model:value="userForm.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">在职</a-select-option>
|
||||
<a-select-option value="inactive">离职</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="部门" name="department" :rules="[{ required: true, message: '请选择部门' }]">
|
||||
<a-select v-model:value="userForm.department" placeholder="请选择部门">
|
||||
<a-select-option value="management">管理部门</a-select-option>
|
||||
<a-select-option value="supervision">监管部门</a-select-option>
|
||||
<a-select-option value="epidemic">疫情防控部门</a-select-option>
|
||||
<a-select-option value="analysis">数据分析部门</a-select-option>
|
||||
<a-select-option value="other">其他部门</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 人员列表数据
|
||||
const userList = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: '张三',
|
||||
employeeId: 'EMP001',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
role: '管理员',
|
||||
department: '管理部门',
|
||||
status: 'active',
|
||||
createTime: '2024-01-15T09:30:00Z',
|
||||
lastLogin: '2024-04-10T14:20:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '李四',
|
||||
employeeId: 'EMP002',
|
||||
phone: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
role: '监管人员',
|
||||
department: '监管部门',
|
||||
status: 'active',
|
||||
createTime: '2024-01-20T10:15:00Z',
|
||||
lastLogin: '2024-04-09T16:45:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '王五',
|
||||
employeeId: 'EMP003',
|
||||
phone: '13800138003',
|
||||
email: 'wangwu@example.com',
|
||||
role: '审核人员',
|
||||
department: '疫情防控部门',
|
||||
status: 'active',
|
||||
createTime: '2024-02-01T14:30:00Z',
|
||||
lastLogin: '2024-04-08T10:15:00Z'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '赵六',
|
||||
employeeId: 'EMP004',
|
||||
phone: '13800138004',
|
||||
email: 'zhaoliu@example.com',
|
||||
role: '分析人员',
|
||||
department: '数据分析部门',
|
||||
status: 'active',
|
||||
createTime: '2024-02-15T11:45:00Z',
|
||||
lastLogin: '2024-04-07T13:30:00Z'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '钱七',
|
||||
employeeId: 'EMP005',
|
||||
phone: '13800138005',
|
||||
email: 'qianqi@example.com',
|
||||
role: '管理员',
|
||||
department: '管理部门',
|
||||
status: 'inactive',
|
||||
createTime: '2024-01-10T09:00:00Z',
|
||||
lastLogin: '2024-03-01T16:20:00Z'
|
||||
}
|
||||
])
|
||||
|
||||
// 人员表格列定义
|
||||
const userColumns = [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '工号',
|
||||
dataIndex: 'employeeId',
|
||||
key: 'employeeId'
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone'
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email'
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role'
|
||||
},
|
||||
{
|
||||
title: '部门',
|
||||
dataIndex: 'department',
|
||||
key: 'department'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
customRender: (text) => dayjs(text).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
dataIndex: 'lastLogin',
|
||||
key: 'lastLogin',
|
||||
customRender: (text) => dayjs(text).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 模态框配置
|
||||
const userModalVisible = ref(false)
|
||||
const userModalTitle = ref('添加人员')
|
||||
const userForm = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
employeeId: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
role: '',
|
||||
department: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
// 搜索人员
|
||||
const handleSearch = (keyword) => {
|
||||
// 这里应该有实际的搜索逻辑
|
||||
console.log('搜索人员:', keyword)
|
||||
}
|
||||
|
||||
// 添加人员
|
||||
const handleAddUser = () => {
|
||||
userModalTitle.value = '添加人员'
|
||||
Object.keys(userForm).forEach(key => {
|
||||
userForm[key] = ''
|
||||
})
|
||||
userForm.status = 'active'
|
||||
userModalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑人员
|
||||
const handleEditUser = (user) => {
|
||||
userModalTitle.value = '编辑人员'
|
||||
Object.keys(userForm).forEach(key => {
|
||||
userForm[key] = user[key] || ''
|
||||
})
|
||||
userModalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看人员
|
||||
const handleViewUser = (user) => {
|
||||
console.log('查看人员:', user)
|
||||
}
|
||||
|
||||
// 删除人员
|
||||
const handleDeleteUser = (id) => {
|
||||
console.log('删除人员:', id)
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = (id) => {
|
||||
console.log('重置密码:', id)
|
||||
}
|
||||
|
||||
// 导入人员
|
||||
const handleImportUsers = () => {
|
||||
console.log('导入人员')
|
||||
}
|
||||
|
||||
// 导出人员
|
||||
const handleExportUsers = () => {
|
||||
console.log('导出人员')
|
||||
}
|
||||
|
||||
// 模态框确认
|
||||
const handleUserModalOk = () => {
|
||||
console.log('提交表单:', userForm)
|
||||
userModalVisible.value = false
|
||||
}
|
||||
|
||||
// 模态框取消
|
||||
const handleUserModalCancel = () => {
|
||||
userModalVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
389
government-admin/src/views/ServiceManagement.vue
Normal file
389
government-admin/src/views/ServiceManagement.vue
Normal file
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>服务管理</h1>
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="8">
|
||||
<a-button type="primary" @click="handleAddService">添加服务</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleImportServices">导入服务</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleExportServices">导出服务</a-button>
|
||||
</a-col>
|
||||
<a-col :span="16" style="text-align: right;">
|
||||
<a-input-search
|
||||
placeholder="搜索服务名称或编号"
|
||||
allow-clear
|
||||
enter-button="搜索"
|
||||
size="large"
|
||||
style="width: 300px;"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 服务列表 -->
|
||||
<a-card title="服务列表">
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filterCategory" placeholder="筛选服务类型" allow-clear>
|
||||
<a-select-option value="policy">政策咨询</a-select-option>
|
||||
<a-select-option value="technical">技术指导</a-select-option>
|
||||
<a-select-option value="training">培训服务</a-select-option>
|
||||
<a-select-option value="epidemic">疫情防控</a-select-option>
|
||||
<a-select-option value="other">其他服务</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filterStatus" placeholder="筛选服务状态" allow-clear>
|
||||
<a-select-option value="active">启用</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filterPriority" placeholder="筛选优先级" allow-clear>
|
||||
<a-select-option value="high">高</a-select-option>
|
||||
<a-select-option value="medium">中</a-select-option>
|
||||
<a-select-option value="low">低</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table :columns="serviceColumns" :data-source="serviceList" :pagination="pagination" row-key="id">
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-switch :checked="record.status === 'active'" @change="handleStatusChange(record.id, $event)" />
|
||||
</template>
|
||||
<template #bodyCell:priority="{ record }">
|
||||
<a-tag :color="getPriorityColor(record.priority)">{{ getPriorityText(record.priority) }}</a-tag>
|
||||
</template>
|
||||
<template #bodyCell:action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleViewService(record)">查看</a-button>
|
||||
<a-button type="link" @click="handleEditService(record)">编辑</a-button>
|
||||
<a-button type="link" danger @click="handleDeleteService(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑服务对话框 -->
|
||||
<a-modal
|
||||
v-model:open="serviceModalVisible"
|
||||
:title="serviceModalTitle"
|
||||
@ok="handleServiceModalOk"
|
||||
@cancel="handleServiceModalCancel"
|
||||
>
|
||||
<a-form :model="serviceForm" layout="vertical">
|
||||
<a-form-item label="服务名称" name="name" :rules="[{ required: true, message: '请输入服务名称' }]">
|
||||
<a-input v-model:value="serviceForm.name" placeholder="请输入服务名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="服务编号" name="code" :rules="[{ required: true, message: '请输入服务编号' }]">
|
||||
<a-input v-model:value="serviceForm.code" placeholder="请输入服务编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="服务类型" name="category" :rules="[{ required: true, message: '请选择服务类型' }]">
|
||||
<a-select v-model:value="serviceForm.category" placeholder="请选择服务类型">
|
||||
<a-select-option value="policy">政策咨询</a-select-option>
|
||||
<a-select-option value="technical">技术指导</a-select-option>
|
||||
<a-select-option value="training">培训服务</a-select-option>
|
||||
<a-select-option value="epidemic">疫情防控</a-select-option>
|
||||
<a-select-option value="other">其他服务</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="服务描述" name="description" :rules="[{ required: true, message: '请输入服务描述' }]">
|
||||
<a-textarea v-model:value="serviceForm.description" placeholder="请输入服务描述" rows="4" />
|
||||
</a-form-item>
|
||||
<a-form-item label="负责人" name="manager" :rules="[{ required: true, message: '请输入负责人' }]">
|
||||
<a-input v-model:value="serviceForm.manager" placeholder="请输入负责人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系方式" name="contact" :rules="[{ required: true, message: '请输入联系方式' }]">
|
||||
<a-input v-model:value="serviceForm.contact" placeholder="请输入联系方式" />
|
||||
</a-form-item>
|
||||
<a-form-item label="优先级" name="priority" :rules="[{ required: true, message: '请选择优先级' }]">
|
||||
<a-select v-model:value="serviceForm.priority" placeholder="请选择优先级">
|
||||
<a-select-option value="high">高</a-select-option>
|
||||
<a-select-option value="medium">中</a-select-option>
|
||||
<a-select-option value="low">低</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status" :rules="[{ required: true, message: '请选择状态' }]">
|
||||
<a-select v-model:value="serviceForm.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">启用</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 筛选条件
|
||||
const filterCategory = ref('')
|
||||
const filterStatus = ref('')
|
||||
const filterPriority = ref('')
|
||||
|
||||
// 服务列表数据
|
||||
const serviceList = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: '政策咨询服务',
|
||||
code: 'SVC001',
|
||||
category: 'policy',
|
||||
description: '提供养殖相关政策咨询和解读服务',
|
||||
manager: '张三',
|
||||
contact: '13800138001',
|
||||
priority: 'high',
|
||||
status: 'active',
|
||||
createTime: '2024-01-15T09:30:00Z',
|
||||
updateTime: '2024-04-10T14:20:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '养殖技术指导',
|
||||
code: 'SVC002',
|
||||
category: 'technical',
|
||||
description: '提供养殖场技术指导和问题解决服务',
|
||||
manager: '李四',
|
||||
contact: '13800138002',
|
||||
priority: 'medium',
|
||||
status: 'active',
|
||||
createTime: '2024-01-20T10:15:00Z',
|
||||
updateTime: '2024-04-09T16:45:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '防疫培训服务',
|
||||
code: 'SVC003',
|
||||
category: 'training',
|
||||
description: '提供动物防疫知识培训和技能提升服务',
|
||||
manager: '王五',
|
||||
contact: '13800138003',
|
||||
priority: 'high',
|
||||
status: 'active',
|
||||
createTime: '2024-02-01T14:30:00Z',
|
||||
updateTime: '2024-04-08T10:15:00Z'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '疫情监测服务',
|
||||
code: 'SVC004',
|
||||
category: 'epidemic',
|
||||
description: '提供动物疫情监测和预警服务',
|
||||
manager: '赵六',
|
||||
contact: '13800138004',
|
||||
priority: 'high',
|
||||
status: 'active',
|
||||
createTime: '2024-02-15T11:45:00Z',
|
||||
updateTime: '2024-04-07T13:30:00Z'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '资料查询服务',
|
||||
code: 'SVC005',
|
||||
category: 'other',
|
||||
description: '提供养殖相关资料查询和借阅服务',
|
||||
manager: '钱七',
|
||||
contact: '13800138005',
|
||||
priority: 'low',
|
||||
status: 'inactive',
|
||||
createTime: '2024-01-10T09:00:00Z',
|
||||
updateTime: '2024-03-01T16:20:00Z'
|
||||
}
|
||||
])
|
||||
|
||||
// 服务表格列定义
|
||||
const serviceColumns = [
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '服务编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code'
|
||||
},
|
||||
{
|
||||
title: '服务类型',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
customRender: (text) => {
|
||||
const categoryMap = {
|
||||
policy: '政策咨询',
|
||||
technical: '技术指导',
|
||||
training: '培训服务',
|
||||
epidemic: '疫情防控',
|
||||
other: '其他服务'
|
||||
}
|
||||
return categoryMap[text] || text
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'manager',
|
||||
key: 'manager'
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
dataIndex: 'contact',
|
||||
key: 'contact'
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
customRender: (text) => dayjs(text).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
customRender: (text) => dayjs(text).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 模态框配置
|
||||
const serviceModalVisible = ref(false)
|
||||
const serviceModalTitle = ref('添加服务')
|
||||
const serviceForm = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
category: '',
|
||||
description: '',
|
||||
manager: '',
|
||||
contact: '',
|
||||
priority: 'medium',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
// 获取优先级颜色
|
||||
const getPriorityColor = (priority) => {
|
||||
const colorMap = {
|
||||
high: 'red',
|
||||
medium: 'orange',
|
||||
low: 'green'
|
||||
}
|
||||
return colorMap[priority] || 'default'
|
||||
}
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority) => {
|
||||
const textMap = {
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
}
|
||||
return textMap[priority] || priority
|
||||
}
|
||||
|
||||
// 搜索服务
|
||||
const handleSearch = (keyword) => {
|
||||
// 这里应该有实际的搜索逻辑
|
||||
console.log('搜索服务:', keyword)
|
||||
}
|
||||
|
||||
// 添加服务
|
||||
const handleAddService = () => {
|
||||
serviceModalTitle.value = '添加服务'
|
||||
Object.keys(serviceForm).forEach(key => {
|
||||
serviceForm[key] = ''
|
||||
})
|
||||
serviceForm.priority = 'medium'
|
||||
serviceForm.status = 'active'
|
||||
serviceModalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑服务
|
||||
const handleEditService = (service) => {
|
||||
serviceModalTitle.value = '编辑服务'
|
||||
Object.keys(serviceForm).forEach(key => {
|
||||
serviceForm[key] = service[key] || ''
|
||||
})
|
||||
serviceModalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看服务
|
||||
const handleViewService = (service) => {
|
||||
console.log('查看服务:', service)
|
||||
}
|
||||
|
||||
// 删除服务
|
||||
const handleDeleteService = (id) => {
|
||||
console.log('删除服务:', id)
|
||||
}
|
||||
|
||||
// 切换服务状态
|
||||
const handleStatusChange = (id, checked) => {
|
||||
console.log('切换服务状态:', id, checked ? 'active' : 'inactive')
|
||||
}
|
||||
|
||||
// 导入服务
|
||||
const handleImportServices = () => {
|
||||
console.log('导入服务')
|
||||
}
|
||||
|
||||
// 导出服务
|
||||
const handleExportServices = () => {
|
||||
console.log('导出服务')
|
||||
}
|
||||
|
||||
// 模态框确认
|
||||
const handleServiceModalOk = () => {
|
||||
console.log('提交表单:', serviceForm)
|
||||
serviceModalVisible.value = false
|
||||
}
|
||||
|
||||
// 模态框取消
|
||||
const handleServiceModalCancel = () => {
|
||||
serviceModalVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
848
government-admin/src/views/SlaughterHarmless.vue
Normal file
848
government-admin/src/views/SlaughterHarmless.vue
Normal file
@@ -0,0 +1,848 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>屠宰无害化</h1>
|
||||
|
||||
<!-- 搜索和操作栏 -->
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: center;">
|
||||
<a-input v-model:value="searchKeyword" placeholder="输入屠宰场名称、批次号或处理编号" style="width: 300px;">
|
||||
<template #prefix>
|
||||
<span class="iconfont icon-sousuo"></span>
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-select v-model:value="slaughterhouseFilter" placeholder="屠宰场" style="width: 150px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option v-for="house in slaughterhouses" :key="house.id" :value="house.id">
|
||||
{{ house.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select v-model:value="processTypeFilter" placeholder="处理类型" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="slaughter">正常屠宰</a-select-option>
|
||||
<a-select-option value="harmless">无害化处理</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select v-model:value="statusFilter" placeholder="处理状态" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
<a-select-option value="processing">处理中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="abnormal">异常</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-range-picker v-model:value="dateRange" style="width: 300px;" />
|
||||
|
||||
<a-button type="primary" @click="handleSearch" style="margin-left: auto;">
|
||||
<span class="iconfont icon-sousuo"></span> 搜索
|
||||
</a-button>
|
||||
|
||||
<a-button type="default" @click="handleReset">重置</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleImport">
|
||||
<span class="iconfont icon-daoru"></span> 导入
|
||||
</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleExport">
|
||||
<span class="iconfont icon-daochu"></span> 导出
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="本月屠宰量" :value="monthlySlaughterCount" suffix="头" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="本月无害化处理量" :value="monthlyHarmlessCount" suffix="头" :valueStyle="{ color: '#1890ff' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="待处理数量" :value="pendingCount" suffix="批" :valueStyle="{ color: '#faad14' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="异常处理数量" :value="abnormalCount" suffix="批" :valueStyle="{ color: '#f5222d' }" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 处理趋势图表 -->
|
||||
<a-card title="屠宰与无害化处理趋势" style="margin-bottom: 16px;">
|
||||
<div style="height: 300px;" ref="trendChartRef"></div>
|
||||
</a-card>
|
||||
|
||||
<!-- 处理列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="processList"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
>
|
||||
<!-- 处理类型列 -->
|
||||
<template #bodyCell:processType="{ record }">
|
||||
<a-tag :color="record.processType === 'slaughter' ? 'green' : 'blue'">
|
||||
{{ getProcessTypeText(record.processType) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #bodyCell:action="{ record }">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a-button size="small" @click="handleView(record)">查看</a-button>
|
||||
<a-button size="small" type="primary" @click="handleEdit(record)" v-if="record.status !== 'completed'">编辑</a-button>
|
||||
<a-button size="small" danger @click="handleDelete(record.id)" v-if="record.status !== 'completed'">删除</a-button>
|
||||
<a-button size="small" @click="handleMarkComplete(record.id)" v-if="record.status === 'processing'">标记完成</a-button>
|
||||
<a-button size="small" @click="handleReportIssue(record.id)" v-if="record.status !== 'completed' && record.status !== 'abnormal'">报告异常</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑处理记录模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isAddEditModalOpen"
|
||||
title="新增/编辑屠宰无害化处理记录"
|
||||
:footer="null"
|
||||
width={700}
|
||||
>
|
||||
<a-form
|
||||
:model="currentProcess"
|
||||
layout="vertical"
|
||||
style="max-width: 500px; margin: 0 auto;"
|
||||
>
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="处理编号" name="processCode" :rules="[{ required: true, message: '请输入处理编号' }]">
|
||||
<a-input v-model:value="currentProcess.processCode" placeholder="请输入处理编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="处理类型" name="processType" :rules="[{ required: true, message: '请选择处理类型' }]">
|
||||
<a-select v-model:value="currentProcess.processType" placeholder="请选择处理类型">
|
||||
<a-select-option value="slaughter">正常屠宰</a-select-option>
|
||||
<a-select-option value="harmless">无害化处理</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="屠宰场" name="slaughterhouseId" :rules="[{ required: true, message: '请选择屠宰场' }]">
|
||||
<a-select v-model:value="currentProcess.slaughterhouseId" placeholder="请选择屠宰场">
|
||||
<a-select-option v-for="house in slaughterhouses" :key="house.id" :value="house.id">
|
||||
{{ house.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="处理数量" name="quantity" :rules="[{ required: true, message: '请输入处理数量' }]">
|
||||
<a-input-number v-model:value="currentProcess.quantity" min={1} style="width: 100%;" placeholder="处理数量" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="处理日期" name="processDate" :rules="[{ required: true, message: '请选择处理日期' }]">
|
||||
<a-date-picker v-model:value="currentProcess.processDate" style="width: 100%;" placeholder="处理日期" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="负责人" name="manager" :rules="[{ required: true, message: '请输入负责人' }]">
|
||||
<a-input v-model:value="currentProcess.manager" placeholder="请输入负责人" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="contactPhone" :rules="[{ required: true, message: '请输入联系电话' }]">
|
||||
<a-input v-model:value="currentProcess.contactPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="处理状态" name="status" :rules="[{ required: true, message: '请选择处理状态' }]">
|
||||
<a-select v-model:value="currentProcess.status" placeholder="请选择处理状态">
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
<a-select-option value="processing">处理中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="abnormal">异常</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="处理原因" name="reason">
|
||||
<a-input.TextArea v-model:value="currentProcess.reason" placeholder="请输入处理原因" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="处理方法" name="method" v-if="currentProcess.processType === 'harmless'">
|
||||
<a-input.TextArea v-model:value="currentProcess.method" placeholder="请输入无害化处理方法" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注信息" name="remark">
|
||||
<a-input.TextArea v-model:value="currentProcess.remark" placeholder="请输入备注信息" :rows="2" />
|
||||
</a-form-item>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 16px;">
|
||||
<a-button @click="isAddEditModalOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看处理详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isViewModalOpen"
|
||||
title="屠宰无害化处理详情"
|
||||
:footer="null"
|
||||
width={800}
|
||||
>
|
||||
<div v-if="viewProcess" class="process-detail">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">处理编号:</div>
|
||||
<div class="detail-value">{{ viewProcess.processCode }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">处理类型:</div>
|
||||
<div class="detail-value">{{ getProcessTypeText(viewProcess.processType) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">屠宰场:</div>
|
||||
<div class="detail-value">{{ getSlaughterhouseName(viewProcess.slaughterhouseId) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">处理数量:</div>
|
||||
<div class="detail-value">{{ viewProcess.quantity }} 头</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">处理日期:</div>
|
||||
<div class="detail-value">{{ viewProcess.processDate }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">负责人:</div>
|
||||
<div class="detail-value">{{ viewProcess.manager }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">联系电话:</div>
|
||||
<div class="detail-value">{{ viewProcess.contactPhone }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">处理状态:</div>
|
||||
<div class="detail-value">
|
||||
<a-tag :color="getStatusColor(viewProcess.status)">{{ getStatusText(viewProcess.status) }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">处理原因:</div>
|
||||
<div class="detail-value">{{ viewProcess.reason || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row" v-if="viewProcess.method">
|
||||
<div class="detail-label">处理方法:</div>
|
||||
<div class="detail-value">{{ viewProcess.method }}</div>
|
||||
</div>
|
||||
<div class="detail-row" v-if="viewProcess.abnormalReason">
|
||||
<div class="detail-label">异常原因:</div>
|
||||
<div class="detail-value">{{ viewProcess.abnormalReason }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">备注信息:</div>
|
||||
<div class="detail-value">{{ viewProcess.remark || '无' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片展示 -->
|
||||
<div class="detail-row" v-if="viewProcess.photos && viewProcess.photos.length > 0">
|
||||
<div class="detail-label">现场照片:</div>
|
||||
<div class="detail-value">
|
||||
<a-image
|
||||
v-for="(photo, index) in viewProcess.photos"
|
||||
:key="index"
|
||||
:src="photo"
|
||||
style="width: 100px; height: 100px; margin-right: 8px; margin-bottom: 8px;"
|
||||
:preview="{ visible: false }"
|
||||
@click="handlePreviewImage(photo)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 24px;">
|
||||
<a-button @click="isViewModalOpen = false">关闭</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<a-popconfirm
|
||||
v-model:open="isConfirmModalOpen"
|
||||
title="确认操作"
|
||||
:description="confirmMessage"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmAction"
|
||||
>
|
||||
<template #reference>
|
||||
<!-- 这里是空的,通过其他按钮触发 -->
|
||||
</template>
|
||||
</a-popconfirm>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<a-modal v-model:open="previewVisible" title="图片预览" footer={null} @cancel="previewVisible = false">
|
||||
<img alt="预览图片" style="width: 100%;" :src="previewImage" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 搜索条件
|
||||
const searchKeyword = ref('')
|
||||
const slaughterhouseFilter = ref('')
|
||||
const processTypeFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
const dateRange = ref([])
|
||||
|
||||
// 统计数据
|
||||
const monthlySlaughterCount = ref(1256)
|
||||
const monthlyHarmlessCount = ref(78)
|
||||
const pendingCount = ref(12)
|
||||
const abnormalCount = ref(3)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 选中的行
|
||||
const selectedRowKeys = ref([])
|
||||
const onSelectChange = (newSelectedRowKeys) => {
|
||||
selectedRowKeys.value = newSelectedRowKeys
|
||||
}
|
||||
|
||||
// 图表引用
|
||||
const trendChartRef = ref(null)
|
||||
|
||||
// 模态框状态
|
||||
const isAddEditModalOpen = ref(false)
|
||||
const isViewModalOpen = ref(false)
|
||||
const isConfirmModalOpen = ref(false)
|
||||
const confirmMessage = ref('')
|
||||
const confirmAction = ref('')
|
||||
const confirmTargetId = ref('')
|
||||
|
||||
// 图片预览
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref('')
|
||||
|
||||
// 当前编辑的处理记录
|
||||
const currentProcess = reactive({
|
||||
id: '',
|
||||
processCode: '',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '',
|
||||
quantity: 1,
|
||||
processDate: '',
|
||||
manager: '',
|
||||
contactPhone: '',
|
||||
status: 'pending',
|
||||
reason: '',
|
||||
method: '',
|
||||
abnormalReason: '',
|
||||
remark: '',
|
||||
photos: []
|
||||
})
|
||||
|
||||
// 查看的处理记录
|
||||
const viewProcess = ref(null)
|
||||
|
||||
// 屠宰场列表
|
||||
const slaughterhouses = ref([
|
||||
{ id: '1', name: '阳光屠宰场' },
|
||||
{ id: '2', name: '绿源屠宰场' },
|
||||
{ id: '3', name: '清真屠宰场' },
|
||||
{ id: '4', name: '放心肉屠宰场' },
|
||||
{ id: '5', name: '现代化屠宰中心' }
|
||||
])
|
||||
|
||||
// 处理列表数据
|
||||
const processList = ref([
|
||||
{
|
||||
id: '1',
|
||||
processCode: 'S2023001',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '1',
|
||||
quantity: 150,
|
||||
processDate: '2023-10-01',
|
||||
manager: '张三',
|
||||
contactPhone: '13800138001',
|
||||
status: 'completed',
|
||||
reason: '正常屠宰',
|
||||
remark: '',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Slaughter+1']
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
processCode: 'H2023001',
|
||||
processType: 'harmless',
|
||||
slaughterhouseId: '2',
|
||||
quantity: 5,
|
||||
processDate: '2023-10-02',
|
||||
manager: '李四',
|
||||
contactPhone: '13800138002',
|
||||
status: 'completed',
|
||||
reason: '疾病死亡',
|
||||
method: '高温焚烧处理',
|
||||
remark: '严格按照无害化处理规程操作',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Harmless+1', 'https://via.placeholder.com/300x200?text=Harmless+2']
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
processCode: 'S2023002',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '3',
|
||||
quantity: 120,
|
||||
processDate: '2023-10-03',
|
||||
manager: '王五',
|
||||
contactPhone: '13800138003',
|
||||
status: 'completed',
|
||||
reason: '正常屠宰',
|
||||
remark: '',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Slaughter+2']
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
processCode: 'S2023003',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '4',
|
||||
quantity: 135,
|
||||
processDate: '2023-10-04',
|
||||
manager: '赵六',
|
||||
contactPhone: '13800138004',
|
||||
status: 'completed',
|
||||
reason: '正常屠宰',
|
||||
remark: '',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Slaughter+3']
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
processCode: 'H2023002',
|
||||
processType: 'harmless',
|
||||
slaughterhouseId: '1',
|
||||
quantity: 8,
|
||||
processDate: '2023-10-05',
|
||||
manager: '孙七',
|
||||
contactPhone: '13800138005',
|
||||
status: 'completed',
|
||||
reason: '运输死亡',
|
||||
method: '高温焚烧处理',
|
||||
remark: '',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Harmless+3']
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
processCode: 'S2023004',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '5',
|
||||
quantity: 200,
|
||||
processDate: '2023-10-06',
|
||||
manager: '周八',
|
||||
contactPhone: '13800138006',
|
||||
status: 'processing',
|
||||
reason: '正常屠宰',
|
||||
remark: '',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Slaughter+4']
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
processCode: 'H2023003',
|
||||
processType: 'harmless',
|
||||
slaughterhouseId: '3',
|
||||
quantity: 3,
|
||||
processDate: '2023-10-07',
|
||||
manager: '吴九',
|
||||
contactPhone: '13800138007',
|
||||
status: 'pending',
|
||||
reason: '疑似疫情',
|
||||
method: '待确定',
|
||||
remark: '需进一步检验确认',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Harmless+4']
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
processCode: 'S2023005',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '2',
|
||||
quantity: 180,
|
||||
processDate: '2023-10-08',
|
||||
manager: '郑十',
|
||||
contactPhone: '13800138008',
|
||||
status: 'pending',
|
||||
reason: '正常屠宰',
|
||||
remark: '',
|
||||
photos: []
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
processCode: 'H2023004',
|
||||
processType: 'harmless',
|
||||
slaughterhouseId: '5',
|
||||
quantity: 12,
|
||||
processDate: '2023-10-09',
|
||||
manager: '钱十一',
|
||||
contactPhone: '13800138009',
|
||||
status: 'abnormal',
|
||||
reason: '批量死亡',
|
||||
method: '高温焚烧处理',
|
||||
abnormalReason: '设备故障,处理不彻底',
|
||||
remark: '已要求整改',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Harmless+5', 'https://via.placeholder.com/300x200?text=Harmless+6']
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
processCode: 'S2023006',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '1',
|
||||
quantity: 160,
|
||||
processDate: '2023-10-10',
|
||||
manager: '孙十二',
|
||||
contactPhone: '13800138010',
|
||||
status: 'processing',
|
||||
reason: '正常屠宰',
|
||||
remark: '',
|
||||
photos: ['https://via.placeholder.com/300x200?text=Slaughter+5']
|
||||
}
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '处理编号',
|
||||
dataIndex: 'processCode',
|
||||
key: 'processCode',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '处理类型',
|
||||
dataIndex: 'processType',
|
||||
key: 'processType',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '屠宰场',
|
||||
dataIndex: 'slaughterhouseId',
|
||||
key: 'slaughterhouseId',
|
||||
width: 120,
|
||||
customRender: ({ text }) => getSlaughterhouseName(text)
|
||||
},
|
||||
{
|
||||
title: '处理数量',
|
||||
dataIndex: 'quantity',
|
||||
key: 'quantity',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '处理日期',
|
||||
dataIndex: 'processDate',
|
||||
key: 'processDate',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'manager',
|
||||
key: 'manager',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contactPhone',
|
||||
key: 'contactPhone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '处理状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取处理类型文本
|
||||
const getProcessTypeText = (processType) => {
|
||||
switch (processType) {
|
||||
case 'slaughter': return '正常屠宰'
|
||||
case 'harmless': return '无害化处理'
|
||||
default: return processType
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'pending': return 'default'
|
||||
case 'processing': return 'blue'
|
||||
case 'completed': return 'green'
|
||||
case 'abnormal': return 'red'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'pending': return '待处理'
|
||||
case 'processing': return '处理中'
|
||||
case 'completed': return '已完成'
|
||||
case 'abnormal': return '异常'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
// 获取屠宰场名称
|
||||
const getSlaughterhouseName = (id) => {
|
||||
const house = slaughterhouses.value.find(item => item.id === id)
|
||||
return house ? house.name : id
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索条件:', {
|
||||
keyword: searchKeyword.value,
|
||||
slaughterhouseId: slaughterhouseFilter.value,
|
||||
processType: processTypeFilter.value,
|
||||
status: statusFilter.value,
|
||||
dateRange: dateRange.value
|
||||
})
|
||||
// 这里应该有实际的搜索逻辑
|
||||
// 模拟搜索后的总数
|
||||
pagination.total = processList.value.length
|
||||
}
|
||||
|
||||
// 重置处理
|
||||
const handleReset = () => {
|
||||
searchKeyword.value = ''
|
||||
slaughterhouseFilter.value = ''
|
||||
processTypeFilter.value = ''
|
||||
statusFilter.value = ''
|
||||
dateRange.value = []
|
||||
selectedRowKeys.value = []
|
||||
}
|
||||
|
||||
// 导入处理
|
||||
const handleImport = () => {
|
||||
console.log('导入处理记录')
|
||||
// 这里应该有实际的导入逻辑
|
||||
}
|
||||
|
||||
// 导出处理
|
||||
const handleExport = () => {
|
||||
console.log('导出处理记录')
|
||||
// 这里应该有实际的导出逻辑
|
||||
}
|
||||
|
||||
// 新增处理记录
|
||||
const handleAddProcess = () => {
|
||||
// 重置当前处理记录数据
|
||||
Object.assign(currentProcess, {
|
||||
id: '',
|
||||
processCode: '',
|
||||
processType: 'slaughter',
|
||||
slaughterhouseId: '',
|
||||
quantity: 1,
|
||||
processDate: '',
|
||||
manager: '',
|
||||
contactPhone: '',
|
||||
status: 'pending',
|
||||
reason: '',
|
||||
method: '',
|
||||
abnormalReason: '',
|
||||
remark: '',
|
||||
photos: []
|
||||
})
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 编辑处理记录
|
||||
const handleEdit = (record) => {
|
||||
// 复制记录数据到当前处理记录
|
||||
Object.assign(currentProcess, JSON.parse(JSON.stringify(record)))
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 查看处理记录
|
||||
const handleView = (record) => {
|
||||
viewProcess.value = JSON.parse(JSON.stringify(record))
|
||||
isViewModalOpen.value = true
|
||||
}
|
||||
|
||||
// 删除处理记录 - 弹出确认对话框
|
||||
const handleDelete = (id) => {
|
||||
confirmMessage.value = '确定要删除该处理记录吗?'
|
||||
confirmAction.value = 'delete'
|
||||
confirmTargetId.value = id
|
||||
isConfirmModalOpen.value = true
|
||||
}
|
||||
|
||||
// 标记完成 - 弹出确认对话框
|
||||
const handleMarkComplete = (id) => {
|
||||
confirmMessage.value = '确定要标记该处理为已完成吗?'
|
||||
confirmAction.value = 'markComplete'
|
||||
confirmTargetId.value = id
|
||||
isConfirmModalOpen.value = true
|
||||
}
|
||||
|
||||
// 报告异常 - 弹出确认对话框
|
||||
const handleReportIssue = (id) => {
|
||||
confirmMessage.value = '确定要报告该处理异常吗?'
|
||||
confirmAction.value = 'reportIssue'
|
||||
confirmTargetId.value = id
|
||||
isConfirmModalOpen.value = true
|
||||
}
|
||||
|
||||
// 确认操作
|
||||
const handleConfirmAction = () => {
|
||||
if (confirmAction.value === 'delete') {
|
||||
console.log('删除处理记录:', confirmTargetId.value)
|
||||
// 这里应该有实际的删除逻辑
|
||||
// 模拟删除成功
|
||||
alert(`成功删除处理记录ID: ${confirmTargetId.value}`)
|
||||
} else if (confirmAction.value === 'markComplete') {
|
||||
console.log('标记完成:', confirmTargetId.value)
|
||||
// 这里应该有实际的标记完成逻辑
|
||||
// 模拟标记成功
|
||||
alert('成功标记为已完成')
|
||||
} else if (confirmAction.value === 'reportIssue') {
|
||||
console.log('报告异常:', confirmTargetId.value)
|
||||
// 这里应该有实际的报告异常逻辑
|
||||
// 模拟报告成功
|
||||
alert('成功报告异常,相关人员将进行处理')
|
||||
}
|
||||
isConfirmModalOpen.value = false
|
||||
}
|
||||
|
||||
// 保存处理记录
|
||||
const handleSave = () => {
|
||||
console.log('保存处理记录:', currentProcess)
|
||||
// 这里应该有实际的保存逻辑
|
||||
// 模拟保存成功
|
||||
isAddEditModalOpen.value = false
|
||||
alert('保存成功')
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
const handlePreviewImage = (image) => {
|
||||
previewImage.value = image
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
// 初始化处理趋势图表
|
||||
const initTrendChart = () => {
|
||||
if (!trendChartRef.value) return
|
||||
|
||||
const chart = echarts.init(trendChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['正常屠宰', '无害化处理']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '正常屠宰',
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [1200, 1320, 1010, 1340, 900, 1200, 1300, 1250, 1100, 1200, 1150, 1300]
|
||||
},
|
||||
{
|
||||
name: '无害化处理',
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [60, 72, 81, 94, 120, 132, 121, 114, 100, 80, 75, 90]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initTrendChart()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
pagination.total = processList.value.length
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.process-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
812
government-admin/src/views/SmartWarehouse.vue
Normal file
812
government-admin/src/views/SmartWarehouse.vue
Normal file
@@ -0,0 +1,812 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>智能仓库</h1>
|
||||
|
||||
<!-- 搜索和操作栏 -->
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: center;">
|
||||
<a-input v-model:value="searchKeyword" placeholder="输入物资名称或编号" style="width: 250px;">
|
||||
<template #prefix>
|
||||
<span class="iconfont icon-sousuo"></span>
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-select v-model:value="categoryFilter" placeholder="物资类别" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="feed">饲料</a-select-option>
|
||||
<a-select-option value="medicine">药品</a-select-option>
|
||||
<a-select-option value="equipment">设备</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select v-model:value="statusFilter" placeholder="库存状态" style="width: 120px;">
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="normal">正常</a-select-option>
|
||||
<a-select-option value="low">低库存</a-select-option>
|
||||
<a-select-option value="out">缺货</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-button type="primary" @click="handleSearch" style="margin-left: auto;">
|
||||
<span class="iconfont icon-sousuo"></span> 搜索
|
||||
</a-button>
|
||||
|
||||
<a-button type="default" @click="handleReset">重置</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleImport">
|
||||
<span class="iconfont icon-daoru"></span> 导入
|
||||
</a-button>
|
||||
|
||||
<a-button type="dashed" @click="handleExport">
|
||||
<span class="iconfont icon-daochu"></span> 导出
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" danger @click="handleAddMaterial">
|
||||
<span class="iconfont icon-tianjia"></span> 新增物资
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="物资总类" :value="totalCategories" suffix="种" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="库存总量" :value="totalQuantity" suffix="单位" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="低库存物资" :value="lowStockCount" suffix="种" :valueStyle="{ color: '#faad14' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="缺货物资" :value="outOfStockCount" suffix="种" :valueStyle="{ color: '#cf1322' }" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 库存预警图表 -->
|
||||
<a-card title="库存预警分析" style="margin-bottom: 16px;">
|
||||
<div style="height: 300px;" ref="stockChartRef"></div>
|
||||
</a-card>
|
||||
|
||||
<!-- 物资列表 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="materialsData"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 库存数量列 -->
|
||||
<template #bodyCell:stockQuantity="{ record }">
|
||||
<span :class="record.status !== 'normal' ? 'text-danger font-weight-bold' : ''">
|
||||
{{ record.stockQuantity }}{{ record.unit }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #bodyCell:action="{ record }">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a-button size="small" @click="handleView(record)">查看</a-button>
|
||||
<a-button size="small" type="primary" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button size="small" danger @click="handleDelete(record.id)">删除</a-button>
|
||||
<a-popconfirm title="确定要入库吗?" @confirm="() => handleStockIn(record.id)">
|
||||
<a-button size="small" type="link">入库</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm title="确定要出库吗?" @confirm="() => handleStockOut(record.id)">
|
||||
<a-button size="small" type="link">出库</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑物资模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isAddEditModalOpen"
|
||||
title="新增/编辑物资"
|
||||
:footer="null"
|
||||
width={700}
|
||||
>
|
||||
<a-form
|
||||
:model="currentMaterial"
|
||||
layout="vertical"
|
||||
style="max-width: 500px; margin: 0 auto;"
|
||||
>
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="物资编号" name="code" :rules="[{ required: true, message: '请输入物资编号' }]">
|
||||
<a-input v-model:value="currentMaterial.code" placeholder="请输入物资编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="物资名称" name="name" :rules="[{ required: true, message: '请输入物资名称' }]">
|
||||
<a-input v-model:value="currentMaterial.name" placeholder="请输入物资名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="物资类别" name="category" :rules="[{ required: true, message: '请选择物资类别' }]">
|
||||
<a-select v-model:value="currentMaterial.category" placeholder="请选择物资类别">
|
||||
<a-select-option value="feed">饲料</a-select-option>
|
||||
<a-select-option value="medicine">药品</a-select-option>
|
||||
<a-select-option value="equipment">设备</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="单位" name="unit" :rules="[{ required: true, message: '请输入单位' }]">
|
||||
<a-input v-model:value="currentMaterial.unit" placeholder="请输入单位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="库存数量" name="stockQuantity" :rules="[{ required: true, message: '请输入库存数量' }]">
|
||||
<a-input-number v-model:value="currentMaterial.stockQuantity" :min="0" placeholder="请输入库存数量" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="预警数量" name="warningQuantity" :rules="[{ required: true, message: '请输入预警数量' }]">
|
||||
<a-input-number v-model:value="currentMaterial.warningQuantity" :min="0" placeholder="请输入预警数量" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="供应商" name="supplier">
|
||||
<a-input v-model:value="currentMaterial.supplier" placeholder="请输入供应商" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注信息" name="remark">
|
||||
<a-input.TextArea v-model:value="currentMaterial.remark" placeholder="请输入备注信息" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 16px;">
|
||||
<a-button @click="isAddEditModalOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看物资详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isViewModalOpen"
|
||||
title="物资详情"
|
||||
:footer="null"
|
||||
width={700}
|
||||
>
|
||||
<div v-if="viewMaterial" class="material-detail">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">物资编号:</div>
|
||||
<div class="detail-value">{{ viewMaterial.code }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">物资名称:</div>
|
||||
<div class="detail-value">{{ viewMaterial.name }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">物资类别:</div>
|
||||
<div class="detail-value">{{ getCategoryText(viewMaterial.category) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">单位:</div>
|
||||
<div class="detail-value">{{ viewMaterial.unit }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">库存数量:</div>
|
||||
<div class="detail-value" :class="viewMaterial.status !== 'normal' ? 'text-danger font-weight-bold' : ''">
|
||||
{{ viewMaterial.stockQuantity }}{{ viewMaterial.unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">预警数量:</div>
|
||||
<div class="detail-value">{{ viewMaterial.warningQuantity }}{{ viewMaterial.unit }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">库存状态:</div>
|
||||
<div class="detail-value">
|
||||
<a-tag :color="getStatusColor(viewMaterial.status)">{{ getStatusText(viewMaterial.status) }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">供应商:</div>
|
||||
<div class="detail-value">{{ viewMaterial.supplier || '无' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">最后更新时间:</div>
|
||||
<div class="detail-value">{{ viewMaterial.updateTime }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">备注信息:</div>
|
||||
<div class="detail-value">{{ viewMaterial.remark || '无' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 24px;">
|
||||
<a-button @click="isViewModalOpen = false">关闭</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 入库/出库模态框 -->
|
||||
<a-modal
|
||||
v-model:open="isStockModalOpen"
|
||||
:title="stockModalTitle"
|
||||
:footer="null"
|
||||
width={500}
|
||||
>
|
||||
<a-form
|
||||
:model="stockForm"
|
||||
layout="vertical"
|
||||
style="max-width: 400px; margin: 0 auto;"
|
||||
>
|
||||
<a-form-item label="物资名称" name="materialName" :rules="[{ required: true }]">
|
||||
<a-input v-model:value="stockForm.materialName" disabled />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="当前库存" name="currentStock" :rules="[{ required: true }]">
|
||||
<a-input v-model:value="stockForm.currentStock" disabled />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="数量" name="quantity" :rules="[{ required: true, message: '请输入数量' }]">
|
||||
<a-input-number v-model:value="stockForm.quantity" :min="1" placeholder="请输入数量" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="操作人" name="operator" :rules="[{ required: true, message: '请输入操作人' }]">
|
||||
<a-input v-model:value="stockForm.operator" placeholder="请输入操作人" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-input.TextArea v-model:value="stockForm.remark" placeholder="请输入备注" :rows="2" />
|
||||
</a-form-item>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 16px;">
|
||||
<a-button @click="isStockModalOpen = false">取消</a-button>
|
||||
<a-button type="primary" @click="handleStockSubmit">确定</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 搜索条件
|
||||
const searchKeyword = ref('')
|
||||
const categoryFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
|
||||
// 统计数据
|
||||
const totalCategories = ref(86)
|
||||
const totalQuantity = ref(12560)
|
||||
const lowStockCount = ref(12)
|
||||
const outOfStockCount = ref(3)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 选中的行
|
||||
const selectedRowKeys = ref([])
|
||||
const onSelectChange = (newSelectedRowKeys) => {
|
||||
selectedRowKeys.value = newSelectedRowKeys
|
||||
}
|
||||
|
||||
// 图表引用
|
||||
const stockChartRef = ref(null)
|
||||
|
||||
// 模态框状态
|
||||
const isAddEditModalOpen = ref(false)
|
||||
const isViewModalOpen = ref(false)
|
||||
const isStockModalOpen = ref(false)
|
||||
const stockModalTitle = ref('入库')
|
||||
const currentStockOperation = ref('in') // 'in' 或 'out'
|
||||
const currentStockMaterialId = ref('')
|
||||
|
||||
// 当前编辑的物资
|
||||
const currentMaterial = reactive({
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
category: '',
|
||||
unit: '',
|
||||
stockQuantity: 0,
|
||||
warningQuantity: 0,
|
||||
status: 'normal',
|
||||
supplier: '',
|
||||
remark: '',
|
||||
updateTime: ''
|
||||
})
|
||||
|
||||
// 查看的物资
|
||||
const viewMaterial = ref(null)
|
||||
|
||||
// 入库/出库表单
|
||||
const stockForm = reactive({
|
||||
materialName: '',
|
||||
currentStock: '',
|
||||
quantity: 1,
|
||||
operator: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 物资列表数据
|
||||
const materialsData = ref([
|
||||
{
|
||||
id: '1',
|
||||
code: 'FEED001',
|
||||
name: '牛用精饲料',
|
||||
category: 'feed',
|
||||
unit: '袋',
|
||||
stockQuantity: 250,
|
||||
warningQuantity: 50,
|
||||
status: 'normal',
|
||||
supplier: '绿源饲料公司',
|
||||
remark: '高蛋白配方',
|
||||
updateTime: '2024-04-10 09:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
code: 'FEED002',
|
||||
name: '粗饲料',
|
||||
category: 'feed',
|
||||
unit: '吨',
|
||||
stockQuantity: 12,
|
||||
warningQuantity: 5,
|
||||
status: 'low',
|
||||
supplier: '草原饲料厂',
|
||||
remark: '优质牧草',
|
||||
updateTime: '2024-04-09 14:20:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
code: 'MED001',
|
||||
name: '牛瘟疫苗',
|
||||
category: 'medicine',
|
||||
unit: '盒',
|
||||
stockQuantity: 0,
|
||||
warningQuantity: 10,
|
||||
status: 'out',
|
||||
supplier: '动保生物公司',
|
||||
remark: '每盒10支',
|
||||
updateTime: '2024-04-08 10:15:00'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
code: 'MED002',
|
||||
name: '驱虫药',
|
||||
category: 'medicine',
|
||||
unit: '瓶',
|
||||
stockQuantity: 85,
|
||||
warningQuantity: 20,
|
||||
status: 'normal',
|
||||
supplier: '兽药批发中心',
|
||||
remark: '广谱驱虫',
|
||||
updateTime: '2024-04-10 11:45:00'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
code: 'EQU001',
|
||||
name: '牛用耳标',
|
||||
category: 'equipment',
|
||||
unit: '个',
|
||||
stockQuantity: 3500,
|
||||
warningQuantity: 500,
|
||||
status: 'normal',
|
||||
supplier: '畜牧设备公司',
|
||||
remark: 'RFID电子耳标',
|
||||
updateTime: '2024-04-07 16:00:00'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
code: 'EQU002',
|
||||
name: '体温计',
|
||||
category: 'equipment',
|
||||
unit: '支',
|
||||
stockQuantity: 15,
|
||||
warningQuantity: 5,
|
||||
status: 'normal',
|
||||
supplier: '医疗器械公司',
|
||||
remark: '兽用电子体温计',
|
||||
updateTime: '2024-04-06 13:30:00'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
code: 'FEED003',
|
||||
name: '矿物质添加剂',
|
||||
category: 'feed',
|
||||
unit: 'kg',
|
||||
stockQuantity: 35,
|
||||
warningQuantity: 10,
|
||||
status: 'normal',
|
||||
supplier: '营养添加剂厂',
|
||||
remark: '补充微量元素',
|
||||
updateTime: '2024-04-05 10:15:00'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
code: 'MED003',
|
||||
name: '抗生素',
|
||||
category: 'medicine',
|
||||
unit: '盒',
|
||||
stockQuantity: 5,
|
||||
warningQuantity: 10,
|
||||
status: 'low',
|
||||
supplier: '兽药批发中心',
|
||||
remark: '需处方使用',
|
||||
updateTime: '2024-04-04 15:45:00'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
code: 'EQU003',
|
||||
name: '消毒设备',
|
||||
category: 'equipment',
|
||||
unit: '台',
|
||||
stockQuantity: 3,
|
||||
warningQuantity: 1,
|
||||
status: 'normal',
|
||||
supplier: '畜牧设备公司',
|
||||
remark: '自动喷雾消毒机',
|
||||
updateTime: '2024-04-03 09:30:00'
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
code: 'OTH001',
|
||||
name: '防护服',
|
||||
category: 'other',
|
||||
unit: '套',
|
||||
stockQuantity: 120,
|
||||
warningQuantity: 30,
|
||||
status: 'normal',
|
||||
supplier: '劳保用品公司',
|
||||
remark: '一次性使用',
|
||||
updateTime: '2024-04-02 14:20:00'
|
||||
}
|
||||
])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '物资编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '物资名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '物资类别',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 100,
|
||||
customRender: ({ text }) => getCategoryText(text)
|
||||
},
|
||||
{
|
||||
title: '单位',
|
||||
dataIndex: 'unit',
|
||||
key: 'unit',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '库存数量',
|
||||
dataIndex: 'stockQuantity',
|
||||
key: 'stockQuantity',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '预警数量',
|
||||
dataIndex: 'warningQuantity',
|
||||
key: 'warningQuantity',
|
||||
width: 100,
|
||||
customRender: ({ text, record }) => `${text}${record.unit}`
|
||||
},
|
||||
{
|
||||
title: '库存状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '供应商',
|
||||
dataIndex: 'supplier',
|
||||
key: 'supplier',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '最后更新',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'normal': return 'green'
|
||||
case 'low': return 'orange'
|
||||
case 'out': return 'red'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'normal': return '正常'
|
||||
case 'low': return '低库存'
|
||||
case 'out': return '缺货'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类别文本
|
||||
const getCategoryText = (category) => {
|
||||
switch (category) {
|
||||
case 'feed': return '饲料'
|
||||
case 'medicine': return '药品'
|
||||
case 'equipment': return '设备'
|
||||
case 'other': return '其他'
|
||||
default: return category
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索条件:', {
|
||||
keyword: searchKeyword.value,
|
||||
category: categoryFilter.value,
|
||||
status: statusFilter.value
|
||||
})
|
||||
// 这里应该有实际的搜索逻辑
|
||||
// 模拟搜索后的总数
|
||||
pagination.total = materialsData.value.length
|
||||
}
|
||||
|
||||
// 重置处理
|
||||
const handleReset = () => {
|
||||
searchKeyword.value = ''
|
||||
categoryFilter.value = ''
|
||||
statusFilter.value = ''
|
||||
selectedRowKeys.value = []
|
||||
}
|
||||
|
||||
// 导入处理
|
||||
const handleImport = () => {
|
||||
console.log('导入物资数据')
|
||||
// 这里应该有实际的导入逻辑
|
||||
}
|
||||
|
||||
// 导出处理
|
||||
const handleExport = () => {
|
||||
console.log('导出物资数据')
|
||||
// 这里应该有实际的导出逻辑
|
||||
}
|
||||
|
||||
// 新增物资
|
||||
const handleAddMaterial = () => {
|
||||
// 重置当前物资数据
|
||||
Object.assign(currentMaterial, {
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
category: '',
|
||||
unit: '',
|
||||
stockQuantity: 0,
|
||||
warningQuantity: 0,
|
||||
status: 'normal',
|
||||
supplier: '',
|
||||
remark: '',
|
||||
updateTime: ''
|
||||
})
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 编辑物资
|
||||
const handleEdit = (record) => {
|
||||
// 复制记录数据到当前物资
|
||||
Object.assign(currentMaterial, JSON.parse(JSON.stringify(record)))
|
||||
isAddEditModalOpen.value = true
|
||||
}
|
||||
|
||||
// 查看物资
|
||||
const handleView = (record) => {
|
||||
viewMaterial.value = JSON.parse(JSON.stringify(record))
|
||||
isViewModalOpen.value = true
|
||||
}
|
||||
|
||||
// 删除物资
|
||||
const handleDelete = (id) => {
|
||||
console.log('删除物资:', id)
|
||||
// 这里应该有实际的删除逻辑和确认提示
|
||||
// 模拟删除成功
|
||||
alert(`成功删除物资ID: ${id}`)
|
||||
}
|
||||
|
||||
// 保存物资
|
||||
const handleSave = () => {
|
||||
console.log('保存物资:', currentMaterial)
|
||||
// 这里应该有实际的保存逻辑
|
||||
// 更新状态
|
||||
if (currentMaterial.stockQuantity === 0) {
|
||||
currentMaterial.status = 'out'
|
||||
} else if (currentMaterial.stockQuantity <= currentMaterial.warningQuantity) {
|
||||
currentMaterial.status = 'low'
|
||||
} else {
|
||||
currentMaterial.status = 'normal'
|
||||
}
|
||||
// 模拟保存成功
|
||||
isAddEditModalOpen.value = false
|
||||
alert('保存成功')
|
||||
}
|
||||
|
||||
// 入库
|
||||
const handleStockIn = (id) => {
|
||||
const material = materialsData.value.find(item => item.id === id)
|
||||
if (material) {
|
||||
currentStockOperation.value = 'in'
|
||||
currentStockMaterialId.value = id
|
||||
stockModalTitle.value = '入库'
|
||||
Object.assign(stockForm, {
|
||||
materialName: material.name,
|
||||
currentStock: `${material.stockQuantity}${material.unit}`,
|
||||
quantity: 1,
|
||||
operator: '',
|
||||
remark: ''
|
||||
})
|
||||
isStockModalOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 出库
|
||||
const handleStockOut = (id) => {
|
||||
const material = materialsData.value.find(item => item.id === id)
|
||||
if (material) {
|
||||
currentStockOperation.value = 'out'
|
||||
currentStockMaterialId.value = id
|
||||
stockModalTitle.value = '出库'
|
||||
Object.assign(stockForm, {
|
||||
materialName: material.name,
|
||||
currentStock: `${material.stockQuantity}${material.unit}`,
|
||||
quantity: 1,
|
||||
operator: '',
|
||||
remark: ''
|
||||
})
|
||||
isStockModalOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 提交入库/出库
|
||||
const handleStockSubmit = () => {
|
||||
console.log(`${currentStockOperation.value === 'in' ? '入库' : '出库'}操作:`, {
|
||||
materialId: currentStockMaterialId.value,
|
||||
quantity: stockForm.quantity,
|
||||
operator: stockForm.operator,
|
||||
remark: stockForm.remark
|
||||
})
|
||||
// 这里应该有实际的入库/出库逻辑
|
||||
// 模拟操作成功
|
||||
isStockModalOpen.value = false
|
||||
alert(`${currentStockOperation.value === 'in' ? '入库' : '出库'}操作成功`)
|
||||
}
|
||||
|
||||
// 初始化库存预警图表
|
||||
const initStockChart = () => {
|
||||
if (!stockChartRef.value) return
|
||||
|
||||
const chart = echarts.init(stockChartRef.value)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '库存状态分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{ value: totalCategories.value - lowStockCount.value - outOfStockCount.value, name: '正常库存', itemStyle: { color: '#52c41a' } },
|
||||
{ value: lowStockCount.value, name: '低库存', itemStyle: { color: '#faad14' } },
|
||||
{ value: outOfStockCount.value, name: '缺货', itemStyle: { color: '#f5222d' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initStockChart()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
pagination.total = materialsData.value.length
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.material-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.font-weight-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
371
government-admin/src/views/SupervisionDashboard.vue
Normal file
371
government-admin/src/views/SupervisionDashboard.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>监管仪表板</h1>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.entityCount }}</div>
|
||||
<div class="stat-label">监管实体总数</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.inspectionCount }}</div>
|
||||
<div class="stat-label">检查总次数</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.pendingCount }}</div>
|
||||
<div class="stat-label">待处理任务</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.abnormalCount }}</div>
|
||||
<div class="stat-label">异常情况数</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row gutter={24} style="margin-top: 24px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="监管趋势图" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="trend-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="监管类型分布" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="distribution-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 最新监管任务 -->
|
||||
<a-card title="最新监管任务" style="margin-top: 24px;">
|
||||
<a-table :columns="taskColumns" :data-source="recentTasks" pagination={false}>
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #bodyCell:action="{ record }">
|
||||
<a-button type="link" @click="viewTaskDetail(record.id)">查看详情</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'antd'
|
||||
import axios from 'axios'
|
||||
import * as echarts from 'echarts'
|
||||
import { getSupervisionStats } from '@/mock'
|
||||
|
||||
const stats = ref({
|
||||
entityCount: 0,
|
||||
inspectionCount: 0,
|
||||
pendingCount: 0,
|
||||
abnormalCount: 0
|
||||
})
|
||||
const recentTasks = ref([])
|
||||
let trendChart = null
|
||||
let distributionChart = null
|
||||
|
||||
// 任务表格列定义
|
||||
const taskColumns = [
|
||||
{
|
||||
title: '任务ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '监管对象',
|
||||
dataIndex: 'target',
|
||||
key: 'target'
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'manager',
|
||||
key: 'manager'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
slots: { customRender: 'status' }
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
slots: { customRender: 'action' }
|
||||
}
|
||||
]
|
||||
|
||||
// 根据状态获取标签颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
pending: 'orange',
|
||||
in_progress: 'blue',
|
||||
completed: 'green',
|
||||
abnormal: 'red',
|
||||
canceled: 'default'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 根据状态获取显示文本
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
pending: '待处理',
|
||||
in_progress: '处理中',
|
||||
completed: '已完成',
|
||||
abnormal: '异常',
|
||||
canceled: '已取消'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 获取监管统计数据
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
// 尝试从API获取数据
|
||||
const response = await axios.get('/api/supervision/stats')
|
||||
if (response.data.code === 200) {
|
||||
const data = response.data.data
|
||||
stats.value.entityCount = data.entityCount || 0
|
||||
stats.value.inspectionCount = data.inspectionCount || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取监管统计数据失败,使用模拟数据:', error)
|
||||
// 使用模拟数据
|
||||
const mockResponse = await getSupervisionStats()
|
||||
if (mockResponse.code === 200) {
|
||||
const data = mockResponse.data
|
||||
stats.value.entityCount = data.entityCount || 0
|
||||
stats.value.inspectionCount = data.inspectionCount || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 设置其他模拟数据
|
||||
stats.value.pendingCount = Math.floor(Math.random() * 20) + 5
|
||||
stats.value.abnormalCount = Math.floor(Math.random() * 10)
|
||||
}
|
||||
|
||||
// 获取最新任务数据
|
||||
const fetchRecentTasks = async () => {
|
||||
try {
|
||||
// 这里应该从API获取数据
|
||||
// 由于没有实际API,使用模拟数据
|
||||
recentTasks.value = [
|
||||
{ id: 1, name: '企业安全检查', target: '某食品加工厂', manager: '张三', status: 'pending', create_time: '2024-01-10 09:00:00' },
|
||||
{ id: 2, name: '环境监测', target: '某化工厂', manager: '李四', status: 'in_progress', create_time: '2024-01-09 14:30:00' },
|
||||
{ id: 3, name: '疫情防控检查', target: '某商场', manager: '王五', status: 'completed', create_time: '2024-01-08 11:20:00' },
|
||||
{ id: 4, name: '消防安全检查', target: '某酒店', manager: '赵六', status: 'abnormal', create_time: '2024-01-07 16:40:00' },
|
||||
{ id: 5, name: '质量检查', target: '某药品生产企业', manager: '钱七', status: 'pending', create_time: '2024-01-06 10:15:00' }
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('获取任务数据失败:', error)
|
||||
message.error('获取任务数据失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 趋势图
|
||||
const trendChartDom = document.getElementById('trend-chart')
|
||||
if (trendChartDom) {
|
||||
trendChart = echarts.init(trendChartDom)
|
||||
const trendOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['检查次数', '异常情况']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '检查次数',
|
||||
type: 'line',
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '异常情况',
|
||||
type: 'line',
|
||||
data: [5, 3, 2, 4, 1, 2],
|
||||
smooth: true
|
||||
}
|
||||
]
|
||||
}
|
||||
trendChart.setOption(trendOption)
|
||||
}
|
||||
|
||||
// 分布饼图
|
||||
const distributionChartDom = document.getElementById('distribution-chart')
|
||||
if (distributionChartDom) {
|
||||
distributionChart = echarts.init(distributionChartDom)
|
||||
const distributionOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '监管类型',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 30,
|
||||
name: '安全检查'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '环境监测'
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
name: '疫情防控'
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
name: '质量检查'
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
name: '其他'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
distributionChart.setOption(distributionOption)
|
||||
}
|
||||
}
|
||||
|
||||
// 响应窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (trendChart) {
|
||||
trendChart.resize()
|
||||
}
|
||||
if (distributionChart) {
|
||||
distributionChart.resize()
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务详情
|
||||
const viewTaskDetail = (taskId) => {
|
||||
message.info(`查看任务ID: ${taskId} 的详情`)
|
||||
// 这里可以跳转到任务详情页面
|
||||
// router.push(`/supervision/task/${taskId}`)
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchRecentTasks()
|
||||
setTimeout(() => {
|
||||
initCharts()
|
||||
}, 100)
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (trendChart) {
|
||||
trendChart.dispose()
|
||||
}
|
||||
if (distributionChart) {
|
||||
distributionChart.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
366
government-admin/src/views/UserManagement.vue
Normal file
366
government-admin/src/views/UserManagement.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>用户管理</h1>
|
||||
<!-- 工具栏 -->
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<a-row gutter={16}>
|
||||
<a-col :span="8">
|
||||
<a-input
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入用户名或ID搜索"
|
||||
allow-clear
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="16" style="text-align: right;">
|
||||
<a-button type="primary" @click="handleAddUser">
|
||||
<user-add-outlined /> 添加用户
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="users"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell:actions="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleEditUser(record)">编辑</a-button>
|
||||
<a-button type="link" danger @click="handleDeleteUser(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell:status="{ record }">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑用户弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showModal"
|
||||
:title="modalTitle"
|
||||
footer=""
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit"
|
||||
>
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="formState.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="真实姓名" name="real_name">
|
||||
<a-input v-model:value="formState.real_name" placeholder="请输入真实姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="formState.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formState.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="isAddUser"
|
||||
label="密码"
|
||||
name="password"
|
||||
>
|
||||
<a-input-password v-model:value="formState.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="用户角色" name="role">
|
||||
<a-select v-model:value="formState.role" placeholder="请选择用户角色">
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-select-option value="user">普通用户</a-select-option>
|
||||
<a-select-option value="guest">访客</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-switch v-model:checked="formState.status" checked-children="启用" un-checked-children="禁用" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" html-type="submit">确认</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { message, Modal } from 'antd'
|
||||
import { UserAddOutlined } from '@ant-design/icons-vue'
|
||||
import axios from 'axios'
|
||||
|
||||
// 搜索文本
|
||||
const searchText = ref('')
|
||||
// 用户数据
|
||||
const users = ref([])
|
||||
// 分页配置
|
||||
const pagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条数据`
|
||||
}
|
||||
|
||||
// 弹窗状态
|
||||
const showModal = ref(false)
|
||||
const isAddUser = ref(true)
|
||||
const modalTitle = computed(() => isAddUser.value ? '添加用户' : '编辑用户')
|
||||
const formRef = ref()
|
||||
|
||||
// 表单状态
|
||||
const formState = reactive({
|
||||
id: '',
|
||||
username: '',
|
||||
real_name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user',
|
||||
status: true
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度应在3到20个字符之间', trigger: 'blur' }
|
||||
],
|
||||
real_name: [
|
||||
{ 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' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 32, message: '密码长度应在6到32个字符之间', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择用户角色', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username'
|
||||
},
|
||||
{
|
||||
title: '真实姓名',
|
||||
dataIndex: 'real_name',
|
||||
key: 'real_name'
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone'
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email'
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
slots: { customRender: 'status' }
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
slots: { customRender: 'actions' }
|
||||
}
|
||||
]
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async (page = 1, pageSize = 10, keyword = '') => {
|
||||
try {
|
||||
// 这里应该从API获取用户数据
|
||||
// 由于没有实际API,使用模拟数据
|
||||
// 模拟分页数据
|
||||
const mockUsers = [
|
||||
{ id: 1, username: 'admin', real_name: '管理员', phone: '13800138000', email: 'admin@example.com', role: 'admin', status: 1, created_at: '2024-01-01 10:00:00' },
|
||||
{ id: 2, username: 'user1', real_name: '用户一', phone: '13800138001', email: 'user1@example.com', role: 'user', status: 1, created_at: '2024-01-02 11:00:00' },
|
||||
{ id: 3, username: 'user2', real_name: '用户二', phone: '13800138002', email: 'user2@example.com', role: 'user', status: 0, created_at: '2024-01-03 12:00:00' },
|
||||
{ id: 4, username: 'user3', real_name: '用户三', phone: '13800138003', email: 'user3@example.com', role: 'guest', status: 1, created_at: '2024-01-04 13:00:00' },
|
||||
{ id: 5, username: 'user4', real_name: '用户四', phone: '13800138004', email: 'user4@example.com', role: 'user', status: 1, created_at: '2024-01-05 14:00:00' }
|
||||
]
|
||||
|
||||
// 模拟搜索
|
||||
const filteredUsers = keyword ?
|
||||
mockUsers.filter(user =>
|
||||
user.username.includes(keyword) ||
|
||||
user.real_name.includes(keyword) ||
|
||||
user.id.toString().includes(keyword)
|
||||
) :
|
||||
mockUsers
|
||||
|
||||
// 模拟分页
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const paginatedUsers = filteredUsers.slice(start, end)
|
||||
|
||||
users.value = paginatedUsers
|
||||
pagination.total = filteredUsers.length
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
message.error('获取用户列表失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (paginationObj) => {
|
||||
pagination.current = paginationObj.current
|
||||
pagination.pageSize = paginationObj.pageSize
|
||||
fetchUsers(pagination.current, pagination.pageSize, searchText.value)
|
||||
}
|
||||
|
||||
// 搜索用户
|
||||
const handleSearch = () => {
|
||||
fetchUsers(1, pagination.pageSize, searchText.value)
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
isAddUser.value = true
|
||||
// 重置表单
|
||||
formState.id = ''
|
||||
formState.username = ''
|
||||
formState.real_name = ''
|
||||
formState.phone = ''
|
||||
formState.email = ''
|
||||
formState.password = ''
|
||||
formState.role = 'user'
|
||||
formState.status = true
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const handleEditUser = (record) => {
|
||||
isAddUser.value = false
|
||||
// 填充表单数据
|
||||
formState.id = record.id
|
||||
formState.username = record.username
|
||||
formState.real_name = record.real_name
|
||||
formState.phone = record.phone
|
||||
formState.email = record.email
|
||||
formState.role = record.role
|
||||
formState.status = record.status === 1
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDeleteUser = (userId) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个用户吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 这里应该调用API删除用户
|
||||
// 由于没有实际API,模拟删除
|
||||
console.log('删除用户:', userId)
|
||||
message.success('用户删除成功')
|
||||
// 重新获取用户列表
|
||||
fetchUsers(pagination.current, pagination.pageSize, searchText.value)
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
message.error('删除用户失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
// 准备提交数据
|
||||
const submitData = {
|
||||
username: formState.username,
|
||||
real_name: formState.real_name,
|
||||
phone: formState.phone,
|
||||
email: formState.email,
|
||||
role: formState.role,
|
||||
status: formState.status ? 1 : 0
|
||||
}
|
||||
|
||||
// 如果是添加用户,添加密码字段
|
||||
if (isAddUser.value) {
|
||||
submitData.password = formState.password
|
||||
}
|
||||
|
||||
// 这里应该调用API提交数据
|
||||
// 由于没有实际API,模拟提交
|
||||
console.log(isAddUser.value ? '添加用户:' : '编辑用户:', submitData)
|
||||
|
||||
// 显示成功消息
|
||||
message.success(isAddUser.value ? '用户添加成功' : '用户编辑成功')
|
||||
|
||||
// 关闭弹窗
|
||||
showModal.value = false
|
||||
|
||||
// 重新获取用户列表
|
||||
fetchUsers(pagination.current, pagination.pageSize, searchText.value)
|
||||
} catch (error) {
|
||||
console.error(isAddUser.value ? '添加用户失败:' : '编辑用户失败:', error)
|
||||
message.error(isAddUser.value ? '添加用户失败,请稍后重试' : '编辑用户失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
showModal.value = false
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
// 监听搜索文本变化
|
||||
searchText.value = ''
|
||||
fetchUsers()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 用户管理页面样式 */
|
||||
</style>
|
||||
874
government-admin/src/views/VisualAnalysis.vue
Normal file
874
government-admin/src/views/VisualAnalysis.vue
Normal file
@@ -0,0 +1,874 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>可视化分析</h1>
|
||||
|
||||
<!-- 图表选择器 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-select v-model:value="selectedChart" placeholder="选择图表类型" style="width: 200px;" @change="changeChart">
|
||||
<a-select-option value="overview">综合概览</a-select-option>
|
||||
<a-select-option value="supervision">监管数据</a-select-option>
|
||||
<a-select-option value="epidemic">疫情数据</a-select-option>
|
||||
<a-select-option value="approval">审批数据</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 综合概览 -->
|
||||
<div v-if="selectedChart === 'overview'">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="16">
|
||||
<a-card title="数据趋势总览" :body-style="{ padding: 0 }">
|
||||
<div style="height: 400px; padding: 10px;">
|
||||
<div id="overview-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card title="数据分类占比" :body-style="{ padding: 0 }">
|
||||
<div style="height: 400px; padding: 10px;">
|
||||
<div id="category-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24} style="margin-top: 24px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="月度活跃度" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="activity-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="区域分布图" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="region-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 监管数据可视化 -->
|
||||
<div v-if="selectedChart === 'supervision'">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-card title="监管类型分布" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="supervision-type-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="异常情况趋势" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="abnormal-trend-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24} style="margin-top: 24px;">
|
||||
<a-col :span="24">
|
||||
<a-card title="监管覆盖率" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="coverage-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 疫情数据可视化 -->
|
||||
<div v-if="selectedChart === 'epidemic'">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="16">
|
||||
<a-card title="疫情发展趋势" :body-style="{ padding: 0 }">
|
||||
<div style="height: 400px; padding: 10px;">
|
||||
<div id="epidemic-trend-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card title="疫苗接种进度" :body-style="{ padding: 0 }">
|
||||
<div style="height: 400px; padding: 10px;">
|
||||
<div id="vaccine-progress-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24} style="margin-top: 24px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="区域疫情分布" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="epidemic-region-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="检测量统计" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="testing-stats-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 审批数据可视化 -->
|
||||
<div v-if="selectedChart === 'approval'">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="12">
|
||||
<a-card title="审批类型分布" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="approval-type-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="审批状态分布" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="approval-status-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row gutter={24} style="margin-top: 24px;">
|
||||
<a-col :span="24">
|
||||
<a-card title="审批处理时效" :body-style="{ padding: 0 }">
|
||||
<div style="height: 300px; padding: 10px;">
|
||||
<div id="approval-timeline-chart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import * as echarts from 'echarts'
|
||||
import { getVisualizationData } from '@/mock'
|
||||
|
||||
const selectedChart = ref('overview')
|
||||
const charts = ref({})
|
||||
const visualizationData = ref(null)
|
||||
|
||||
// 图表初始化函数
|
||||
const initCharts = () => {
|
||||
// 确保在切换图表类型时销毁之前的图表实例
|
||||
Object.values(charts.value).forEach(chart => {
|
||||
if (chart && chart.dispose) {
|
||||
chart.dispose()
|
||||
}
|
||||
})
|
||||
charts.value = {}
|
||||
|
||||
if (selectedChart.value === 'overview') {
|
||||
initOverviewCharts()
|
||||
} else if (selectedChart.value === 'supervision') {
|
||||
initSupervisionCharts()
|
||||
} else if (selectedChart.value === 'epidemic') {
|
||||
initEpidemicCharts()
|
||||
} else if (selectedChart.value === 'approval') {
|
||||
initApprovalCharts()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化综合概览图表
|
||||
const initOverviewCharts = () => {
|
||||
// 数据趋势总览
|
||||
const overviewChartDom = document.getElementById('overview-chart')
|
||||
if (overviewChartDom) {
|
||||
charts.value.overviewChart = echarts.init(overviewChartDom)
|
||||
const overviewOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['监管事件', '审批数量', '疫情相关数据']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '监管事件',
|
||||
type: 'line',
|
||||
data: [120, 132, 101, 134, 90, 230, 210, 180, 190, 230, 210, 250],
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '审批数量',
|
||||
type: 'line',
|
||||
data: [220, 182, 191, 234, 290, 330, 310, 280, 290, 330, 310, 350],
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '疫情相关数据',
|
||||
type: 'line',
|
||||
data: [150, 232, 201, 154, 190, 330, 410, 380, 390, 430, 410, 450],
|
||||
smooth: true
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.overviewChart.setOption(overviewOption)
|
||||
}
|
||||
|
||||
// 数据分类占比
|
||||
const categoryChartDom = document.getElementById('category-chart')
|
||||
if (categoryChartDom) {
|
||||
charts.value.categoryChart = echarts.init(categoryChartDom)
|
||||
const categoryOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '数据分类',
|
||||
type: 'pie',
|
||||
radius: '65%',
|
||||
data: [
|
||||
{
|
||||
value: 30,
|
||||
name: '监管数据'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '审批数据'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '疫情数据'
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
name: '用户数据'
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
name: '其他数据'
|
||||
}
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.categoryChart.setOption(categoryOption)
|
||||
}
|
||||
|
||||
// 月度活跃度
|
||||
const activityChartDom = document.getElementById('activity-chart')
|
||||
if (activityChartDom) {
|
||||
charts.value.activityChart = echarts.init(activityChartDom)
|
||||
const activityOption = {
|
||||
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: 'bar',
|
||||
barWidth: '60%'
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.activityChart.setOption(activityOption)
|
||||
}
|
||||
|
||||
// 区域分布图
|
||||
const regionChartDom = document.getElementById('region-chart')
|
||||
if (regionChartDom) {
|
||||
charts.value.regionChart = echarts.init(regionChartDom)
|
||||
const regionOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '东区', max: 100 },
|
||||
{ name: '西区', max: 100 },
|
||||
{ name: '南区', max: 100 },
|
||||
{ name: '北区', max: 100 },
|
||||
{ name: '中区', max: 100 }
|
||||
]
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: [80, 65, 70, 75, 60],
|
||||
name: '数据分布'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.regionChart.setOption(regionOption)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化监管数据图表
|
||||
const initSupervisionCharts = () => {
|
||||
// 监管类型分布
|
||||
const supervisionTypeChartDom = document.getElementById('supervision-type-chart')
|
||||
if (supervisionTypeChartDom) {
|
||||
charts.value.supervisionTypeChart = echarts.init(supervisionTypeChartDom)
|
||||
const supervisionTypeOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '监管类型',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 35,
|
||||
name: '安全检查'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '质量监管'
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
name: '环保监测'
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
name: '疫情防控'
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
name: '其他'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.supervisionTypeChart.setOption(supervisionTypeOption)
|
||||
}
|
||||
|
||||
// 异常情况趋势
|
||||
const abnormalTrendChartDom = document.getElementById('abnormal-trend-chart')
|
||||
if (abnormalTrendChartDom) {
|
||||
charts.value.abnormalTrendChart = echarts.init(abnormalTrendChartDom)
|
||||
const abnormalTrendOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [12, 19, 15, 10, 8, 12],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.abnormalTrendChart.setOption(abnormalTrendOption)
|
||||
}
|
||||
|
||||
// 监管覆盖率
|
||||
const coverageChartDom = document.getElementById('coverage-chart')
|
||||
if (coverageChartDom) {
|
||||
charts.value.coverageChart = echarts.init(coverageChartDom)
|
||||
const coverageOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['计划覆盖率', '实际覆盖率']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: [0, 0.01]
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: ['企业', '学校', '医院', '商场', '餐饮', '娱乐场所']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '计划覆盖率',
|
||||
type: 'bar',
|
||||
data: [100, 100, 100, 100, 100, 100]
|
||||
},
|
||||
{
|
||||
name: '实际覆盖率',
|
||||
type: 'bar',
|
||||
data: [85, 90, 95, 80, 88, 75]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.coverageChart.setOption(coverageOption)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化疫情数据图表
|
||||
const initEpidemicCharts = () => {
|
||||
// 疫情发展趋势
|
||||
const epidemicTrendChartDom = document.getElementById('epidemic-trend-chart')
|
||||
if (epidemicTrendChartDom) {
|
||||
charts.value.epidemicTrendChart = echarts.init(epidemicTrendChartDom)
|
||||
const epidemicTrendOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['确诊病例', '疑似病例', '隔离观察']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '确诊病例',
|
||||
type: 'line',
|
||||
data: [120, 132, 101, 134, 90, 230, 210, 180, 190, 230, 210, 250],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#ff4d4f'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '疑似病例',
|
||||
type: 'line',
|
||||
data: [80, 82, 91, 84, 70, 130, 110, 90, 100, 130, 110, 150],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#fa8c16'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '隔离观察',
|
||||
type: 'line',
|
||||
data: [150, 172, 161, 184, 160, 280, 260, 240, 250, 280, 260, 300],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#1890ff'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.epidemicTrendChart.setOption(epidemicTrendOption)
|
||||
}
|
||||
|
||||
// 疫苗接种进度
|
||||
const vaccineProgressChartDom = document.getElementById('vaccine-progress-chart')
|
||||
if (vaccineProgressChartDom) {
|
||||
charts.value.vaccineProgressChart = echarts.init(vaccineProgressChartDom)
|
||||
const vaccineProgressOption = {
|
||||
tooltip: {
|
||||
formatter: '{a} <br/>{b} : {c}%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '接种进度',
|
||||
type: 'gauge',
|
||||
detail: {
|
||||
formatter: '{value}%'
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 75,
|
||||
name: '接种率'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.vaccineProgressChart.setOption(vaccineProgressOption)
|
||||
}
|
||||
|
||||
// 区域疫情分布
|
||||
const epidemicRegionChartDom = document.getElementById('epidemic-region-chart')
|
||||
if (epidemicRegionChartDom) {
|
||||
charts.value.epidemicRegionChart = echarts.init(epidemicRegionChartDom)
|
||||
const epidemicRegionOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '区域分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 30,
|
||||
name: '东区'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '西区'
|
||||
},
|
||||
{
|
||||
value: 22,
|
||||
name: '南区'
|
||||
},
|
||||
{
|
||||
value: 23,
|
||||
name: '北区'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.epidemicRegionChart.setOption(epidemicRegionOption)
|
||||
}
|
||||
|
||||
// 检测量统计
|
||||
const testingStatsChartDom = document.getElementById('testing-stats-chart')
|
||||
if (testingStatsChartDom) {
|
||||
charts.value.testingStatsChart = echarts.init(testingStatsChartDom)
|
||||
const testingStatsOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [220, 182, 191, 234, 290, 330],
|
||||
type: 'bar',
|
||||
barWidth: '60%'
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.testingStatsChart.setOption(testingStatsOption)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化审批数据图表
|
||||
const initApprovalCharts = () => {
|
||||
// 审批类型分布
|
||||
const approvalTypeChartDom = document.getElementById('approval-type-chart')
|
||||
if (approvalTypeChartDom) {
|
||||
charts.value.approvalTypeChart = echarts.init(approvalTypeChartDom)
|
||||
const approvalTypeOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '审批类型',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 35,
|
||||
name: '企业资质'
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
name: '许可证'
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
name: '项目审批'
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
name: '其他'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.approvalTypeChart.setOption(approvalTypeOption)
|
||||
}
|
||||
|
||||
// 审批状态分布
|
||||
const approvalStatusChartDom = document.getElementById('approval-status-chart')
|
||||
if (approvalStatusChartDom) {
|
||||
charts.value.approvalStatusChart = echarts.init(approvalStatusChartDom)
|
||||
const approvalStatusOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '审批状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 40,
|
||||
name: '已通过'
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
name: '待审批'
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
name: '处理中'
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
name: '已拒绝'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.approvalStatusChart.setOption(approvalStatusOption)
|
||||
}
|
||||
|
||||
// 审批处理时效
|
||||
const approvalTimelineChartDom = document.getElementById('approval-timeline-chart')
|
||||
if (approvalTimelineChartDom) {
|
||||
charts.value.approvalTimelineChart = echarts.init(approvalTimelineChartDom)
|
||||
const approvalTimelineOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['平均处理时间(天)', '最长处理时间(天)', '最短处理时间(天)']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['企业资质', '许可证', '项目审批', '其他']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '平均处理时间(天)',
|
||||
type: 'bar',
|
||||
data: [7, 10, 15, 8]
|
||||
},
|
||||
{
|
||||
name: '最长处理时间(天)',
|
||||
type: 'bar',
|
||||
data: [15, 20, 30, 16]
|
||||
},
|
||||
{
|
||||
name: '最短处理时间(天)',
|
||||
type: 'bar',
|
||||
data: [3, 5, 7, 4]
|
||||
}
|
||||
]
|
||||
}
|
||||
charts.value.approvalTimelineChart.setOption(approvalTimelineOption)
|
||||
}
|
||||
}
|
||||
|
||||
// 响应窗口大小变化
|
||||
const handleResize = () => {
|
||||
Object.values(charts.value).forEach(chart => {
|
||||
if (chart && chart.resize) {
|
||||
chart.resize()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换图表类型
|
||||
const changeChart = () => {
|
||||
// 延迟初始化,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
initCharts()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 获取可视化数据
|
||||
const fetchVisualizationData = async () => {
|
||||
try {
|
||||
// 尝试从API获取数据
|
||||
const response = await axios.get('/api/visualization/data')
|
||||
if (response.data.code === 200) {
|
||||
visualizationData.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取可视化数据失败,使用默认数据:', error)
|
||||
// 使用默认数据
|
||||
const mockResponse = await getVisualizationData()
|
||||
if (mockResponse.code === 200) {
|
||||
visualizationData.value = mockResponse.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
fetchVisualizationData()
|
||||
setTimeout(() => {
|
||||
initCharts()
|
||||
}, 100)
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
Object.values(charts.value).forEach(chart => {
|
||||
if (chart && chart.dispose) {
|
||||
chart.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式可以根据需要进行调整 */
|
||||
</style>
|
||||
504
government-admin/src/views/WarehouseManagement.vue
Normal file
504
government-admin/src/views/WarehouseManagement.vue
Normal file
@@ -0,0 +1,504 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>仓库管理</h1>
|
||||
<a-card style="margin-bottom: 16px;">
|
||||
<a-row gutter={24}>
|
||||
<a-col :span="8">
|
||||
<a-button type="primary" @click="handleAddItem">添加物资</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleImportItems">导入物资</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="handleExportItems">导出物资</a-button>
|
||||
</a-col>
|
||||
<a-col :span="16" style="text-align: right;">
|
||||
<a-input-search
|
||||
placeholder="搜索物资名称或编号"
|
||||
allow-clear
|
||||
enter-button="搜索"
|
||||
size="large"
|
||||
style="width: 300px;"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据概览卡片 -->
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.totalItems }}</div>
|
||||
<div class="stat-label">物资种类</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.totalQuantity }}</div>
|
||||
<div class="stat-label">总数量</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.lowStock }}</div>
|
||||
<div class="stat-label">低库存</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.expiringItems }}</div>
|
||||
<div class="stat-label">临期物资</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 物资列表 -->
|
||||
<a-card title="物资列表">
|
||||
<a-row gutter={24} style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filterCategory" placeholder="筛选物资类型" allow-clear>
|
||||
<a-select-option value="medicine">药品</a-select-option>
|
||||
<a-select-option value="equipment">设备</a-select-option>
|
||||
<a-select-option value="supplies">耗材</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filterStatus" placeholder="筛选库存状态" allow-clear>
|
||||
<a-select-option value="normal">正常</a-select-option>
|
||||
<a-select-option value="low">低库存</a-select-option>
|
||||
<a-select-option value="out">缺货</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="filterWarehouse" placeholder="筛选仓库" allow-clear>
|
||||
<a-select-option value="main">主仓库</a-select-option>
|
||||
<a-select-option value="sub1">副仓库1</a-select-option>
|
||||
<a-select-option value="sub2">副仓库2</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table :columns="itemColumns" :data-source="itemList" :pagination="pagination" row-key="id">
|
||||
<template #bodyCell:quantity="{ record }">
|
||||
<span :class="getQuantityClass(record)">{{ record.quantity }}</span>
|
||||
</template>
|
||||
<template #bodyCell:expiryDate="{ record }">
|
||||
<span :class="getExpiryClass(record)">{{ record.expiryDate ? dayjs(record.expiryDate).format('YYYY-MM-DD') : '-' }}</span>
|
||||
</template>
|
||||
<template #bodyCell:action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleViewItem(record)">查看</a-button>
|
||||
<a-button type="link" @click="handleEditItem(record)">编辑</a-button>
|
||||
<a-button type="link" danger @click="handleDeleteItem(record.id)">删除</a-button>
|
||||
<a-button type="link" @click="handleStockInOut(record, 'in')">入库</a-button>
|
||||
<a-button type="link" @click="handleStockInOut(record, 'out')">出库</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑物资对话框 -->
|
||||
<a-modal
|
||||
v-model:open="itemModalVisible"
|
||||
:title="itemModalTitle"
|
||||
@ok="handleItemModalOk"
|
||||
@cancel="handleItemModalCancel"
|
||||
>
|
||||
<a-form :model="itemForm" layout="vertical">
|
||||
<a-form-item label="物资名称" name="name" :rules="[{ required: true, message: '请输入物资名称' }]">
|
||||
<a-input v-model:value="itemForm.name" placeholder="请输入物资名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="物资编号" name="code" :rules="[{ required: true, message: '请输入物资编号' }]">
|
||||
<a-input v-model:value="itemForm.code" placeholder="请输入物资编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="物资类型" name="category" :rules="[{ required: true, message: '请选择物资类型' }]">
|
||||
<a-select v-model:value="itemForm.category" placeholder="请选择物资类型">
|
||||
<a-select-option value="medicine">药品</a-select-option>
|
||||
<a-select-option value="equipment">设备</a-select-option>
|
||||
<a-select-option value="supplies">耗材</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="当前库存" name="quantity" :rules="[{ required: true, message: '请输入当前库存' }]">
|
||||
<a-input-number v-model:value="itemForm.quantity" placeholder="请输入当前库存" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="预警库存" name="warningStock" :rules="[{ required: true, message: '请输入预警库存' }]">
|
||||
<a-input-number v-model:value="itemForm.warningStock" placeholder="请输入预警库存" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所属仓库" name="warehouse" :rules="[{ required: true, message: '请选择所属仓库' }]">
|
||||
<a-select v-model:value="itemForm.warehouse" placeholder="请选择所属仓库">
|
||||
<a-select-option value="main">主仓库</a-select-option>
|
||||
<a-select-option value="sub1">副仓库1</a-select-option>
|
||||
<a-select-option value="sub2">副仓库2</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="有效期至" name="expiryDate">
|
||||
<a-date-picker v-model:value="itemForm.expiryDate" style="width: 100%;" />
|
||||
</a-form-item>
|
||||
<a-form-item label="物资描述" name="description">
|
||||
<a-textarea v-model:value="itemForm.description" placeholder="请输入物资描述" rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 入库/出库对话框 -->
|
||||
<a-modal
|
||||
v-model:open="stockModalVisible"
|
||||
:title="stockModalTitle"
|
||||
@ok="handleStockModalOk"
|
||||
@cancel="handleStockModalCancel"
|
||||
>
|
||||
<a-form :model="stockForm" layout="vertical">
|
||||
<a-form-item label="物资名称">
|
||||
<a-input :value="stockForm.itemName" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="当前库存">
|
||||
<a-input :value="stockForm.currentStock" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="数量" name="quantity" :rules="[{ required: true, message: '请输入数量' }]">
|
||||
<a-input-number v-model:value="stockForm.quantity" placeholder="请输入数量" :min="1" />
|
||||
</a-form-item>
|
||||
<a-form-item label="操作人" name="operator" :rules="[{ required: true, message: '请输入操作人' }]">
|
||||
<a-input v-model:value="stockForm.operator" placeholder="请输入操作人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="stockForm.remark" placeholder="请输入备注" rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
totalItems: 5,
|
||||
totalQuantity: 1500,
|
||||
lowStock: 2,
|
||||
expiringItems: 1
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 筛选条件
|
||||
const filterCategory = ref('')
|
||||
const filterStatus = ref('')
|
||||
const filterWarehouse = ref('')
|
||||
|
||||
// 物资列表数据
|
||||
const itemList = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: '消毒液A',
|
||||
code: 'ITM001',
|
||||
category: 'supplies',
|
||||
quantity: 50,
|
||||
warningStock: 20,
|
||||
warehouse: 'main',
|
||||
expiryDate: '2024-12-31T00:00:00Z',
|
||||
description: '养殖场用消毒液',
|
||||
createTime: '2024-01-15T09:30:00Z',
|
||||
updateTime: '2024-04-10T14:20:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '疫苗B',
|
||||
code: 'ITM002',
|
||||
category: 'medicine',
|
||||
quantity: 10,
|
||||
warningStock: 50,
|
||||
warehouse: 'main',
|
||||
expiryDate: '2024-06-30T00:00:00Z',
|
||||
description: '动物疫苗',
|
||||
createTime: '2024-01-20T10:15:00Z',
|
||||
updateTime: '2024-04-09T16:45:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '防护服',
|
||||
code: 'ITM003',
|
||||
category: 'supplies',
|
||||
quantity: 200,
|
||||
warningStock: 50,
|
||||
warehouse: 'sub1',
|
||||
expiryDate: null,
|
||||
description: '防疫用防护服',
|
||||
createTime: '2024-02-01T14:30:00Z',
|
||||
updateTime: '2024-04-08T10:15:00Z'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '体温枪',
|
||||
code: 'ITM004',
|
||||
category: 'equipment',
|
||||
quantity: 5,
|
||||
warningStock: 3,
|
||||
warehouse: 'sub2',
|
||||
expiryDate: null,
|
||||
description: '动物体温测量设备',
|
||||
createTime: '2024-02-15T11:45:00Z',
|
||||
updateTime: '2024-04-07T13:30:00Z'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '口罩',
|
||||
code: 'ITM005',
|
||||
category: 'supplies',
|
||||
quantity: 1000,
|
||||
warningStock: 100,
|
||||
warehouse: 'sub1',
|
||||
expiryDate: '2025-03-31T00:00:00Z',
|
||||
description: '防护口罩',
|
||||
createTime: '2024-01-10T09:00:00Z',
|
||||
updateTime: '2024-03-01T16:20:00Z'
|
||||
}
|
||||
])
|
||||
|
||||
// 物资表格列定义
|
||||
const itemColumns = [
|
||||
{
|
||||
title: '物资名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '物资编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code'
|
||||
},
|
||||
{
|
||||
title: '物资类型',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
customRender: (text) => {
|
||||
const categoryMap = {
|
||||
medicine: '药品',
|
||||
equipment: '设备',
|
||||
supplies: '耗材',
|
||||
other: '其他'
|
||||
}
|
||||
return categoryMap[text] || text
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '当前库存',
|
||||
dataIndex: 'quantity',
|
||||
key: 'quantity'
|
||||
},
|
||||
{
|
||||
title: '预警库存',
|
||||
dataIndex: 'warningStock',
|
||||
key: 'warningStock'
|
||||
},
|
||||
{
|
||||
title: '所属仓库',
|
||||
dataIndex: 'warehouse',
|
||||
key: 'warehouse',
|
||||
customRender: (text) => {
|
||||
const warehouseMap = {
|
||||
main: '主仓库',
|
||||
sub1: '副仓库1',
|
||||
sub2: '副仓库2'
|
||||
}
|
||||
return warehouseMap[text] || text
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '有效期至',
|
||||
dataIndex: 'expiryDate',
|
||||
key: 'expiryDate'
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
customRender: (text) => dayjs(text).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 模态框配置 - 物资
|
||||
const itemModalVisible = ref(false)
|
||||
const itemModalTitle = ref('添加物资')
|
||||
const itemForm = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
category: '',
|
||||
quantity: 0,
|
||||
warningStock: 0,
|
||||
warehouse: '',
|
||||
expiryDate: null,
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 模态框配置 - 库存操作
|
||||
const stockModalVisible = ref(false)
|
||||
const stockModalTitle = ref('入库')
|
||||
const stockForm = reactive({
|
||||
itemId: '',
|
||||
itemName: '',
|
||||
currentStock: 0,
|
||||
quantity: 0,
|
||||
operator: '',
|
||||
remark: '',
|
||||
type: 'in' // 'in' 入库, 'out' 出库
|
||||
})
|
||||
|
||||
// 获取库存数量样式类
|
||||
const getQuantityClass = (record) => {
|
||||
if (record.quantity === 0) {
|
||||
return 'text-danger'
|
||||
} else if (record.quantity <= record.warningStock) {
|
||||
return 'text-warning'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取有效期样式类
|
||||
const getExpiryClass = (record) => {
|
||||
if (!record.expiryDate) return ''
|
||||
|
||||
const daysLeft = dayjs(record.expiryDate).diff(dayjs(), 'day')
|
||||
if (daysLeft <= 30) {
|
||||
return 'text-danger'
|
||||
} else if (daysLeft <= 90) {
|
||||
return 'text-warning'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 搜索物资
|
||||
const handleSearch = (keyword) => {
|
||||
// 这里应该有实际的搜索逻辑
|
||||
console.log('搜索物资:', keyword)
|
||||
}
|
||||
|
||||
// 添加物资
|
||||
const handleAddItem = () => {
|
||||
itemModalTitle.value = '添加物资'
|
||||
Object.keys(itemForm).forEach(key => {
|
||||
itemForm[key] = key === 'quantity' || key === 'warningStock' ? 0 : ''
|
||||
})
|
||||
itemModalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑物资
|
||||
const handleEditItem = (item) => {
|
||||
itemModalTitle.value = '编辑物资'
|
||||
Object.keys(itemForm).forEach(key => {
|
||||
itemForm[key] = item[key] || (key === 'quantity' || key === 'warningStock' ? 0 : '')
|
||||
})
|
||||
itemModalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看物资
|
||||
const handleViewItem = (item) => {
|
||||
console.log('查看物资:', item)
|
||||
}
|
||||
|
||||
// 删除物资
|
||||
const handleDeleteItem = (id) => {
|
||||
console.log('删除物资:', id)
|
||||
}
|
||||
|
||||
// 入库/出库
|
||||
const handleStockInOut = (item, type) => {
|
||||
stockModalTitle.value = type === 'in' ? '入库' : '出库'
|
||||
stockForm.itemId = item.id
|
||||
stockForm.itemName = item.name
|
||||
stockForm.currentStock = item.quantity
|
||||
stockForm.quantity = 0
|
||||
stockForm.operator = ''
|
||||
stockForm.remark = ''
|
||||
stockForm.type = type
|
||||
stockModalVisible.value = true
|
||||
}
|
||||
|
||||
// 导入物资
|
||||
const handleImportItems = () => {
|
||||
console.log('导入物资')
|
||||
}
|
||||
|
||||
// 导出物资
|
||||
const handleExportItems = () => {
|
||||
console.log('导出物资')
|
||||
}
|
||||
|
||||
// 模态框确认 - 物资
|
||||
const handleItemModalOk = () => {
|
||||
console.log('提交表单:', itemForm)
|
||||
itemModalVisible.value = false
|
||||
}
|
||||
|
||||
// 模态框取消 - 物资
|
||||
const handleItemModalCancel = () => {
|
||||
itemModalVisible.value = false
|
||||
}
|
||||
|
||||
// 模态框确认 - 库存操作
|
||||
const handleStockModalOk = () => {
|
||||
console.log('提交库存操作:', stockForm)
|
||||
stockModalVisible.value = false
|
||||
}
|
||||
|
||||
// 模态框取消 - 库存操作
|
||||
const handleStockModalCancel = () => {
|
||||
stockModalVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f5222d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<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>
|
||||
@@ -1,98 +0,0 @@
|
||||
<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>
|
||||
@@ -1,887 +0,0 @@
|
||||
<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>
|
||||
@@ -1,111 +0,0 @@
|
||||
<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>
|
||||
@@ -1,59 +0,0 @@
|
||||
<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>
|
||||
@@ -1,144 +0,0 @@
|
||||
<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>
|
||||
@@ -1,578 +0,0 @@
|
||||
<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>
|
||||
@@ -1,104 +0,0 @@
|
||||
<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>
|
||||
@@ -1,63 +0,0 @@
|
||||
<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>
|
||||
@@ -1,79 +0,0 @@
|
||||
<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>
|
||||
@@ -1,133 +0,0 @@
|
||||
<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>
|
||||
@@ -1,83 +0,0 @@
|
||||
<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>
|
||||
@@ -1,85 +0,0 @@
|
||||
<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>
|
||||
@@ -1,81 +0,0 @@
|
||||
<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>
|
||||
@@ -1,975 +0,0 @@
|
||||
<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>
|
||||
@@ -1,948 +0,0 @@
|
||||
<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 { useAuthStore } from '@/stores/auth'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
|
||||
const router = useRouter()
|
||||
const governmentStore = useGovernmentStore()
|
||||
const authStore = useAuthStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// 响应式数据
|
||||
const farmDistributionPeriod = ref('month')
|
||||
const healthTrendType = ref('month')
|
||||
|
||||
// 用户信息 - 从store获取
|
||||
const userInfo = computed(() => ({
|
||||
name: authStore.userInfo?.name || '管理员',
|
||||
role: authStore.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(),
|
||||
authStore.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>
|
||||
@@ -1,63 +0,0 @@
|
||||
<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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,147 +0,0 @@
|
||||
<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>
|
||||
@@ -1,92 +0,0 @@
|
||||
<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>
|
||||
@@ -1,134 +0,0 @@
|
||||
<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>
|
||||
@@ -1,441 +0,0 @@
|
||||
<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>
|
||||
@@ -1,262 +0,0 @@
|
||||
<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>
|
||||
@@ -1,299 +0,0 @@
|
||||
<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>
|
||||
@@ -1,54 +0,0 @@
|
||||
<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>
|
||||
@@ -1,869 +0,0 @@
|
||||
<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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user