修改保险后端代码,政府前端代码

This commit is contained in:
2025-09-22 17:56:30 +08:00
parent 3143c3ad0b
commit 02a25515a9
206 changed files with 35119 additions and 43073 deletions

View File

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

View File

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

View File

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

View File

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

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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']
}
})

View File

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

View File

@@ -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']
}
})

View File

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

View File

@@ -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']
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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;">支持上传 WordPDFExcel图片等格式文件</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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

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

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

View 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">小型(&lt;50)</a-select-option>
<a-select-option value="medium">中型(50-200)</a-select-option>
<a-select-option value="large">大型(&gt;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">小型(&lt;50)</a-select-option>
<a-select-option value="medium">中型(50-200)</a-select-option>
<a-select-option value="large">大型(&gt;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 '小型(&lt;50头)'
case 'medium': return '中型(50-200头)'
case 'large': return '大型(&gt;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>

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

File diff suppressed because it is too large Load Diff

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

View File

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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