添加银行后端接口,前端代码

This commit is contained in:
2025-09-23 17:57:18 +08:00
parent 325c114c38
commit bdc1b29934
67 changed files with 12682 additions and 7085 deletions

View File

@@ -0,0 +1,164 @@
# 监管任务前后端集成完成总结
## 🎯 项目概述
已成功完成银行端监管任务管理系统的前后端集成实现了完整的CRUD功能使用真实的后端API接口替代了前端模拟数据。
## ✅ 已完成的功能
### 后端功能
1. **数据库模型** (`models/SupervisionTask.js`)
- 完整的监管任务数据模型
- 支持所有必要字段:申请单号、合同编号、客户信息、监管信息等
- 与用户模型的关联关系
2. **数据库迁移** (`migrations/20241220000003-create-supervision-tasks.js`)
- 创建监管任务表结构
- 添加必要的索引优化查询性能
3. **API控制器** (`controllers/supervisionTaskController.js`)
- 获取监管任务列表(支持分页、搜索、筛选)
- 获取监管任务详情
- 创建监管任务(完整的数据验证)
- 更新监管任务
- 删除监管任务
- 获取监管任务统计
- 批量更新状态
- 批量删除
4. **API路由** (`routes/supervisionTasks.js`)
- 完整的RESTful API路由配置
- 统一的认证中间件保护
5. **测试数据** (`scripts/seed-supervision-tasks.js`)
- 8条完整的测试数据
- 涵盖不同状态和类型的监管任务
### 前端功能
1. **API集成** (`src/utils/api.js`)
- 完整的监管任务API方法
- 统一的错误处理和认证
2. **页面改造** (`src/views/SupervisionTasks.vue`)
- 移除模拟数据使用真实API
- 实现完整的CRUD操作
- 添加搜索和筛选功能
- 分页功能
- 错误处理和用户反馈
## 📊 数据字段说明
### 核心字段
- **申请单号** (`applicationNumber`): 唯一标识最大50字符
- **放款合同编号** (`contractNumber`): 唯一标识最大50字符
- **产品名称** (`productName`): 最大100字符
- **客户姓名** (`customerName`): 最大50字符
- **证件类型** (`idType`): 枚举值(身份证、护照、其他)
- **证件号码** (`idNumber`): 最大50字符
- **养殖生资种类** (`assetType`): 枚举值(牛、羊、猪、家禽、其他)
- **监管生资数量** (`assetQuantity`): 非负整数
- **监管状态** (`supervisionStatus`): 枚举值(待监管、监管中、已完成、已暂停)
### 时间字段
- **任务导入时间** (`importTime`): 自动生成
- **监管起始时间** (`startTime`): 必填
- **监管结束时间** (`endTime`): 必填
### 扩展字段
- **贷款金额** (`loanAmount`): 精确到分
- **利率** (`interestRate`): 精确到万分位
- **贷款期限** (`loanTerm`): 月数
- **监管员姓名** (`supervisorName`): 最大50字符
- **监管员电话** (`supervisorPhone`): 最大20字符
- **养殖场地址** (`farmAddress`): 最大200字符
- **备注** (`remarks`): 文本字段
## 🔧 API接口列表
### 基础CRUD
- `GET /api/supervision-tasks` - 获取监管任务列表
- `GET /api/supervision-tasks/:id` - 获取监管任务详情
- `POST /api/supervision-tasks` - 创建监管任务
- `PUT /api/supervision-tasks/:id` - 更新监管任务
- `DELETE /api/supervision-tasks/:id` - 删除监管任务
### 统计和批量操作
- `GET /api/supervision-tasks/stats` - 获取监管任务统计
- `PUT /api/supervision-tasks/batch/status` - 批量更新状态
- `DELETE /api/supervision-tasks/batch` - 批量删除
### 查询参数
- `page`: 页码默认1
- `limit`: 每页数量默认10
- `search`: 搜索关键词(申请单号、合同编号、客户姓名、产品名称)
- `supervisionStatus`: 状态筛选
- `dateRange`: 日期范围筛选
- `sortBy`: 排序字段
- `sortOrder`: 排序方向
## 🚀 使用方法
### 1. 启动后端服务
```bash
cd bank-backend
npm start
```
### 2. 启动前端服务
```bash
cd bank-frontend
npm run dev
```
### 3. 访问页面
- 监管任务页面: `http://localhost:5300/supervision-tasks`
- API测试页面: `http://localhost:5300/test-supervision-tasks.html`
### 4. 功能测试
1. 登录系统admin/Admin123456
2. 查看监管任务列表
3. 使用搜索和筛选功能
4. 创建新的监管任务
5. 编辑和删除任务
## 📝 数据验证规则
### 必填字段
- 申请单号、放款合同编号、产品名称、客户姓名、证件号码、监管起始时间、监管结束时间
### 唯一性约束
- 申请单号、放款合同编号必须唯一
### 数据格式
- 日期格式YYYY-MM-DD
- 金额格式:精确到分
- 利率格式0-1之间的小数
### 业务规则
- 监管结束时间必须晚于开始时间
- 监管生资数量不能为负数
- 贷款金额不能为负数
## 🔍 测试数据
系统已预置8条测试数据涵盖
- 不同监管状态(待监管、监管中、已完成、已暂停)
- 不同养殖生资种类(牛、羊、猪、家禽、其他)
- 不同证件类型(身份证、护照)
- 不同贷款金额和期限
## 📚 相关文档
- [监管任务API文档](docs/SUPERVISION_TASKS_API.md)
- [项目API文档](docs/PROJECT_API.md)
- [前端API集成指南](API_INTEGRATION_COMPLETE.md)
## 🎉 总结
监管任务管理系统已完全集成,提供了:
- ✅ 完整的后端API服务
- ✅ 功能丰富的前端界面
- ✅ 真实的数据存储和查询
- ✅ 完善的错误处理
- ✅ 用户友好的交互体验
系统现在可以正常使用,支持监管任务的完整生命周期管理。

View File

@@ -0,0 +1,460 @@
const { Project, User } = require('../models');
const { Op } = require('sequelize');
// 获取项目列表
const getProjects = async (req, res) => {
try {
const {
page = 1,
limit = 10,
search = '',
status = '',
sortBy = 'createdAt',
sortOrder = 'DESC'
} = req.query;
const offset = (page - 1) * limit;
// 构建查询条件
const where = {};
// 搜索条件
if (search) {
where[Op.or] = [
{ name: { [Op.like]: `%${search}%` } },
{ farmName: { [Op.like]: `%${search}%` } },
{ loanOfficer: { [Op.like]: `%${search}%` } }
];
}
// 状态筛选
if (status) {
where.status = status;
}
// 查询项目列表
const { count, rows: projects } = await Project.findAndCountAll({
where,
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'username', 'real_name']
},
{
model: User,
as: 'updater',
attributes: ['id', 'username', 'real_name']
}
],
order: [[sortBy, sortOrder.toUpperCase()]],
limit: parseInt(limit),
offset: parseInt(offset)
});
// 计算分页信息
const totalPages = Math.ceil(count / limit);
const hasNextPage = page < totalPages;
const hasPrevPage = page > 1;
res.json({
success: true,
data: {
projects,
pagination: {
current: parseInt(page),
pageSize: parseInt(limit),
total: count,
totalPages,
hasNextPage,
hasPrevPage
}
},
message: '获取项目列表成功'
});
} catch (error) {
console.error('获取项目列表失败:', error);
res.status(500).json({
success: false,
message: '获取项目列表失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 获取项目详情
const getProjectById = async (req, res) => {
try {
const { id } = req.params;
const project = await Project.findByPk(id, {
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'username', 'name']
},
{
model: User,
as: 'updater',
attributes: ['id', 'username', 'name']
}
]
});
if (!project) {
return res.status(404).json({
success: false,
message: '项目不存在'
});
}
res.json({
success: true,
data: project,
message: '获取项目详情成功'
});
} catch (error) {
console.error('获取项目详情失败:', error);
res.status(500).json({
success: false,
message: '获取项目详情失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 创建项目
const createProject = async (req, res) => {
try {
const {
name,
status = 'supervision',
farmName,
supervisionObject,
supervisionQuantity = 0,
supervisionPeriod,
supervisionAmount = 0.00,
startTime,
endTime,
earTag = 0,
collar = 0,
host = 0,
loanOfficer,
description
} = req.body;
// 验证必填字段
const requiredFields = ['name', 'farmName', 'supervisionObject', 'supervisionPeriod', 'startTime', 'endTime'];
const missingFields = requiredFields.filter(field => !req.body[field]);
if (missingFields.length > 0) {
return res.status(400).json({
success: false,
message: `请填写所有必填字段: ${missingFields.join(', ')}`
});
}
// 验证字段长度
if (name.length > 100) {
return res.status(400).json({
success: false,
message: '项目名称不能超过100个字符'
});
}
if (farmName.length > 200) {
return res.status(400).json({
success: false,
message: '养殖场名称不能超过200个字符'
});
}
// 验证数值字段
if (supervisionQuantity < 0) {
return res.status(400).json({
success: false,
message: '监管数量不能为负数'
});
}
if (supervisionAmount < 0) {
return res.status(400).json({
success: false,
message: '监管金额不能为负数'
});
}
if (earTag < 0 || collar < 0 || host < 0) {
return res.status(400).json({
success: false,
message: '设备数量不能为负数'
});
}
// 验证状态
if (!['supervision', 'completed'].includes(status)) {
return res.status(400).json({
success: false,
message: '项目状态只能是 supervision 或 completed'
});
}
// 验证日期格式和逻辑
const startDate = new Date(startTime);
const endDate = new Date(endTime);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return res.status(400).json({
success: false,
message: '日期格式不正确'
});
}
if (startDate >= endDate) {
return res.status(400).json({
success: false,
message: '结束时间必须晚于开始时间'
});
}
// 检查项目名称是否已存在
const existingProject = await Project.findOne({
where: { name }
});
if (existingProject) {
return res.status(400).json({
success: false,
message: '项目名称已存在,请使用其他名称'
});
}
// 创建项目
const project = await Project.create({
name,
status,
farmName,
supervisionObject,
supervisionQuantity: parseInt(supervisionQuantity),
supervisionPeriod,
supervisionAmount: parseFloat(supervisionAmount),
startTime,
endTime,
earTag: parseInt(earTag),
collar: parseInt(collar),
host: parseInt(host),
loanOfficer,
description,
createdBy: req.user?.userId || req.user?.id,
updatedBy: req.user?.userId || req.user?.id
});
// 获取创建的项目详情(包含关联信息)
const createdProject = await Project.findByPk(project.id, {
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'username', 'real_name']
},
{
model: User,
as: 'updater',
attributes: ['id', 'username', 'real_name']
}
]
});
res.status(201).json({
success: true,
data: createdProject,
message: '创建项目成功'
});
} catch (error) {
console.error('创建项目失败:', error);
// 处理数据库约束错误
if (error.name === 'SequelizeValidationError') {
const validationErrors = error.errors.map(err => err.message).join(', ');
return res.status(400).json({
success: false,
message: `数据验证失败: ${validationErrors}`
});
}
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({
success: false,
message: '项目名称已存在,请使用其他名称'
});
}
res.status(500).json({
success: false,
message: '创建项目失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 更新项目
const updateProject = async (req, res) => {
try {
const { id } = req.params;
const updateData = req.body;
// 验证日期
if (updateData.startTime && updateData.endTime) {
if (new Date(updateData.startTime) >= new Date(updateData.endTime)) {
return res.status(400).json({
success: false,
message: '结束时间必须晚于开始时间'
});
}
}
const project = await Project.findByPk(id);
if (!project) {
return res.status(404).json({
success: false,
message: '项目不存在'
});
}
// 更新项目
await project.update({
...updateData,
updatedBy: req.user?.id
});
res.json({
success: true,
data: project,
message: '更新项目成功'
});
} catch (error) {
console.error('更新项目失败:', error);
res.status(500).json({
success: false,
message: '更新项目失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 删除项目
const deleteProject = async (req, res) => {
try {
const { id } = req.params;
const project = await Project.findByPk(id);
if (!project) {
return res.status(404).json({
success: false,
message: '项目不存在'
});
}
await project.destroy();
res.json({
success: true,
message: '删除项目成功'
});
} catch (error) {
console.error('删除项目失败:', error);
res.status(500).json({
success: false,
message: '删除项目失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 获取项目统计
const getProjectStats = async (req, res) => {
try {
const totalProjects = await Project.count();
const supervisionProjects = await Project.count({ where: { status: 'supervision' } });
const completedProjects = await Project.count({ where: { status: 'completed' } });
// 计算总监管金额
const totalAmount = await Project.sum('supervisionAmount');
// 计算总监管数量
const totalQuantity = await Project.sum('supervisionQuantity');
res.json({
success: true,
data: {
total: totalProjects,
supervision: supervisionProjects,
completed: completedProjects,
totalAmount: totalAmount || 0,
totalQuantity: totalQuantity || 0
},
message: '获取项目统计成功'
});
} catch (error) {
console.error('获取项目统计失败:', error);
res.status(500).json({
success: false,
message: '获取项目统计失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 批量更新项目状态
const batchUpdateStatus = async (req, res) => {
try {
const { projectIds, status } = req.body;
if (!projectIds || !Array.isArray(projectIds) || projectIds.length === 0) {
return res.status(400).json({
success: false,
message: '请选择要更新的项目'
});
}
if (!['supervision', 'completed'].includes(status)) {
return res.status(400).json({
success: false,
message: '无效的项目状态'
});
}
await Project.update(
{
status,
updatedBy: req.user?.id
},
{
where: { id: { [Op.in]: projectIds } }
}
);
res.json({
success: true,
message: `成功更新 ${projectIds.length} 个项目的状态`
});
} catch (error) {
console.error('批量更新项目状态失败:', error);
res.status(500).json({
success: false,
message: '批量更新项目状态失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
module.exports = {
getProjects,
getProjectById,
createProject,
updateProject,
deleteProject,
getProjectStats,
batchUpdateStatus
};

View File

@@ -0,0 +1,573 @@
/**
* 监管任务控制器
* @file supervisionTaskController.js
* @description 监管任务相关的API控制器
*/
const { SupervisionTask, User } = require('../models');
const { Op } = require('sequelize');
// 获取监管任务列表
const getSupervisionTasks = async (req, res) => {
try {
const {
page = 1,
limit = 10,
search = '',
supervisionStatus = '',
dateRange = '',
sortBy = 'createdAt',
sortOrder = 'DESC'
} = req.query;
const offset = (page - 1) * limit;
// 构建查询条件
const where = {};
// 搜索条件
if (search) {
where[Op.or] = [
{ applicationNumber: { [Op.like]: `%${search}%` } },
{ contractNumber: { [Op.like]: `%${search}%` } },
{ customerName: { [Op.like]: `%${search}%` } },
{ productName: { [Op.like]: `%${search}%` } }
];
}
// 状态筛选
if (supervisionStatus) {
where.supervisionStatus = supervisionStatus;
}
// 日期范围筛选
if (dateRange) {
const [startDate, endDate] = dateRange.split(',');
if (startDate && endDate) {
where.importTime = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
}
// 查询监管任务列表
const { count, rows: tasks } = await SupervisionTask.findAndCountAll({
where,
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'username', 'real_name']
},
{
model: User,
as: 'updater',
attributes: ['id', 'username', 'real_name']
}
],
order: [[sortBy, sortOrder.toUpperCase()]],
limit: parseInt(limit),
offset: parseInt(offset)
});
// 计算分页信息
const totalPages = Math.ceil(count / limit);
const hasNextPage = page < totalPages;
const hasPrevPage = page > 1;
res.status(200).json({
success: true,
message: '监管任务列表获取成功',
data: {
tasks,
pagination: {
total: count,
currentPage: parseInt(page),
pageSize: parseInt(limit),
totalPages,
hasNextPage,
hasPrevPage
}
}
});
} catch (error) {
console.error('获取监管任务列表失败:', error);
res.status(500).json({
success: false,
message: '获取监管任务列表失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 获取监管任务详情
const getSupervisionTaskById = async (req, res) => {
try {
const { id } = req.params;
const task = await SupervisionTask.findByPk(id, {
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'username', 'real_name']
},
{
model: User,
as: 'updater',
attributes: ['id', 'username', 'real_name']
}
]
});
if (!task) {
return res.status(404).json({
success: false,
message: '监管任务不存在'
});
}
res.status(200).json({
success: true,
message: '监管任务详情获取成功',
data: task
});
} catch (error) {
console.error('获取监管任务详情失败:', error);
res.status(500).json({
success: false,
message: '获取监管任务详情失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 创建监管任务
const createSupervisionTask = async (req, res) => {
try {
const {
applicationNumber,
contractNumber,
productName,
customerName,
idType = 'id_card',
idNumber,
assetType = 'cattle',
assetQuantity = 0,
supervisionStatus = 'pending',
startTime,
endTime,
loanAmount = 0,
interestRate = 0,
loanTerm = 12,
supervisorName,
supervisorPhone,
farmAddress,
remarks
} = req.body;
// 验证必填字段
const requiredFields = ['applicationNumber', 'contractNumber', 'productName', 'customerName', 'idNumber', 'startTime', 'endTime'];
const missingFields = requiredFields.filter(field => !req.body[field]);
if (missingFields.length > 0) {
return res.status(400).json({
success: false,
message: `请填写所有必填字段: ${missingFields.join(', ')}`
});
}
// 验证字段长度
if (applicationNumber.length > 50) {
return res.status(400).json({
success: false,
message: '申请单号不能超过50个字符'
});
}
if (contractNumber.length > 50) {
return res.status(400).json({
success: false,
message: '放款合同编号不能超过50个字符'
});
}
if (productName.length > 100) {
return res.status(400).json({
success: false,
message: '产品名称不能超过100个字符'
});
}
if (customerName.length > 50) {
return res.status(400).json({
success: false,
message: '客户姓名不能超过50个字符'
});
}
// 验证数值字段
if (assetQuantity < 0) {
return res.status(400).json({
success: false,
message: '监管生资数量不能为负数'
});
}
if (loanAmount < 0) {
return res.status(400).json({
success: false,
message: '贷款金额不能为负数'
});
}
if (interestRate < 0 || interestRate > 1) {
return res.status(400).json({
success: false,
message: '利率必须在0-1之间'
});
}
if (loanTerm < 0) {
return res.status(400).json({
success: false,
message: '贷款期限不能为负数'
});
}
// 验证状态和类型
if (!['pending', 'supervising', 'completed', 'suspended'].includes(supervisionStatus)) {
return res.status(400).json({
success: false,
message: '监管状态无效'
});
}
if (!['id_card', 'passport', 'other'].includes(idType)) {
return res.status(400).json({
success: false,
message: '证件类型无效'
});
}
if (!['cattle', 'sheep', 'pig', 'poultry', 'other'].includes(assetType)) {
return res.status(400).json({
success: false,
message: '养殖生资种类无效'
});
}
// 验证日期格式和逻辑
const startDate = new Date(startTime);
const endDate = new Date(endTime);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return res.status(400).json({
success: false,
message: '日期格式不正确'
});
}
if (startDate >= endDate) {
return res.status(400).json({
success: false,
message: '监管结束时间必须晚于开始时间'
});
}
// 检查申请单号和合同编号是否已存在
const existingTask = await SupervisionTask.findOne({
where: {
[Op.or]: [
{ applicationNumber },
{ contractNumber }
]
}
});
if (existingTask) {
return res.status(400).json({
success: false,
message: existingTask.applicationNumber === applicationNumber ?
'申请单号已存在' : '放款合同编号已存在'
});
}
// 创建监管任务
const task = await SupervisionTask.create({
applicationNumber,
contractNumber,
productName,
customerName,
idType,
idNumber,
assetType,
assetQuantity: parseInt(assetQuantity),
supervisionStatus,
startTime,
endTime,
loanAmount: parseFloat(loanAmount),
interestRate: parseFloat(interestRate),
loanTerm: parseInt(loanTerm),
supervisorName,
supervisorPhone,
farmAddress,
remarks,
createdBy: req.user?.userId || req.user?.id,
updatedBy: req.user?.userId || req.user?.id
});
// 获取创建的任务详情(包含关联信息)
const createdTask = await SupervisionTask.findByPk(task.id, {
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'username', 'real_name']
},
{
model: User,
as: 'updater',
attributes: ['id', 'username', 'real_name']
}
]
});
res.status(201).json({
success: true,
data: createdTask,
message: '监管任务创建成功'
});
} catch (error) {
console.error('创建监管任务失败:', error);
// 处理数据库约束错误
if (error.name === 'SequelizeValidationError') {
const validationErrors = error.errors.map(err => err.message).join(', ');
return res.status(400).json({
success: false,
message: `数据验证失败: ${validationErrors}`
});
}
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({
success: false,
message: '申请单号或放款合同编号已存在'
});
}
res.status(500).json({
success: false,
message: '创建监管任务失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 更新监管任务
const updateSupervisionTask = async (req, res) => {
try {
const { id } = req.params;
const updateData = req.body;
// 验证日期
if (updateData.startTime && updateData.endTime) {
const startDate = new Date(updateData.startTime);
const endDate = new Date(updateData.endTime);
if (startDate >= endDate) {
return res.status(400).json({
success: false,
message: '监管结束时间必须晚于开始时间'
});
}
}
const task = await SupervisionTask.findByPk(id);
if (!task) {
return res.status(404).json({
success: false,
message: '监管任务不存在'
});
}
// 如果更新申请单号或合同编号,检查是否重复
if (updateData.applicationNumber || updateData.contractNumber) {
const whereCondition = { id: { [Op.ne]: id } };
if (updateData.applicationNumber) {
whereCondition.applicationNumber = updateData.applicationNumber;
}
if (updateData.contractNumber) {
whereCondition.contractNumber = updateData.contractNumber;
}
const existingTask = await SupervisionTask.findOne({ where: whereCondition });
if (existingTask) {
return res.status(400).json({
success: false,
message: '申请单号或放款合同编号已存在'
});
}
}
await task.update({
...updateData,
updatedBy: req.user?.userId || req.user?.id
});
res.status(200).json({
success: true,
data: task,
message: '监管任务更新成功'
});
} catch (error) {
console.error('更新监管任务失败:', error);
res.status(500).json({
success: false,
message: '更新监管任务失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 删除监管任务
const deleteSupervisionTask = async (req, res) => {
try {
const { id } = req.params;
const task = await SupervisionTask.findByPk(id);
if (!task) {
return res.status(404).json({
success: false,
message: '监管任务不存在'
});
}
await task.destroy();
res.status(200).json({
success: true,
message: '监管任务删除成功'
});
} catch (error) {
console.error('删除监管任务失败:', error);
res.status(500).json({
success: false,
message: '删除监管任务失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 获取监管任务统计
const getSupervisionTaskStats = async (req, res) => {
try {
const total = await SupervisionTask.count();
const pending = await SupervisionTask.count({ where: { supervisionStatus: 'pending' } });
const supervising = await SupervisionTask.count({ where: { supervisionStatus: 'supervising' } });
const completed = await SupervisionTask.count({ where: { supervisionStatus: 'completed' } });
const suspended = await SupervisionTask.count({ where: { supervisionStatus: 'suspended' } });
res.status(200).json({
success: true,
message: '监管任务统计获取成功',
data: {
total,
pending,
supervising,
completed,
suspended
}
});
} catch (error) {
console.error('获取监管任务统计失败:', error);
res.status(500).json({
success: false,
message: '获取监管任务统计失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 批量更新监管任务状态
const batchUpdateStatus = async (req, res) => {
try {
const { ids, supervisionStatus } = req.body;
if (!Array.isArray(ids) || ids.length === 0 || !supervisionStatus) {
return res.status(400).json({
success: false,
message: '请求参数无效'
});
}
if (!['pending', 'supervising', 'completed', 'suspended'].includes(supervisionStatus)) {
return res.status(400).json({
success: false,
message: '监管状态无效'
});
}
const [updatedCount] = await SupervisionTask.update(
{
supervisionStatus,
updatedBy: req.user?.userId || req.user?.id
},
{ where: { id: { [Op.in]: ids } } }
);
res.status(200).json({
success: true,
message: `成功更新 ${updatedCount} 个监管任务状态`,
data: { updatedCount }
});
} catch (error) {
console.error('批量更新监管任务状态失败:', error);
res.status(500).json({
success: false,
message: '批量更新监管任务状态失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// 批量删除监管任务
const batchDeleteTasks = async (req, res) => {
try {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({
success: false,
message: '请求参数无效'
});
}
const deletedCount = await SupervisionTask.destroy({
where: { id: { [Op.in]: ids } }
});
res.status(200).json({
success: true,
message: `成功删除 ${deletedCount} 个监管任务`,
data: { deletedCount }
});
} catch (error) {
console.error('批量删除监管任务失败:', error);
res.status(500).json({
success: false,
message: '批量删除监管任务失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
module.exports = {
getSupervisionTasks,
getSupervisionTaskById,
createSupervisionTask,
updateSupervisionTask,
deleteSupervisionTask,
getSupervisionTaskStats,
batchUpdateStatus,
batchDeleteTasks
};

View File

@@ -0,0 +1,403 @@
# 项目清单 API 接口文档
## 概述
项目清单管理系统的后端API接口提供项目的增删改查功能。
## 基础信息
- **基础URL**: `http://localhost:5351/api/projects`
- **认证方式**: Bearer Token
- **内容类型**: `application/json`
## 接口列表
### 1. 创建项目
**POST** `/api/projects`
#### 请求头
```
Authorization: Bearer <token>
Content-Type: application/json
```
#### 请求参数
| 字段名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| name | string | 是 | 项目名称最大100字符 | "张洪彬养殖项目" |
| farmName | string | 是 | 养殖场名称最大200字符 | "张洪彬养殖场" |
| supervisionObject | string | 是 | 监管对象 | "牛" |
| supervisionPeriod | string | 是 | 监管周期 | "12个月" |
| startTime | string | 是 | 起始时间格式YYYY-MM-DD | "2024-01-01" |
| endTime | string | 是 | 结束时间格式YYYY-MM-DD | "2024-12-31" |
| status | string | 否 | 项目状态默认supervision | "supervision" 或 "completed" |
| supervisionQuantity | number | 否 | 监管数量默认0 | 100 |
| supervisionAmount | number | 否 | 监管金额默认0.00 | 500000.00 |
| earTag | number | 否 | 耳标数量默认0 | 50 |
| collar | number | 否 | 项圈数量默认0 | 30 |
| host | number | 否 | 主机数量默认0 | 20 |
| loanOfficer | string | 否 | 贷款专员 | "张专员" |
| description | string | 否 | 项目描述 | "这是一个测试项目" |
#### 请求示例
```json
{
"name": "张洪彬养殖项目",
"farmName": "张洪彬养殖场",
"supervisionObject": "牛",
"supervisionPeriod": "12个月",
"startTime": "2024-01-01",
"endTime": "2024-12-31",
"status": "supervision",
"supervisionQuantity": 100,
"supervisionAmount": 500000.00,
"earTag": 50,
"collar": 30,
"host": 20,
"loanOfficer": "张专员",
"description": "这是一个测试项目"
}
```
#### 响应示例
**成功响应 (201)**
```json
{
"success": true,
"message": "创建项目成功",
"data": {
"id": 9,
"name": "张洪彬养殖项目",
"status": "supervision",
"farmName": "张洪彬养殖场",
"supervisionObject": "牛",
"supervisionQuantity": 100,
"supervisionPeriod": "12个月",
"supervisionAmount": "500000.00",
"startTime": "2024-01-01",
"endTime": "2024-12-31",
"earTag": 50,
"collar": 30,
"host": 20,
"loanOfficer": "张专员",
"description": "这是一个测试项目",
"createdBy": 1,
"updatedBy": 1,
"createdAt": "2024-12-20T10:30:00.000Z",
"updatedAt": "2024-12-20T10:30:00.000Z",
"creator": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"updater": {
"id": 1,
"username": "admin",
"real_name": "管理员"
}
}
}
```
**错误响应 (400)**
```json
{
"success": false,
"message": "请填写所有必填字段: name, farmName"
}
```
**错误响应 (400)**
```json
{
"success": false,
"message": "项目名称已存在,请使用其他名称"
}
```
**错误响应 (400)**
```json
{
"success": false,
"message": "结束时间必须晚于开始时间"
}
```
### 2. 获取项目列表
**GET** `/api/projects`
#### 查询参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| page | number | 否 | 页码默认1 | 1 |
| limit | number | 否 | 每页数量默认10 | 12 |
| search | string | 否 | 搜索关键词 | "张洪彬" |
| status | string | 否 | 状态筛选 | "supervision" 或 "completed" |
| sortBy | string | 否 | 排序字段默认createdAt | "name" |
| sortOrder | string | 否 | 排序方向默认DESC | "ASC" 或 "DESC" |
#### 请求示例
```
GET /api/projects?page=1&limit=12&search=张洪彬&status=supervision&sortBy=name&sortOrder=ASC
```
#### 响应示例
```json
{
"success": true,
"message": "获取项目列表成功",
"data": {
"projects": [
{
"id": 1,
"name": "张洪彬",
"status": "completed",
"farmName": "大数据中心",
"supervisionObject": "牛",
"supervisionQuantity": 100,
"supervisionPeriod": "12个月",
"supervisionAmount": "500000.00",
"startTime": "2024-01-01",
"endTime": "2024-12-31",
"earTag": 50,
"collar": 30,
"host": 20,
"loanOfficer": "张专员",
"description": "项目描述",
"createdAt": "2024-12-20T10:30:00.000Z",
"updatedAt": "2024-12-20T10:30:00.000Z",
"creator": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"updater": {
"id": 1,
"username": "admin",
"real_name": "管理员"
}
}
],
"pagination": {
"total": 8,
"currentPage": 1,
"pageSize": 12,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}
}
```
### 3. 获取项目详情
**GET** `/api/projects/:id`
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | number | 是 | 项目ID |
#### 响应示例
```json
{
"success": true,
"message": "获取项目详情成功",
"data": {
"id": 1,
"name": "张洪彬",
"status": "completed",
"farmName": "大数据中心",
"supervisionObject": "牛",
"supervisionQuantity": 100,
"supervisionPeriod": "12个月",
"supervisionAmount": "500000.00",
"startTime": "2024-01-01",
"endTime": "2024-12-31",
"earTag": 50,
"collar": 30,
"host": 20,
"loanOfficer": "张专员",
"description": "项目描述",
"createdAt": "2024-12-20T10:30:00.000Z",
"updatedAt": "2024-12-20T10:30:00.000Z",
"creator": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"updater": {
"id": 1,
"username": "admin",
"real_name": "管理员"
}
}
}
```
### 4. 更新项目
**PUT** `/api/projects/:id`
#### 请求参数
与创建项目相同,所有字段都是可选的。
#### 响应示例
```json
{
"success": true,
"message": "项目更新成功",
"data": {
"id": 1,
"name": "更新后的项目名称",
"status": "completed",
// ... 其他字段
}
}
```
### 5. 删除项目
**DELETE** `/api/projects/:id`
#### 响应示例
```json
{
"success": true,
"message": "项目删除成功"
}
```
### 6. 获取项目统计
**GET** `/api/projects/stats`
#### 响应示例
```json
{
"success": true,
"message": "项目统计获取成功",
"data": {
"total": 8,
"supervision": 3,
"completed": 5
}
}
```
### 7. 批量更新项目状态
**PUT** `/api/projects/batch/status`
#### 请求参数
```json
{
"ids": [1, 2, 3],
"status": "completed"
}
```
#### 响应示例
```json
{
"success": true,
"message": "成功更新 3 个项目状态",
"data": {
"updatedCount": 3
}
}
```
## 错误码说明
| 状态码 | 说明 |
|--------|------|
| 200 | 请求成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 401 | 未授权,需要登录 |
| 403 | 禁止访问,权限不足 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
## 数据验证规则
### 必填字段验证
- `name`: 项目名称,不能为空
- `farmName`: 养殖场名称,不能为空
- `supervisionObject`: 监管对象,不能为空
- `supervisionPeriod`: 监管周期,不能为空
- `startTime`: 起始时间,不能为空
- `endTime`: 结束时间,不能为空
### 字段长度限制
- `name`: 最大100个字符
- `farmName`: 最大200个字符
- `supervisionObject`: 最大50个字符
- `supervisionPeriod`: 最大50个字符
- `loanOfficer`: 最大100个字符
### 数值字段验证
- `supervisionQuantity`: 不能为负数
- `supervisionAmount`: 不能为负数
- `earTag`: 不能为负数
- `collar`: 不能为负数
- `host`: 不能为负数
### 日期验证
- `startTime``endTime` 必须是有效的日期格式 (YYYY-MM-DD)
- `endTime` 必须晚于 `startTime`
### 状态验证
- `status` 只能是 "supervision" 或 "completed"
## 使用示例
### JavaScript (axios)
```javascript
// 创建项目
const createProject = async (projectData) => {
try {
const response = await axios.post('/api/projects', projectData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('创建项目失败:', error.response.data);
throw error;
}
};
// 获取项目列表
const getProjects = async (params = {}) => {
try {
const response = await axios.get('/api/projects', {
headers: {
'Authorization': `Bearer ${token}`
},
params
});
return response.data;
} catch (error) {
console.error('获取项目列表失败:', error.response.data);
throw error;
}
};
```
### cURL
```bash
# 创建项目
curl -X POST http://localhost:5351/api/projects \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "测试项目",
"farmName": "测试养殖场",
"supervisionObject": "牛",
"supervisionPeriod": "12个月",
"startTime": "2024-01-01",
"endTime": "2024-12-31"
}'
# 获取项目列表
curl -X GET "http://localhost:5351/api/projects?page=1&limit=10" \
-H "Authorization: Bearer YOUR_TOKEN"
```

View File

@@ -0,0 +1,459 @@
# 监管任务 API 接口文档
## 概述
监管任务管理系统的后端API接口提供监管任务的增删改查功能。
## 基础信息
- **基础URL**: `http://localhost:5351/api/supervision-tasks`
- **认证方式**: Bearer Token
- **内容类型**: `application/json`
## 接口列表
### 1. 创建监管任务
**POST** `/api/supervision-tasks`
#### 请求头
```
Authorization: Bearer <token>
Content-Type: application/json
```
#### 请求参数
| 字段名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| applicationNumber | string | 是 | 申请单号最大50字符唯一 | "APP001" |
| contractNumber | string | 是 | 放款合同编号最大50字符唯一 | "CONTRACT001" |
| productName | string | 是 | 产品名称最大100字符 | "农业贷款产品A" |
| customerName | string | 是 | 客户姓名最大50字符 | "张三" |
| idType | string | 是 | 证件类型 | "id_card" (身份证), "passport" (护照), "other" (其他) |
| idNumber | string | 是 | 证件号码最大50字符 | "110101199001011234" |
| assetType | string | 是 | 养殖生资种类 | "cattle" (牛), "sheep" (羊), "pig" (猪), "poultry" (家禽), "other" (其他) |
| assetQuantity | number | 是 | 监管生资数量,非负数 | 10 |
| startTime | string | 是 | 监管起始时间格式YYYY-MM-DD | "2024-01-15" |
| endTime | string | 是 | 监管结束时间格式YYYY-MM-DD | "2024-12-15" |
| supervisionStatus | string | 否 | 监管状态默认pending | "pending", "supervising", "completed", "suspended" |
| loanAmount | number | 否 | 贷款金额默认0 | 500000.00 |
| interestRate | number | 否 | 利率默认0范围0-1 | 0.0650 |
| loanTerm | number | 否 | 贷款期限默认12 | 12 |
| supervisorName | string | 否 | 监管员姓名最大50字符 | "李监管员" |
| supervisorPhone | string | 否 | 监管员电话最大20字符 | "13800138001" |
| farmAddress | string | 否 | 养殖场地址最大200字符 | "北京市朝阳区某某养殖场" |
| remarks | string | 否 | 备注 | "重点监管项目,需要定期检查" |
#### 请求示例
```json
{
"applicationNumber": "APP001",
"contractNumber": "CONTRACT001",
"productName": "农业贷款产品A",
"customerName": "张三",
"idType": "id_card",
"idNumber": "110101199001011234",
"assetType": "cattle",
"assetQuantity": 10,
"supervisionStatus": "pending",
"startTime": "2024-01-15",
"endTime": "2024-12-15",
"loanAmount": 500000.00,
"interestRate": 0.0650,
"loanTerm": 12,
"supervisorName": "李监管员",
"supervisorPhone": "13800138001",
"farmAddress": "北京市朝阳区某某养殖场",
"remarks": "重点监管项目,需要定期检查"
}
```
#### 响应示例
**成功响应 (201)**
```json
{
"success": true,
"message": "监管任务创建成功",
"data": {
"id": 1,
"applicationNumber": "APP001",
"contractNumber": "CONTRACT001",
"productName": "农业贷款产品A",
"customerName": "张三",
"idType": "id_card",
"idNumber": "110101199001011234",
"assetType": "cattle",
"assetQuantity": 10,
"supervisionStatus": "pending",
"importTime": "2024-12-20T10:30:00.000Z",
"startTime": "2024-01-15",
"endTime": "2024-12-15",
"loanAmount": "500000.00",
"interestRate": "0.0650",
"loanTerm": 12,
"supervisorName": "李监管员",
"supervisorPhone": "13800138001",
"farmAddress": "北京市朝阳区某某养殖场",
"remarks": "重点监管项目,需要定期检查",
"createdBy": 1,
"updatedBy": 1,
"createdAt": "2024-12-20T10:30:00.000Z",
"updatedAt": "2024-12-20T10:30:00.000Z",
"creator": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"updater": {
"id": 1,
"username": "admin",
"real_name": "管理员"
}
}
}
```
### 2. 获取监管任务列表
**GET** `/api/supervision-tasks`
#### 查询参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| page | number | 否 | 页码默认1 | 1 |
| limit | number | 否 | 每页数量默认10 | 10 |
| search | string | 否 | 搜索关键词(申请单号、合同编号、客户姓名、产品名称) | "张三" |
| supervisionStatus | string | 否 | 状态筛选 | "pending", "supervising", "completed", "suspended" |
| dateRange | string | 否 | 日期范围筛选格式startDate,endDate | "2024-01-01,2024-12-31" |
| sortBy | string | 否 | 排序字段默认createdAt | "applicationNumber", "customerName", "supervisionStatus" |
| sortOrder | string | 否 | 排序方向默认DESC | "ASC", "DESC" |
#### 请求示例
```
GET /api/supervision-tasks?page=1&limit=10&search=张三&supervisionStatus=supervising&dateRange=2024-01-01,2024-12-31&sortBy=customerName&sortOrder=ASC
```
#### 响应示例
```json
{
"success": true,
"message": "监管任务列表获取成功",
"data": {
"tasks": [
{
"id": 1,
"applicationNumber": "APP001",
"contractNumber": "CONTRACT001",
"productName": "农业贷款产品A",
"customerName": "张三",
"idType": "id_card",
"idNumber": "110101199001011234",
"assetType": "cattle",
"assetQuantity": 10,
"supervisionStatus": "supervising",
"importTime": "2024-01-15T10:30:00.000Z",
"startTime": "2024-01-15",
"endTime": "2024-12-15",
"loanAmount": "500000.00",
"interestRate": "0.0650",
"loanTerm": 12,
"supervisorName": "李监管员",
"supervisorPhone": "13800138001",
"farmAddress": "北京市朝阳区某某养殖场",
"remarks": "重点监管项目,需要定期检查",
"createdAt": "2024-12-20T10:30:00.000Z",
"updatedAt": "2024-12-20T10:30:00.000Z",
"creator": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"updater": {
"id": 1,
"username": "admin",
"real_name": "管理员"
}
}
],
"pagination": {
"total": 8,
"currentPage": 1,
"pageSize": 10,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}
}
```
### 3. 获取监管任务详情
**GET** `/api/supervision-tasks/:id`
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | number | 是 | 监管任务ID |
#### 响应示例
```json
{
"success": true,
"message": "监管任务详情获取成功",
"data": {
"id": 1,
"applicationNumber": "APP001",
"contractNumber": "CONTRACT001",
"productName": "农业贷款产品A",
"customerName": "张三",
"idType": "id_card",
"idNumber": "110101199001011234",
"assetType": "cattle",
"assetQuantity": 10,
"supervisionStatus": "supervising",
"importTime": "2024-01-15T10:30:00.000Z",
"startTime": "2024-01-15",
"endTime": "2024-12-15",
"loanAmount": "500000.00",
"interestRate": "0.0650",
"loanTerm": 12,
"supervisorName": "李监管员",
"supervisorPhone": "13800138001",
"farmAddress": "北京市朝阳区某某养殖场",
"remarks": "重点监管项目,需要定期检查",
"createdAt": "2024-12-20T10:30:00.000Z",
"updatedAt": "2024-12-20T10:30:00.000Z",
"creator": {
"id": 1,
"username": "admin",
"real_name": "管理员"
},
"updater": {
"id": 1,
"username": "admin",
"real_name": "管理员"
}
}
}
```
### 4. 更新监管任务
**PUT** `/api/supervision-tasks/:id`
#### 请求参数
与创建监管任务相同,所有字段都是可选的。
#### 响应示例
```json
{
"success": true,
"message": "监管任务更新成功",
"data": {
"id": 1,
"applicationNumber": "APP001",
"contractNumber": "CONTRACT001",
"productName": "农业贷款产品A",
"customerName": "张三",
"supervisionStatus": "supervising",
"remarks": "更新后的备注信息",
"updatedAt": "2024-12-20T11:00:00.000Z"
}
}
```
### 5. 删除监管任务
**DELETE** `/api/supervision-tasks/:id`
#### 响应示例
```json
{
"success": true,
"message": "监管任务删除成功"
}
```
### 6. 获取监管任务统计
**GET** `/api/supervision-tasks/stats`
#### 响应示例
```json
{
"success": true,
"message": "监管任务统计获取成功",
"data": {
"total": 8,
"pending": 2,
"supervising": 3,
"completed": 2,
"suspended": 1
}
}
```
### 7. 批量更新监管任务状态
**PUT** `/api/supervision-tasks/batch/status`
#### 请求参数
```json
{
"ids": [1, 2, 3],
"supervisionStatus": "completed"
}
```
#### 响应示例
```json
{
"success": true,
"message": "成功更新 3 个监管任务状态",
"data": {
"updatedCount": 3
}
}
```
### 8. 批量删除监管任务
**DELETE** `/api/supervision-tasks/batch`
#### 请求参数
```json
{
"ids": [1, 2, 3]
}
```
#### 响应示例
```json
{
"success": true,
"message": "成功删除 3 个监管任务",
"data": {
"deletedCount": 3
}
}
```
## 错误码说明
| 状态码 | 说明 |
|--------|------|
| 200 | 请求成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 401 | 未授权,需要登录 |
| 403 | 禁止访问,权限不足 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
## 数据验证规则
### 必填字段验证
- `applicationNumber`: 申请单号不能为空最大50字符唯一
- `contractNumber`: 放款合同编号不能为空最大50字符唯一
- `productName`: 产品名称不能为空最大100字符
- `customerName`: 客户姓名不能为空最大50字符
- `idNumber`: 证件号码不能为空最大50字符
- `startTime`: 监管起始时间,不能为空
- `endTime`: 监管结束时间,不能为空
### 枚举值验证
- `idType`: 只能是 "id_card", "passport", "other"
- `assetType`: 只能是 "cattle", "sheep", "pig", "poultry", "other"
- `supervisionStatus`: 只能是 "pending", "supervising", "completed", "suspended"
### 数值字段验证
- `assetQuantity`: 不能为负数
- `loanAmount`: 不能为负数
- `interestRate`: 必须在0-1之间
- `loanTerm`: 不能为负数
### 日期验证
- `startTime``endTime` 必须是有效的日期格式 (YYYY-MM-DD)
- `endTime` 必须晚于 `startTime`
## 使用示例
### JavaScript (axios)
```javascript
// 创建监管任务
const createSupervisionTask = async (taskData) => {
try {
const response = await axios.post('/api/supervision-tasks', taskData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('创建监管任务失败:', error.response.data);
throw error;
}
};
// 获取监管任务列表
const getSupervisionTasks = async (params = {}) => {
try {
const response = await axios.get('/api/supervision-tasks', {
headers: {
'Authorization': `Bearer ${token}`
},
params
});
return response.data;
} catch (error) {
console.error('获取监管任务列表失败:', error.response.data);
throw error;
}
};
```
### cURL
```bash
# 创建监管任务
curl -X POST http://localhost:5351/api/supervision-tasks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"applicationNumber": "APP001",
"contractNumber": "CONTRACT001",
"productName": "农业贷款产品A",
"customerName": "张三",
"idType": "id_card",
"idNumber": "110101199001011234",
"assetType": "cattle",
"assetQuantity": 10,
"startTime": "2024-01-15",
"endTime": "2024-12-15"
}'
# 获取监管任务列表
curl -X GET "http://localhost:5351/api/supervision-tasks?page=1&limit=10" \
-H "Authorization: Bearer YOUR_TOKEN"
```
## 数据库表结构
### supervision_tasks 表
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | INTEGER | 主键,自增 |
| applicationNumber | VARCHAR(50) | 申请单号,唯一 |
| contractNumber | VARCHAR(50) | 放款合同编号,唯一 |
| productName | VARCHAR(100) | 产品名称 |
| customerName | VARCHAR(50) | 客户姓名 |
| idType | ENUM | 证件类型 |
| idNumber | VARCHAR(50) | 证件号码 |
| assetType | ENUM | 养殖生资种类 |
| assetQuantity | INTEGER | 监管生资数量 |
| supervisionStatus | ENUM | 监管状态 |
| importTime | DATETIME | 任务导入时间 |
| startTime | DATE | 监管起始时间 |
| endTime | DATE | 监管结束时间 |
| loanAmount | DECIMAL(15,2) | 贷款金额 |
| interestRate | DECIMAL(5,4) | 利率 |
| loanTerm | INTEGER | 贷款期限(月) |
| supervisorName | VARCHAR(50) | 监管员姓名 |
| supervisorPhone | VARCHAR(20) | 监管员电话 |
| farmAddress | VARCHAR(200) | 养殖场地址 |
| remarks | TEXT | 备注 |
| createdBy | INTEGER | 创建人ID |
| updatedBy | INTEGER | 更新人ID |
| createdAt | DATETIME | 创建时间 |
| updatedAt | DATETIME | 更新时间 |

View File

@@ -0,0 +1,121 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('projects', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
name: {
type: Sequelize.STRING(100),
allowNull: false,
comment: '项目名称'
},
status: {
type: Sequelize.ENUM('supervision', 'completed'),
allowNull: false,
defaultValue: 'supervision',
comment: '项目状态supervision-监管中completed-已结项'
},
farmName: {
type: Sequelize.STRING(200),
allowNull: false,
comment: '养殖场名称'
},
supervisionObject: {
type: Sequelize.STRING(50),
allowNull: false,
comment: '监管对象'
},
supervisionQuantity: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '监管数量'
},
supervisionPeriod: {
type: Sequelize.STRING(50),
allowNull: false,
comment: '监管周期'
},
supervisionAmount: {
type: Sequelize.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00,
comment: '监管金额'
},
startTime: {
type: Sequelize.DATEONLY,
allowNull: false,
comment: '起始时间'
},
endTime: {
type: Sequelize.DATEONLY,
allowNull: false,
comment: '结束时间'
},
earTag: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '耳标数量'
},
collar: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '项圈数量'
},
host: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '主机数量'
},
loanOfficer: {
type: Sequelize.STRING(100),
allowNull: true,
comment: '贷款专员'
},
description: {
type: Sequelize.TEXT,
allowNull: true,
comment: '项目描述'
},
createdBy: {
type: Sequelize.INTEGER,
allowNull: true,
comment: '创建人ID'
},
updatedBy: {
type: Sequelize.INTEGER,
allowNull: true,
comment: '更新人ID'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW
}
});
// 添加索引
await queryInterface.addIndex('projects', ['status']);
await queryInterface.addIndex('projects', ['farmName']);
await queryInterface.addIndex('projects', ['createdBy']);
await queryInterface.addIndex('projects', ['startTime']);
await queryInterface.addIndex('projects', ['endTime']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('projects');
}
};

View File

@@ -0,0 +1,152 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('supervision_tasks', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
applicationNumber: {
type: Sequelize.STRING(50),
allowNull: false,
unique: true,
comment: '申请单号'
},
contractNumber: {
type: Sequelize.STRING(50),
allowNull: false,
unique: true,
comment: '放款合同编号'
},
productName: {
type: Sequelize.STRING(100),
allowNull: false,
comment: '产品名称'
},
customerName: {
type: Sequelize.STRING(50),
allowNull: false,
comment: '客户姓名'
},
idType: {
type: Sequelize.ENUM('id_card', 'passport', 'other'),
allowNull: false,
defaultValue: 'id_card',
comment: '证件类型id_card-身份证passport-护照other-其他'
},
idNumber: {
type: Sequelize.STRING(50),
allowNull: false,
comment: '证件号码'
},
assetType: {
type: Sequelize.ENUM('cattle', 'sheep', 'pig', 'poultry', 'other'),
allowNull: false,
defaultValue: 'cattle',
comment: '养殖生资种类cattle-牛sheep-羊pig-猪poultry-家禽other-其他'
},
assetQuantity: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '监管生资数量'
},
supervisionStatus: {
type: Sequelize.ENUM('pending', 'supervising', 'completed', 'suspended'),
allowNull: false,
defaultValue: 'pending',
comment: '监管状态pending-待监管supervising-监管中completed-已完成suspended-已暂停'
},
importTime: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: '任务导入时间'
},
startTime: {
type: Sequelize.DATEONLY,
allowNull: false,
comment: '监管起始时间'
},
endTime: {
type: Sequelize.DATEONLY,
allowNull: false,
comment: '监管结束时间'
},
loanAmount: {
type: Sequelize.DECIMAL(15, 2),
allowNull: true,
defaultValue: 0.00,
comment: '贷款金额'
},
interestRate: {
type: Sequelize.DECIMAL(5, 4),
allowNull: true,
defaultValue: 0.0000,
comment: '利率'
},
loanTerm: {
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: 12,
comment: '贷款期限(月)'
},
supervisorName: {
type: Sequelize.STRING(50),
allowNull: true,
comment: '监管员姓名'
},
supervisorPhone: {
type: Sequelize.STRING(20),
allowNull: true,
comment: '监管员电话'
},
farmAddress: {
type: Sequelize.STRING(200),
allowNull: true,
comment: '养殖场地址'
},
remarks: {
type: Sequelize.TEXT,
allowNull: true,
comment: '备注'
},
createdBy: {
type: Sequelize.INTEGER,
allowNull: true,
comment: '创建人ID'
},
updatedBy: {
type: Sequelize.INTEGER,
allowNull: true,
comment: '更新人ID'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
}
});
// 添加索引
await queryInterface.addIndex('supervision_tasks', ['applicationNumber']);
await queryInterface.addIndex('supervision_tasks', ['contractNumber']);
await queryInterface.addIndex('supervision_tasks', ['customerName']);
await queryInterface.addIndex('supervision_tasks', ['supervisionStatus']);
await queryInterface.addIndex('supervision_tasks', ['createdBy']);
await queryInterface.addIndex('supervision_tasks', ['startTime']);
await queryInterface.addIndex('supervision_tasks', ['endTime']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('supervision_tasks');
}
};

View File

@@ -0,0 +1,119 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Project = sequelize.define('Project', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '项目名称'
},
status: {
type: DataTypes.ENUM('supervision', 'completed'),
allowNull: false,
defaultValue: 'supervision',
comment: '项目状态supervision-监管中completed-已结项'
},
farmName: {
type: DataTypes.STRING(200),
allowNull: false,
comment: '养殖场名称'
},
supervisionObject: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '监管对象'
},
supervisionQuantity: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '监管数量'
},
supervisionPeriod: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '监管周期'
},
supervisionAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00,
comment: '监管金额'
},
startTime: {
type: DataTypes.DATEONLY,
allowNull: false,
comment: '起始时间'
},
endTime: {
type: DataTypes.DATEONLY,
allowNull: false,
comment: '结束时间'
},
earTag: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '耳标数量'
},
collar: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '项圈数量'
},
host: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '主机数量'
},
loanOfficer: {
type: DataTypes.STRING(100),
allowNull: true,
comment: '贷款专员'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '项目描述'
},
createdBy: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '创建人ID'
},
updatedBy: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '更新人ID'
}
}, {
tableName: 'projects',
timestamps: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt',
comment: '项目清单表'
});
// 定义关联关系
Project.associate = (models) => {
// 项目与用户关联(创建人)
Project.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
// 项目与用户关联(更新人)
Project.belongsTo(models.User, {
foreignKey: 'updatedBy',
as: 'updater'
});
};
module.exports = Project;

View File

@@ -0,0 +1,166 @@
/**
* 监管任务模型
* @file SupervisionTask.js
* @description 监管任务数据模型定义
*/
const { DataTypes } = require('sequelize');
const BaseModel = require('./BaseModel');
const { sequelize } = require('../config/database');
class SupervisionTask extends BaseModel {}
SupervisionTask.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
applicationNumber: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '申请单号'
},
contractNumber: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '放款合同编号'
},
productName: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '产品名称'
},
customerName: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '客户姓名'
},
idType: {
type: DataTypes.ENUM('id_card', 'passport', 'other'),
allowNull: false,
defaultValue: 'id_card',
comment: '证件类型id_card-身份证passport-护照other-其他'
},
idNumber: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '证件号码'
},
assetType: {
type: DataTypes.ENUM('cattle', 'sheep', 'pig', 'poultry', 'other'),
allowNull: false,
defaultValue: 'cattle',
comment: '养殖生资种类cattle-牛sheep-羊pig-猪poultry-家禽other-其他'
},
assetQuantity: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '监管生资数量'
},
supervisionStatus: {
type: DataTypes.ENUM('pending', 'supervising', 'completed', 'suspended'),
allowNull: false,
defaultValue: 'pending',
comment: '监管状态pending-待监管supervising-监管中completed-已完成suspended-已暂停'
},
importTime: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '任务导入时间'
},
startTime: {
type: DataTypes.DATEONLY,
allowNull: false,
comment: '监管起始时间'
},
endTime: {
type: DataTypes.DATEONLY,
allowNull: false,
comment: '监管结束时间'
},
loanAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: true,
defaultValue: 0.00,
comment: '贷款金额'
},
interestRate: {
type: DataTypes.DECIMAL(5, 4),
allowNull: true,
defaultValue: 0.0000,
comment: '利率'
},
loanTerm: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 12,
comment: '贷款期限(月)'
},
supervisorName: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '监管员姓名'
},
supervisorPhone: {
type: DataTypes.STRING(20),
allowNull: true,
comment: '监管员电话'
},
farmAddress: {
type: DataTypes.STRING(200),
allowNull: true,
comment: '养殖场地址'
},
remarks: {
type: DataTypes.TEXT,
allowNull: true,
comment: '备注'
},
createdBy: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '创建人ID'
},
updatedBy: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '更新人ID'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
}
}, {
sequelize,
modelName: 'SupervisionTask',
tableName: 'supervision_tasks',
timestamps: true,
underscored: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt',
comment: '监管任务表'
});
// 定义关联关系
SupervisionTask.associate = (models) => {
SupervisionTask.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
SupervisionTask.belongsTo(models.User, {
foreignKey: 'updatedBy',
as: 'updater'
});
};
module.exports = SupervisionTask;

View File

@@ -15,6 +15,8 @@ const Employee = require('./Employee');
const Department = require('./Department');
const Position = require('./Position');
const Report = require('./Report');
const Project = require('./Project');
const SupervisionTask = require('./SupervisionTask');
// 定义模型关联关系
@@ -91,6 +93,54 @@ User.hasMany(Report, {
as: 'reports'
});
// 项目与用户关联(创建人)
Project.belongsTo(User, {
foreignKey: 'createdBy',
as: 'creator',
targetKey: 'id'
});
User.hasMany(Project, {
foreignKey: 'createdBy',
as: 'createdProjects'
});
// 项目与用户关联(更新人)
Project.belongsTo(User, {
foreignKey: 'updatedBy',
as: 'updater',
targetKey: 'id'
});
User.hasMany(Project, {
foreignKey: 'updatedBy',
as: 'updatedProjects'
});
// 监管任务与用户关联(创建人)
SupervisionTask.belongsTo(User, {
foreignKey: 'createdBy',
as: 'creator',
targetKey: 'id'
});
User.hasMany(SupervisionTask, {
foreignKey: 'createdBy',
as: 'createdSupervisionTasks'
});
// 监管任务与用户关联(更新人)
SupervisionTask.belongsTo(User, {
foreignKey: 'updatedBy',
as: 'updater',
targetKey: 'id'
});
User.hasMany(SupervisionTask, {
foreignKey: 'updatedBy',
as: 'updatedSupervisionTasks'
});
// 导出所有模型和数据库实例
module.exports = {
sequelize,
@@ -102,5 +152,7 @@ module.exports = {
Employee,
Department,
Position,
Report
Report,
Project,
SupervisionTask
};

View File

@@ -0,0 +1,37 @@
const express = require('express');
const router = express.Router();
const projectController = require('../controllers/projectController');
const { authMiddleware } = require('../middleware/auth');
// 应用认证中间件到所有路由
router.use(authMiddleware);
// 获取项目列表
// GET /api/projects
router.get('/', projectController.getProjects);
// 获取项目统计
// GET /api/projects/stats
router.get('/stats', projectController.getProjectStats);
// 获取项目详情
// GET /api/projects/:id
router.get('/:id', projectController.getProjectById);
// 创建项目
// POST /api/projects
router.post('/', projectController.createProject);
// 更新项目
// PUT /api/projects/:id
router.put('/:id', projectController.updateProject);
// 删除项目
// DELETE /api/projects/:id
router.delete('/:id', projectController.deleteProject);
// 批量更新项目状态
// PUT /api/projects/batch/status
router.put('/batch/status', projectController.batchUpdateStatus);
module.exports = router;

View File

@@ -0,0 +1,41 @@
const express = require('express');
const router = express.Router();
const supervisionTaskController = require('../controllers/supervisionTaskController');
const { authMiddleware } = require('../middleware/auth');
// 应用认证中间件到所有路由
router.use(authMiddleware);
// 获取监管任务列表
// GET /api/supervision-tasks
router.get('/', supervisionTaskController.getSupervisionTasks);
// 获取监管任务统计
// GET /api/supervision-tasks/stats
router.get('/stats', supervisionTaskController.getSupervisionTaskStats);
// 获取监管任务详情
// GET /api/supervision-tasks/:id
router.get('/:id', supervisionTaskController.getSupervisionTaskById);
// 创建监管任务
// POST /api/supervision-tasks
router.post('/', supervisionTaskController.createSupervisionTask);
// 更新监管任务
// PUT /api/supervision-tasks/:id
router.put('/:id', supervisionTaskController.updateSupervisionTask);
// 删除监管任务
// DELETE /api/supervision-tasks/:id
router.delete('/:id', supervisionTaskController.deleteSupervisionTask);
// 批量更新监管任务状态
// PUT /api/supervision-tasks/batch/status
router.put('/batch/status', supervisionTaskController.batchUpdateStatus);
// 批量删除监管任务
// DELETE /api/supervision-tasks/batch
router.delete('/batch', supervisionTaskController.batchDeleteTasks);
module.exports = router;

View File

@@ -0,0 +1,200 @@
const { Project, User } = require('../models');
const { sequelize } = require('../config/database');
// 项目测试数据
const projectsData = [
{
name: '张洪彬',
status: 'completed',
farmName: '大数据中心',
supervisionObject: '牛',
supervisionQuantity: 10,
supervisionPeriod: '23天',
supervisionAmount: 10000.00,
startTime: '2024-02-21',
endTime: '2024-03-15',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: 'mapleaf',
description: '大数据中心养殖项目'
},
{
name: '田小平',
status: 'completed',
farmName: '139****5685_养殖场',
supervisionObject: '牛',
supervisionQuantity: 0,
supervisionPeriod: '0天',
supervisionAmount: 0.00,
startTime: '2022-12-12',
endTime: '2023-12-12',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: '',
description: '田小平养殖场项目'
},
{
name: '杜宝民',
status: 'completed',
farmName: '杜宝民养殖场',
supervisionObject: '牛',
supervisionQuantity: 1,
supervisionPeriod: '20天',
supervisionAmount: 1000000.00,
startTime: '2023-05-08',
endTime: '2028-05-08',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: 'mapleaf',
description: '杜宝民养殖场长期项目'
},
{
name: '满良',
status: 'completed',
farmName: '满良养殖场',
supervisionObject: '牛',
supervisionQuantity: 30,
supervisionPeriod: '365天',
supervisionAmount: 420000.00,
startTime: '2023-01-01',
endTime: '2024-01-01',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: 'mapleaf',
description: '满良养殖场年度项目'
},
{
name: '敖日布仁琴',
status: 'supervision',
farmName: '敖日布仁琴养殖场',
supervisionObject: '牛',
supervisionQuantity: 38,
supervisionPeriod: '1827天',
supervisionAmount: 530000.00,
startTime: '2019-01-01',
endTime: '2024-01-01',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: 'mapleaf',
description: '敖日布仁琴养殖场长期监管项目'
},
{
name: '那顺乌日图',
status: 'supervision',
farmName: '那顺乌日图养殖场',
supervisionObject: '牛',
supervisionQuantity: 36,
supervisionPeriod: '1827天',
supervisionAmount: 500000.00,
startTime: '2019-01-01',
endTime: '2024-01-01',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: 'mapleaf',
description: '那顺乌日图养殖场长期监管项目'
},
{
name: '巴特尔',
status: 'supervision',
farmName: '巴特尔养殖场',
supervisionObject: '牛',
supervisionQuantity: 41,
supervisionPeriod: '1827天',
supervisionAmount: 570000.00,
startTime: '2019-01-01',
endTime: '2024-01-01',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: 'mapleaf',
description: '巴特尔养殖场长期监管项目'
},
{
name: '王五',
status: 'completed',
farmName: '王五养殖场',
supervisionObject: '牛',
supervisionQuantity: 50,
supervisionPeriod: '365天',
supervisionAmount: 700000.00,
startTime: '2023-01-01',
endTime: '2024-01-01',
earTag: 0,
collar: 0,
host: 0,
loanOfficer: 'mapleaf',
description: '王五养殖场年度项目'
}
];
async function seedProjects() {
try {
console.log('开始创建项目测试数据...');
// 测试数据库连接
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 查找管理员用户作为创建人
const adminUser = await User.findOne({ where: { username: 'admin' } });
const createdBy = adminUser ? adminUser.id : null;
// 清空现有项目数据
await Project.destroy({ where: {} });
console.log('✅ 清空现有项目数据');
// 创建项目数据
for (const projectData of projectsData) {
await Project.create({
...projectData,
createdBy: createdBy,
updatedBy: createdBy
});
}
console.log(`✅ 成功创建 ${projectsData.length} 个项目`);
// 验证数据
const projectCount = await Project.count();
console.log(`📊 数据库中现有项目数量: ${projectCount}`);
// 显示统计信息
const supervisionCount = await Project.count({ where: { status: 'supervision' } });
const completedCount = await Project.count({ where: { status: 'completed' } });
const totalAmount = await Project.sum('supervisionAmount');
const totalQuantity = await Project.sum('supervisionQuantity');
console.log('\n📈 项目统计信息:');
console.log(` 监管中项目: ${supervisionCount}`);
console.log(` 已结项项目: ${completedCount}`);
console.log(` 总监管金额: ${totalAmount?.toFixed(2) || 0}`);
console.log(` 总监管数量: ${totalQuantity || 0}`);
console.log('\n🎉 项目数据创建完成!');
} catch (error) {
console.error('❌ 创建项目数据失败:', error);
throw error;
}
}
// 如果直接运行此脚本
if (require.main === module) {
seedProjects()
.then(() => {
console.log('✅ 脚本执行完成');
process.exit(0);
})
.catch((error) => {
console.error('❌ 脚本执行失败:', error);
process.exit(1);
});
}
module.exports = seedProjects;

View File

@@ -0,0 +1,261 @@
/**
* 监管任务测试数据种子文件
* @file seed-supervision-tasks.js
* @description 为监管任务表添加测试数据
*/
const { sequelize, SupervisionTask, User } = require('../models');
async function seedSupervisionTasks() {
try {
console.log('🌱 开始添加监管任务测试数据...');
// 检查是否已有数据
const existingCount = await SupervisionTask.count();
if (existingCount > 0) {
console.log(`⚠️ 监管任务表已有 ${existingCount} 条数据,跳过种子数据添加`);
return;
}
// 获取管理员用户ID
const adminUser = await User.findOne({ where: { username: 'admin' } });
if (!adminUser) {
console.log('❌ 未找到管理员用户,请先运行用户种子数据');
return;
}
const adminId = adminUser.id;
// 监管任务测试数据
const supervisionTasks = [
{
applicationNumber: 'APP001',
contractNumber: 'CONTRACT001',
productName: '农业贷款产品A',
customerName: '张三',
idType: 'id_card',
idNumber: '110101199001011234',
assetType: 'cattle',
assetQuantity: 10,
supervisionStatus: 'supervising',
importTime: new Date('2024-01-15 10:30:00'),
startTime: '2024-01-15',
endTime: '2024-12-15',
loanAmount: 500000.00,
interestRate: 0.0650,
loanTerm: 12,
supervisorName: '李监管员',
supervisorPhone: '13800138001',
farmAddress: '北京市朝阳区某某养殖场',
remarks: '重点监管项目,需要定期检查',
createdBy: adminId,
updatedBy: adminId
},
{
applicationNumber: 'APP002',
contractNumber: 'CONTRACT002',
productName: '农业贷款产品B',
customerName: '李四',
idType: 'id_card',
idNumber: '110101199002021234',
assetType: 'sheep',
assetQuantity: 20,
supervisionStatus: 'pending',
importTime: new Date('2024-01-16 14:20:00'),
startTime: '2024-01-16',
endTime: '2024-12-16',
loanAmount: 300000.00,
interestRate: 0.0600,
loanTerm: 12,
supervisorName: '王监管员',
supervisorPhone: '13800138002',
farmAddress: '北京市海淀区某某农场',
remarks: '新申请项目,待开始监管',
createdBy: adminId,
updatedBy: adminId
},
{
applicationNumber: 'APP003',
contractNumber: 'CONTRACT003',
productName: '农业贷款产品C',
customerName: '王五',
idType: 'id_card',
idNumber: '110101199003031234',
assetType: 'pig',
assetQuantity: 15,
supervisionStatus: 'completed',
importTime: new Date('2024-01-10 09:15:00'),
startTime: '2024-01-10',
endTime: '2024-01-20',
loanAmount: 200000.00,
interestRate: 0.0550,
loanTerm: 6,
supervisorName: '赵监管员',
supervisorPhone: '13800138003',
farmAddress: '北京市丰台区某某猪场',
remarks: '监管任务已完成,客户还款正常',
createdBy: adminId,
updatedBy: adminId
},
{
applicationNumber: 'APP004',
contractNumber: 'CONTRACT004',
productName: '农业贷款产品D',
customerName: '赵六',
idType: 'id_card',
idNumber: '110101199004041234',
assetType: 'poultry',
assetQuantity: 50,
supervisionStatus: 'supervising',
importTime: new Date('2024-01-20 11:45:00'),
startTime: '2024-01-20',
endTime: '2024-06-20',
loanAmount: 150000.00,
interestRate: 0.0700,
loanTerm: 6,
supervisorName: '孙监管员',
supervisorPhone: '13800138004',
farmAddress: '北京市通州区某某鸡场',
remarks: '家禽养殖项目,需要特别关注防疫情况',
createdBy: adminId,
updatedBy: adminId
},
{
applicationNumber: 'APP005',
contractNumber: 'CONTRACT005',
productName: '农业贷款产品E',
customerName: '孙七',
idType: 'id_card',
idNumber: '110101199005051234',
assetType: 'cattle',
assetQuantity: 25,
supervisionStatus: 'suspended',
importTime: new Date('2024-01-25 16:30:00'),
startTime: '2024-01-25',
endTime: '2024-12-25',
loanAmount: 800000.00,
interestRate: 0.0625,
loanTerm: 18,
supervisorName: '周监管员',
supervisorPhone: '13800138005',
farmAddress: '北京市昌平区某某牧场',
remarks: '因客户原因暂停监管,等待进一步通知',
createdBy: adminId,
updatedBy: adminId
},
{
applicationNumber: 'APP006',
contractNumber: 'CONTRACT006',
productName: '农业贷款产品F',
customerName: '周八',
idType: 'passport',
idNumber: 'P123456789',
assetType: 'other',
assetQuantity: 30,
supervisionStatus: 'supervising',
importTime: new Date('2024-02-01 08:20:00'),
startTime: '2024-02-01',
endTime: '2024-08-01',
loanAmount: 400000.00,
interestRate: 0.0680,
loanTerm: 6,
supervisorName: '吴监管员',
supervisorPhone: '13800138006',
farmAddress: '北京市顺义区某某特种养殖场',
remarks: '特种养殖项目,需要专业监管',
createdBy: adminId,
updatedBy: adminId
},
{
applicationNumber: 'APP007',
contractNumber: 'CONTRACT007',
productName: '农业贷款产品G',
customerName: '吴九',
idType: 'id_card',
idNumber: '110101199007071234',
assetType: 'sheep',
assetQuantity: 40,
supervisionStatus: 'pending',
importTime: new Date('2024-02-05 13:10:00'),
startTime: '2024-02-05',
endTime: '2024-12-05',
loanAmount: 600000.00,
interestRate: 0.0590,
loanTerm: 12,
supervisorName: '郑监管员',
supervisorPhone: '13800138007',
farmAddress: '北京市房山区某某羊场',
remarks: '大规模羊群养殖,需要加强监管',
createdBy: adminId,
updatedBy: adminId
},
{
applicationNumber: 'APP008',
contractNumber: 'CONTRACT008',
productName: '农业贷款产品H',
customerName: '郑十',
idType: 'id_card',
idNumber: '110101199008081234',
assetType: 'pig',
assetQuantity: 35,
supervisionStatus: 'completed',
importTime: new Date('2024-01-05 15:45:00'),
startTime: '2024-01-05',
endTime: '2024-03-05',
loanAmount: 350000.00,
interestRate: 0.0575,
loanTerm: 3,
supervisorName: '冯监管员',
supervisorPhone: '13800138008',
farmAddress: '北京市大兴区某某养猪场',
remarks: '短期养殖项目,已顺利完成监管',
createdBy: adminId,
updatedBy: adminId
}
];
// 批量创建监管任务
await SupervisionTask.bulkCreate(supervisionTasks);
console.log(`✅ 成功添加 ${supervisionTasks.length} 条监管任务测试数据`);
// 显示统计信息
const stats = await SupervisionTask.findAll({
attributes: [
'supervisionStatus',
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
group: ['supervisionStatus'],
raw: true
});
console.log('📊 监管任务状态统计:');
stats.forEach(stat => {
const statusNames = {
'pending': '待监管',
'supervising': '监管中',
'completed': '已完成',
'suspended': '已暂停'
};
console.log(` ${statusNames[stat.supervisionStatus] || stat.supervisionStatus}: ${stat.count}`);
});
} catch (error) {
console.error('❌ 添加监管任务测试数据失败:', error);
throw error;
}
}
// 如果直接运行此文件
if (require.main === module) {
seedSupervisionTasks()
.then(() => {
console.log('🎉 监管任务种子数据添加完成');
process.exit(0);
})
.catch((error) => {
console.error('💥 监管任务种子数据添加失败:', error);
process.exit(1);
});
}
module.exports = seedSupervisionTasks;

View File

@@ -0,0 +1,60 @@
const { sequelize } = require('../config/database');
const { Project } = require('../models');
const seedProjects = require('./seed-projects');
async function setupProjectsSimple() {
try {
console.log('🚀 开始设置项目清单功能...\n');
// 1. 测试数据库连接
console.log('1⃣ 测试数据库连接...');
await sequelize.authenticate();
console.log('✅ 数据库连接成功\n');
// 2. 同步项目模型(创建表)
console.log('2⃣ 创建项目表...');
await Project.sync({ force: false }); // force: false 表示如果表已存在则不删除
console.log('✅ 项目表创建成功\n');
// 3. 创建项目测试数据
console.log('3⃣ 创建项目测试数据...');
await seedProjects();
console.log('✅ 项目测试数据创建完成\n');
// 4. 验证数据
console.log('4⃣ 验证项目数据...');
const projectCount = await Project.count();
const supervisionCount = await Project.count({ where: { status: 'supervision' } });
const completedCount = await Project.count({ where: { status: 'completed' } });
console.log(` 总项目数: ${projectCount}`);
console.log(` 监管中项目: ${supervisionCount}`);
console.log(` 已结项项目: ${completedCount}`);
console.log('✅ 项目数据验证完成\n');
console.log('🎉 项目清单功能设置完成!');
console.log('📝 接下来可以:');
console.log(' 1. 启动后端服务器: npm start');
console.log(' 2. 运行API测试: node test-projects-api.js');
console.log(' 3. 在前端访问项目清单页面');
} catch (error) {
console.error('❌ 设置项目清单功能失败:', error);
throw error;
}
}
// 如果直接运行此脚本
if (require.main === module) {
setupProjectsSimple()
.then(() => {
console.log('✅ 脚本执行完成');
process.exit(0);
})
.catch((error) => {
console.error('❌ 脚本执行失败:', error);
process.exit(1);
});
}
module.exports = setupProjectsSimple;

View File

@@ -0,0 +1,79 @@
const { sequelize } = require('../config/database');
const seedProjects = require('./seed-projects');
async function setupProjects() {
try {
console.log('🚀 开始设置项目清单功能...\n');
// 1. 测试数据库连接
console.log('1⃣ 测试数据库连接...');
await sequelize.authenticate();
console.log('✅ 数据库连接成功\n');
// 2. 运行项目表迁移
console.log('2⃣ 运行项目表迁移...');
try {
const { QueryInterface } = require('sequelize');
const queryInterface = sequelize.getQueryInterface();
// 检查表是否已存在
const tableExists = await queryInterface.showAllTables().then(tables =>
tables.includes('projects')
);
if (!tableExists) {
// 运行迁移
const migration = require('../migrations/20241220000002-create-projects');
await migration.up(queryInterface, sequelize);
console.log('✅ 项目表创建成功\n');
} else {
console.log('✅ 项目表已存在\n');
}
} catch (error) {
console.error('❌ 项目表迁移失败:', error.message);
throw error;
}
// 3. 创建项目测试数据
console.log('3⃣ 创建项目测试数据...');
await seedProjects();
console.log('✅ 项目测试数据创建完成\n');
// 4. 验证数据
console.log('4⃣ 验证项目数据...');
const { Project } = require('../models');
const projectCount = await Project.count();
const supervisionCount = await Project.count({ where: { status: 'supervision' } });
const completedCount = await Project.count({ where: { status: 'completed' } });
console.log(` 总项目数: ${projectCount}`);
console.log(` 监管中项目: ${supervisionCount}`);
console.log(` 已结项项目: ${completedCount}`);
console.log('✅ 项目数据验证完成\n');
console.log('🎉 项目清单功能设置完成!');
console.log('📝 接下来可以:');
console.log(' 1. 启动后端服务器: npm start');
console.log(' 2. 运行API测试: node test-projects-api.js');
console.log(' 3. 在前端访问项目清单页面');
} catch (error) {
console.error('❌ 设置项目清单功能失败:', error);
throw error;
}
}
// 如果直接运行此脚本
if (require.main === module) {
setupProjects()
.then(() => {
console.log('✅ 脚本执行完成');
process.exit(0);
})
.catch((error) => {
console.error('❌ 脚本执行失败:', error);
process.exit(1);
});
}
module.exports = setupProjects;

View File

@@ -0,0 +1,43 @@
/**
* 监管任务设置脚本
* @file setup-supervision-tasks.js
* @description 创建监管任务表并添加测试数据
*/
const { sequelize, SupervisionTask } = require('../models');
const seedSupervisionTasks = require('./seed-supervision-tasks');
async function setupSupervisionTasks() {
try {
console.log('🚀 开始设置监管任务...');
// 测试数据库连接
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 同步监管任务模型(创建表)
await sequelize.sync({ force: false });
console.log('✅ 数据库表同步完成');
// 添加测试数据
await seedSupervisionTasks();
console.log('🎉 监管任务设置完成!');
} catch (error) {
console.error('❌ 监管任务设置失败:', error);
throw error;
} finally {
await sequelize.close();
}
}
// 运行设置
setupSupervisionTasks()
.then(() => {
console.log('✅ 所有操作完成');
process.exit(0);
})
.catch((error) => {
console.error('💥 操作失败:', error);
process.exit(1);
});

View File

@@ -74,6 +74,8 @@ app.use('/api/transactions', require('./routes/transactions'));
app.use('/api/dashboard', require('./routes/dashboard'));
app.use('/api/loan-products', require('./routes/loanProducts'));
app.use('/api/employees', require('./routes/employees'));
app.use('/api/projects', require('./routes/projects'));
app.use('/api/supervision-tasks', require('./routes/supervisionTasks'));
// app.use('/api/reports', require('./routes/reports'));
// 根路径

View File

@@ -0,0 +1,96 @@
/**
* 测试创建项目接口
*/
const axios = require('axios');
const API_BASE_URL = 'http://localhost:5351';
async function testCreateProject() {
try {
console.log('🚀 开始测试创建项目接口...\n');
// 1. 先登录获取token
console.log('1. 登录获取认证token...');
const loginResponse = await axios.post(`${API_BASE_URL}/api/auth/login`, {
username: 'admin',
password: 'Admin123456'
});
if (!loginResponse.data.success) {
throw new Error('登录失败: ' + loginResponse.data.message);
}
const token = loginResponse.data.data.token;
console.log('✅ 登录成功获取到token\n');
// 2. 测试创建项目
console.log('2. 测试创建新项目...');
const newProject = {
name: '测试项目_' + new Date().getTime(),
status: 'supervision',
farmName: '测试养殖场',
supervisionObject: '牛',
supervisionQuantity: 100,
supervisionPeriod: '12个月',
supervisionAmount: 500000.00,
startTime: '2024-01-01',
endTime: '2024-12-31',
earTag: 50,
collar: 30,
host: 20,
loanOfficer: '张专员',
description: '这是一个测试项目,用于验证创建接口功能'
};
const createResponse = await axios.post(`${API_BASE_URL}/api/projects`, newProject, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (createResponse.data.success) {
console.log('✅ 项目创建成功!');
console.log('📋 创建的项目信息:');
console.log(` - 项目ID: ${createResponse.data.data.id}`);
console.log(` - 项目名称: ${createResponse.data.data.name}`);
console.log(` - 养殖场: ${createResponse.data.data.farmName}`);
console.log(` - 监管对象: ${createResponse.data.data.supervisionObject}`);
console.log(` - 监管数量: ${createResponse.data.data.supervisionQuantity}`);
console.log(` - 监管金额: ${createResponse.data.data.supervisionAmount}`);
console.log(` - 贷款专员: ${createResponse.data.data.loanOfficer}`);
console.log(` - 创建时间: ${createResponse.data.data.createdAt}`);
} else {
console.log('❌ 项目创建失败:', createResponse.data.message);
}
// 3. 验证项目是否在列表中
console.log('\n3. 验证项目是否在列表中...');
const listResponse = await axios.get(`${API_BASE_URL}/api/projects`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (listResponse.data.success) {
const projects = listResponse.data.data.projects;
const createdProject = projects.find(p => p.name === newProject.name);
if (createdProject) {
console.log('✅ 项目已成功添加到列表中');
console.log(`📊 当前总项目数: ${projects.length}`);
} else {
console.log('❌ 项目未在列表中找到');
}
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error('响应数据:', error.response.data);
}
}
}
// 运行测试
testCreateProject();

View File

@@ -0,0 +1,34 @@
const { Project } = require('./models');
async function testProjectDirect() {
try {
console.log('🚀 直接测试项目模型...');
// 测试基本查询
const count = await Project.count();
console.log('✅ 项目总数:', count);
// 测试获取前5个项目
const projects = await Project.findAll({
limit: 5,
order: [['createdAt', 'DESC']]
});
console.log('✅ 获取项目成功,数量:', projects.length);
if (projects.length > 0) {
console.log('第一个项目:', {
id: projects[0].id,
name: projects[0].name,
status: projects[0].status,
farmName: projects[0].farmName
});
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
console.error('错误堆栈:', error.stack);
}
}
testProjectDirect();

View File

@@ -0,0 +1,388 @@
const http = require('http');
// 测试配置
const API_BASE_URL = 'http://localhost:5351';
let authToken = '';
// 辅助函数发送HTTP请求
function makeRequest(options, data = null) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
try {
const result = {
statusCode: res.statusCode,
headers: res.headers,
data: responseData ? JSON.parse(responseData) : null
};
resolve(result);
} catch (error) {
reject(new Error(`解析响应失败: ${error.message}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
if (data) {
req.write(JSON.stringify(data));
}
req.end();
});
}
// 登录获取认证令牌
async function login() {
console.log('🔐 正在登录...');
const options = {
hostname: 'localhost',
port: 5351,
path: '/api/auth/login',
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
const loginData = {
username: 'admin',
password: 'Admin123456'
};
try {
const response = await makeRequest(options, loginData);
if (response.statusCode === 200 && response.data.success) {
authToken = response.data.data.token;
console.log('✅ 登录成功');
return true;
} else {
console.error('❌ 登录失败:', response.data.message);
return false;
}
} catch (error) {
console.error('❌ 登录请求失败:', error.message);
return false;
}
}
// 测试获取项目列表
async function testGetProjects() {
console.log('\n📋 测试获取项目列表...');
const options = {
hostname: 'localhost',
port: 5351,
path: '/api/projects',
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
};
try {
const response = await makeRequest(options);
if (response.statusCode === 200 && response.data.success) {
console.log('✅ 获取项目列表成功');
console.log(` 项目数量: ${response.data.data.projects.length}`);
console.log(` 总数量: ${response.data.data.pagination.total}`);
// 显示前3个项目的基本信息
const projects = response.data.data.projects.slice(0, 3);
projects.forEach((project, index) => {
console.log(` 项目${index + 1}: ${project.name} (${project.status}) - ${project.farmName}`);
});
return true;
} else {
console.error('❌ 获取项目列表失败:', response.data.message);
return false;
}
} catch (error) {
console.error('❌ 获取项目列表请求失败:', error.message);
return false;
}
}
// 测试获取项目统计
async function testGetProjectStats() {
console.log('\n📊 测试获取项目统计...');
const options = {
hostname: 'localhost',
port: 5351,
path: '/api/projects/stats',
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
};
try {
const response = await makeRequest(options);
if (response.statusCode === 200 && response.data.success) {
console.log('✅ 获取项目统计成功');
const stats = response.data.data;
console.log(` 总项目数: ${stats.total}`);
console.log(` 监管中: ${stats.supervision}`);
console.log(` 已结项: ${stats.completed}`);
console.log(` 总监管金额: ${stats.totalAmount?.toFixed(2) || 0}`);
console.log(` 总监管数量: ${stats.totalQuantity || 0}`);
return true;
} else {
console.error('❌ 获取项目统计失败:', response.data.message);
return false;
}
} catch (error) {
console.error('❌ 获取项目统计请求失败:', error.message);
return false;
}
}
// 测试创建项目
async function testCreateProject() {
console.log('\n 测试创建项目...');
const options = {
hostname: 'localhost',
port: 5351,
path: '/api/projects',
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
};
const projectData = {
name: '测试项目',
status: 'supervision',
farmName: '测试养殖场',
supervisionObject: '牛',
supervisionQuantity: 20,
supervisionPeriod: '30天',
supervisionAmount: 50000.00,
startTime: '2024-01-01',
endTime: '2024-12-31',
earTag: 10,
collar: 10,
host: 1,
loanOfficer: '测试专员',
description: '这是一个测试项目'
};
try {
const response = await makeRequest(options, projectData);
if (response.statusCode === 201 && response.data.success) {
console.log('✅ 创建项目成功');
console.log(` 项目ID: ${response.data.data.id}`);
console.log(` 项目名称: ${response.data.data.name}`);
return response.data.data.id;
} else {
console.error('❌ 创建项目失败:', response.data.message);
return null;
}
} catch (error) {
console.error('❌ 创建项目请求失败:', error.message);
return null;
}
}
// 测试获取项目详情
async function testGetProjectById(projectId) {
console.log('\n🔍 测试获取项目详情...');
const options = {
hostname: 'localhost',
port: 5351,
path: `/api/projects/${projectId}`,
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
};
try {
const response = await makeRequest(options);
if (response.statusCode === 200 && response.data.success) {
console.log('✅ 获取项目详情成功');
const project = response.data.data;
console.log(` 项目名称: ${project.name}`);
console.log(` 养殖场: ${project.farmName}`);
console.log(` 监管对象: ${project.supervisionObject}`);
console.log(` 监管数量: ${project.supervisionQuantity}`);
console.log(` 监管金额: ${project.supervisionAmount}`);
return true;
} else {
console.error('❌ 获取项目详情失败:', response.data.message);
return false;
}
} catch (error) {
console.error('❌ 获取项目详情请求失败:', error.message);
return false;
}
}
// 测试更新项目
async function testUpdateProject(projectId) {
console.log('\n✏ 测试更新项目...');
const options = {
hostname: 'localhost',
port: 5351,
path: `/api/projects/${projectId}`,
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
};
const updateData = {
supervisionQuantity: 25,
supervisionAmount: 60000.00,
description: '更新后的项目描述'
};
try {
const response = await makeRequest(options, updateData);
if (response.statusCode === 200 && response.data.success) {
console.log('✅ 更新项目成功');
const project = response.data.data;
console.log(` 更新后监管数量: ${project.supervisionQuantity}`);
console.log(` 更新后监管金额: ${project.supervisionAmount}`);
return true;
} else {
console.error('❌ 更新项目失败:', response.data.message);
return false;
}
} catch (error) {
console.error('❌ 更新项目请求失败:', error.message);
return false;
}
}
// 测试删除项目
async function testDeleteProject(projectId) {
console.log('\n🗑 测试删除项目...');
const options = {
hostname: 'localhost',
port: 5351,
path: `/api/projects/${projectId}`,
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
};
try {
const response = await makeRequest(options);
if (response.statusCode === 200 && response.data.success) {
console.log('✅ 删除项目成功');
return true;
} else {
console.error('❌ 删除项目失败:', response.data.message);
return false;
}
} catch (error) {
console.error('❌ 删除项目请求失败:', error.message);
return false;
}
}
// 测试搜索项目
async function testSearchProjects() {
console.log('\n🔍 测试搜索项目...');
const options = {
hostname: 'localhost',
port: 5351,
path: '/api/projects?search=张洪彬&status=completed',
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
};
try {
const response = await makeRequest(options);
if (response.statusCode === 200 && response.data.success) {
console.log('✅ 搜索项目成功');
console.log(` 搜索结果数量: ${response.data.data.projects.length}`);
const projects = response.data.data.projects;
projects.forEach((project, index) => {
console.log(` 结果${index + 1}: ${project.name} - ${project.farmName}`);
});
return true;
} else {
console.error('❌ 搜索项目失败:', response.data.message);
return false;
}
} catch (error) {
console.error('❌ 搜索项目请求失败:', error.message);
return false;
}
}
// 主测试函数
async function runTests() {
console.log('🚀 开始测试项目清单API接口...\n');
// 1. 登录
const loginSuccess = await login();
if (!loginSuccess) {
console.log('❌ 登录失败,无法继续测试');
return;
}
// 2. 测试获取项目列表
await testGetProjects();
// 3. 测试获取项目统计
await testGetProjectStats();
// 4. 测试搜索项目
await testSearchProjects();
// 5. 测试创建项目
const projectId = await testCreateProject();
if (projectId) {
// 6. 测试获取项目详情
await testGetProjectById(projectId);
// 7. 测试更新项目
await testUpdateProject(projectId);
// 8. 测试删除项目
await testDeleteProject(projectId);
}
console.log('\n🎉 项目清单API接口测试完成');
}
// 运行测试
runTests().catch(console.error);

View File

@@ -0,0 +1,58 @@
const http = require('http');
// 测试项目接口
function testProjectsAPI() {
const options = {
hostname: 'localhost',
port: 5351,
path: '/api/projects?page=1&limit=12&search=&status=',
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('状态码:', res.statusCode);
console.log('响应头:', res.headers);
console.log('响应体:', data);
if (res.statusCode === 500) {
console.log('\n❌ 服务器内部错误');
try {
const errorData = JSON.parse(data);
console.log('错误信息:', errorData.message);
if (errorData.error) {
console.log('详细错误:', errorData.error);
}
} catch (e) {
console.log('无法解析错误响应');
}
} else if (res.statusCode === 200) {
console.log('\n✅ 请求成功');
try {
const responseData = JSON.parse(data);
console.log('项目数量:', responseData.data?.projects?.length || 0);
} catch (e) {
console.log('无法解析成功响应');
}
}
});
});
req.on('error', (error) => {
console.error('请求错误:', error.message);
});
req.end();
}
console.log('🚀 测试项目接口...');
testProjectsAPI();

View File

@@ -0,0 +1,172 @@
/**
* 监管任务API测试脚本
* @file test-supervision-tasks-api.js
* @description 测试监管任务相关的API接口
*/
const axios = require('axios');
const API_BASE_URL = 'http://localhost:5351';
async function testSupervisionTasksAPI() {
try {
console.log('🚀 开始测试监管任务API...\n');
// 1. 先登录获取token
console.log('1. 登录获取认证token...');
const loginResponse = await axios.post(`${API_BASE_URL}/api/auth/login`, {
username: 'admin',
password: 'Admin123456'
});
if (!loginResponse.data.success) {
throw new Error('登录失败: ' + loginResponse.data.message);
}
const token = loginResponse.data.data.token;
console.log('✅ 登录成功获取到token\n');
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
// 2. 测试获取监管任务列表
console.log('2. 测试获取监管任务列表...');
const listResponse = await axios.get(`${API_BASE_URL}/api/supervision-tasks`, { headers });
if (listResponse.data.success) {
console.log('✅ 获取监管任务列表成功');
console.log(`📊 共 ${listResponse.data.data.tasks.length} 个监管任务`);
console.log(`📈 分页信息: 第${listResponse.data.data.pagination.currentPage}页,共${listResponse.data.data.pagination.totalPages}\n`);
} else {
console.log('❌ 获取监管任务列表失败:', listResponse.data.message);
}
// 3. 测试获取监管任务统计
console.log('3. 测试获取监管任务统计...');
const statsResponse = await axios.get(`${API_BASE_URL}/api/supervision-tasks/stats`, { headers });
if (statsResponse.data.success) {
console.log('✅ 获取监管任务统计成功');
console.log('📊 统计信息:');
console.log(` 总计: ${statsResponse.data.data.total}`);
console.log(` 待监管: ${statsResponse.data.data.pending}`);
console.log(` 监管中: ${statsResponse.data.data.supervising}`);
console.log(` 已完成: ${statsResponse.data.data.completed}`);
console.log(` 已暂停: ${statsResponse.data.data.suspended}\n`);
} else {
console.log('❌ 获取监管任务统计失败:', statsResponse.data.message);
}
// 4. 测试创建监管任务
console.log('4. 测试创建监管任务...');
const newTask = {
applicationNumber: 'APP_TEST_' + Date.now(),
contractNumber: 'CONTRACT_TEST_' + Date.now(),
productName: '测试农业贷款产品',
customerName: '测试客户',
idType: 'id_card',
idNumber: '110101199001011234',
assetType: 'cattle',
assetQuantity: 10,
supervisionStatus: 'pending',
startTime: '2024-12-20',
endTime: '2024-12-31',
loanAmount: 100000.00,
interestRate: 0.0600,
loanTerm: 12,
supervisorName: '测试监管员',
supervisorPhone: '13800138000',
farmAddress: '测试养殖场地址',
remarks: '这是一个测试监管任务'
};
const createResponse = await axios.post(`${API_BASE_URL}/api/supervision-tasks`, newTask, { headers });
if (createResponse.data.success) {
console.log('✅ 创建监管任务成功');
console.log(`📋 任务ID: ${createResponse.data.data.id}`);
console.log(`📋 申请单号: ${createResponse.data.data.applicationNumber}\n`);
const taskId = createResponse.data.data.id;
// 5. 测试获取监管任务详情
console.log('5. 测试获取监管任务详情...');
const detailResponse = await axios.get(`${API_BASE_URL}/api/supervision-tasks/${taskId}`, { headers });
if (detailResponse.data.success) {
console.log('✅ 获取监管任务详情成功');
console.log(`📋 客户姓名: ${detailResponse.data.data.customerName}`);
console.log(`📋 监管状态: ${detailResponse.data.data.supervisionStatus}\n`);
} else {
console.log('❌ 获取监管任务详情失败:', detailResponse.data.message);
}
// 6. 测试更新监管任务
console.log('6. 测试更新监管任务...');
const updateData = {
supervisionStatus: 'supervising',
remarks: '更新后的备注信息'
};
const updateResponse = await axios.put(`${API_BASE_URL}/api/supervision-tasks/${taskId}`, updateData, { headers });
if (updateResponse.data.success) {
console.log('✅ 更新监管任务成功');
console.log(`📋 新状态: ${updateResponse.data.data.supervisionStatus}\n`);
} else {
console.log('❌ 更新监管任务失败:', updateResponse.data.message);
}
// 7. 测试批量更新状态
console.log('7. 测试批量更新状态...');
const batchUpdateData = {
ids: [taskId],
supervisionStatus: 'completed'
};
const batchUpdateResponse = await axios.put(`${API_BASE_URL}/api/supervision-tasks/batch/status`, batchUpdateData, { headers });
if (batchUpdateResponse.data.success) {
console.log('✅ 批量更新状态成功');
console.log(`📋 更新数量: ${batchUpdateResponse.data.data.updatedCount}\n`);
} else {
console.log('❌ 批量更新状态失败:', batchUpdateResponse.data.message);
}
// 8. 测试删除监管任务
console.log('8. 测试删除监管任务...');
const deleteResponse = await axios.delete(`${API_BASE_URL}/api/supervision-tasks/${taskId}`, { headers });
if (deleteResponse.data.success) {
console.log('✅ 删除监管任务成功\n');
} else {
console.log('❌ 删除监管任务失败:', deleteResponse.data.message);
}
} else {
console.log('❌ 创建监管任务失败:', createResponse.data.message);
}
// 9. 测试搜索功能
console.log('9. 测试搜索功能...');
const searchResponse = await axios.get(`${API_BASE_URL}/api/supervision-tasks?search=张三&supervisionStatus=supervising`, { headers });
if (searchResponse.data.success) {
console.log('✅ 搜索功能测试成功');
console.log(`📊 搜索结果: ${searchResponse.data.data.tasks.length} 条记录\n`);
} else {
console.log('❌ 搜索功能测试失败:', searchResponse.data.message);
}
console.log('🎉 所有API测试完成');
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error('响应数据:', error.response.data);
}
}
}
// 运行测试
testSupervisionTasksAPI();

View File

@@ -0,0 +1,55 @@
/**
* 简单监管任务API测试
*/
const axios = require('axios');
const API_BASE_URL = 'http://localhost:5351';
async function testSupervisionTasksSimple() {
try {
console.log('🚀 开始简单测试监管任务API...\n');
// 1. 测试登录
console.log('1. 测试登录...');
const loginResponse = await axios.post(`${API_BASE_URL}/api/auth/login`, {
username: 'admin',
password: 'Admin123456'
});
console.log('登录响应:', loginResponse.data);
if (!loginResponse.data.success) {
throw new Error('登录失败: ' + loginResponse.data.message);
}
const token = loginResponse.data.data.token;
console.log('✅ 登录成功\n');
// 2. 测试获取监管任务列表
console.log('2. 测试获取监管任务列表...');
const listResponse = await axios.get(`${API_BASE_URL}/api/supervision-tasks`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('列表响应:', listResponse.data);
if (listResponse.data.success) {
console.log('✅ 获取监管任务列表成功');
console.log(`📊 共 ${listResponse.data.data.tasks.length} 个监管任务`);
} else {
console.log('❌ 获取监管任务列表失败:', listResponse.data.message);
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error('响应状态:', error.response.status);
console.error('响应数据:', error.response.data);
}
}
}
testSupervisionTasksSimple();

View File

@@ -9,70 +9,67 @@
</div>
<!-- 桌面端布局 -->
<a-layout v-else class="desktop-layout">
<a-layout-header class="header">
<div class="logo">
<a-button
type="text"
@click="settingsStore.toggleSidebar"
style="color: white; margin-right: 8px;"
>
<menu-unfold-outlined v-if="sidebarCollapsed" />
<menu-fold-outlined v-else />
</a-button>
银行管理后台系统
</div>
<div class="user-info">
<a-dropdown>
<a-button type="text" style="color: white;">
<user-outlined />
{{ userData?.real_name || userData?.username }}
<down-outlined />
<div v-else class="desktop-layout">
<!-- 固定头部 -->
<header class="header">
<div class="header-content">
<div class="logo">
<a-button
type="text"
@click="settingsStore.toggleSidebar"
class="menu-toggle"
>
<menu-unfold-outlined v-if="sidebarCollapsed" />
<menu-fold-outlined v-else />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="profile" @click="goToProfile">
<user-outlined />
个人中心
</a-menu-item>
<a-menu-item key="settings" @click="goToSettings">
<setting-outlined />
系统设置
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<span class="logo-text">银行管理后台系统</span>
</div>
<div class="user-info">
<a-dropdown>
<a-button type="text" class="user-button">
<user-outlined />
{{ userData?.real_name || userData?.username }}
<down-outlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="profile" @click="goToProfile">
<user-outlined />
个人中心
</a-menu-item>
<a-menu-item key="settings" @click="goToSettings">
<setting-outlined />
系统设置
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</a-layout-header>
</header>
<a-layout class="main-layout">
<a-layout-sider
class="sidebar"
width="200"
:collapsed="sidebarCollapsed"
collapsible
>
<DynamicMenu :collapsed="sidebarCollapsed" />
</a-layout-sider>
<a-layout class="content-layout">
<a-layout-content class="main-content">
<div class="content-wrapper">
<router-view />
</div>
</a-layout-content>
<a-layout-footer class="footer">
银行管理后台系统 ©2025
</a-layout-footer>
</a-layout>
</a-layout>
</a-layout>
<!-- 侧边栏 -->
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }">
<DynamicMenu :collapsed="sidebarCollapsed" />
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<div class="content-wrapper">
<router-view />
</div>
</main>
<!-- 页脚 -->
<footer class="footer">
银行管理后台系统 ©2025
</footer>
</div>
</div>
<div v-else>
<router-view />
@@ -156,57 +153,139 @@ onUnmounted(() => {
</script>
<style scoped>
/* 全局重置 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow-x: hidden;
}
#app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
/* 桌面端布局样式 */
.desktop-layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main";
grid-template-columns: 200px 1fr;
grid-template-rows: 64px 1fr;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.main-layout {
height: calc(100vh - 64px);
overflow: hidden;
.desktop-layout.collapsed {
grid-template-columns: 80px 1fr;
}
/* 头部样式 */
/* 头部样式 - 完全修复 */
.header {
grid-area: header;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
z-index: 1001;
background: #001529;
color: white;
height: 64px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 100vw;
margin: 0;
padding: 0;
border: none;
display: flex;
align-items: center;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
background: #001529;
color: white;
width: 100%;
height: 100%;
padding: 0 24px;
height: 64px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
margin: 0;
}
.logo {
font-size: 18px;
font-weight: bold;
display: flex;
align-items: center;
height: 100%;
margin: 0;
padding: 0;
}
.menu-toggle {
color: white !important;
margin-right: 8px;
height: 100%;
display: flex;
align-items: center;
padding: 0 8px;
}
.logo-text {
font-size: 18px;
font-weight: bold;
line-height: 64px;
height: 64px;
display: flex;
align-items: center;
margin: 0;
padding: 0;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
height: 100%;
margin: 0;
padding: 0;
}
/* 侧边栏样式 */
.user-button {
color: white !important;
height: 100%;
display: flex;
align-items: center;
padding: 0 8px;
}
/* 侧边栏样式 - 完全修复 */
.sidebar {
grid-area: sidebar;
position: fixed;
top: 64px;
left: 0;
width: 200px;
height: calc(100vh - 64px);
background: #001529 !important;
z-index: 999;
background: #001529;
z-index: 1000;
overflow-y: auto;
overflow-x: hidden;
padding: 0;
margin: 0;
border: none;
transition: width 0.2s;
}
.sidebar.collapsed {
width: 80px;
}
.sidebar::-webkit-scrollbar {
@@ -226,35 +305,48 @@ onUnmounted(() => {
background: #40a9ff;
}
/* 内容区域样式 */
.content-layout {
/* 内容区域样式 - 完全修复 */
.main-content {
grid-area: main;
margin-left: 200px;
height: calc(100vh - 64px);
transition: margin-left 0.2s;
}
.main-content {
height: calc(100vh - 64px - 70px);
overflow-y: auto;
overflow-x: hidden;
background: #f0f2f5;
padding: 0;
margin: 0;
transition: margin-left 0.2s;
}
.desktop-layout.collapsed .main-content {
margin-left: 80px;
}
.content-wrapper {
padding: 24px;
min-height: 100%;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
min-height: calc(100vh - 64px - 70px);
background: #f0f2f5;
margin: 0;
}
/* 页脚样式 - 完全修复 */
.footer {
position: fixed;
bottom: 0;
left: 200px;
right: 0;
height: 70px;
line-height: 70px;
text-align: center;
background: #fff;
border-top: 1px solid #f0f0f0;
color: #666;
z-index: 999;
transition: left 0.2s;
}
.desktop-layout.collapsed .footer {
left: 80px;
}
/* 侧边栏折叠时的样式 */

View File

@@ -128,11 +128,103 @@ watch(
</script>
<style scoped>
/* 完全重置菜单样式 - 确保从顶部开始 */
.ant-menu {
border-right: none;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
border: none !important;
background: transparent !important;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
overflow: visible !important;
}
.ant-menu-root {
border: none !important;
background: transparent !important;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
.ant-menu-vertical {
border: none !important;
background: transparent !important;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
.ant-menu-inline {
border: none !important;
background: transparent !important;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
/* 强制移除所有菜单容器的空白 */
.ant-menu ul {
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
}
.ant-menu li {
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
border: none !important;
}
.ant-menu li:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
border-top: none !important;
}
/* 菜单项样式重置 */
.ant-menu-item {
margin: 0 !important;
padding: 0 16px !important;
height: 48px !important;
line-height: 48px !important;
border: none !important;
background: transparent !important;
display: flex !important;
align-items: center !important;
}
.ant-menu-item:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
border-top: none !important;
}
.ant-menu-submenu-title {
margin: 0 !important;
padding: 0 16px !important;
height: 48px !important;
line-height: 48px !important;
border: none !important;
background: transparent !important;
display: flex !important;
align-items: center !important;
}
/* 确保菜单项图标和文字正确对齐 */
.ant-menu-item .anticon,
.ant-menu-submenu-title .anticon {
margin-right: 8px !important;
font-size: 16px !important;
flex-shrink: 0 !important;
}
.ant-menu-item span,
.ant-menu-submenu-title span {
flex: 1 !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.ant-menu::-webkit-scrollbar {
@@ -158,6 +250,13 @@ watch(
align-items: center;
height: 48px;
line-height: 48px;
margin: 0 !important;
padding: 0 16px !important;
}
.ant-menu-item:first-child,
.ant-menu-submenu:first-child .ant-menu-submenu-title {
margin-top: 0 !important;
}
.ant-menu-item .anticon,

View File

@@ -17,14 +17,22 @@ import {
CreditCardOutlined,
DesktopOutlined,
TeamOutlined,
UserSwitchOutlined
UserSwitchOutlined,
ProjectOutlined,
ClockCircleOutlined,
ToolOutlined,
CheckCircleOutlined,
SearchOutlined,
DollarOutlined,
BellOutlined,
WarningOutlined
} from '@ant-design/icons-vue'
// 路由配置
const routes = [
{
path: '/',
redirect: '/dashboard'
redirect: '/project-list',
},
{
path: '/login',
@@ -37,14 +45,13 @@ const routes = [
}
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
path: '/test-projects',
name: 'TestProjects',
component: () => import('@/views/TestProjects.vue'),
meta: {
title: '仪表盘',
icon: DashboardOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
title: '项目接口测试',
requiresAuth: false,
hideInMenu: true
}
},
{
@@ -53,7 +60,40 @@ const routes = [
component: () => import('@/views/ProjectList.vue'),
meta: {
title: '项目清单',
icon: FileTextOutlined,
icon: ProjectOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: '/supervision-tasks',
name: 'SupervisionTasks',
component: () => import('@/views/SupervisionTasks.vue'),
meta: {
title: '监管任务',
icon: ClockCircleOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: '/pending-installation',
name: 'PendingInstallation',
component: () => import('@/views/PendingInstallation.vue'),
meta: {
title: '待安装任务',
icon: ToolOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: '/completed-supervision',
name: 'CompletedSupervision',
component: () => import('@/views/CompletedSupervision.vue'),
meta: {
title: '监管任务已结项',
icon: CheckCircleOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
@@ -64,62 +104,28 @@ const routes = [
component: () => import('@/views/SystemCheck.vue'),
meta: {
title: '系统日检',
icon: SafetyOutlined,
icon: SearchOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
}
},
{
path: '/market-trends',
name: 'MarketTrends',
component: () => import('@/views/MarketTrends.vue'),
meta: {
title: '市场行情',
icon: LineChartOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/users',
name: 'Users',
component: () => import('@/views/Users.vue'),
meta: {
title: '用户管理',
icon: UserOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
}
},
{
path: '/accounts',
name: 'Accounts',
component: () => import('@/views/Accounts.vue'),
meta: {
title: '账户管理',
icon: BankOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/transactions',
name: 'Transactions',
component: () => import('@/views/Transactions.vue'),
meta: {
title: '交易管理',
icon: TransactionOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
// {
// path: '/market-trends',
// name: 'MarketTrends',
// component: () => import('@/views/MarketTrends.vue'),
// meta: {
// title: '市场行情',
// icon: BarChartOutlined,
// requiresAuth: true,
// roles: ['admin', 'manager', 'teller', 'user']
// }
// },
{
path: '/loan-management',
name: 'LoanManagement',
component: () => import('@/views/LoanManagement.vue'),
meta: {
title: '贷款管理',
icon: CreditCardOutlined,
icon: DollarOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
},
@@ -166,15 +172,26 @@ const routes = [
}
]
},
// {
// path: '/message-notification',
// name: 'MessageNotification',
// component: () => import('@/views/MessageNotification.vue'),
// meta: {
// title: '消息通知',
// icon: BellOutlined,
// requiresAuth: true,
// roles: ['admin', 'manager', 'teller', 'user']
// }
// },
{
path: '/hardware-management',
name: 'HardwareManagement',
component: () => import('@/views/HardwareManagement.vue'),
path: '/device-warning',
name: 'DeviceWarning',
component: () => import('@/views/DeviceWarning.vue'),
meta: {
title: '硬件管理',
icon: DesktopOutlined,
title: '设备预警',
icon: WarningOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
@@ -188,17 +205,6 @@ const routes = [
roles: ['admin', 'manager']
}
},
{
path: '/reports',
name: 'Reports',
component: () => import('@/views/Reports.vue'),
meta: {
title: '报表统计',
icon: BarChartOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: '/settings',
name: 'Settings',

View File

@@ -9,6 +9,189 @@
box-sizing: border-box;
}
/* 强制重置所有元素的默认样式 */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 确保body和html没有默认空白 */
html {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
height: 100% !important;
overflow-x: hidden !important;
box-sizing: border-box !important;
font-size: 14px !important;
line-height: 1.5715 !important;
}
body {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
height: 100% !important;
overflow-x: hidden !important;
box-sizing: border-box !important;
position: relative !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif !important;
color: #262626 !important;
background-color: #f0f2f5 !important;
}
/* 确保#app容器没有空白 */
#app {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
height: 100% !important;
position: relative !important;
}
/* 强制修复Ant Design菜单空白问题 */
.ant-menu,
.ant-menu-root,
.ant-menu-vertical,
.ant-menu-inline {
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
}
.ant-menu-item,
.ant-menu-submenu-title {
margin: 0 !important;
padding: 0 16px !important;
}
.ant-menu-item:first-child,
.ant-menu-submenu:first-child .ant-menu-submenu-title {
margin-top: 0 !important;
padding-top: 0 !important;
}
/* 确保侧边栏菜单从顶部开始 */
.ant-layout-sider .ant-menu,
.ant-layout-sider .ant-menu-root,
.ant-layout-sider .ant-menu-vertical,
.ant-layout-sider .ant-menu-inline {
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
}
/* 强制移除菜单容器的所有空白 */
.ant-menu > li:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
}
.ant-menu .ant-menu-item:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
}
/* 针对侧边栏的特殊处理 */
.ant-layout-sider .ant-menu > li:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
}
.ant-layout-sider .ant-menu .ant-menu-item:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
}
/* 最终强制重置 - 确保没有任何空白 */
.ant-layout-sider {
padding: 0 !important;
margin: 0 !important;
border: none !important;
background: #001529 !important;
}
.ant-layout-sider .ant-menu {
padding: 0 !important;
margin: 0 !important;
border: none !important;
background: transparent !important;
height: 100% !important;
}
.ant-layout-sider .ant-menu ul {
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
}
.ant-layout-sider .ant-menu li {
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
}
.ant-layout-sider .ant-menu li:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
border-top: none !important;
}
/* 强制重置菜单项样式 */
.ant-layout-sider .ant-menu-item {
margin: 0 !important;
padding: 0 16px !important;
height: 48px !important;
line-height: 48px !important;
border: none !important;
}
.ant-layout-sider .ant-menu-item:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
border-top: none !important;
}
.ant-layout-sider .ant-menu-submenu-title {
margin: 0 !important;
padding: 0 16px !important;
height: 48px !important;
line-height: 48px !important;
border: none !important;
}
/* 确保头部完全显示 */
.ant-layout-header {
padding: 0 !important;
margin: 0 !important;
line-height: 64px !important;
height: 64px !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
z-index: 1001 !important;
width: 100vw !important;
box-sizing: border-box !important;
}
/* 强制重置所有可能的头部样式 */
.ant-layout-header * {
box-sizing: border-box !important;
}
/* 确保头部内容不被截断 */
.ant-layout-header .logo,
.ant-layout-header .user-info {
line-height: 64px !important;
height: 64px !important;
display: flex !important;
align-items: center !important;
}
html, body {
margin: 0;
padding: 0;

View File

@@ -654,6 +654,147 @@ export const api = {
async getStats(params = {}) {
return api.get('/transactions/stats', { params })
}
},
// 项目清单API
projects: {
/**
* 获取项目列表
* @param {Object} params - 查询参数
* @returns {Promise} 项目列表
*/
async getList(params = {}) {
return api.get('/projects', { params })
},
/**
* 获取项目详情
* @param {number} id - 项目ID
* @returns {Promise} 项目详情
*/
async getById(id) {
return api.get(`/projects/${id}`)
},
/**
* 创建项目
* @param {Object} data - 项目数据
* @returns {Promise} 创建结果
*/
async create(data) {
return api.post('/projects', data)
},
/**
* 更新项目
* @param {number} id - 项目ID
* @param {Object} data - 项目数据
* @returns {Promise} 更新结果
*/
async update(id, data) {
return api.put(`/projects/${id}`, data)
},
/**
* 删除项目
* @param {number} id - 项目ID
* @returns {Promise} 删除结果
*/
async delete(id) {
return api.delete(`/projects/${id}`)
},
/**
* 获取项目统计
* @returns {Promise} 统计数据
*/
async getStats() {
return api.get('/projects/stats')
},
/**
* 批量更新项目状态
* @param {Object} data - 批量更新数据
* @returns {Promise} 更新结果
*/
async batchUpdateStatus(data) {
return api.put('/projects/batch/status', data)
}
},
// 监管任务API
supervisionTasks: {
/**
* 获取监管任务列表
* @param {Object} params - 查询参数
* @returns {Promise} 监管任务列表
*/
async getList(params = {}) {
return api.get('/supervision-tasks', { params })
},
/**
* 获取监管任务详情
* @param {number} id - 监管任务ID
* @returns {Promise} 监管任务详情
*/
async getById(id) {
return api.get(`/supervision-tasks/${id}`)
},
/**
* 创建监管任务
* @param {Object} data - 监管任务数据
* @returns {Promise} 创建结果
*/
async create(data) {
return api.post('/supervision-tasks', data)
},
/**
* 更新监管任务
* @param {number} id - 监管任务ID
* @param {Object} data - 监管任务数据
* @returns {Promise} 更新结果
*/
async update(id, data) {
return api.put(`/supervision-tasks/${id}`, data)
},
/**
* 删除监管任务
* @param {number} id - 监管任务ID
* @returns {Promise} 删除结果
*/
async delete(id) {
return api.delete(`/supervision-tasks/${id}`)
},
/**
* 获取监管任务统计
* @returns {Promise} 统计数据
*/
async getStats() {
return api.get('/supervision-tasks/stats')
},
/**
* 批量更新监管任务状态
* @param {Object} data - 批量更新数据
* @returns {Promise} 更新结果
*/
async batchUpdateStatus(data) {
return api.put('/supervision-tasks/batch/status', data)
},
/**
* 批量删除监管任务
* @param {Object} data - 批量删除数据
* @returns {Promise} 删除结果
*/
async batchDelete(data) {
return api.delete('/supervision-tasks/batch', { data })
}
}
}

View File

@@ -0,0 +1,336 @@
<template>
<div class="completed-supervision-page">
<!-- 页面头部 -->
<div class="page-header">
<h1>监管任务结项</h1>
<div class="header-actions">
<a-button type="primary" @click="handleBatchImport">
<upload-outlined />
批量导入
</a-button>
</div>
</div>
<!-- 搜索筛选区域 -->
<div class="search-section">
<a-row :gutter="16" align="middle">
<a-col :span="4">
<a-select
v-model:value="searchForm.contractNumber"
placeholder="合同编号"
allow-clear
style="width: 100%"
>
<a-select-option value="LOAN2024001">LOAN2024001</a-select-option>
<a-select-option value="LOAN2024002">LOAN2024002</a-select-option>
<a-select-option value="LOAN2024003">LOAN2024003</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-input
v-model:value="searchForm.keyword"
placeholder="请输入关键字"
allow-clear
/>
</a-col>
<a-col :span="4">
<a-space>
<a-button type="primary" @click="handleSearch">
<search-outlined />
搜索
</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-col>
</a-row>
</div>
<!-- 数据表格 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredTasks"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
:locale="{ emptyText: '暂无数据' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'settlementStatus'">
<a-tag :color="getSettlementStatusColor(record.settlementStatus)">
{{ getSettlementStatusName(record.settlementStatus) }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewTask(record)">查看</a-button>
<a-button type="link" size="small" @click="editTask(record)">编辑</a-button>
<a-button type="link" size="small" @click="exportTask(record)">导出</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { UploadOutlined, SearchOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
const loading = ref(false)
const tasks = ref([])
const searchForm = reactive({
contractNumber: undefined,
keyword: '',
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
})
const columns = [
{ title: '申请单号', dataIndex: 'applicationNumber', key: 'applicationNumber', sorter: true },
{ title: '放款合同编号', dataIndex: 'contractNumber', key: 'contractNumber', sorter: true },
{ title: '产品名称', dataIndex: 'productName', key: 'productName' },
{ title: '客户姓名', dataIndex: 'customerName', key: 'customerName' },
{ title: '证件类型', dataIndex: 'idType', key: 'idType' },
{ title: '证件号码', dataIndex: 'idNumber', key: 'idNumber' },
{ title: '养殖生资种类', dataIndex: 'assetType', key: 'assetType' },
{ title: '监管生资数量', dataIndex: 'assetQuantity', key: 'assetQuantity', sorter: true },
{ title: '总还款期数', dataIndex: 'totalRepaymentPeriods', key: 'totalRepaymentPeriods', sorter: true },
{ title: '结清状态', dataIndex: 'settlementStatus', key: 'settlementStatus', filters: [
{ text: '已结清', value: 'settled' },
{ text: '未结清', value: 'unsettled' },
{ text: '部分结清', value: 'partial' },
]},
{ title: '结清日期', dataIndex: 'settlementDate', key: 'settlementDate', sorter: true },
{ title: '结清任务导入时间', dataIndex: 'importTime', key: 'importTime', sorter: true },
{ title: '操作', key: 'action', fixed: 'right', width: 180 },
]
// 模拟数据
const mockTasks = [
{
id: 'S001',
applicationNumber: 'APP2024001',
contractNumber: 'LOAN2024001',
productName: '生猪养殖贷',
customerName: '张三',
idType: 'ID_CARD',
idNumber: '4401XXXXXXXXXXXXXX',
assetType: '生猪',
assetQuantity: 500,
totalRepaymentPeriods: 12,
settlementStatus: 'settled',
settlementDate: '2024-01-15',
importTime: '2024-01-15 10:30:00',
},
{
id: 'S002',
applicationNumber: 'APP2024002',
contractNumber: 'LOAN2024002',
productName: '肉牛养殖贷',
customerName: '李四',
idType: 'ID_CARD',
idNumber: '4402XXXXXXXXXXXXXX',
assetType: '肉牛',
assetQuantity: 150,
totalRepaymentPeriods: 24,
settlementStatus: 'partial',
settlementDate: '2024-01-20',
importTime: '2024-01-20 14:20:00',
},
{
id: 'S003',
applicationNumber: 'APP2024003',
contractNumber: 'LOAN2024003',
productName: '蛋鸡养殖贷',
customerName: '王五',
idType: 'ID_CARD',
idNumber: '4403XXXXXXXXXXXXXX',
assetType: '蛋鸡',
assetQuantity: 10000,
totalRepaymentPeriods: 18,
settlementStatus: 'unsettled',
settlementDate: null,
importTime: '2024-01-10 09:15:00',
},
]
const fetchTasks = async () => {
loading.value = true
try {
// 实际项目中这里会调用API获取数据
// const response = await api.completedSupervision.getList({
// page: pagination.current,
// pageSize: pagination.pageSize,
// ...searchForm,
// })
// 使用模拟数据
tasks.value = mockTasks.map(task => ({
...task,
settlementDate: task.settlementDate ? dayjs(task.settlementDate) : null,
importTime: dayjs(task.importTime),
}))
pagination.total = mockTasks.length
} catch (error) {
console.error('获取结项任务失败:', error)
message.error('获取结项任务失败')
} finally {
loading.value = false
}
}
const filteredTasks = computed(() => {
let result = tasks.value
if (searchForm.contractNumber) {
result = result.filter(task => task.contractNumber === searchForm.contractNumber)
}
if (searchForm.keyword) {
result = result.filter(task =>
task.applicationNumber.toLowerCase().includes(searchForm.keyword.toLowerCase()) ||
task.customerName.toLowerCase().includes(searchForm.keyword.toLowerCase()) ||
task.productName.toLowerCase().includes(searchForm.keyword.toLowerCase())
)
}
return result
})
const handleSearch = () => {
pagination.current = 1
fetchTasks()
}
const handleReset = () => {
searchForm.contractNumber = undefined
searchForm.keyword = ''
pagination.current = 1
fetchTasks()
}
const handleBatchImport = () => {
message.info('批量导入功能开发中...')
}
const handleTableChange = (pag, filters, sorter) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchTasks()
}
const getSettlementStatusColor = (status) => {
const colors = {
'settled': 'green',
'unsettled': 'red',
'partial': 'orange',
}
return colors[status] || 'default'
}
const getSettlementStatusName = (status) => {
const names = {
'settled': '已结清',
'unsettled': '未结清',
'partial': '部分结清',
}
return names[status] || status
}
const viewTask = (record) => {
message.info(`查看任务: ${record.applicationNumber}`)
}
const editTask = (record) => {
message.info(`编辑任务: ${record.applicationNumber}`)
}
const exportTask = (record) => {
message.success(`导出任务: ${record.applicationNumber}`)
}
onMounted(() => {
fetchTasks()
})
</script>
<style scoped>
.completed-supervision-page {
padding: 0;
background: #f0f2f5;
min-height: calc(100vh - 134px);
margin: 0;
}
.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.06);
}
.page-header h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
gap: 12px;
}
.search-section {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
}
.table-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.header-actions {
justify-content: center;
}
.search-section .ant-col {
margin-bottom: 16px;
}
.search-section .ant-col:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,421 @@
<template>
<div class="device-warning">
<div class="page-header">
<h1>设备预警</h1>
<p>监控和管理银行设备运行状态预警</p>
</div>
<div class="content">
<!-- 预警统计 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="总预警数"
:value="warningStats.total"
:value-style="{ color: '#ff4d4f' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="严重预警"
:value="warningStats.critical"
:value-style="{ color: '#ff4d4f' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="一般预警"
:value="warningStats.warning"
:value-style="{ color: '#faad14' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="已处理"
:value="warningStats.resolved"
:value-style="{ color: '#52c41a' }"
/>
</a-card>
</a-col>
</a-row>
</div>
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索设备名称"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="levelFilter"
placeholder="预警级别"
allow-clear
@change="handleFilter"
>
<a-select-option value="critical">严重</a-select-option>
<a-select-option value="warning">一般</a-select-option>
<a-select-option value="info">信息</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="处理状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="pending">待处理</a-select-option>
<a-select-option value="processing">处理中</a-select-option>
<a-select-option value="resolved">已解决</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="refreshWarnings">
<reload-outlined /> 刷新
</a-button>
</a-col>
</a-row>
</div>
<!-- 预警列表 -->
<a-table
:columns="columns"
:data-source="filteredWarnings"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'">
<a-tag :color="getLevelColor(record.level)">
{{ getLevelName(record.level) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusName(record.status) }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewWarning(record)">查看</a-button>
<a-button type="link" size="small" @click="handleWarning(record.id)">处理</a-button>
<a-button type="link" size="small" @click="resolveWarning(record.id)">解决</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 预警详情对话框 -->
<a-modal
v-model:open="detailModalVisible"
title="预警详情"
width="800px"
:footer="null"
>
<div v-if="selectedWarning" class="warning-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="设备名称">
{{ selectedWarning.deviceName }}
</a-descriptions-item>
<a-descriptions-item label="预警级别">
<a-tag :color="getLevelColor(selectedWarning.level)">
{{ getLevelName(selectedWarning.level) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="预警类型">
{{ selectedWarning.type }}
</a-descriptions-item>
<a-descriptions-item label="处理状态">
<a-tag :color="getStatusColor(selectedWarning.status)">
{{ getStatusName(selectedWarning.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="发生时间">
{{ selectedWarning.occurTime }}
</a-descriptions-item>
<a-descriptions-item label="处理人员">
{{ selectedWarning.handler || '未分配' }}
</a-descriptions-item>
<a-descriptions-item label="预警描述" :span="2">
{{ selectedWarning.description }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const levelFilter = ref(undefined)
const statusFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedWarning = ref(null)
// 预警统计
const warningStats = ref({
total: 23,
critical: 5,
warning: 12,
resolved: 6
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '设备名称',
dataIndex: 'deviceName',
key: 'deviceName',
sorter: true,
},
{
title: '预警级别',
dataIndex: 'level',
key: 'level',
filters: [
{ text: '严重', value: 'critical' },
{ text: '一般', value: 'warning' },
{ text: '信息', value: 'info' },
],
},
{
title: '预警类型',
dataIndex: 'type',
key: 'type',
},
{
title: '处理状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '待处理', value: 'pending' },
{ text: '处理中', value: 'processing' },
{ text: '已解决', value: 'resolved' },
],
},
{
title: '发生时间',
dataIndex: 'occurTime',
key: 'occurTime',
sorter: true,
},
{
title: '处理人员',
dataIndex: 'handler',
key: 'handler',
},
{
title: '操作',
key: 'action',
width: 200,
},
]
// 模拟预警数据
const warnings = ref([
{
id: 1,
deviceName: 'ATM-001',
level: 'critical',
type: '硬件故障',
status: 'pending',
occurTime: '2024-01-22 14:30:25',
handler: null,
description: 'ATM机读卡器故障无法正常读取银行卡'
},
{
id: 2,
deviceName: 'POS-002',
level: 'warning',
type: '网络异常',
status: 'processing',
occurTime: '2024-01-22 13:15:10',
handler: '张三',
description: 'POS机网络连接不稳定偶尔出现断线'
},
{
id: 3,
deviceName: 'SERVER-003',
level: 'critical',
type: '系统异常',
status: 'resolved',
occurTime: '2024-01-22 10:20:30',
handler: '李四',
description: '服务器CPU使用率过高已重启解决'
}
])
// 计算属性
const filteredWarnings = computed(() => {
let result = warnings.value
if (searchText.value) {
result = result.filter(warning =>
warning.deviceName.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (levelFilter.value) {
result = result.filter(warning => warning.level === levelFilter.value)
}
if (statusFilter.value) {
result = result.filter(warning => warning.status === statusFilter.value)
}
return result
})
// 方法
const getLevelColor = (level) => {
const colors = {
'critical': 'red',
'warning': 'orange',
'info': 'blue'
}
return colors[level] || 'default'
}
const getLevelName = (level) => {
const names = {
'critical': '严重',
'warning': '一般',
'info': '信息'
}
return names[level] || level
}
const getStatusColor = (status) => {
const colors = {
'pending': 'red',
'processing': 'orange',
'resolved': 'green'
}
return colors[status] || 'default'
}
const getStatusName = (status) => {
const names = {
'pending': '待处理',
'processing': '处理中',
'resolved': '已解决'
}
return names[status] || status
}
const handleSearch = () => {
pagination.value.current = 1
}
const handleFilter = () => {
pagination.value.current = 1
}
const handleTableChange = (pag) => {
pagination.value = pag
}
const refreshWarnings = () => {
loading.value = true
setTimeout(() => {
loading.value = false
message.success('预警信息已刷新')
}, 1000)
}
const viewWarning = (warning) => {
selectedWarning.value = warning
detailModalVisible.value = true
}
const handleWarning = (id) => {
const warning = warnings.value.find(w => w.id === id)
if (warning) {
warning.status = 'processing'
warning.handler = '当前用户'
message.success('预警已开始处理')
}
}
const resolveWarning = (id) => {
const warning = warnings.value.find(w => w.id === id)
if (warning) {
warning.status = 'resolved'
message.success('预警已解决')
}
}
onMounted(() => {
pagination.value.total = warnings.value.length
})
</script>
<style scoped>
.device-warning {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.overview-section {
margin-bottom: 24px;
}
.search-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
}
.warning-detail {
padding: 16px 0;
}
</style>

View File

@@ -1,168 +1,64 @@
<template>
<div class="employee-management">
<div class="employee-management-page">
<div class="page-header">
<h1>员工管理</h1>
<p>管理和维护银行员工信息</p>
</div>
<div class="content">
<!-- 员工概览 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="员工总数"
:value="employeeStats.total"
:value-style="{ color: '#1890ff' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="在职员工"
:value="employeeStats.active"
:value-style="{ color: '#52c41a' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="离职员工"
:value="employeeStats.inactive"
:value-style="{ color: '#ff4d4f' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="本月入职"
:value="employeeStats.newHires"
:value-style="{ color: '#faad14' }"
/>
</a-card>
</a-col>
</a-row>
</div>
<div class="search-filter-section">
<a-row :gutter="16" align="middle">
<a-col :span="4">
<a-button type="primary" @click="handleAddEmployee">
<plus-outlined /> 新增人员
</a-button>
</a-col>
<a-col :span="8">
<a-input
v-model:value="searchText"
placeholder="请输入员工姓名"
allow-clear
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleSearch">
<search-outlined /> 搜索
</a-button>
</a-col>
<a-col :span="4">
<a-button @click="handleReset">重置</a-button>
</a-col>
</a-row>
</div>
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索员工姓名或工号"
enter-button="搜索"
@search="handleSearch"
<div class="employees-table-section">
<a-table
:columns="columns"
:data-source="filteredEmployees"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-switch
v-model:checked="record.status"
checked-children="启用"
un-checked-children="禁用"
@change="handleToggleStatus(record)"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="员工状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="active">在职</a-select-option>
<a-select-option value="inactive">离职</a-select-option>
<a-select-option value="suspended">停职</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="departmentFilter"
placeholder="部门"
allow-clear
@change="handleFilter"
>
<a-select-option value="admin">行政部</a-select-option>
<a-select-option value="finance">财务部</a-select-option>
<a-select-option value="it">技术部</a-select-option>
<a-select-option value="hr">人事部</a-select-option>
<a-select-option value="sales">销售部</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="positionFilter"
placeholder="职位"
allow-clear
@change="handleFilter"
>
<a-select-option value="manager">经理</a-select-option>
<a-select-option value="supervisor">主管</a-select-option>
<a-select-option value="staff">员工</a-select-option>
<a-select-option value="intern">实习生</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="handleAddEmployee">
<PlusOutlined />
添加员工
<a-button type="link" size="small" @click="handleResetPassword(record)">
重设密码
</a-button>
<a-button @click="handleExport">
<DownloadOutlined />
导出
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
</a-space>
</a-col>
</a-row>
</div>
<!-- 员工列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredEmployees"
:pagination="pagination"
:loading="loading"
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 === 'department'">
<a-tag :color="getDepartmentColor(record.department)">
{{ getDepartmentText(record.department) }}
</a-tag>
</template>
<template v-else-if="column.key === 'position'">
{{ getPositionText(record.position) }}
</template>
<template v-else-if="column.key === 'avatar'">
<a-avatar :src="record.avatar" :size="32">
{{ record.name.charAt(0) }}
</a-avatar>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button
type="link"
size="small"
@click="handleToggleStatus(record)"
:danger="record.status === 'active'"
>
{{ record.status === 'active' ? '停职' : '复职' }}
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
</a-table>
</div>
<!-- 员工详情模态框 -->
@@ -266,26 +162,15 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, DownloadOutlined } from '@ant-design/icons-vue'
import { PlusOutlined, SearchOutlined } from '@ant-design/icons-vue'
import { api } from '@/utils/api'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const departmentFilter = ref(undefined)
const positionFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedEmployee = ref(null)
// 员工统计
const employeeStats = ref({
total: 156,
active: 142,
inactive: 14,
newHires: 8
})
// 分页配置
const pagination = ref({
current: 1,
@@ -293,58 +178,45 @@ const pagination = ref({
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total} `
showTotal: (total) => `${total}`
})
// 表格列配置
const columns = [
{
title: '头像',
dataIndex: 'avatar',
key: 'avatar',
width: 80
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
customRender: ({ index }) => index + 1
},
{
title: '姓名',
title: '员工姓名',
dataIndex: 'name',
key: 'name',
width: 100
},
{
title: '工号',
dataIndex: 'employeeId',
key: 'employeeId',
width: 120
},
{
title: '部门',
dataIndex: 'department',
key: 'department',
width: 100
},
{
title: '职位',
dataIndex: 'position',
key: 'position',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80
},
{
title: '手机号',
title: '联系电话',
dataIndex: 'phone',
key: 'phone',
width: 150
},
{
title: '贷款专员',
dataIndex: 'isLoanSpecialist',
key: 'isLoanSpecialist',
width: 120
},
{
title: '入职日期',
dataIndex: 'hireDate',
key: 'hireDate',
width: 120
title: '账号状态',
dataIndex: 'status',
key: 'status',
width: 120,
filters: [
{ text: '启用', value: true },
{ text: '禁用', value: false }
]
},
{
title: '操作',
@@ -361,21 +233,22 @@ const employees = ref([])
const mockEmployees = [
{
id: 1,
name: '张三',
name: '刘超',
phone: '150****1368',
isLoanSpecialist: '否',
status: true,
employeeId: 'EMP001',
gender: '男',
age: 28,
phone: '13800138000',
email: 'zhangsan@bank.com',
email: 'liuchao@bank.com',
idCard: '110101199001011234',
hireDate: '2020-03-15',
department: 'admin',
position: 'manager',
status: 'active',
supervisor: '李总',
salaryLevel: 'L5',
workLocation: '总行',
emergencyContact: '四',
emergencyContact: '四',
emergencyPhone: '13900139000',
bio: '具有5年银行管理经验擅长团队管理和业务规划',
avatar: null,
@@ -387,76 +260,6 @@ const mockEmployees = [
startDate: '2020-03-15',
endDate: null,
description: '负责行政部日常管理工作'
},
{
id: 2,
title: '行政专员',
company: '某银行',
startDate: '2018-06-01',
endDate: '2020-03-14',
description: '负责行政事务处理'
}
]
},
{
id: 2,
name: '李四',
employeeId: 'EMP002',
gender: '女',
age: 25,
phone: '13900139000',
email: 'lisi@bank.com',
idCard: '110101199002021234',
hireDate: '2021-07-01',
department: 'finance',
position: 'staff',
status: 'active',
supervisor: '王经理',
salaryLevel: 'L3',
workLocation: '分行',
emergencyContact: '李五',
emergencyPhone: '13700137000',
bio: '财务专业毕业具有3年财务工作经验',
avatar: null,
experience: [
{
id: 1,
title: '财务专员',
company: '某银行',
startDate: '2021-07-01',
endDate: null,
description: '负责财务核算和报表编制'
}
]
},
{
id: 3,
name: '王五',
employeeId: 'EMP003',
gender: '男',
age: 32,
phone: '13700137000',
email: 'wangwu@bank.com',
idCard: '110101199003031234',
hireDate: '2019-01-10',
department: 'it',
position: 'supervisor',
status: 'inactive',
supervisor: '赵总',
salaryLevel: 'L4',
workLocation: '总行',
emergencyContact: '王六',
emergencyPhone: '13600136000',
bio: '计算机专业具有8年IT工作经验擅长系统开发',
avatar: null,
experience: [
{
id: 1,
title: '技术主管',
company: '某银行',
startDate: '2019-01-10',
endDate: '2024-01-15',
description: '负责技术团队管理和系统开发'
}
]
}
@@ -468,46 +271,24 @@ const filteredEmployees = computed(() => {
if (searchText.value) {
result = result.filter(employee =>
employee.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
employee.employeeId.toLowerCase().includes(searchText.value.toLowerCase())
employee.name.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(employee => employee.status === statusFilter.value)
}
if (departmentFilter.value) {
result = result.filter(employee => employee.department === departmentFilter.value)
}
if (positionFilter.value) {
result = result.filter(employee => employee.position === positionFilter.value)
}
return result
})
// 方法
const handleAddEmployee = () => {
message.info('新增人员功能开发中...')
}
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
}
const handleAddEmployee = () => {
message.info('添加员工功能开发中...')
}
const handleExport = () => {
message.info('导出功能开发中...')
const handleReset = () => {
searchText.value = ''
}
const handleView = (record) => {
@@ -519,10 +300,13 @@ const handleEdit = (record) => {
message.info(`编辑员工: ${record.name}`)
}
const handleResetPassword = (record) => {
message.info(`重设密码: ${record.name}`)
}
const handleToggleStatus = (record) => {
const newStatus = record.status === 'active' ? 'inactive' : 'active'
record.status = newStatus
message.success(`员工已${newStatus === 'active' ? '复职' : '停职'}`)
record.status = !record.status
message.success(`员工账号已${record.status ? '启用' : '禁用'}`)
}
const getStatusColor = (status) => {
@@ -583,9 +367,6 @@ const fetchEmployees = async (params = {}) => {
page: pagination.value.current,
limit: pagination.value.pageSize,
search: searchText.value,
status: statusFilter.value,
department: departmentFilter.value,
position: positionFilter.value,
...params
})
@@ -609,32 +390,6 @@ const fetchEmployees = async (params = {}) => {
}
}
const fetchEmployeeStats = async () => {
try {
const response = await api.employees.getStats()
if (response.success) {
employeeStats.value = {
total: response.data.total || 0,
active: response.data.active || 0,
inactive: response.data.inactive || 0,
newHires: 0 // 这个需要单独计算
}
}
} catch (error) {
console.error('获取员工统计失败:', error)
}
}
const handleSearch = () => {
pagination.value.current = 1
fetchEmployees()
}
const handleFilter = () => {
pagination.value.current = 1
fetchEmployees()
}
const handleTableChange = (paginationInfo) => {
pagination.value = paginationInfo
fetchEmployees()
@@ -643,49 +398,47 @@ const handleTableChange = (paginationInfo) => {
// 生命周期
onMounted(() => {
fetchEmployees()
fetchEmployeeStats()
})
</script>
<style scoped>
.employee-management {
.employee-management-page {
padding: 24px;
background-color: #f0f2f5;
min-height: calc(100vh - 134px);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
font-size: 28px;
color: #333;
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
.search-filter-section {
background-color: #fff;
padding: 24px;
}
.overview-section {
border-radius: 8px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
.employees-table-section {
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow-x: auto;
}
.employee-detail {
@@ -756,4 +509,49 @@ onMounted(() => {
font-size: 12px;
line-height: 1.5;
}
/* 表格样式优化 */
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f5f5f5;
}
/* 操作按钮样式 */
:deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
line-height: 1.2;
}
/* 分页样式 */
:deep(.ant-pagination) {
margin-top: 16px;
text-align: right;
}
/* 响应式调整 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.search-filter-section .ant-col {
margin-bottom: 16px;
}
.search-filter-section .ant-col:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,397 @@
<template>
<div class="message-notification">
<div class="page-header">
<h1>消息通知</h1>
<p>管理和查看系统消息通知</p>
</div>
<div class="content">
<!-- 消息统计 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="总消息数"
:value="messageStats.total"
:value-style="{ color: '#1890ff' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="未读消息"
:value="messageStats.unread"
:value-style="{ color: '#ff4d4f' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="系统通知"
:value="messageStats.system"
:value-style="{ color: '#52c41a' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="业务通知"
:value="messageStats.business"
:value-style="{ color: '#faad14' }"
/>
</a-card>
</a-col>
</a-row>
</div>
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索消息内容"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="typeFilter"
placeholder="消息类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="system">系统通知</a-select-option>
<a-select-option value="business">业务通知</a-select-option>
<a-select-option value="warning">预警通知</a-select-option>
<a-select-option value="info">信息通知</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="阅读状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="unread">未读</a-select-option>
<a-select-option value="read">已读</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="markAllRead">
<check-outlined /> 全部标记为已读
</a-button>
</a-col>
</a-row>
</div>
<!-- 消息列表 -->
<a-table
:columns="columns"
:data-source="filteredMessages"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeName(record.type) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'read' ? 'green' : 'red'">
{{ record.status === 'read' ? '已读' : '未读' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewMessage(record)">查看</a-button>
<a-button type="link" size="small" @click="markAsRead(record.id)">标记已读</a-button>
<a-button type="link" size="small" danger @click="deleteMessage(record.id)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 消息详情对话框 -->
<a-modal
v-model:open="detailModalVisible"
title="消息详情"
width="600px"
:footer="null"
>
<div v-if="selectedMessage" class="message-detail">
<a-descriptions :column="1" bordered>
<a-descriptions-item label="消息标题">
{{ selectedMessage.title }}
</a-descriptions-item>
<a-descriptions-item label="消息类型">
<a-tag :color="getTypeColor(selectedMessage.type)">
{{ getTypeName(selectedMessage.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="发送时间">
{{ selectedMessage.sendTime }}
</a-descriptions-item>
<a-descriptions-item label="发送人">
{{ selectedMessage.sender }}
</a-descriptions-item>
<a-descriptions-item label="消息内容">
<div class="message-content">{{ selectedMessage.content }}</div>
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { CheckOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const typeFilter = ref(undefined)
const statusFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedMessage = ref(null)
// 消息统计
const messageStats = ref({
total: 45,
unread: 12,
system: 20,
business: 25
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '消息标题',
dataIndex: 'title',
key: 'title',
sorter: true,
},
{
title: '消息类型',
dataIndex: 'type',
key: 'type',
filters: [
{ text: '系统通知', value: 'system' },
{ text: '业务通知', value: 'business' },
{ text: '预警通知', value: 'warning' },
{ text: '信息通知', value: 'info' },
],
},
{
title: '发送人',
dataIndex: 'sender',
key: 'sender',
},
{
title: '发送时间',
dataIndex: 'sendTime',
key: 'sendTime',
sorter: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '未读', value: 'unread' },
{ text: '已读', value: 'read' },
],
},
{
title: '操作',
key: 'action',
width: 200,
},
]
// 模拟消息数据
const messages = ref([
{
id: 1,
title: '系统维护通知',
type: 'system',
sender: '系统管理员',
sendTime: '2024-01-22 14:30:25',
status: 'unread',
content: '系统将于今晚22:00-24:00进行维护期间可能影响部分功能使用请提前做好准备。'
},
{
id: 2,
title: '新用户注册提醒',
type: 'business',
sender: '业务系统',
sendTime: '2024-01-22 13:15:10',
status: 'read',
content: '有新的用户注册,请及时审核用户信息。'
},
{
id: 3,
title: '设备异常预警',
type: 'warning',
sender: '监控系统',
sendTime: '2024-01-22 10:20:30',
status: 'unread',
content: 'ATM-001设备出现异常请及时检查处理。'
}
])
// 计算属性
const filteredMessages = computed(() => {
let result = messages.value
if (searchText.value) {
result = result.filter(msg =>
msg.title.toLowerCase().includes(searchText.value.toLowerCase()) ||
msg.content.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (typeFilter.value) {
result = result.filter(msg => msg.type === typeFilter.value)
}
if (statusFilter.value) {
result = result.filter(msg => msg.status === statusFilter.value)
}
return result
})
// 方法
const getTypeColor = (type) => {
const colors = {
'system': 'blue',
'business': 'green',
'warning': 'orange',
'info': 'purple'
}
return colors[type] || 'default'
}
const getTypeName = (type) => {
const names = {
'system': '系统通知',
'business': '业务通知',
'warning': '预警通知',
'info': '信息通知'
}
return names[type] || type
}
const handleSearch = () => {
pagination.value.current = 1
}
const handleFilter = () => {
pagination.value.current = 1
}
const handleTableChange = (pag) => {
pagination.value = pag
}
const markAllRead = () => {
messages.value.forEach(msg => {
msg.status = 'read'
})
message.success('所有消息已标记为已读')
}
const viewMessage = (msg) => {
selectedMessage.value = msg
detailModalVisible.value = true
// 查看消息时自动标记为已读
if (msg.status === 'unread') {
msg.status = 'read'
}
}
const markAsRead = (id) => {
const msg = messages.value.find(m => m.id === id)
if (msg) {
msg.status = 'read'
message.success('消息已标记为已读')
}
}
const deleteMessage = (id) => {
const index = messages.value.findIndex(m => m.id === id)
if (index > -1) {
messages.value.splice(index, 1)
message.success('消息已删除')
}
}
onMounted(() => {
pagination.value.total = messages.value.length
})
</script>
<style scoped>
.message-notification {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.overview-section {
margin-bottom: 24px;
}
.search-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
}
.message-detail {
padding: 16px 0;
}
.message-content {
white-space: pre-wrap;
line-height: 1.6;
}
</style>

View File

@@ -1 +1,16 @@
<template>\n <div class=page-container>404 Not Found</div>\n</template>\n\n<script setup>\n</script>\n\n<style scoped>\n.page-container { padding: 24px; text-align:center; color:#8c8c8c; }\n</style>
<template>
<div class="page-container">
404 Not Found
</div>
</template>
<script setup>
</script>
<style scoped>
.page-container {
padding: 24px;
text-align: center;
color: #8c8c8c;
}
</style>

View File

@@ -0,0 +1,351 @@
<template>
<div class="pending-installation-page">
<!-- 页面头部 -->
<div class="page-header">
<h1>待安装任务</h1>
<div class="header-actions">
<a-button type="primary" @click="handleExport">
<download-outlined />
安装任务导出
</a-button>
</div>
</div>
<!-- 搜索筛选区域 -->
<div class="search-section">
<a-row :gutter="16" align="middle">
<a-col :span="6">
<a-input
v-model:value="searchForm.contractNumber"
placeholder="请输入合同编号"
allow-clear
/>
</a-col>
<a-col :span="8">
<a-range-picker
v-model:value="searchForm.dateRange"
:placeholder="['开始日期', '结束日期']"
style="width: 100%"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="searchForm.installationStatus"
placeholder="请选择安装状态"
allow-clear
style="width: 100%"
>
<a-select-option value="pending">待安装</a-select-option>
<a-select-option value="in-progress">安装中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="failed">安装失败</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-space>
<a-button type="primary" @click="handleSearch">
<search-outlined />
搜索
</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-col>
</a-row>
</div>
<!-- 数据表格 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredTasks"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
:locale="{ emptyText: '暂无数据' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'installationStatus'">
<a-tag :color="getStatusColor(record.installationStatus)">
{{ getStatusName(record.installationStatus) }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewTask(record)">查看</a-button>
<a-button type="link" size="small" @click="editTask(record)">编辑</a-button>
<a-button type="link" size="small" @click="startInstallation(record)">开始安装</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { DownloadOutlined, SearchOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
const loading = ref(false)
const tasks = ref([])
const searchForm = reactive({
contractNumber: '',
dateRange: [],
installationStatus: undefined,
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
})
const columns = [
{ title: '申请单号', dataIndex: 'applicationNumber', key: 'applicationNumber', sorter: true },
{ title: '放款合同编号', dataIndex: 'contractNumber', key: 'contractNumber', sorter: true },
{ title: '产品名称', dataIndex: 'productName', key: 'productName' },
{ title: '客户姓名', dataIndex: 'customerName', key: 'customerName' },
{ title: '证件类型', dataIndex: 'idType', key: 'idType' },
{ title: '证件号码', dataIndex: 'idNumber', key: 'idNumber' },
{ title: '养殖生资种类', dataIndex: 'assetType', key: 'assetType' },
{ title: '待安装设备', dataIndex: 'equipmentToInstall', key: 'equipmentToInstall' },
{ title: '安装状态', dataIndex: 'installationStatus', key: 'installationStatus', filters: [
{ text: '待安装', value: 'pending' },
{ text: '安装中', value: 'in-progress' },
{ text: '已完成', value: 'completed' },
{ text: '安装失败', value: 'failed' },
]},
{ title: '生成安装任务时间', dataIndex: 'taskGenerationTime', key: 'taskGenerationTime', sorter: true },
{ title: '安装完成生效时间', dataIndex: 'completionTime', key: 'completionTime', sorter: true },
{ title: '操作', key: 'action', fixed: 'right', width: 180 },
]
// 模拟数据
const mockTasks = [
{
id: 'I001',
applicationNumber: 'APP2024001',
contractNumber: 'LOAN2024001',
productName: '生猪养殖贷',
customerName: '张三',
idType: 'ID_CARD',
idNumber: '4401XXXXXXXXXXXXXX',
assetType: '生猪',
equipmentToInstall: '耳标设备',
installationStatus: 'pending',
taskGenerationTime: '2024-01-15 10:30:00',
completionTime: null,
},
{
id: 'I002',
applicationNumber: 'APP2024002',
contractNumber: 'LOAN2024002',
productName: '肉牛养殖贷',
customerName: '李四',
idType: 'ID_CARD',
idNumber: '4402XXXXXXXXXXXXXX',
assetType: '肉牛',
equipmentToInstall: '项圈设备',
installationStatus: 'in-progress',
taskGenerationTime: '2024-01-16 14:20:00',
completionTime: null,
},
{
id: 'I003',
applicationNumber: 'APP2024003',
contractNumber: 'LOAN2024003',
productName: '蛋鸡养殖贷',
customerName: '王五',
idType: 'ID_CARD',
idNumber: '4403XXXXXXXXXXXXXX',
assetType: '蛋鸡',
equipmentToInstall: '监控设备',
installationStatus: 'completed',
taskGenerationTime: '2024-01-10 09:15:00',
completionTime: '2024-01-20 16:30:00',
},
]
const fetchTasks = async () => {
loading.value = true
try {
// 实际项目中这里会调用API获取数据
// const response = await api.installationTasks.getList({
// page: pagination.current,
// pageSize: pagination.pageSize,
// ...searchForm,
// })
// 使用模拟数据
tasks.value = mockTasks.map(task => ({
...task,
taskGenerationTime: dayjs(task.taskGenerationTime),
completionTime: task.completionTime ? dayjs(task.completionTime) : null,
}))
pagination.total = mockTasks.length
} catch (error) {
console.error('获取安装任务失败:', error)
message.error('获取安装任务失败')
} finally {
loading.value = false
}
}
const filteredTasks = computed(() => {
let result = tasks.value
if (searchForm.contractNumber) {
result = result.filter(task =>
task.contractNumber.toLowerCase().includes(searchForm.contractNumber.toLowerCase())
)
}
if (searchForm.installationStatus) {
result = result.filter(task => task.installationStatus === searchForm.installationStatus)
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
const [startDate, endDate] = searchForm.dateRange
result = result.filter(task => {
const taskTime = dayjs(task.taskGenerationTime)
return taskTime.isAfter(startDate.startOf('day')) && taskTime.isBefore(endDate.endOf('day'))
})
}
return result
})
const handleSearch = () => {
pagination.current = 1
fetchTasks()
}
const handleReset = () => {
searchForm.contractNumber = ''
searchForm.dateRange = []
searchForm.installationStatus = undefined
pagination.current = 1
fetchTasks()
}
const handleExport = () => {
message.info('安装任务导出功能开发中...')
}
const handleTableChange = (pag, filters, sorter) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchTasks()
}
const getStatusColor = (status) => {
const colors = {
'pending': 'blue',
'in-progress': 'orange',
'completed': 'green',
'failed': 'red',
}
return colors[status] || 'default'
}
const getStatusName = (status) => {
const names = {
'pending': '待安装',
'in-progress': '安装中',
'completed': '已完成',
'failed': '安装失败',
}
return names[status] || status
}
const viewTask = (record) => {
message.info(`查看任务: ${record.applicationNumber}`)
}
const editTask = (record) => {
message.info(`编辑任务: ${record.applicationNumber}`)
}
const startInstallation = (record) => {
message.success(`开始安装任务: ${record.applicationNumber}`)
}
onMounted(() => {
fetchTasks()
})
</script>
<style scoped>
.pending-installation-page {
padding: 0;
background: #f0f2f5;
min-height: calc(100vh - 134px);
margin: 0;
}
.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.06);
}
.page-header h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
gap: 12px;
}
.search-section {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
}
.table-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.header-actions {
justify-content: center;
}
.search-section .ant-col {
margin-bottom: 16px;
}
.search-section .ant-col:last-child {
margin-bottom: 0;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,691 @@
<template>
<div class="supervision-tasks">
<!-- 页面头部 -->
<div class="page-header">
<h1>监管任务导入</h1>
<div class="header-actions">
<a-button type="primary" @click="showAddTaskModal">
<plus-outlined /> 新增监管任务
</a-button>
<a-button type="primary" @click="showBatchAddModal">
<plus-outlined /> 批量新增
</a-button>
</div>
</div>
<!-- 搜索筛选区域 -->
<div class="search-section">
<div class="search-form">
<a-input
v-model:value="searchForm.contractNumber"
placeholder="请输入合同编号"
class="search-input"
/>
<a-range-picker
v-model:value="searchForm.dateRange"
:placeholder="['开始日期', '结束日期']"
class="date-picker"
/>
<a-select
v-model:value="searchForm.supervisionStatus"
placeholder="请选择监管状态"
class="status-select"
>
<a-select-option value="pending">待监管</a-select-option>
<a-select-option value="supervising">监管中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="suspended">已暂停</a-select-option>
</a-select>
<div class="action-buttons">
<a-button type="primary" @click="handleSearch">
<search-outlined /> 搜索
</a-button>
<a-button @click="handleExport">
<download-outlined /> 任务导出
</a-button>
<a-button @click="handleReset">
重置
</a-button>
</div>
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredTasks"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
:locale="{ emptyText: '暂无数据' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'supervisionStatus'">
<a-tag :color="getStatusColor(record.supervisionStatus)">
{{ getStatusName(record.supervisionStatus) }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewTask(record)">查看</a-button>
<a-button type="link" size="small" @click="editTask(record)">编辑</a-button>
<a-button type="link" size="small" @click="deleteTask(record.id)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 新增监管任务对话框 -->
<a-modal
v-model:open="addTaskModalVisible"
title="新增监管任务"
width="800px"
@ok="handleAddTask"
@cancel="handleCancelAdd"
>
<a-form
ref="addTaskFormRef"
:model="addTaskForm"
:rules="addTaskRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="申请单号" name="applicationNumber">
<a-input v-model:value="addTaskForm.applicationNumber" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="放款合同编号" name="contractNumber">
<a-input v-model:value="addTaskForm.contractNumber" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="产品名称" name="productName">
<a-input v-model:value="addTaskForm.productName" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="客户姓名" name="customerName">
<a-input v-model:value="addTaskForm.customerName" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证件类型" name="idType">
<a-select v-model:value="addTaskForm.idType">
<a-select-option value="id_card">身份证</a-select-option>
<a-select-option value="passport">护照</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="idNumber">
<a-input v-model:value="addTaskForm.idNumber" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="养殖生资种类" name="assetType">
<a-select v-model:value="addTaskForm.assetType">
<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="assetQuantity">
<a-input-number v-model:value="addTaskForm.assetQuantity" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="监管起始时间" name="startTime">
<a-date-picker v-model:value="addTaskForm.startTime" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="监管结束时间" name="endTime">
<a-date-picker v-model:value="addTaskForm.endTime" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
<!-- 任务详情对话框 -->
<a-modal
v-model:open="detailModalVisible"
title="任务详情"
width="800px"
:footer="null"
>
<div v-if="selectedTask" class="task-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="申请单号">
{{ selectedTask.applicationNumber }}
</a-descriptions-item>
<a-descriptions-item label="放款合同编号">
{{ selectedTask.contractNumber }}
</a-descriptions-item>
<a-descriptions-item label="产品名称">
{{ selectedTask.productName }}
</a-descriptions-item>
<a-descriptions-item label="客户姓名">
{{ selectedTask.customerName }}
</a-descriptions-item>
<a-descriptions-item label="证件类型">
{{ getIDTypeName(selectedTask.idType) }}
</a-descriptions-item>
<a-descriptions-item label="证件号码">
{{ selectedTask.idNumber }}
</a-descriptions-item>
<a-descriptions-item label="养殖生资种类">
{{ getAssetTypeName(selectedTask.assetType) }}
</a-descriptions-item>
<a-descriptions-item label="监管生资数量">
{{ selectedTask.assetQuantity }}
</a-descriptions-item>
<a-descriptions-item label="监管状态">
<a-tag :color="getStatusColor(selectedTask.supervisionStatus)">
{{ getStatusName(selectedTask.supervisionStatus) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="任务导入时间">
{{ selectedTask.importTime }}
</a-descriptions-item>
<a-descriptions-item label="监管起始时间">
{{ selectedTask.startTime }}
</a-descriptions-item>
<a-descriptions-item label="监管结束时间">
{{ selectedTask.endTime }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { api } from '@/utils/api'
import {
PlusOutlined,
SearchOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const addTaskModalVisible = ref(false)
const detailModalVisible = ref(false)
const selectedTask = ref(null)
const addTaskFormRef = ref()
// 搜索表单
const searchForm = ref({
contractNumber: '',
dateRange: [],
supervisionStatus: undefined
})
// 新增任务表单
const addTaskForm = ref({
applicationNumber: '',
contractNumber: '',
productName: '',
customerName: '',
idType: '',
idNumber: '',
assetType: '',
assetQuantity: 0,
startTime: null,
endTime: null
})
// 表单验证规则
const addTaskRules = {
applicationNumber: [
{ required: true, message: '请输入申请单号', trigger: 'blur' }
],
contractNumber: [
{ required: true, message: '请输入放款合同编号', trigger: 'blur' }
],
productName: [
{ required: true, message: '请输入产品名称', trigger: 'blur' }
],
customerName: [
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
],
idType: [
{ required: true, message: '请选择证件类型', trigger: 'change' }
],
idNumber: [
{ required: true, message: '请输入证件号码', trigger: 'blur' }
],
assetType: [
{ required: true, message: '请选择养殖生资种类', trigger: 'change' }
],
assetQuantity: [
{ required: true, message: '请输入监管生资数量', trigger: 'blur' }
]
}
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${total}`
})
// 表格列配置
const columns = [
{
title: '申请单号',
dataIndex: 'applicationNumber',
key: 'applicationNumber',
width: 120,
},
{
title: '放款合同编号',
dataIndex: 'contractNumber',
key: 'contractNumber',
width: 150,
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
width: 120,
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
width: 100,
},
{
title: '证件类型',
dataIndex: 'idType',
key: 'idType',
width: 100,
},
{
title: '证件号码',
dataIndex: 'idNumber',
key: 'idNumber',
width: 150,
},
{
title: '养殖生资种类',
dataIndex: 'assetType',
key: 'assetType',
width: 120,
},
{
title: '监管生资数量',
dataIndex: 'assetQuantity',
key: 'assetQuantity',
width: 120,
},
{
title: '监管状态',
dataIndex: 'supervisionStatus',
key: 'supervisionStatus',
width: 100,
},
{
title: '任务导入时间',
dataIndex: 'importTime',
key: 'importTime',
width: 150,
},
{
title: '监管起始时间',
dataIndex: 'startTime',
key: 'startTime',
width: 120,
},
{
title: '监管结束时间',
dataIndex: 'endTime',
key: 'endTime',
width: 120,
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right'
},
]
// 监管任务数据
const tasks = ref([])
// 计算属性
const filteredTasks = computed(() => {
return tasks.value
})
// 方法
const getStatusColor = (status) => {
const colors = {
'pending': 'orange',
'supervising': 'blue',
'completed': 'green',
'suspended': 'red'
}
return colors[status] || 'default'
}
const getStatusName = (status) => {
const names = {
'pending': '待监管',
'supervising': '监管中',
'completed': '已完成',
'suspended': '已暂停'
}
return names[status] || status
}
const getIDTypeName = (type) => {
const names = {
'id_card': '身份证',
'passport': '护照',
'other': '其他'
}
return names[type] || type
}
const getAssetTypeName = (type) => {
const names = {
'cattle': '牛',
'sheep': '羊',
'pig': '猪',
'poultry': '家禽'
}
return names[type] || type
}
const showAddTaskModal = () => {
addTaskModalVisible.value = true
}
const showBatchAddModal = () => {
message.info('批量新增功能开发中...')
}
const resetAddTaskForm = () => {
addTaskForm.value = {
applicationNumber: '',
contractNumber: '',
productName: '',
customerName: '',
idType: '',
idNumber: '',
assetType: '',
assetQuantity: 0,
startTime: null,
endTime: null
}
}
const viewTask = (task) => {
selectedTask.value = task
detailModalVisible.value = true
}
// API调用函数
const fetchTasks = async (params = {}) => {
try {
loading.value = true
console.log('开始获取监管任务列表...', {
page: pagination.value.current,
limit: pagination.value.pageSize,
search: searchForm.value.contractNumber,
supervisionStatus: searchForm.value.supervisionStatus,
dateRange: searchForm.value.dateRange ?
`${searchForm.value.dateRange[0].format('YYYY-MM-DD')},${searchForm.value.dateRange[1].format('YYYY-MM-DD')}` : ''
})
const response = await api.supervisionTasks.getList({
page: pagination.value.current,
limit: pagination.value.pageSize,
search: searchForm.value.contractNumber,
supervisionStatus: searchForm.value.supervisionStatus,
dateRange: searchForm.value.dateRange ?
`${searchForm.value.dateRange[0].format('YYYY-MM-DD')},${searchForm.value.dateRange[1].format('YYYY-MM-DD')}` : '',
...params
})
console.log('监管任务列表响应:', response)
if (response.success) {
tasks.value = response.data.tasks || []
pagination.value.total = response.data.pagination?.total || 0
console.log('监管任务数据已更新:', tasks.value.length, '个任务')
} else {
console.error('API返回错误:', response.message)
message.error(response.message || '获取监管任务列表失败')
}
} catch (error) {
console.error('获取监管任务列表失败:', error)
message.error('获取监管任务列表失败: ' + error.message)
} finally {
loading.value = false
}
}
const handleTableChange = (paginationInfo) => {
pagination.value = paginationInfo
fetchTasks()
}
const handleSearch = () => {
pagination.value.current = 1
fetchTasks()
}
const handleReset = () => {
searchForm.value = {
contractNumber: '',
dateRange: null,
supervisionStatus: ''
}
pagination.value.current = 1
fetchTasks()
}
const editTask = async (task) => {
try {
// 这里可以实现编辑功能
message.info(`编辑任务: ${task.applicationNumber}`)
} catch (error) {
console.error('编辑任务失败:', error)
message.error('编辑任务失败')
}
}
const deleteTask = async (id) => {
try {
const response = await api.supervisionTasks.delete(id)
if (response.success) {
message.success('任务删除成功')
await fetchTasks()
} else {
message.error(response.message || '删除任务失败')
}
} catch (error) {
console.error('删除任务失败:', error)
message.error('删除任务失败')
}
}
const handleAddTask = async () => {
try {
await addTaskFormRef.value.validate()
const response = await api.supervisionTasks.create(addTaskForm.value)
if (response.success) {
message.success('监管任务创建成功')
addTaskModalVisible.value = false
addTaskFormRef.value.resetFields()
await fetchTasks()
} else {
message.error(response.message || '创建监管任务失败')
}
} catch (error) {
console.error('创建监管任务失败:', error)
message.error('创建监管任务失败')
}
}
const handleCancelAdd = () => {
addTaskModalVisible.value = false
addTaskFormRef.value.resetFields()
}
const handleExport = () => {
message.info('任务导出功能开发中...')
}
onMounted(() => {
fetchTasks()
})
</script>
<style scoped>
.supervision-tasks {
padding: 0;
background: #f0f2f5;
min-height: calc(100vh - 134px);
margin: 0;
}
.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.06);
}
.page-header h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
gap: 12px;
}
.search-section {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
}
.search-form {
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
.search-input {
width: 200px;
}
.date-picker {
width: 300px;
}
.status-select {
width: 180px;
}
.action-buttons {
display: flex;
gap: 12px;
margin-left: auto;
}
.table-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.task-detail {
padding: 16px 0;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.search-form {
flex-direction: column;
align-items: stretch;
}
.action-buttons {
margin-left: 0;
justify-content: flex-end;
}
.search-input,
.date-picker,
.status-select {
width: 100%;
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.header-actions {
justify-content: center;
}
.action-buttons {
flex-wrap: wrap;
}
}
</style>

View File

@@ -2,12 +2,11 @@
<div class="system-check">
<div class="page-header">
<h1>系统日检</h1>
<p>每日系统健康检查和监控</p>
</div>
<div class="content">
<!-- 检查概览 -->
<div class="overview-section">
<!-- <div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
@@ -46,7 +45,7 @@
</a-card>
</a-col>
</a-row>
</div>
</div> -->
<!-- 操作按钮 -->
<div class="action-section">

View File

@@ -0,0 +1,147 @@
<template>
<div class="test-projects">
<h1>项目接口测试页面</h1>
<div class="test-section">
<h2>1. 登录测试</h2>
<a-button @click="testLogin" :loading="loginLoading">
测试登录
</a-button>
<div v-if="loginResult" class="result">
<pre>{{ loginResult }}</pre>
</div>
</div>
<div class="test-section">
<h2>2. 项目接口测试</h2>
<a-button @click="testProjects" :loading="projectsLoading" :disabled="!token">
测试项目接口
</a-button>
<div v-if="projectsResult" class="result">
<pre>{{ projectsResult }}</pre>
</div>
</div>
<div class="test-section">
<h2>3. 项目数据展示</h2>
<div v-if="projects.length > 0">
<p> {{ projects.length }} 个项目</p>
<div v-for="project in projects" :key="project.id" class="project-item">
<h3>{{ project.name }}</h3>
<p>状态: {{ project.status }}</p>
<p>养殖场: {{ project.farmName }}</p>
<p>监管对象: {{ project.supervisionObject }}</p>
<p>监管数量: {{ project.supervisionQuantity }}</p>
<p>监管金额: {{ project.supervisionAmount }} </p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { api } from '@/utils/api'
const loginLoading = ref(false)
const projectsLoading = ref(false)
const loginResult = ref('')
const projectsResult = ref('')
const token = ref('')
const projects = ref([])
const testLogin = async () => {
try {
loginLoading.value = true
loginResult.value = '正在登录...'
const response = await api.auth.login('admin', 'Admin123456')
if (response.success) {
token.value = response.data.token
loginResult.value = `登录成功!\nToken: ${token.value.substring(0, 50)}...\n用户: ${response.data.user.username}`
message.success('登录成功')
} else {
loginResult.value = `登录失败: ${response.message}`
message.error('登录失败')
}
} catch (error) {
loginResult.value = `登录错误: ${error.message}`
message.error('登录错误')
} finally {
loginLoading.value = false
}
}
const testProjects = async () => {
try {
projectsLoading.value = true
projectsResult.value = '正在获取项目列表...'
const response = await api.projects.getList({
page: 1,
limit: 12,
search: '',
status: ''
})
if (response.success) {
projects.value = response.data.projects || []
projectsResult.value = `获取成功!\n项目数量: ${projects.value.length}\n总数量: ${response.data.pagination.total}`
message.success('获取项目列表成功')
} else {
projectsResult.value = `获取失败: ${response.message}`
message.error('获取项目列表失败')
}
} catch (error) {
projectsResult.value = `获取错误: ${error.message}`
message.error('获取项目列表错误')
} finally {
projectsLoading.value = false
}
}
</script>
<style scoped>
.test-projects {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.test-section {
margin-bottom: 32px;
padding: 16px;
border: 1px solid #d9d9d9;
border-radius: 8px;
}
.result {
margin-top: 16px;
padding: 12px;
background-color: #f5f5f5;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
}
.project-item {
margin-bottom: 16px;
padding: 12px;
border: 1px solid #e8e8e8;
border-radius: 4px;
background-color: #fafafa;
}
.project-item h3 {
margin: 0 0 8px 0;
color: #1890ff;
}
.project-item p {
margin: 4px 0;
color: #666;
}
</style>

View File

@@ -1,120 +1,80 @@
<template>
<div class="loan-applications">
<div class="loan-applications-page">
<div class="page-header">
<h1>贷款申请进度</h1>
<p>管理和跟踪贷款申请流程</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索申请人或申请编号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="申请状态"
allow-clear
@change="handleFilter"
>
<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="4">
<a-select
v-model:value="typeFilter"
placeholder="贷款类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="personal">个人贷款</a-select-option>
<a-select-option value="business">企业贷款</a-select-option>
<a-select-option value="mortgage">抵押贷款</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model:value="dateRange"
@change="handleFilter"
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleAddApplication">
<PlusOutlined />
新建申请
</a-button>
</a-col>
</a-row>
</div>
<div class="search-filter-section">
<a-row :gutter="16" align="middle">
<a-col :span="4">
<a-select
v-model:value="searchQuery.field"
placeholder="申请单号"
style="width: 100%"
>
<a-select-option value="applicationNumber">申请单号</a-select-option>
<a-select-option value="customerName">客户姓名</a-select-option>
<a-select-option value="productName">贷款产品</a-select-option>
</a-select>
</a-col>
<a-col :span="8">
<a-input
v-model:value="searchQuery.value"
placeholder="请输入内容"
allow-clear
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleSearch">
<search-outlined /> 搜索
</a-button>
</a-col>
<a-col :span="4">
<a-button @click="handleReset">重置</a-button>
</a-col>
</a-row>
</div>
<!-- 申请列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredApplications"
:pagination="pagination"
:loading="loading"
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 === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.amount) }}
</template>
<template v-else-if="column.key === 'progress'">
<a-progress
:percent="getProgressPercent(record.status)"
:status="getProgressStatus(record.status)"
size="small"
/>
</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="handleApprove(record)"
v-if="record.status === 'pending'"
>
审核
</a-button>
<a-button
type="link"
size="small"
@click="handleReject(record)"
v-if="record.status === 'pending'"
danger
>
拒绝
</a-button>
</a-space>
</template>
<div class="applications-table-section">
<a-table
:columns="columns"
:data-source="filteredApplications"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
:expand-row-by-click="false"
:expand-icon-column-index="0"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
</a-table>
</div>
<template v-else-if="column.key === 'policyInfo'">
<a-button type="link" size="small" @click="viewPolicy(record)">
查看保单
</a-button>
</template>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.amount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleReject(record)">
打回
</a-button>
<a-button type="link" size="small" @click="handleApprove(record)">
通过
</a-button>
<a-button type="link" size="small" @click="handleView(record)">
详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 申请详情模态框 -->
@@ -222,14 +182,14 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { SearchOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const dateRange = ref([])
const searchQuery = ref({
field: 'applicationNumber',
value: ''
})
const detailModalVisible = ref(false)
const auditModalVisible = ref(false)
const selectedApplication = ref(null)
@@ -245,52 +205,85 @@ const pagination = ref({
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total} `
showTotal: (total) => `${total}`
})
// 表格列配置
const columns = [
{
title: '申请编号',
title: '',
key: 'expand',
width: 50,
customRender: () => '>'
},
{
title: '申请单号',
dataIndex: 'applicationNumber',
key: 'applicationNumber',
width: 150
width: 180
},
{
title: '申请人',
dataIndex: 'applicantName',
key: 'applicantName',
title: '贷款产品',
dataIndex: 'productName',
key: 'productName',
width: 200
},
{
title: '申请养殖户',
dataIndex: 'farmerName',
key: 'farmerName',
width: 120
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
title: '贷款人姓名',
dataIndex: 'borrowerName',
key: 'borrowerName',
width: 120
},
{
title: '贷款人身份证号',
dataIndex: 'borrowerIdNumber',
key: 'borrowerIdNumber',
width: 180
},
{
title: '生资种类',
dataIndex: 'assetType',
key: 'assetType',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
title: '申请数量',
dataIndex: 'applicationQuantity',
key: 'applicationQuantity',
width: 120,
sorter: true
},
{
title: '保单信息',
dataIndex: 'policyInfo',
key: 'policyInfo',
width: 100
},
{
title: '申请额',
title: '申请额',
dataIndex: 'amount',
key: 'amount',
width: 120
width: 120,
sorter: true
},
{
title: '申请时间',
dataIndex: 'applicationTime',
key: 'applicationTime',
width: 150
},
{
title: '进度',
dataIndex: 'progress',
key: 'progress',
width: 150
title: '当前状态',
dataIndex: 'status',
key: 'status',
width: 120,
filters: [
{ text: '待初审', value: 'pending_review' },
{ text: '核验待放款', value: 'verification_pending' },
{ text: '待绑定', value: 'pending_binding' },
{ text: '已通过', value: 'approved' },
{ text: '已拒绝', value: 'rejected' }
]
},
{
title: '操作',
@@ -304,87 +297,86 @@ const columns = [
const applications = ref([
{
id: 1,
applicationNumber: 'APP-202401180001',
applicantName: '张三',
type: 'personal',
status: 'pending',
amount: 200000,
term: 24,
interestRate: 6.5,
applicationTime: '2024-01-18 09:30:00',
applicationNumber: '20240325123703784',
productName: '惠农贷',
farmerName: '刘超',
borrowerName: '11',
borrowerIdNumber: '511***********3017',
assetType: '牛',
applicationQuantity: '10头',
policyInfo: '查看保单',
amount: 100000.00,
status: 'pending_review',
applicationTime: '2024-03-25 12:37:03',
phone: '13800138000',
idCard: '110101199001011234',
purpose: '个人消费',
purpose: '养殖贷款',
remark: '',
auditRecords: [
{
id: 1,
action: 'submit',
auditor: '张三',
time: '2024-01-18 09:30:00',
auditor: '刘超',
time: '2024-03-25 12:37:03',
comment: '提交申请'
}
]
},
{
id: 2,
applicationNumber: 'APP-202401180002',
applicantName: '李四',
type: 'business',
status: 'approved',
amount: 1000000,
term: 36,
interestRate: 5.8,
applicationTime: '2024-01-17 14:20:00',
applicationNumber: '20240229110801968',
productName: '中国工商银行扎旗支行"畜禽活体抵押"',
farmerName: '刘超',
borrowerName: '1',
borrowerIdNumber: '511***********3017',
assetType: '牛',
applicationQuantity: '10头',
policyInfo: '查看保单',
amount: 100000.00,
status: 'verification_pending',
applicationTime: '2024-02-29 11:08:01',
phone: '13900139000',
idCard: '110101199002021234',
purpose: '企业经营',
purpose: '养殖贷款',
remark: '',
auditRecords: [
{
id: 1,
action: 'submit',
auditor: '李四',
time: '2024-01-17 14:20:00',
auditor: '刘超',
time: '2024-02-29 11:08:01',
comment: '提交申请'
},
{
id: 2,
action: 'approve',
auditor: '王经理',
time: '2024-01-18 10:15:00',
time: '2024-03-01 10:15:00',
comment: '资料齐全,符合条件,同意放款'
}
]
},
{
id: 3,
applicationNumber: 'APP-202401180003',
applicantName: '王五',
type: 'mortgage',
status: 'rejected',
amount: 500000,
term: 120,
interestRate: 4.5,
applicationTime: '2024-01-16 16:45:00',
applicationNumber: '20240229105806431',
productName: '惠农贷',
farmerName: '刘超',
borrowerName: '1',
borrowerIdNumber: '511***********3017',
assetType: '牛',
applicationQuantity: '10头',
policyInfo: '查看保单',
amount: 100000.00,
status: 'pending_binding',
applicationTime: '2024-02-29 10:58:06',
phone: '13700137000',
idCard: '110101199003031234',
purpose: '购房',
purpose: '养殖贷款',
remark: '',
auditRecords: [
{
id: 1,
action: 'submit',
auditor: '王五',
time: '2024-01-16 16:45:00',
auditor: '刘超',
time: '2024-02-29 10:58:06',
comment: '提交申请'
},
{
id: 2,
action: 'reject',
auditor: '赵经理',
time: '2024-01-17 11:30:00',
comment: '抵押物价值不足,不符合放款条件'
}
]
}
@@ -394,19 +386,21 @@ const applications = ref([
const filteredApplications = computed(() => {
let result = applications.value
if (searchText.value) {
result = result.filter(app =>
app.applicantName.toLowerCase().includes(searchText.value.toLowerCase()) ||
app.applicationNumber.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(app => app.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(app => app.type === typeFilter.value)
if (searchQuery.value.value) {
const searchValue = searchQuery.value.value.toLowerCase()
const field = searchQuery.value.field
result = result.filter(app => {
if (field === 'applicationNumber') {
return app.applicationNumber.toLowerCase().includes(searchValue)
} else if (field === 'customerName') {
return app.borrowerName.toLowerCase().includes(searchValue) ||
app.farmerName.toLowerCase().includes(searchValue)
} else if (field === 'productName') {
return app.productName.toLowerCase().includes(searchValue)
}
return true
})
}
return result
@@ -417,8 +411,11 @@ const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
const handleReset = () => {
searchQuery.value = {
field: 'applicationNumber',
value: ''
}
}
const handleTableChange = (pag) => {
@@ -426,10 +423,6 @@ const handleTableChange = (pag) => {
pagination.value.pageSize = pag.pageSize
}
const handleAddApplication = () => {
message.info('新建申请功能开发中...')
}
const handleView = (record) => {
selectedApplication.value = record
detailModalVisible.value = true
@@ -449,6 +442,11 @@ const handleReject = (record) => {
auditModalVisible.value = true
}
const viewPolicy = (record) => {
message.info(`查看保单: ${record.applicationNumber}`)
// 实际项目中这里会打开保单详情页面
}
const handleAuditSubmit = () => {
if (!auditForm.value.comment) {
message.error('请输入审核意见')
@@ -478,20 +476,22 @@ const handleAuditCancel = () => {
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
pending_review: 'blue',
verification_pending: 'blue',
pending_binding: 'blue',
approved: 'green',
rejected: 'red',
processing: 'blue'
rejected: 'red'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
pending: '待审',
pending_review: '待审',
verification_pending: '核验待放款',
pending_binding: '待绑定',
approved: '已通过',
rejected: '已拒绝',
processing: '处理中'
rejected: '已拒绝'
}
return texts[status] || status
}
@@ -549,10 +549,7 @@ const getAuditActionText = (action) => {
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
return `${amount.toFixed(2)}`
}
// 生命周期
@@ -562,40 +559,43 @@ onMounted(() => {
</script>
<style scoped>
.loan-applications {
.loan-applications-page {
padding: 24px;
background-color: #f0f2f5;
min-height: calc(100vh - 134px);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
font-size: 28px;
color: #333;
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
.search-filter-section {
background-color: #fff;
padding: 24px;
}
.search-section {
border-radius: 8px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.table-section {
margin-top: 16px;
.applications-table-section {
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow-x: auto;
}
.application-detail {
@@ -644,4 +644,56 @@ onMounted(() => {
padding: 8px;
border-radius: 4px;
}
/* 表格样式优化 */
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f5f5f5;
}
/* 状态标签样式 */
:deep(.ant-tag) {
border-radius: 4px;
font-size: 12px;
padding: 2px 8px;
}
/* 操作按钮样式 */
:deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
line-height: 1.2;
}
/* 分页样式 */
:deep(.ant-pagination) {
margin-top: 16px;
text-align: right;
}
/* 响应式调整 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.search-filter-section .ant-col {
margin-bottom: 16px;
}
.search-filter-section .ant-col:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -1,112 +1,74 @@
<template>
<div class="loan-contracts">
<div class="loan-contracts-page">
<div class="page-header">
<h1>贷款合同</h1>
<p>管理和跟踪贷款合同状态</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索合同编号或客户姓名"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="合同状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="draft">草稿</a-select-option>
<a-select-option value="pending">待签署</a-select-option>
<a-select-option value="signed">已签署</a-select-option>
<a-select-option value="active">生效中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="terminated">已终止</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="typeFilter"
placeholder="合同类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="personal">个人贷款</a-select-option>
<a-select-option value="business">企业贷款</a-select-option>
<a-select-option value="mortgage">抵押贷款</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model:value="dateRange"
@change="handleFilter"
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleCreateContract">
<PlusOutlined />
新建合同
</a-button>
</a-col>
</a-row>
</div>
<div class="search-filter-section">
<a-row :gutter="16" align="middle">
<a-col :span="4">
<a-select
v-model:value="searchQuery.field"
placeholder="申请单号"
style="width: 100%"
>
<a-select-option value="applicationNumber">申请单号</a-select-option>
<a-select-option value="customerName">客户姓名</a-select-option>
<a-select-option value="productName">贷款产品</a-select-option>
</a-select>
</a-col>
<a-col :span="8">
<a-input
v-model:value="searchQuery.value"
placeholder="请输入内容"
allow-clear
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleSearch">
<search-outlined /> 搜索
</a-button>
</a-col>
<a-col :span="4">
<a-button @click="handleReset">重置</a-button>
</a-col>
</a-row>
</div>
<!-- 合同列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredContracts"
:pagination="pagination"
:loading="loading"
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 === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.amount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="link" size="small" @click="handleDownload(record)">
下载
</a-button>
<a-button
type="link"
size="small"
@click="handleSign(record)"
v-if="record.status === 'pending'"
>
签署
</a-button>
</a-space>
</template>
<div class="contracts-table-section">
<a-table
:columns="columns"
:data-source="filteredContracts"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
:locale="{ emptyText: '暂无数据' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
</a-table>
</div>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.amount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="link" size="small" @click="handleDownload(record)">
下载
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 合同详情模态框 -->
@@ -232,14 +194,14 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { SearchOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const dateRange = ref([])
const searchQuery = ref({
field: 'applicationNumber',
value: ''
})
const detailModalVisible = ref(false)
const signModalVisible = ref(false)
const selectedContract = ref(null)
@@ -255,202 +217,97 @@ const pagination = ref({
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total} `
showTotal: (total) => `${total}`
})
// 表格列配置
const columns = [
{
title: '合同编号',
dataIndex: 'contractNumber',
key: 'contractNumber',
width: 150
title: '申请单号',
dataIndex: 'applicationNumber',
key: 'applicationNumber',
width: 180
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
title: '贷款产品',
dataIndex: 'productName',
key: 'productName',
width: 200
},
{
title: '申请养殖户',
dataIndex: 'farmerName',
key: 'farmerName',
width: 120
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
title: '贷款人姓名',
dataIndex: 'borrowerName',
key: 'borrowerName',
width: 120
},
{
title: '贷款人身份证号',
dataIndex: 'borrowerIdNumber',
key: 'borrowerIdNumber',
width: 180
},
{
title: '生资种类',
dataIndex: 'assetType',
key: 'assetType',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
title: '申请数量',
dataIndex: 'applicationQuantity',
key: 'applicationQuantity',
width: 120,
sorter: true
},
{
title: '贷款金额',
title: '申请额度',
dataIndex: 'amount',
key: 'amount',
width: 120
width: 120,
sorter: true
},
{
title: '期限',
dataIndex: 'term',
key: 'term',
width: 80
},
{
title: '年利率',
dataIndex: 'interestRate',
key: 'interestRate',
width: 100
},
{
title: '签署日期',
dataIndex: 'signDate',
key: 'signDate',
title: '当前状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '操作',
key: 'action',
width: 250,
width: 200,
fixed: 'right'
}
]
// 模拟合同数据
const contracts = ref([
{
id: 1,
contractNumber: 'CON-202401180001',
customerName: '张三',
type: 'personal',
status: 'signed',
amount: 200000,
term: 24,
interestRate: 6.5,
repaymentMethod: 'equal_installment',
signDate: '2024-01-18',
effectiveDate: '2024-01-18',
maturityDate: '2026-01-18',
phone: '13800138000',
idCard: '110101199001011234',
terms: [
'借款人应按期还款,不得逾期',
'借款人应按时支付利息',
'借款人不得将贷款用于非法用途',
'借款人应配合银行进行贷后管理'
],
history: [
{
id: 1,
action: 'create',
operator: '系统',
time: '2024-01-18 09:30:00',
comment: '合同创建'
},
{
id: 2,
action: 'sign',
operator: '张三',
time: '2024-01-18 10:15:00',
comment: '客户签署合同'
}
]
},
{
id: 2,
contractNumber: 'CON-202401180002',
customerName: '李四',
type: 'business',
status: 'pending',
amount: 1000000,
term: 36,
interestRate: 5.8,
repaymentMethod: 'balloon',
signDate: null,
effectiveDate: null,
maturityDate: '2027-01-18',
phone: '13900139000',
idCard: '110101199002021234',
terms: [
'企业应按期还款,不得逾期',
'企业应按时支付利息',
'企业应提供财务报表',
'企业应配合银行进行贷后管理'
],
history: [
{
id: 1,
action: 'create',
operator: '系统',
time: '2024-01-18 14:20:00',
comment: '合同创建'
}
]
},
{
id: 3,
contractNumber: 'CON-202401180003',
customerName: '王五',
type: 'mortgage',
status: 'active',
amount: 500000,
term: 120,
interestRate: 4.5,
repaymentMethod: 'equal_installment',
signDate: '2024-01-17',
effectiveDate: '2024-01-17',
maturityDate: '2034-01-17',
phone: '13700137000',
idCard: '110101199003031234',
terms: [
'借款人应按期还款,不得逾期',
'借款人应按时支付利息',
'抵押物不得转让或处置',
'借款人应配合银行进行贷后管理'
],
history: [
{
id: 1,
action: 'create',
operator: '系统',
time: '2024-01-17 16:45:00',
comment: '合同创建'
},
{
id: 2,
action: 'sign',
operator: '王五',
time: '2024-01-17 17:30:00',
comment: '客户签署合同'
},
{
id: 3,
action: 'activate',
operator: '系统',
time: '2024-01-17 18:00:00',
comment: '合同生效'
}
]
}
])
// 模拟合同数据 - 设置为空数据以匹配图片
const contracts = ref([])
// 计算属性
const filteredContracts = computed(() => {
let result = contracts.value
if (searchText.value) {
result = result.filter(contract =>
contract.customerName.toLowerCase().includes(searchText.value.toLowerCase()) ||
contract.contractNumber.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(contract => contract.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(contract => contract.type === typeFilter.value)
if (searchQuery.value.value) {
const searchValue = searchQuery.value.value.toLowerCase()
const field = searchQuery.value.field
result = result.filter(contract => {
if (field === 'applicationNumber') {
return contract.applicationNumber.toLowerCase().includes(searchValue)
} else if (field === 'customerName') {
return contract.borrowerName.toLowerCase().includes(searchValue) ||
contract.farmerName.toLowerCase().includes(searchValue)
} else if (field === 'productName') {
return contract.productName.toLowerCase().includes(searchValue)
}
return true
})
}
return result
@@ -461,8 +318,11 @@ const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
const handleReset = () => {
searchQuery.value = {
field: 'applicationNumber',
value: ''
}
}
const handleTableChange = (pag) => {
@@ -470,28 +330,17 @@ const handleTableChange = (pag) => {
pagination.value.pageSize = pag.pageSize
}
const handleCreateContract = () => {
message.info('新建合同功能开发中...')
}
const handleView = (record) => {
selectedContract.value = record
detailModalVisible.value = true
}
const handleEdit = (record) => {
message.info(`编辑合同: ${record.contractNumber}`)
message.info(`编辑合同: ${record.applicationNumber}`)
}
const handleDownload = (record) => {
message.info(`下载合同: ${record.contractNumber}`)
}
const handleSign = (record) => {
selectedContract.value = record
signForm.value.password = ''
signForm.value.comment = ''
signModalVisible.value = true
message.info(`下载合同: ${record.applicationNumber}`)
}
const handleSignSubmit = () => {
@@ -525,9 +374,12 @@ const handleSignCancel = () => {
const getStatusColor = (status) => {
const colors = {
draft: 'default',
pending: 'orange',
signed: 'blue',
pending_review: 'blue',
verification_pending: 'blue',
pending_binding: 'blue',
approved: 'green',
rejected: 'red',
signed: 'green',
active: 'green',
completed: 'success',
terminated: 'red'
@@ -537,8 +389,11 @@ const getStatusColor = (status) => {
const getStatusText = (status) => {
const texts = {
draft: '草稿',
pending: '待签署',
pending_review: '待初审',
verification_pending: '核验待放款',
pending_binding: '待绑定',
approved: '已通过',
rejected: '已拒绝',
signed: '已签署',
active: '生效中',
completed: '已完成',
@@ -596,10 +451,7 @@ const getHistoryActionText = (action) => {
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
return `${amount.toFixed(2)}`
}
// 生命周期
@@ -609,40 +461,43 @@ onMounted(() => {
</script>
<style scoped>
.loan-contracts {
.loan-contracts-page {
padding: 24px;
background-color: #f0f2f5;
min-height: calc(100vh - 134px);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
font-size: 28px;
color: #333;
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
.search-filter-section {
background-color: #fff;
padding: 24px;
}
.search-section {
border-radius: 8px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.table-section {
margin-top: 16px;
.contracts-table-section {
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow-x: auto;
}
.contract-detail {
@@ -713,4 +568,66 @@ onMounted(() => {
.sign-content {
padding: 16px 0;
}
/* 表格样式优化 */
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f5f5f5;
}
/* 状态标签样式 */
:deep(.ant-tag) {
border-radius: 4px;
font-size: 12px;
padding: 2px 8px;
}
/* 操作按钮样式 */
:deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
line-height: 1.2;
}
/* 分页样式 */
:deep(.ant-pagination) {
margin-top: 16px;
text-align: right;
}
/* 空数据样式 */
:deep(.ant-empty) {
padding: 40px 0;
}
:deep(.ant-empty-description) {
color: #999;
font-size: 14px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.search-filter-section .ant-col {
margin-bottom: 16px;
}
.search-filter-section .ant-col:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -1,469 +1,387 @@
<template>
<div class="loan-products">
<div class="loan-products-page">
<!-- 页面头部 -->
<div class="page-header">
<h1>贷款商品</h1>
<p>管理和配置银行贷款产品</p>
<a-button type="primary" @click="handleAddProduct">
<plus-outlined />
新增贷款
</a-button>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="8">
<a-input-search
v-model:value="searchText"
placeholder="搜索产品名称或编号"
enter-button="搜索"
@search="handleSearch"
<!-- 搜索筛选区域 -->
<div class="search-section">
<a-row :gutter="16" align="middle">
<a-col :span="8">
<a-input
v-model:value="searchText"
placeholder="请输入贷款产品"
allow-clear
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleSearch">
<search-outlined />
搜索
</a-button>
</a-col>
<a-col :span="4">
<a-button @click="handleReset">重置</a-button>
</a-col>
</a-row>
</div>
<!-- 数据表格 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredProducts"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
:locale="{ emptyText: '暂无数据' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'onSaleStatus'">
<a-switch
v-model:checked="record.onSaleStatus"
@change="handleToggleStatus(record)"
/>
</a-col>
<a-col :span="6">
<a-select
v-model:value="statusFilter"
placeholder="产品状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="active">启用</a-select-option>
<a-select-option value="inactive">停用</a-select-option>
<a-select-option value="draft">草稿</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-select
v-model:value="typeFilter"
placeholder="产品类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="personal">个人贷款</a-select-option>
<a-select-option value="business">企业贷款</a-select-option>
<a-select-option value="mortgage">抵押贷款</a-select-option>
<a-select-option value="credit">信用贷款</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleAddProduct">
<PlusOutlined />
新建产品
</a-button>
</a-col>
</a-row>
</div>
<!-- 产品列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredProducts"
:pagination="pagination"
:loading="loading"
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 === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'interestRate'">
{{ record.interestRate }}% - {{ record.maxInterestRate }}%
</template>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.minAmount) }} - {{ formatAmount(record.maxAmount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button
type="link"
size="small"
@click="handleToggleStatus(record)"
:danger="record.status === 'active'"
>
{{ record.status === 'active' ? '停用' : '启用' }}
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" @click="handleView(record)">详情</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 产品详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="产品详情"
width="800px"
:footer="null"
>
<div v-if="selectedProduct" class="product-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="产品名称">
{{ selectedProduct.name }}
</a-descriptions-item>
<a-descriptions-item label="产品编号">
{{ selectedProduct.code }}
</a-descriptions-item>
<a-descriptions-item label="产品类型">
<a-tag :color="getTypeColor(selectedProduct.type)">
{{ getTypeText(selectedProduct.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="产品状态">
<a-tag :color="getStatusColor(selectedProduct.status)">
{{ getStatusText(selectedProduct.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="贷款额度">
{{ formatAmount(selectedProduct.minAmount) }} - {{ formatAmount(selectedProduct.maxAmount) }}
</a-descriptions-item>
<a-descriptions-item label="贷款期限">
{{ selectedProduct.minTerm }} - {{ selectedProduct.maxTerm }} 个月
</a-descriptions-item>
<a-descriptions-item label="利率范围">
{{ selectedProduct.interestRate }}% - {{ selectedProduct.maxInterestRate }}%
</a-descriptions-item>
<a-descriptions-item label="申请条件">
{{ selectedProduct.requirements }}
</a-descriptions-item>
<a-descriptions-item label="产品描述" :span="2">
{{ selectedProduct.description }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { api } from '@/utils/api'
import { PlusOutlined, SearchOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedProduct = ref(null)
// 分页配置
const pagination = ref({
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,${total}`
showTotal: (total) => `${total}`,
})
// 表格列配置
const columns = [
{
title: '产品名称',
dataIndex: 'name',
key: 'name',
{
title: '贷款产品',
dataIndex: 'productName',
key: 'productName',
sorter: true,
width: 200
},
{
title: '产品编号',
dataIndex: 'code',
key: 'code',
width: 120
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '贷款额度',
dataIndex: 'amount',
key: 'amount',
width: 200
},
{
title: '利率范围',
dataIndex: 'interestRate',
key: 'interestRate',
{
title: '贷款额度',
dataIndex: 'loanAmount',
key: 'loanAmount',
sorter: true,
width: 150
},
{
title: '期',
dataIndex: 'term',
key: 'term',
{
title: '贷款周期',
dataIndex: 'loanTerm',
key: 'loanTerm',
sorter: true,
width: 100
},
{
title: '贷款利率',
dataIndex: 'interestRate',
key: 'interestRate',
sorter: true,
width: 100
},
{
title: '服务区域',
dataIndex: 'serviceArea',
key: 'serviceArea',
width: 200
},
{
title: '服务电话',
dataIndex: 'servicePhone',
key: 'servicePhone',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
{
title: '服务客户总数量',
dataIndex: 'totalCustomers',
key: 'totalCustomers',
sorter: true,
width: 120
},
{
title: '监管中客户',
dataIndex: 'supervisionCustomers',
key: 'supervisionCustomers',
sorter: true,
width: 120
},
{
title: '已结项客户',
dataIndex: 'completedCustomers',
key: 'completedCustomers',
sorter: true,
width: 120
},
{
title: '添加时间',
dataIndex: 'createTime',
key: 'createTime',
sorter: true,
width: 150
},
{
title: '在售状态',
dataIndex: 'onSaleStatus',
key: 'onSaleStatus',
width: 100
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 120
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
// 产品数据
const products = ref([])
// 模拟产品数据(作为备用)
const mockProducts = [
// 模拟数据
const products = ref([
{
id: 1,
productName: '惠农贷',
loanAmount: '50000~5000000元',
loanTerm: '24',
interestRate: '3.90%',
serviceArea: '内蒙古自治区:通辽市',
servicePhone: '15004901368',
totalCustomers: 16,
supervisionCustomers: 11,
completedCustomers: 5,
createTime: '2023-12-18 16:23:03',
onSaleStatus: true,
},
{
id: 2,
name: '个人消费贷款',
code: 'LOAN-002',
type: 'personal',
status: 'active',
minAmount: 10000,
maxAmount: 500000,
minTerm: 6,
maxTerm: 60,
interestRate: 6.8,
maxInterestRate: 12.5,
requirements: '年满18周岁有稳定收入来源信用记录良好',
description: '用于个人消费支出的信用贷款产品',
createTime: '2024-01-05'
productName: '中国工商银行扎旗支行"畜禽活体抵押"',
loanAmount: '200000~1000000',
loanTerm: '12',
interestRate: '4.70%',
serviceArea: '内蒙古自治区:通辽市',
servicePhone: '15004901368',
totalCustomers: 10,
supervisionCustomers: 5,
completedCustomers: 5,
createTime: '2023-06-20 17:36:17',
onSaleStatus: true,
},
{
id: 3,
name: '企业经营贷款',
code: 'LOAN-003',
type: 'business',
status: 'active',
minAmount: 500000,
maxAmount: 50000000,
minTerm: 12,
maxTerm: 120,
interestRate: 5.2,
maxInterestRate: 8.5,
requirements: '企业成立满2年年营业额达到500万以上',
description: '为企业经营发展提供的流动资金贷款',
createTime: '2024-01-10'
productName: '中国银行扎旗支行"畜禽活体抵押"',
loanAmount: '200000~1000000',
loanTerm: '12',
interestRate: '4.60%',
serviceArea: '内蒙古自治区:通辽市',
servicePhone: '15004901368',
totalCustomers: 2,
supervisionCustomers: 2,
completedCustomers: 0,
createTime: '2023-06-20 17:34:33',
onSaleStatus: true,
},
{
id: 4,
name: '小微企业贷款',
code: 'LOAN-004',
type: 'business',
status: 'draft',
minAmount: 50000,
maxAmount: 1000000,
minTerm: 6,
maxTerm: 36,
interestRate: 7.5,
maxInterestRate: 10.5,
requirements: '小微企业年营业额100万以上',
description: '专为小微企业提供的快速贷款产品',
createTime: '2024-01-15'
}
];
productName: '中国农业银行扎旗支行"畜禽活体抵押"',
loanAmount: '200000~1000000',
loanTerm: '12',
interestRate: '4.80%',
serviceArea: '内蒙古自治区:通辽市',
servicePhone: '15004901368',
totalCustomers: 26,
supervisionCustomers: 24,
completedCustomers: 2,
createTime: '2023-06-20 17:09:39',
onSaleStatus: true,
},
])
// 计算属性
const filteredProducts = computed(() => {
let result = products.value
if (searchText.value) {
result = result.filter(product =>
product.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
product.code.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(product => product.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(product => product.type === typeFilter.value)
}
return result
})
// 方法
const handleAddProduct = () => {
message.info('新建产品功能开发中...')
}
const handleView = (record) => {
selectedProduct.value = record
detailModalVisible.value = true
}
const handleEdit = (record) => {
message.info(`编辑产品: ${record.name}`)
}
const handleToggleStatus = (record) => {
const newStatus = record.status === 'active' ? 'inactive' : 'active'
record.status = newStatus
message.success(`产品已${newStatus === 'active' ? '启用' : '停用'}`)
}
const getStatusColor = (status) => {
const colors = {
active: 'green',
inactive: 'red',
draft: 'orange'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
active: '启用',
inactive: '停用',
draft: '草稿'
}
return texts[status] || status
}
const getTypeColor = (type) => {
const colors = {
personal: 'blue',
business: 'green',
mortgage: 'purple',
credit: 'orange'
}
return colors[type] || 'default'
}
const getTypeText = (type) => {
const texts = {
personal: '个人贷款',
business: '企业贷款',
mortgage: '抵押贷款',
credit: '信用贷款'
}
return texts[type] || type
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// API调用函数
const fetchProducts = async (params = {}) => {
const fetchProducts = async () => {
loading.value = true
try {
loading.value = true
const response = await api.loanProducts.getList({
page: pagination.value.current,
limit: pagination.value.pageSize,
search: searchText.value,
status: statusFilter.value,
type: typeFilter.value,
...params
})
// 实际项目中这里会调用API获取数据
// const response = await api.loanProducts.getList({
// page: pagination.current,
// pageSize: pagination.pageSize,
// search: searchText.value,
// })
if (response.success) {
products.value = response.data.products || []
pagination.value.total = response.data.pagination?.total || 0
} else {
message.error(response.message || '获取产品列表失败')
// 使用模拟数据作为备用
products.value = mockProducts
pagination.value.total = mockProducts.length
}
// 使用模拟数据
pagination.total = products.value.length
} catch (error) {
console.error('获取产品列表失败:', error)
message.error('获取产品列表失败')
// 使用模拟数据作为备用
products.value = mockProducts
pagination.value.total = mockProducts.length
console.error('获取贷款商品失败:', error)
message.error('获取贷款商品失败')
} finally {
loading.value = false
}
}
const filteredProducts = computed(() => {
let result = products.value
if (searchText.value) {
result = result.filter(product =>
product.productName.toLowerCase().includes(searchText.value.toLowerCase())
)
}
return result
})
const handleSearch = () => {
pagination.value.current = 1
pagination.current = 1
fetchProducts()
}
const handleFilter = () => {
pagination.value.current = 1
const handleReset = () => {
searchText.value = ''
pagination.current = 1
fetchProducts()
}
const handleTableChange = (paginationInfo) => {
pagination.value = paginationInfo
const handleAddProduct = () => {
message.info('新增贷款功能开发中...')
}
const handleEdit = (record) => {
message.info(`编辑产品: ${record.productName}`)
}
const handleView = (record) => {
message.info(`查看详情: ${record.productName}`)
}
const handleToggleStatus = (record) => {
const status = record.onSaleStatus ? '启用' : '停用'
message.success(`${record.productName}${status}`)
}
const handleTableChange = (pag, filters, sorter) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchProducts()
}
// 生命周期
onMounted(() => {
fetchProducts()
})
</script>
<style scoped>
.loan-products {
padding: 24px;
.loan-products-page {
padding: 0;
background: #f0f2f5;
min-height: calc(100vh - 134px);
margin: 0;
}
.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.06);
}
.page-header h1 {
margin: 0 0 8px 0;
margin: 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
color: #333;
}
.search-section {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.product-detail {
padding: 16px 0;
/* 表格样式优化 */
:deep(.ant-table-thead > tr > th) {
background: #fafafa;
font-weight: 600;
color: #262626;
border-bottom: 1px solid #f0f0f0;
}
</style>
:deep(.ant-table-tbody > tr:hover > td) {
background: #f5f5f5;
}
:deep(.ant-table-tbody > tr:nth-child(even) > td) {
background: #fafafa;
}
:deep(.ant-table-tbody > tr:nth-child(odd) > td) {
background: #fff;
}
/* 开关样式 */
:deep(.ant-switch-checked) {
background-color: #1890ff;
}
/* 链接按钮样式 */
:deep(.ant-btn-link) {
color: #1890ff;
padding: 4px 8px;
}
:deep(.ant-btn-link:hover) {
color: #40a9ff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.search-section .ant-col {
margin-bottom: 16px;
}
.search-section .ant-col:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -1,125 +1,72 @@
<template>
<div class="loan-release">
<div class="loan-release-page">
<div class="page-header">
<h1>贷款解押</h1>
<p>管理和处理贷款抵押物解押业务</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索客户姓名或合同编号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="解押状态"
allow-clear
@change="handleFilter"
>
<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="rejected">已拒绝</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="typeFilter"
placeholder="抵押物类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="house">房产</a-select-option>
<a-select-option value="car">车辆</a-select-option>
<a-select-option value="land">土地</a-select-option>
<a-select-option value="equipment">设备</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model:value="dateRange"
@change="handleFilter"
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleCreateRelease">
<PlusOutlined />
新建解押
</a-button>
</a-col>
</a-row>
</div>
<div class="search-filter-section">
<a-row :gutter="16" align="middle">
<a-col :span="4">
<a-select
v-model:value="searchQuery.field"
placeholder="申请单号"
style="width: 100%"
>
<a-select-option value="applicationNumber">申请单号</a-select-option>
<a-select-option value="customerName">客户姓名</a-select-option>
<a-select-option value="productName">贷款产品</a-select-option>
</a-select>
</a-col>
<a-col :span="8">
<a-input
v-model:value="searchQuery.value"
placeholder="请输入内容"
allow-clear
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleSearch">
<search-outlined /> 搜索
</a-button>
</a-col>
<a-col :span="4">
<a-button @click="handleReset">重置</a-button>
</a-col>
</a-row>
</div>
<!-- 解押列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredReleases"
:pagination="pagination"
:loading="loading"
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 === 'collateralType'">
<a-tag :color="getCollateralTypeColor(record.collateralType)">
{{ getCollateralTypeText(record.collateralType) }}
</a-tag>
</template>
<template v-else-if="column.key === 'loanAmount'">
{{ formatAmount(record.loanAmount) }}
</template>
<template v-else-if="column.key === 'collateralValue'">
{{ formatAmount(record.collateralValue) }}
</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="handleProcess(record)"
v-if="record.status === 'pending'"
>
处理
</a-button>
<a-button
type="link"
size="small"
@click="handleComplete(record)"
v-if="record.status === 'processing'"
>
完成
</a-button>
<a-button
type="link"
size="small"
@click="handleReject(record)"
v-if="record.status === 'pending'"
danger
>
拒绝
</a-button>
</a-space>
</template>
<div class="releases-table-section">
<a-table
:columns="columns"
:data-source="filteredReleases"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
:expand-row-by-click="false"
:expand-icon-column-index="0"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
</a-table>
</div>
<template v-else-if="column.key === 'releaseAmount'">
{{ formatAmount(record.releaseAmount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="link" size="small" @click="handleView(record)">
详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 解押详情模态框 -->
@@ -245,14 +192,14 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { SearchOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const dateRange = ref([])
const searchQuery = ref({
field: 'applicationNumber',
value: ''
})
const detailModalVisible = ref(false)
const processModalVisible = ref(false)
const selectedRelease = ref(null)
@@ -269,58 +216,78 @@ const pagination = ref({
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total} `
showTotal: (total) => `${total}`
})
// 表格列配置
const columns = [
{
title: '解押编号',
dataIndex: 'releaseNumber',
key: 'releaseNumber',
width: 150
title: '',
key: 'expand',
width: 50,
customRender: () => '>'
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
title: '申请单号',
dataIndex: 'applicationNumber',
key: 'applicationNumber',
width: 180
},
{
title: '贷款产品',
dataIndex: 'productName',
key: 'productName',
width: 200
},
{
title: '申请养殖户',
dataIndex: 'farmerName',
key: 'farmerName',
width: 120
},
{
title: '合同编号',
dataIndex: 'contractNumber',
key: 'contractNumber',
width: 150
},
{
title: '抵押物类型',
dataIndex: 'collateralType',
key: 'collateralType',
title: '申请人姓名',
dataIndex: 'applicantName',
key: 'applicantName',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
title: '申请人身份证号',
dataIndex: 'applicantIdNumber',
key: 'applicantIdNumber',
width: 180
},
{
title: '申请人电话',
dataIndex: 'applicantPhone',
key: 'applicantPhone',
width: 150
},
{
title: '生资种类',
dataIndex: 'assetType',
key: 'assetType',
width: 100
},
{
title: '贷款金额',
dataIndex: 'loanAmount',
key: 'loanAmount',
width: 120
title: '申请解押数量',
dataIndex: 'releaseQuantity',
key: 'releaseQuantity',
width: 150,
sorter: true
},
{
title: '抵押物价值',
dataIndex: 'collateralValue',
key: 'collateralValue',
width: 120
title: '申请解押额度',
dataIndex: 'releaseAmount',
key: 'releaseAmount',
width: 150,
sorter: true
},
{
title: '申请时间',
dataIndex: 'applicationTime',
key: 'applicationTime',
width: 150
title: '当前状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '操作',
@@ -334,102 +301,180 @@ const columns = [
const releases = ref([
{
id: 1,
releaseNumber: 'REL-202401180001',
customerName: '张三',
contractNumber: 'CON-202401180001',
collateralType: 'house',
status: 'pending',
collateralDescription: '北京市朝阳区某小区3室2厅建筑面积120平米',
loanAmount: 200000,
collateralValue: 500000,
applicationTime: '2024-01-18 09:30:00',
processTime: null,
completeTime: null,
phone: '13800138000',
idCard: '110101199001011234',
reason: '贷款已还清,申请解押房产',
remark: '',
applicationNumber: '20240227145555918',
productName: '中国工商银行扎旗支行"畜禽活体抵押"',
farmerName: '刘超',
applicantName: '1',
applicantIdNumber: '511***********3017',
applicantPhone: '138****0459',
assetType: '牛',
releaseQuantity: '10头',
releaseAmount: 10000.00,
status: 'released',
applicationTime: '2024-02-27 14:55:55',
history: [
{
id: 1,
action: 'apply',
operator: '张三',
time: '2024-01-18 09:30:00',
operator: '刘超',
time: '2024-02-27 14:55:55',
comment: '提交解押申请'
},
{
id: 2,
action: 'complete',
operator: '系统',
time: '2024-02-27 15:30:00',
comment: '解押手续办理完成'
}
]
},
{
id: 2,
releaseNumber: 'REL-202401180002',
customerName: '李四',
contractNumber: 'CON-202401180002',
collateralType: 'car',
status: 'processing',
collateralDescription: '2020年宝马X5车牌号京A12345',
loanAmount: 500000,
collateralValue: 600000,
applicationTime: '2024-01-17 14:20:00',
processTime: '2024-01-18 10:15:00',
completeTime: null,
phone: '13900139000',
idCard: '110101199002021234',
reason: '车辆贷款已还清,申请解押车辆',
remark: '',
applicationNumber: '20240226113416302',
productName: '惠农贷',
farmerName: '刘超',
applicantName: '1',
applicantIdNumber: '511***********3017',
applicantPhone: '138****0459',
assetType: '牛',
releaseQuantity: '10头',
releaseAmount: 0.00,
status: 'released',
applicationTime: '2024-02-26 11:34:16',
history: [
{
id: 1,
action: 'apply',
operator: '李四',
time: '2024-01-17 14:20:00',
operator: '刘超',
time: '2024-02-26 11:34:16',
comment: '提交解押申请'
},
{
id: 2,
action: 'process',
operator: '王经理',
time: '2024-01-18 10:15:00',
comment: '开始处理解押申请'
action: 'complete',
operator: '系统',
time: '2024-02-26 12:00:00',
comment: '解押手续办理完成'
}
]
},
{
id: 3,
releaseNumber: 'REL-202401180003',
customerName: '王五',
contractNumber: 'CON-202401180003',
collateralType: 'land',
status: 'completed',
collateralDescription: '北京市海淀区某地块面积500平米',
loanAmount: 1000000,
collateralValue: 2000000,
applicationTime: '2024-01-16 16:45:00',
processTime: '2024-01-17 09:30:00',
completeTime: '2024-01-17 15:20:00',
phone: '13700137000',
idCard: '110101199003031234',
reason: '土地贷款已还清,申请解押土地',
remark: '',
applicationNumber: '20240223140542290',
productName: '惠农贷',
farmerName: '刘超',
applicantName: '张洪彬',
applicantIdNumber: '511***********3017',
applicantPhone: '138****0459',
assetType: '牛',
releaseQuantity: '10头',
releaseAmount: 1000000.00,
status: 'released',
applicationTime: '2024-02-23 14:05:42',
history: [
{
id: 1,
action: 'apply',
operator: '王五',
time: '2024-01-16 16:45:00',
operator: '张洪彬',
time: '2024-02-23 14:05:42',
comment: '提交解押申请'
},
{
id: 2,
action: 'process',
operator: '赵经理',
time: '2024-01-17 09:30:00',
comment: '开始处理解押申请'
action: 'complete',
operator: '系统',
time: '2024-02-23 15:00:00',
comment: '解押手续办理完成'
}
]
},
{
id: 4,
applicationNumber: '20231131890123456',
productName: '中国工商银行扎旗支行"畜禽活体抵押"',
farmerName: '田小平',
applicantName: '田小平',
applicantIdNumber: '150***********3140',
applicantPhone: '139****5685',
assetType: '牛',
releaseQuantity: '30头',
releaseAmount: 420000.00,
status: 'released',
applicationTime: '2023-11-31 08:90:12',
history: [
{
id: 1,
action: 'apply',
operator: '田小平',
time: '2023-11-31 08:90:12',
comment: '提交解押申请'
},
{
id: 3,
id: 2,
action: 'complete',
operator: '赵经理',
time: '2024-01-17 15:20:00',
operator: '系统',
time: '2023-11-31 10:00:00',
comment: '解押手续办理完成'
}
]
},
{
id: 5,
applicationNumber: '20231131789012345',
productName: '中国农业银行扎旗支行"畜禽活体抵押"',
farmerName: '杜宝民',
applicantName: '杜宝民',
applicantIdNumber: '150***********7238',
applicantPhone: '159****2749',
assetType: '牛',
releaseQuantity: '30头',
releaseAmount: 420000.00,
status: 'released',
applicationTime: '2023-11-31 07:89:01',
history: [
{
id: 1,
action: 'apply',
operator: '杜宝民',
time: '2023-11-31 07:89:01',
comment: '提交解押申请'
},
{
id: 2,
action: 'complete',
operator: '系统',
time: '2023-11-31 09:00:00',
comment: '解押手续办理完成'
}
]
},
{
id: 6,
applicationNumber: '20231131901234567',
productName: '中国农业银行扎旗支行"畜禽活体抵押"',
farmerName: '满良',
applicantName: '满良',
applicantIdNumber: '150***********5140',
applicantPhone: '158****9502',
assetType: '牛',
releaseQuantity: '38头',
releaseAmount: 530000.00,
status: 'released',
applicationTime: '2023-11-31 09:01:23',
history: [
{
id: 1,
action: 'apply',
operator: '满良',
time: '2023-11-31 09:01:23',
comment: '提交解押申请'
},
{
id: 2,
action: 'complete',
operator: '系统',
time: '2023-11-31 11:00:00',
comment: '解押手续办理完成'
}
]
@@ -440,19 +485,21 @@ const releases = ref([
const filteredReleases = computed(() => {
let result = releases.value
if (searchText.value) {
result = result.filter(release =>
release.customerName.toLowerCase().includes(searchText.value.toLowerCase()) ||
release.contractNumber.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(release => release.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(release => release.collateralType === typeFilter.value)
if (searchQuery.value.value) {
const searchValue = searchQuery.value.value.toLowerCase()
const field = searchQuery.value.field
result = result.filter(release => {
if (field === 'applicationNumber') {
return release.applicationNumber.toLowerCase().includes(searchValue)
} else if (field === 'customerName') {
return release.applicantName.toLowerCase().includes(searchValue) ||
release.farmerName.toLowerCase().includes(searchValue)
} else if (field === 'productName') {
return release.productName.toLowerCase().includes(searchValue)
}
return true
})
}
return result
@@ -463,8 +510,11 @@ const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
const handleReset = () => {
searchQuery.value = {
field: 'applicationNumber',
value: ''
}
}
const handleTableChange = (pag) => {
@@ -472,51 +522,13 @@ const handleTableChange = (pag) => {
pagination.value.pageSize = pag.pageSize
}
const handleCreateRelease = () => {
message.info('新建解押功能开发中...')
}
const handleView = (record) => {
selectedRelease.value = record
detailModalVisible.value = true
}
const handleProcess = (record) => {
selectedRelease.value = record
processForm.value.result = 'approve'
processForm.value.comment = ''
processForm.value.remark = ''
processModalVisible.value = true
}
const handleComplete = (record) => {
record.status = 'completed'
record.completeTime = new Date().toLocaleString()
record.history.push({
id: Date.now(),
action: 'complete',
operator: '当前用户',
time: new Date().toLocaleString(),
comment: '解押手续办理完成'
})
message.success('解押完成')
}
const handleReject = (record) => {
record.status = 'rejected'
record.processTime = new Date().toLocaleString()
record.history.push({
id: Date.now(),
action: 'reject',
operator: '当前用户',
time: new Date().toLocaleString(),
comment: '解押申请被拒绝'
})
message.success('解押申请已拒绝')
const handleEdit = (record) => {
message.info(`编辑解押申请: ${record.applicationNumber}`)
}
const handleProcessSubmit = () => {
@@ -548,6 +560,7 @@ const handleProcessCancel = () => {
const getStatusColor = (status) => {
const colors = {
released: 'default',
pending: 'orange',
processing: 'blue',
completed: 'green',
@@ -558,6 +571,7 @@ const getStatusColor = (status) => {
const getStatusText = (status) => {
const texts = {
released: '已解押',
pending: '待处理',
processing: '处理中',
completed: '已完成',
@@ -607,10 +621,7 @@ const getHistoryActionText = (action) => {
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
return `${amount.toFixed(2)}`
}
// 生命周期
@@ -620,40 +631,43 @@ onMounted(() => {
</script>
<style scoped>
.loan-release {
.loan-release-page {
padding: 24px;
background-color: #f0f2f5;
min-height: calc(100vh - 134px);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
font-size: 28px;
color: #333;
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
.search-filter-section {
background-color: #fff;
padding: 24px;
}
.search-section {
border-radius: 8px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.table-section {
margin-top: 16px;
.releases-table-section {
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow-x: auto;
}
.release-detail {
@@ -706,4 +720,56 @@ onMounted(() => {
.process-content {
padding: 16px 0;
}
/* 表格样式优化 */
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f5f5f5;
}
/* 状态标签样式 */
:deep(.ant-tag) {
border-radius: 4px;
font-size: 12px;
padding: 2px 8px;
}
/* 操作按钮样式 */
:deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
line-height: 1.2;
}
/* 分页样式 */
:deep(.ant-pagination) {
margin-top: 16px;
text-align: right;
}
/* 响应式调整 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.search-filter-section .ant-col {
margin-bottom: 16px;
}
.search-filter-section .ant-col:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html>
<head>
<title>新增项目功能测试</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #1890ff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #40a9ff; }
.result { margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 4px; }
.success { background: #f6ffed; border: 1px solid #b7eb8f; }
.error { background: #fff2f0; border: 1px solid #ffccc7; }
</style>
</head>
<body>
<h1>新增项目功能测试</h1>
<div class="form-group">
<label>项目名称:</label>
<input type="text" id="name" value="测试项目_20241220" />
</div>
<div class="form-group">
<label>养殖场名称:</label>
<input type="text" id="farmName" value="测试养殖场" />
</div>
<div class="form-group">
<label>监管对象:</label>
<input type="text" id="supervisionObject" value="牛" />
</div>
<div class="form-group">
<label>监管周期:</label>
<input type="text" id="supervisionPeriod" value="12个月" />
</div>
<div class="form-group">
<label>监管数量:</label>
<input type="number" id="supervisionQuantity" value="100" />
</div>
<div class="form-group">
<label>监管金额:</label>
<input type="number" id="supervisionAmount" value="500000" step="0.01" />
</div>
<div class="form-group">
<label>起始时间:</label>
<input type="date" id="startTime" value="2024-01-01" />
</div>
<div class="form-group">
<label>结束时间:</label>
<input type="date" id="endTime" value="2024-12-31" />
</div>
<div class="form-group">
<label>耳标数量:</label>
<input type="number" id="earTag" value="50" />
</div>
<div class="form-group">
<label>项圈数量:</label>
<input type="number" id="collar" value="30" />
</div>
<div class="form-group">
<label>主机数量:</label>
<input type="number" id="host" value="20" />
</div>
<div class="form-group">
<label>贷款专员:</label>
<input type="text" id="loanOfficer" value="张专员" />
</div>
<div class="form-group">
<label>项目描述:</label>
<textarea id="description" rows="3">这是一个测试项目</textarea>
</div>
<button onclick="testCreateProject()">测试创建项目</button>
<div id="result" class="result" style="display: none;"></div>
<script>
let token = '';
async function testCreateProject() {
try {
// 1. 先登录
console.log('1. 登录获取token...');
const loginResponse = await fetch('http://localhost:5351/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'Admin123456'
})
});
const loginData = await loginResponse.json();
if (!loginData.success) {
throw new Error('登录失败: ' + loginData.message);
}
token = loginData.data.token;
console.log('登录成功token:', token.substring(0, 20) + '...');
// 2. 创建项目
console.log('2. 创建项目...');
const projectData = {
name: document.getElementById('name').value,
status: 'supervision',
farmName: document.getElementById('farmName').value,
supervisionObject: document.getElementById('supervisionObject').value,
supervisionQuantity: parseInt(document.getElementById('supervisionQuantity').value),
supervisionPeriod: document.getElementById('supervisionPeriod').value,
supervisionAmount: parseFloat(document.getElementById('supervisionAmount').value),
startTime: document.getElementById('startTime').value,
endTime: document.getElementById('endTime').value,
earTag: parseInt(document.getElementById('earTag').value),
collar: parseInt(document.getElementById('collar').value),
host: parseInt(document.getElementById('host').value),
loanOfficer: document.getElementById('loanOfficer').value,
description: document.getElementById('description').value
};
console.log('项目数据:', projectData);
const createResponse = await fetch('http://localhost:5351/api/projects', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(projectData)
});
const createData = await createResponse.json();
console.log('创建响应:', createData);
const resultDiv = document.getElementById('result');
resultDiv.style.display = 'block';
if (createData.success) {
resultDiv.className = 'result success';
resultDiv.innerHTML = `
<h3>✅ 项目创建成功!</h3>
<p><strong>项目ID:</strong> ${createData.data.id}</p>
<p><strong>项目名称:</strong> ${createData.data.name}</p>
<p><strong>养殖场:</strong> ${createData.data.farmName}</p>
<p><strong>监管对象:</strong> ${createData.data.supervisionObject}</p>
<p><strong>监管数量:</strong> ${createData.data.supervisionQuantity}</p>
<p><strong>监管金额:</strong> ${createData.data.supervisionAmount}元</p>
<p><strong>创建时间:</strong> ${new Date(createData.data.createdAt).toLocaleString()}</p>
`;
} else {
resultDiv.className = 'result error';
resultDiv.innerHTML = `
<h3>❌ 项目创建失败</h3>
<p><strong>错误信息:</strong> ${createData.message}</p>
`;
}
} catch (error) {
console.error('测试失败:', error);
const resultDiv = document.getElementById('result');
resultDiv.style.display = 'block';
resultDiv.className = 'result error';
resultDiv.innerHTML = `
<h3>❌ 测试失败</h3>
<p><strong>错误信息:</strong> ${error.message}</p>
`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<title>API测试</title>
</head>
<body>
<h1>API测试页面</h1>
<button onclick="testLogin()">测试登录</button>
<button onclick="testProjects()">测试项目接口</button>
<div id="result"></div>
<script>
let token = '';
async function testLogin() {
try {
const response = await fetch('http://localhost:5351/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'Admin123456'
})
});
const data = await response.json();
console.log('登录响应:', data);
if (data.success) {
token = data.data.token;
document.getElementById('result').innerHTML =
'<p style="color: green;">登录成功Token: ' + token.substring(0, 20) + '...</p>';
} else {
document.getElementById('result').innerHTML =
'<p style="color: red;">登录失败: ' + data.message + '</p>';
}
} catch (error) {
console.error('登录错误:', error);
document.getElementById('result').innerHTML =
'<p style="color: red;">登录错误: ' + error.message + '</p>';
}
}
async function testProjects() {
if (!token) {
document.getElementById('result').innerHTML =
'<p style="color: red;">请先登录!</p>';
return;
}
try {
const response = await fetch('http://localhost:5351/api/projects?page=1&limit=12&search=&status=', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log('项目接口响应:', data);
if (data.success) {
document.getElementById('result').innerHTML =
'<p style="color: green;">项目接口成功!项目数量: ' + data.data.projects.length + '</p>' +
'<pre>' + JSON.stringify(data.data.projects.slice(0, 2), null, 2) + '</pre>';
} else {
document.getElementById('result').innerHTML =
'<p style="color: red;">项目接口失败: ' + data.message + '</p>';
}
} catch (error) {
console.error('项目接口错误:', error);
document.getElementById('result').innerHTML =
'<p style="color: red;">项目接口错误: ' + error.message + '</p>';
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html>
<head>
<title>监管任务API测试</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.test-section { margin-bottom: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #1890ff; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
button:hover { background: #40a9ff; }
.result { margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; white-space: pre-wrap; font-family: monospace; font-size: 12px; }
.success { background: #f6ffed; border: 1px solid #b7eb8f; }
.error { background: #fff2f0; border: 1px solid #ffccc7; }
</style>
</head>
<body>
<h1>监管任务API测试页面</h1>
<div class="test-section">
<h2>1. 登录测试</h2>
<button onclick="testLogin()">测试登录</button>
<div id="loginResult" class="result" style="display: none;"></div>
</div>
<div class="test-section">
<h2>2. 获取监管任务列表</h2>
<button onclick="testGetTasks()" id="getTasksBtn" disabled>获取监管任务列表</button>
<div id="tasksResult" class="result" style="display: none;"></div>
</div>
<div class="test-section">
<h2>3. 获取监管任务统计</h2>
<button onclick="testGetStats()" id="getStatsBtn" disabled>获取监管任务统计</button>
<div id="statsResult" class="result" style="display: none;"></div>
</div>
<div class="test-section">
<h2>4. 创建监管任务</h2>
<button onclick="testCreateTask()" id="createTaskBtn" disabled>创建监管任务</button>
<div id="createResult" class="result" style="display: none;"></div>
</div>
<script>
let token = '';
async function testLogin() {
try {
const response = await fetch('http://localhost:5351/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'Admin123456'
})
});
const data = await response.json();
const resultDiv = document.getElementById('loginResult');
resultDiv.style.display = 'block';
if (data.success) {
token = data.data.token;
resultDiv.className = 'result success';
resultDiv.textContent = `登录成功!\nToken: ${token.substring(0, 50)}...\n用户: ${data.data.user.username}`;
// 启用其他按钮
document.getElementById('getTasksBtn').disabled = false;
document.getElementById('getStatsBtn').disabled = false;
document.getElementById('createTaskBtn').disabled = false;
} else {
resultDiv.className = 'result error';
resultDiv.textContent = `登录失败: ${data.message}`;
}
} catch (error) {
const resultDiv = document.getElementById('loginResult');
resultDiv.style.display = 'block';
resultDiv.className = 'result error';
resultDiv.textContent = `登录错误: ${error.message}`;
}
}
async function testGetTasks() {
if (!token) {
alert('请先登录!');
return;
}
try {
const response = await fetch('http://localhost:5351/api/supervision-tasks?page=1&limit=10', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
const resultDiv = document.getElementById('tasksResult');
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.className = 'result success';
resultDiv.textContent = `获取监管任务列表成功!\n任务数量: ${data.data.tasks.length}\n总数量: ${data.data.pagination.total}\n\n前3个任务:\n${JSON.stringify(data.data.tasks.slice(0, 3), null, 2)}`;
} else {
resultDiv.className = 'result error';
resultDiv.textContent = `获取失败: ${data.message}`;
}
} catch (error) {
const resultDiv = document.getElementById('tasksResult');
resultDiv.style.display = 'block';
resultDiv.className = 'result error';
resultDiv.textContent = `请求错误: ${error.message}`;
}
}
async function testGetStats() {
if (!token) {
alert('请先登录!');
return;
}
try {
const response = await fetch('http://localhost:5351/api/supervision-tasks/stats', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
const resultDiv = document.getElementById('statsResult');
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.className = 'result success';
resultDiv.textContent = `获取监管任务统计成功!\n总计: ${data.data.total}\n待监管: ${data.data.pending}\n监管中: ${data.data.supervising}\n已完成: ${data.data.completed}\n已暂停: ${data.data.suspended}`;
} else {
resultDiv.className = 'result error';
resultDiv.textContent = `获取失败: ${data.message}`;
}
} catch (error) {
const resultDiv = document.getElementById('statsResult');
resultDiv.style.display = 'block';
resultDiv.className = 'result error';
resultDiv.textContent = `请求错误: ${error.message}`;
}
}
async function testCreateTask() {
if (!token) {
alert('请先登录!');
return;
}
try {
const newTask = {
applicationNumber: 'APP_TEST_' + Date.now(),
contractNumber: 'CONTRACT_TEST_' + Date.now(),
productName: '测试农业贷款产品',
customerName: '测试客户',
idType: 'id_card',
idNumber: '110101199001011234',
assetType: 'cattle',
assetQuantity: 10,
supervisionStatus: 'pending',
startTime: '2024-12-20',
endTime: '2024-12-31',
loanAmount: 100000.00,
interestRate: 0.0600,
loanTerm: 12,
supervisorName: '测试监管员',
supervisorPhone: '13800138000',
farmAddress: '测试养殖场地址',
remarks: '这是一个测试监管任务'
};
const response = await fetch('http://localhost:5351/api/supervision-tasks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(newTask)
});
const data = await response.json();
const resultDiv = document.getElementById('createResult');
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.className = 'result success';
resultDiv.textContent = `创建监管任务成功!\n任务ID: ${data.data.id}\n申请单号: ${data.data.applicationNumber}\n客户姓名: ${data.data.customerName}\n监管状态: ${data.data.supervisionStatus}`;
} else {
resultDiv.className = 'result error';
resultDiv.textContent = `创建失败: ${data.message}`;
}
} catch (error) {
const resultDiv = document.getElementById('createResult');
resultDiv.style.display = 'block';
resultDiv.className = 'result error';
resultDiv.textContent = `请求错误: ${error.message}`;
}
}
</script>
</body>
</html>

View File

@@ -6,13 +6,7 @@
<h2 v-if="!collapsed">政府管理系统</h2>
<h2 v-else>政府</h2>
</div>
<a-menu
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="inline"
:items="menus"
@click="handleMenuClick"
/>
<Sidebar />
</a-layout-sider>
<!-- 主内容区 -->
@@ -70,288 +64,46 @@
</template>
<script setup>
import { ref, onMounted, h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ref } from 'vue'
import { useRouter } 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
LogoutOutlined
} from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
import Sidebar from '@/layout/Sidebar.vue'
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();
});
userStore.logout()
router.push('/login')
}
</script>
<style scoped>
.logo {
height: 32px;
margin: 16px;
color: white;
height: 64px;
line-height: 64px;
background: #002140;
text-align: center;
overflow: hidden;
}
.logo h2 {
color: white;
margin: 0;
font-size: 18px;
}
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
@@ -359,4 +111,11 @@ onMounted(() => {
.trigger:hover {
color: #1890ff;
}
.ant-dropdown-link {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
</style>

View File

@@ -4,198 +4,148 @@
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 key="/index/data_center">
<template #icon><BarChartOutlined /></template>
<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-menu-item key="/price/price_list">
<template #icon><LineChartOutlined /></template>
<span>市场行情</span>
</a-menu-item>
<!-- 人员管理 -->
<a-sub-menu
v-if="hasPermission('personnel')"
key="personnel"
:icon="UserOutlined"
>
<a-sub-menu key="personnel">
<template #icon><UserOutlined /></template>
<template #title>
<span>人员管理</span>
</template>
<a-menu-item key="/personnel/list">
<span>人员列表</span>
<a-menu-item key="/personnel/department">
<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 key="/personnel/staff">
<span>行政人员</span>
</a-menu-item>
</a-sub-menu>
<!-- 系统设置 -->
<a-sub-menu
v-if="hasPermission('system')"
key="system"
:icon="SettingOutlined"
>
<!-- 养殖户管理 -->
<a-menu-item key="/farmer">
<template #icon><TeamOutlined /></template>
<span>养殖户管理</span>
</a-menu-item>
<!-- 智慧仓库 -->
<a-sub-menu key="smart-warehouse">
<template #icon><HddOutlined /></template>
<template #title>
<span>系统设置</span>
<span>智慧仓库</span>
</template>
<a-menu-item key="/system/config">
<span>系统配置</span>
<a-menu-item key="/smart-warehouse/smart-collar">
<span>智能项圈</span>
</a-menu-item>
<a-menu-item key="/system/logs">
<span>系统日志</span>
<a-menu-item key="/smart-warehouse/smart-earmark">
<span>智能耳标</span>
</a-menu-item>
<a-menu-item key="/system/backup">
<span>数据备份</span>
<a-menu-item key="/smart-warehouse/smart-host">
<span>智能主机</span>
</a-menu-item>
</a-sub-menu>
<!-- 帮助中心 -->
<a-menu-item key="/help" :icon="QuestionCircleOutlined">
<span>帮助中心</span>
<!-- 无纸化服务 -->
<a-menu-item key="/paperless">
<template #icon><FileTextOutlined /></template>
<span>无纸化服务</span>
</a-menu-item>
<!-- 屠宰无害化 -->
<a-menu-item key="/slaughter">
<template #icon><SafetyOutlined /></template>
<span>屠宰无害化</span>
</a-menu-item>
<!-- 生资认证 -->
<a-menu-item key="/examine/index">
<template #icon><CheckCircleOutlined /></template>
<span>生资认证</span>
</a-menu-item>
<!-- 线上问诊 -->
<a-menu-item key="/consultation">
<template #icon><MedicineBoxOutlined /></template>
<span>线上问诊</span>
</a-menu-item>
<!-- 养牛学院 -->
<a-menu-item key="/academy">
<template #icon><ReadOutlined /></template>
<span>养牛学院</span>
</a-menu-item>
<!-- 消息通知 -->
<a-menu-item key="/notification">
<template #icon><BellOutlined /></template>
<span>消息通知</span>
</a-menu-item>
</a-menu>
</template>
<script lang="js">
import { ref, onMounted, watch } from 'vue'
import { ref, onMounted, watch, h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import {
DashboardOutlined,
FileOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
AlertOutlined,
FormOutlined,
BarChartOutlined,
LineChartOutlined,
UserOutlined,
SettingOutlined,
QuestionCircleOutlined
TeamOutlined,
HddOutlined,
ExperimentOutlined,
FileTextOutlined,
SafetyOutlined,
BankOutlined,
CheckCircleOutlined,
ShoppingOutlined,
CommentOutlined,
MedicineBoxOutlined,
ReadOutlined,
BellOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
export default {
setup() {
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
// 当前选中的菜单项
const selectedKeys = ref([])
// 当前选中的菜单项
const selectedKeys = ref([])
// 展开的菜单项
const openKeys = ref([])
// 展开的菜单项
const openKeys = ref([])
// 检查是否有权限
const hasPermission = (permission) => {
return authStore.hasPermission([permission])
}
// 检查是否有权限
const hasPermission = (permission) => {
return authStore.hasPermission([permission])
}
// 处理菜单选择
const handleMenuSelect = ({ key }) => {
router.push(key)
}
// 处理菜单选择
const handleMenuSelect = ({ key }) => {
// 确保路由存在
if (key) {
console.log('导航到路由:', key)
// 使用replace而不是push避免历史记录堆积
router.replace(key).catch(err => {
console.error('路由导航失败:', err)
})
}
}
// 处理展开/收起
const handleOpenChange = (keys) => {
@@ -211,7 +161,8 @@ const getParentMenuKey = (path) => {
'/epidemic': 'epidemic',
'/approval': 'approval',
'/personnel': 'personnel',
'/system': 'system'
'/system': 'system',
'/smart-warehouse': 'smart-warehouse'
}
for (const [prefix, key] of Object.entries(menuMap)) {
@@ -219,6 +170,12 @@ const getParentMenuKey = (path) => {
return key
}
}
// 特殊处理智慧仓库路径
if (path.includes('smart-warehouse')) {
return 'smart-warehouse'
}
return ''
}
@@ -243,10 +200,37 @@ watch(
}
)
// 返回需要在模板中使用的内容
return {
hasPermission,
handleMenuSelect,
handleOpenChange,
selectedKeys,
openKeys,
DashboardOutlined: () => h(DashboardOutlined),
BarChartOutlined: () => h(BarChartOutlined),
LineChartOutlined: () => h(LineChartOutlined),
UserOutlined: () => h(UserOutlined),
TeamOutlined: () => h(TeamOutlined),
HddOutlined: () => h(HddOutlined),
ExperimentOutlined: () => h(ExperimentOutlined),
FileTextOutlined: () => h(FileTextOutlined),
SafetyOutlined: () => h(SafetyOutlined),
BankOutlined: () => h(BankOutlined),
CheckCircleOutlined: () => h(CheckCircleOutlined),
ShoppingOutlined: () => h(ShoppingOutlined),
CommentOutlined: () => h(CommentOutlined),
MedicineBoxOutlined: () => h(MedicineBoxOutlined),
ReadOutlined: () => h(ReadOutlined),
BellOutlined: () => h(BellOutlined)
}
// 组件挂载时初始化
onMounted(() => {
updateSelectedState()
})
}
}
</script>
<style scoped>

View File

@@ -1,8 +1,9 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './stores'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import { ConfigProvider } from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import dayjs from 'dayjs'
import './mock' // 导入mock服务
@@ -21,9 +22,25 @@ globalThis.dayjs = dayjs
// 创建应用实例
const app = createApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
app.use(router)
app.use(store)
app.use(pinia)
app.use(Antd)
// 配置 Ant Design Vue
app.use(ConfigProvider, {
locale: zhCN,
// 明确配置日期库为dayjs
dateFormatter: 'dayjs',
// 提供完整配置的dayjs实例确保在组件中能正确访问到配置好的dayjs
getDayjsInstance: () => {
// 确保返回一个已经正确配置了语言和插件的dayjs实例
return dayjs
},
// 安全地获取弹出层容器防止trigger为null导致的错误
getPopupContainer: (trigger) => trigger?.parentElement || document.body
})
app.mount('#app')

View File

@@ -2,16 +2,16 @@ import { createRouter, createWebHistory } from 'vue-router'
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 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 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 WarehouseManagement from '@/views/WarehouseManagement.vue'
// import LogManagement from '@/views/LogManagement.vue'
// 新增页面组件
import MarketPrice from '@/views/MarketPrice.vue'
import DataCenter from '@/views/DataCenter.vue'
@@ -25,6 +25,8 @@ import CommunicationCommunity from '@/views/CommunicationCommunity.vue'
import OnlineConsultation from '@/views/OnlineConsultation.vue'
import CattleAcademy from '@/views/CattleAcademy.vue'
import MessageNotification from '@/views/MessageNotification.vue'
import AdminDepartment from '@/views/AdminDepartment.vue'
import AdminStaff from '@/views/AdminStaff.vue'
const routes = [
{
@@ -59,7 +61,21 @@ const routes = [
path: 'personnel',
name: 'PersonnelManagement',
component: PersonnelManagement,
meta: { title: '人员管理' }
meta: { title: '人员管理' },
children: [
{
path: 'department',
name: 'AdminDepartment',
component: AdminDepartment,
meta: { title: '行政部门' }
},
{
path: 'staff',
name: 'AdminStaff',
component: AdminStaff,
meta: { title: '行政人员' }
}
]
},
{
path: 'farmer',
@@ -71,7 +87,27 @@ const routes = [
path: 'smart-warehouse',
name: 'SmartWarehouse',
component: SmartWarehouse,
meta: { title: '智仓库' }
meta: { title: '智仓库' },
children: [
{
path: 'smart-collar',
name: 'SmartCollar',
component: () => import('@/views/smart-warehouse/smart-hardware/SmartCollar.vue'),
meta: { title: '智能项圈' }
},
{
path: 'smart-earmark',
name: 'SmartEarmark',
component: () => import('@/views/smart-warehouse/smart-hardware/SmartEarmark.vue'),
meta: { title: '智能耳标' }
},
{
path: 'smart-host',
name: 'SmartHost',
component: () => import('@/views/smart-warehouse/smart-hardware/SmartHost.vue'),
meta: { title: '智能主机' }
}
]
},
{
path: 'breed-improvement',
@@ -133,60 +169,60 @@ const routes = [
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: '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: '可视化分析' }
// }
]
}
]

View File

@@ -0,0 +1,244 @@
<template>
<div class="admin-department-container">
<div class="header">
<div class="title">部门防疫部门</div>
<div class="actions">
<a-button type="primary" class="action-btn">删除部门</a-button>
<a-button type="primary" class="action-btn">新增岗位</a-button>
</div>
</div>
<a-table :columns="columns" :data-source="departmentData" :pagination="false" class="department-table">
<template #headerCell="{ column }">
<span>{{ column.title }}</span>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'permission'">
<a-tag v-if="record.permission === '已设置权限'" color="success">{{ record.permission }}</a-tag>
<a-tag v-else color="warning">{{ record.permission }}</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="editPosition(record)">编辑岗位名称</a-button>
<a-button type="link" @click="deletePosition(record)">删除岗位</a-button>
<a-button type="link" @click="setPermission(record)">设置权限</a-button>
</a-space>
</template>
</template>
</a-table>
<div class="header">
<div class="title">部门检疫部门</div>
<div class="actions">
<a-button type="primary" class="action-btn">删除部门</a-button>
<a-button type="primary" class="action-btn">新增岗位</a-button>
</div>
</div>
<a-table :columns="columns" :data-source="inspectionData" :pagination="false" class="department-table">
<template #headerCell="{ column }">
<span>{{ column.title }}</span>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'permission'">
<a-tag v-if="record.permission === '已设置权限'" color="success">{{ record.permission }}</a-tag>
<a-tag v-else color="warning">{{ record.permission }}</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="editPosition(record)">编辑岗位名称</a-button>
<a-button type="link" @click="deletePosition(record)">删除岗位</a-button>
<a-button type="link" @click="setPermission(record)">设置权限</a-button>
</a-space>
</template>
</template>
</a-table>
<div class="header">
<div class="title">部门冷配管理部门</div>
<div class="actions">
<a-button type="primary" class="action-btn">删除部门</a-button>
<a-button type="primary" class="action-btn">新增岗位</a-button>
</div>
</div>
<a-table :columns="columns" :data-source="coldChainData" :pagination="false" class="department-table">
<template #headerCell="{ column }">
<span>{{ column.title }}</span>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'permission'">
<a-tag v-if="record.permission === '已设置权限'" color="success">{{ record.permission }}</a-tag>
<a-tag v-else color="warning">{{ record.permission }}</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="editPosition(record)">编辑岗位名称</a-button>
<a-button type="link" @click="deletePosition(record)">删除岗位</a-button>
<a-button type="link" @click="setPermission(record)">设置权限</a-button>
</a-space>
</template>
</template>
</a-table>
<div class="header">
<div class="title">部门测试部门</div>
<div class="actions">
<a-button type="primary" class="action-btn">删除部门</a-button>
<a-button type="primary" class="action-btn">新增岗位</a-button>
</div>
</div>
<a-table :columns="columns" :data-source="testingData" :pagination="false" class="department-table">
<template #headerCell="{ column }">
<span>{{ column.title }}</span>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'permission'">
<a-tag v-if="record.permission === '已设置权限'" color="success">{{ record.permission }}</a-tag>
<a-tag v-else color="warning">{{ record.permission }}</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="editPosition(record)">编辑岗位名称</a-button>
<a-button type="link" @click="deletePosition(record)">删除岗位</a-button>
<a-button type="link" @click="setPermission(record)">设置权限</a-button>
</a-space>
</template>
</template>
</a-table>
<div class="add-department">
<a-button type="primary" @click="showAddDepartmentModal">新增部门</a-button>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
// 表格列定义
const columns = [
{
title: '岗位',
dataIndex: 'position',
key: 'position',
},
{
title: '系统基础权限',
dataIndex: 'permission',
key: 'permission',
},
{
title: '操作',
key: 'action',
}
]
// 防疫部门数据
const departmentData = ref([
{
key: '1',
position: '防疫员',
permission: '已设置权限',
},
{
key: '2',
position: '防疫管理员',
permission: '已设置权限',
}
])
// 检疫部门数据
const inspectionData = ref([
{
key: '1',
position: '检疫员',
permission: '未设置权限',
}
])
// 冷配管理部门数据
const coldChainData = ref([
{
key: '1',
position: '冷配员',
permission: '已设置权限',
}
])
// 测试部门数据
const testingData = ref([
{
key: '1',
position: '内部测试账号',
permission: '已设置权限',
}
])
// 编辑岗位
const editPosition = (record) => {
message.info(`编辑岗位:${record.position}`)
}
// 删除岗位
const deletePosition = (record) => {
message.info(`删除岗位:${record.position}`)
}
// 设置权限
const setPermission = (record) => {
message.info(`设置权限:${record.position}`)
}
// 显示新增部门模态框
const showAddDepartmentModal = () => {
message.info('显示新增部门对话框')
}
</script>
<style scoped>
.admin-department-container {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: bold;
}
.actions {
display: flex;
gap: 10px;
}
.action-btn {
margin-left: 8px;
}
.department-table {
margin-bottom: 24px;
}
.add-department {
margin-top: 24px;
display: flex;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,265 @@
<template>
<div class="admin-staff-container">
<div class="header">
<a-button type="primary">新增人员</a-button>
<a-input-search
v-model:value="searchValue"
placeholder="请输入员工姓名"
style="width: 200px"
@search="onSearch"
/>
</div>
<a-table :columns="columns" :data-source="staffData" :pagination="pagination" class="staff-table">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-switch v-model:checked="record.status" />
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="editStaff(record)">编辑</a-button>
<a-button type="link" @click="resetPassword(record)">修改密码</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
// 搜索值
const searchValue = ref('')
// 表格列定义
const columns = [
{
title: '员工姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '所属部门',
dataIndex: 'department',
key: 'department',
},
{
title: '任职岗位',
dataIndex: 'position',
key: 'position',
},
{
title: '联系电话',
dataIndex: 'phone',
key: 'phone',
},
{
title: '身份证号码',
dataIndex: 'idCard',
key: 'idCard',
},
{
title: '账号使用状态',
dataIndex: 'status',
key: 'status',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
},
{
title: '操作',
key: 'action',
}
]
// 员工数据
const staffData = ref([
{
key: '1',
name: '内部测试账号',
department: '测试部门',
position: '内部测试账号',
phone: '187****4778',
idCard: '--',
status: true,
createdAt: '2023-11-24 17:24:32'
},
{
key: '2',
name: '扎拉嘎',
department: '冷配管理部门',
position: '冷配员',
phone: '195****9912',
idCard: '--',
status: true,
createdAt: '2023-10-10 13:59:03'
},
{
key: '3',
name: '董立鹏',
department: '防疫部',
position: '防疫管理员',
phone: '151****7022',
idCard: '--',
status: true,
createdAt: '2023-05-19 15:47:54'
},
{
key: '4',
name: '李海涛',
department: '防疫部',
position: '防疫管理员',
phone: '139****6955',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:29:15'
},
{
key: '5',
name: '高凤鸣',
department: '防疫部',
position: '防疫管理员',
phone: '158****4601',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:28:57'
},
{
key: '6',
name: '刘全',
department: '防疫部',
position: '防疫管理员',
phone: '139****6009',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:28:39'
},
{
key: '7',
name: '李志伟',
department: '防疫部',
position: '防疫管理员',
phone: '153****0457',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:28:14'
},
{
key: '8',
name: '李景学',
department: '防疫部',
position: '防疫管理员',
phone: '153****5588',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:27:57'
},
{
key: '9',
name: '王海龙',
department: '防疫部',
position: '防疫管理员',
phone: '150****7222',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:27:39'
},
{
key: '10',
name: '德力根',
department: '防疫部',
position: '防疫管理员',
phone: '150****2938',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:27:22'
},
{
key: '11',
name: '巴日斯',
department: '防疫部',
position: '防疫管理员',
phone: '131****1366',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:27:03'
},
{
key: '12',
name: '邵志勇',
department: '防疫部',
position: '防疫管理员',
phone: '139****0778',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:26:26'
},
{
key: '13',
name: '张日林',
department: '防疫部',
position: '防疫管理员',
phone: '156****6300',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:25:37'
},
{
key: '14',
name: '李燕',
department: '防疫部',
position: '防疫管理员',
phone: '151****1613',
idCard: '--',
status: true,
createdAt: '2023-05-09 14:25:12'
}
])
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 10,
total: staffData.value.length,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
// 搜索处理
const onSearch = (value) => {
message.info(`搜索: ${value}`)
}
// 编辑员工
const editStaff = (record) => {
message.info(`编辑员工: ${record.name}`)
}
// 重置密码
const resetPassword = (record) => {
message.info(`重置密码: ${record.name}`)
}
</script>
<style scoped>
.admin-staff-container {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.staff-table {
margin-top: 16px;
}
</style>

View File

@@ -1,152 +1,158 @@
<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="data-center-container">
<!-- 顶部统计卡片 -->
<div class="stat-cards">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-header">
<div class="stat-title">养殖户总数</div>
<div class="stat-value">{{养殖户总数}}</div>
<div class="stat-change">
<span class="text-success">+5.2%</span> 较上月
<div class="stat-icon">
<span class="iconfont icon-renyuanguanli"></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 class="stat-body">
<div class="stat-value">{{ 养殖户总数 }}</div>
<div class="stat-trend">
<span class="trend-value">+5.2%</span>
<span class="trend-label">较上月</span>
</div>
</div>
<div class="stat-content">
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-header">
<div class="stat-title">存栏总量</div>
<div class="stat-value">{{存栏总量}}</div>
<div class="stat-change">
<span class="text-success">+2.8%</span> 较上月
<div class="stat-icon">
<span class="iconfont icon-niu"></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 class="stat-body">
<div class="stat-value">{{ 存栏总量 }}</div>
<div class="stat-trend">
<span class="trend-value">+2.8%</span>
<span class="trend-label">较上月</span>
</div>
</div>
<div class="stat-content">
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-header">
<div class="stat-title">交易量</div>
<div class="stat-value">{{交易量}}</div>
<div class="stat-change">
<span class="text-danger">-1.5%</span> 较上月
<div class="stat-icon">
<span class="iconfont icon-shengzijiaoyi"></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 class="stat-body">
<div class="stat-value">{{ 交易量 }}</div>
<div class="stat-trend negative">
<span class="trend-value">-1.5%</span>
<span class="trend-label">较上月</span>
</div>
</div>
<div class="stat-content">
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-header">
<div class="stat-title">活跃用户</div>
<div class="stat-value">{{活跃用户}}</div>
<div class="stat-change">
<span class="text-success">+8.3%</span> 较上月
<div class="stat-icon">
<span class="iconfont icon-yonghu"></span>
</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
<div class="stat-body">
<div class="stat-value">{{ 活跃用户 }}</div>
<div class="stat-trend">
<span class="trend-value">+8.3%</span>
<span class="trend-label">较上月</span>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 主要图表区域 -->
<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>
<div class="chart-section">
<a-row :gutter="16">
<a-col :span="16">
<a-card class="chart-card" title="月度交易量趋势">
<div ref="transactionChartRef" class="chart-container"></div>
</a-card>
</a-col>
<a-col :span="8">
<a-card class="chart-card" title="品类占比分析">
<div ref="categoryChartRef" class="chart-container"></div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 第二行图表 -->
<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>
<div class="chart-section">
<a-row :gutter="16">
<a-col :span="12">
<a-card class="chart-card" title="养殖区域分布">
<div ref="mapChartRef" class="chart-container"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card class="chart-card" title="养殖规模分布">
<div ref="scaleChartRef" class="chart-container"></div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 最近数据更新信息 -->
<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 class="update-section">
<a-card class="update-card" title="数据更新信息">
<a-list>
<a-list-item>
<div class="update-item">
<div class="update-icon blue"></div>
<div class="update-content">
<div class="update-title">市场行情数据更新</div>
<div class="update-time">2024-04-10 15:30:00</div>
<div class="update-desc">更新全国主要地区牛肉牛奶饲料价格数据</div>
</div>
</div>
</a-list-item>
<a-list-item>
<div class="update-item">
<div class="update-icon green"></div>
<div class="update-content">
<div class="update-title">养殖户数据同步</div>
<div class="update-time">2024-04-10 10:15:00</div>
<div class="update-desc">新增56家养殖户信息更新32家养殖户状态</div>
</div>
</div>
</a-list-item>
<a-list-item>
<div class="update-item">
<div class="update-icon orange"></div>
<div class="update-content">
<div class="update-title">产品认证数据导入</div>
<div class="update-time">2024-04-09 16:45:00</div>
<div class="update-desc">导入120条生资产品认证信息</div>
</div>
</div>
</a-list-item>
<a-list-item>
<div class="update-item">
<div class="update-icon red"></div>
<div class="update-content">
<div class="update-title">疫病监测数据更新</div>
<div class="update-time">2024-04-09 09:20:00</div>
<div class="update-desc">更新本周疫病监测数据暂无异常情况</div>
</div>
</div>
</a-list-item>
</a-list>
</a-card>
</div>
</div>
</template>
@@ -166,9 +172,122 @@ 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 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: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(58, 77, 233, 0.8)' },
{ offset: 1, color: 'rgba(58, 77, 233, 0.1)' }
])
},
emphasis: {
focus: 'series'
},
data: [2.8, 3.1, 3.5, 3.26, 3.4, 3.6],
itemStyle: {
color: '#3a4de9'
},
lineStyle: {
width: 3,
color: '#3a4de9'
},
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: {
bottom: '5%',
left: 'center'
},
series: [
{
name: '品类占比',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '40%'],
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: 45, name: '肉牛养殖', itemStyle: { color: '#3a4de9' } },
{ value: 30, name: '奶牛养殖', itemStyle: { color: '#4cd137' } },
{ value: 15, name: '犊牛养殖', itemStyle: { color: '#e1b12c' } },
{ value: 10, name: '其他养殖', itemStyle: { color: '#44bd32' } }
]
}
]
}
chart.setOption(option)
window.addEventListener('resize', () => {
chart.resize()
})
}
// 初始化养殖区域分布图表
const initMapChart = () => {
@@ -182,8 +301,8 @@ const initMapChart = () => {
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
bottom: '5%',
left: 'center',
data: ['东北地区', '华北地区', '华东地区', '华南地区', '西南地区', '西北地区']
},
series: [
@@ -212,12 +331,12 @@ const initMapChart = () => {
show: false
},
data: [
{ value: 28, name: '东北地区' },
{ value: 22, name: '华北地区' },
{ value: 18, name: '华东地区' },
{ value: 12, name: '华南地区' },
{ value: 15, name: '西南地区' },
{ value: 5, name: '西北地区' }
{ value: 28, name: '东北地区', itemStyle: { color: '#3a4de9' } },
{ value: 22, name: '华北地区', itemStyle: { color: '#4cd137' } },
{ value: 18, name: '华东地区', itemStyle: { color: '#e1b12c' } },
{ value: 12, name: '华南地区', itemStyle: { color: '#44bd32' } },
{ value: 15, name: '西南地区', itemStyle: { color: '#0097e6' } },
{ value: 5, name: '西北地区', itemStyle: { color: '#8c7ae6' } }
]
}
]
@@ -265,9 +384,8 @@ const initScaleChart = () => {
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' }
{ offset: 0, color: '#3a4de9' },
{ offset: 1, color: '#8c7ae6' }
])
}
}
@@ -281,210 +399,162 @@ const initScaleChart = () => {
})
}
// 初始化交易量趋势图表
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()
initMapChart()
initScaleChart()
}, 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;
}
.data-center-container {
padding: 20px;
}
.stat-cards {
margin-bottom: 20px;
}
.stat-card {
height: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.stat-title {
font-size: 16px;
color: #666;
}
.stat-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #3a4de9;
font-size: 20px;
}
.stat-body {
padding: 16px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.stat-trend {
display: flex;
align-items: center;
color: #52c41a;
}
.stat-trend.negative {
color: #f5222d;
}
.trend-value {
font-weight: bold;
margin-right: 5px;
}
.trend-label {
font-size: 12px;
color: #999;
}
.chart-section {
margin-bottom: 20px;
}
.chart-card {
height: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.chart-container {
height: 300px;
}
.update-section {
margin-bottom: 20px;
}
.update-card {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.update-item {
display: flex;
align-items: flex-start;
padding: 8px 0;
}
.update-icon {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 16px;
margin-top: 5px;
}
.update-icon.blue {
background-color: #1890ff;
}
.update-icon.green {
background-color: #52c41a;
}
.update-icon.orange {
background-color: #fa8c16;
}
.update-icon.red {
background-color: #f5222d;
}
.update-content {
flex: 1;
}
.update-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 4px;
}
.update-time {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.update-desc {
font-size: 13px;
color: #666;
}
</style>

View File

@@ -1,436 +0,0 @@
<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

@@ -1,290 +1,147 @@
<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 style="display: flex; justify-content: space-between; margin-bottom: 16px;">
<div>
<a-button type="primary" @click="handleAddFarmer">新增养殖监管</a-button>
<a-button style="margin-left: 8px;" @click="handleExport">导出表格</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>
<div>
<a-input-search
placeholder="请输入"
style="width: 200px; margin-right: 8px;"
v-model:value="searchKeyword"
@search="handleSearch"
/>
<a-select
placeholder="账号"
style="width: 120px;"
v-model:value="accountFilter"
@change="handleSearch"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="account1">账号筛选1</a-select-option>
<a-select-option value="account2">账号筛选2</a-select-option>
</a-select>
</div>
</div>
<!-- 养殖户列表 -->
<a-card>
<a-table
:columns="columns"
:data-source="farmersData"
:pagination="pagination"
row-key="id"
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
:scroll="{ x: 'max-content' }"
>
<a-table
:columns="columns"
:data-source="farmersData"
:pagination="pagination"
row-key="id"
: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 #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
</template>
<template v-if="column.key === 'action'">
<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>
</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'
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
// 搜索条件
// 搜索关键词
const searchKeyword = ref('')
const statusFilter = ref('')
const scaleFilter = ref('')
const regionFilter = ref('')
const accountFilter = ref('')
// 统计数据
const totalFarmers = ref(1256)
const activeFarmers = ref(986)
const inactiveFarmers = ref(158)
const closedFarmers = ref(112)
// 表格列定义
const columns = [
{
title: '账号',
dataIndex: 'account',
key: 'account',
width: 150
},
{
title: '机构识别码',
dataIndex: 'orgCode',
key: 'orgCode',
width: 150
},
{
title: '账号昵称',
dataIndex: 'nickname',
key: 'nickname',
width: 150
},
{
title: '真实姓名',
dataIndex: 'realName',
key: 'realName',
width: 150
},
{
title: '养殖场名称',
dataIndex: 'farmName',
key: 'farmName',
width: 180
},
{
title: '养殖场类型',
dataIndex: 'farmType',
key: 'farmType',
width: 120
},
{
title: '养殖场种类',
dataIndex: 'animalType',
key: 'animalType',
width: 120
},
{
title: '数量',
dataIndex: 'animalCount',
key: 'animalCount',
width: 100
},
{
title: '养殖场地址',
dataIndex: 'address',
key: 'address',
width: 250
},
{
title: '登记时间',
dataIndex: 'registerTime',
key: 'registerTime',
width: 150,
sorter: (a, b) => new Date(a.registerTime) - new Date(b.registerTime)
},
{
title: '登记人',
dataIndex: 'registrar',
key: 'registrar',
width: 120
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: 150,
sorter: (a, b) => new Date(a.updateTime) - new Date(b.updateTime)
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 180
}
]
// 分页配置
const pagination = reactive({
@@ -292,417 +149,152 @@ const pagination = reactive({
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'
account: '176****4187',
orgCode: '18094-785778',
nickname: '齐五旺',
realName: '齐五旺',
farmName: '齐五旺养殖场',
farmType: '散养',
animalType: '',
animalCount: 10,
address: '内蒙古自治区通辽市扎鲁特旗鲁北镇额尔格图木塔格镇',
registerTime: '2023-05-22 08:59:44',
registrar: '齐五旺',
updateTime: '2025-08-10 06:07:41',
status: 'active'
},
{
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'
account: '132****9345',
orgCode: '384708-449642',
nickname: '扎鲁特数据中心',
realName: '扎鲁特数据中心',
farmName: '扎鲁特数据中心',
farmType: '散养',
animalType: '',
animalCount: 10,
address: '内蒙古自治区通辽市扎鲁特旗',
registerTime: '2024-07-23 13:37:00',
registrar: '扎鲁特数据中心',
updateTime: '2024-10-16 11:33:02',
status: 'active'
},
{
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'
account: '139****1221',
orgCode: '382987-319364',
nickname: '邵晓艳',
realName: '邵晓艳',
farmName: '扎鲁特旗百顺养殖专业合作社',
farmType: '规模',
animalType: '',
animalCount: 741,
address: '内蒙古自治区通辽市扎鲁特旗巴日合镇巴日合村委会',
registerTime: '2023-07-10 11:17:50',
registrar: '赵忠林',
updateTime: '2024-07-03 11:53:47',
status: 'active'
},
{
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'
account: '151****1013',
orgCode: '1069-899484',
nickname: '双成家庭农场',
realName: '双成',
farmName: '双成家庭农场',
farmType: '散养',
animalType: '',
animalCount: 74,
address: '内蒙古自治区通辽市扎鲁特旗鲁北镇宝力根花镇',
registerTime: '2023-04-24 10:54:22',
registrar: '双成',
updateTime: '2024-05-08 11:14:48',
status: 'active'
},
{
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'
account: '150****1368',
orgCode: '1451-668414',
nickname: '刘超',
realName: '刘超',
farmName: '大数据中心',
farmType: '散养',
animalType: '',
animalCount: 11,
address: '内蒙古自治区通辽市扎鲁特旗鲁北镇',
registerTime: '2023-04-11 15:36:37',
registrar: '孙主任',
updateTime: '2024-03-04 11:18:24',
status: 'active'
}
])
// 表格列定义
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'
}
]
// 初始化数据
onMounted(() => {
pagination.total = farmersData.value.length
})
// 处理搜索
const handleSearch = () => {
// 实际应用中这里应该调用API进行搜索
message.success('搜索条件已应用')
}
// 处理导出
const handleExport = () => {
message.success('数据导出中...')
}
// 处理添加养殖户
const handleAddFarmer = () => {
message.success('打开新增养殖监管表单')
}
// 处理查看详情
const handleView = (record) => {
message.info(`查看养殖户: ${record.farmName}`)
}
// 处理编辑
const handleEdit = (record) => {
message.info(`编辑养殖户: ${record.farmName}`)
}
// 处理删除
const handleDelete = (id) => {
message.success(`删除养殖户ID: ${id}`)
}
// 获取状态颜色
const getStatusColor = (status) => {
switch (status) {
case 'active': return 'green'
case 'inactive': return 'orange'
case 'closed': return 'red'
default: return 'default'
const statusMap = {
active: 'green',
inactive: 'orange',
closed: 'red'
}
return statusMap[status] || 'default'
}
// 获取状态文本
const getStatusText = (status) => {
switch (status) {
case 'active': return '正常经营'
case 'inactive': return '暂停经营'
case 'closed': return '关闭'
default: return status
const statusMap = {
active: '正常',
inactive: '暂停',
closed: '关闭'
}
return statusMap[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;
}
.ant-table {
margin-top: 16px;
}
</style>

View File

@@ -1,270 +0,0 @@
<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>

View File

@@ -1,386 +0,0 @@
<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

@@ -1,347 +1,198 @@
<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>
<div class="market-price-container">
<!-- 产品类型选项卡 -->
<div class="product-tabs">
<a-tabs v-model:activeKey="activeProductTab">
<a-tab-pane key="beef" tab="育肥牛"></a-tab-pane>
<a-tab-pane key="dairy" tab="繁殖牛"></a-tab-pane>
<a-tab-pane key="milk" tab="奶牛"></a-tab-pane>
</a-tabs>
</div>
<!-- 价格走势图表 -->
<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 class="content-layout">
<!-- 左侧内容 -->
<div class="left-content">
<!-- 本市价格信息卡片 -->
<a-card class="price-info-card">
<template #title>
<div class="card-title">
<span>本市{{ productTypeText }}价格</span>
<span class="price-date">2025-01-01</span>
</div>
</template>
<div class="price-info">
<div class="current-price">
<span class="price-value">{{ currentPrice }}</span>
<span class="price-unit">/公斤</span>
</div>
<div class="price-change">
<span class="change-value">+0.30</span>
<span class="change-percent">+1.32%</span>
</div>
</div>
</a-card>
<!-- 价格走势图表 -->
<a-card class="chart-card" title="价格走势">
<div ref="priceChartRef" class="chart-container"></div>
</a-card>
</div>
</a-card>
<!-- 右侧内容 -->
<div class="right-content">
<!-- 各省价格对比图表 -->
<a-card class="province-chart-card" title="各省价格对比">
<div ref="provinceChartRef" class="province-chart-container"></div>
</a-card>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import axios from 'axios'
import dayjs from 'dayjs'
import { ref, computed, onMounted } from 'vue'
import * as echarts from 'echarts'
// 选择的产品和地区
const selectedProduct = ref('beef')
const selectedRegion = ref('national')
// 当前选中的产品类型
const activeProductTab = ref('beef')
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
// 产品类型文本映射
const productTypeText = computed(() => {
const map = {
beef: '育肥牛',
dairy: '繁殖牛',
milk: '奶牛'
}
return map[activeProductTab.value] || '育肥牛'
})
// 当前价格
const currentPrice = ref('--')
// 图表引用
const chartRef = ref(null)
const priceChartRef = ref(null)
const provinceChartRef = 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 initPriceChart = () => {
if (!priceChartRef.value) return
const chart = echarts.init(chartRef.value)
const chart = echarts.init(priceChartRef.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 dates = ['2024-12-19', '2024-12-20', '2024-12-21', '2024-12-22', '2024-12-23',
'2024-12-24', '2024-12-25', '2024-12-26', '2024-12-27', '2024-12-28',
'2024-12-29', '2024-12-30', '2024-12-31', '2025-01-01']
const prices = [23.10, 20.65, 24.90, 21.78, 22.15, 24.30, 23.20, 21.60, 20.35, 24.05, 23.45, 20.95, 22.50, 21.80]
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%',
bottom: '3%',
top: '8%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: historicalData.map(item => item.date)
xAxis: {
type: 'category',
boundaryGap: false,
data: dates,
axisLabel: {
rotate: 45,
fontSize: 10
}
],
yAxis: [
{
type: 'value',
name: '价格',
position: 'left'
}
],
},
yAxis: {
type: 'value',
min: 0,
max: 25,
interval: 5
},
series: [
{
name: '牛肉(元/公斤)',
type: 'line',
data: historicalData.map(item => item.beef),
data: prices,
symbol: 'circle',
symbolSize: 8,
symbolSize: 6,
itemStyle: {
color: '#1890ff'
},
lineStyle: {
width: 3
}
},
{
name: '牛奶(元/公斤)',
type: 'line',
data: historicalData.map(item => item.milk),
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#52c41a'
width: 2
},
lineStyle: {
width: 3
label: {
show: true,
position: 'top',
formatter: '{c}'
}
},
}
]
}
chart.setOption(option)
// 响应式调整
window.addEventListener('resize', () => {
chart.resize()
})
}
// 初始化各省价格对比图表
const initProvinceChart = () => {
if (!provinceChartRef.value) return
const chart = echarts.init(provinceChartRef.value)
// 模拟各省价格数据
const provinces = ['上海市', '云南省', '内蒙古自治区', '北京市', '吉林省', '四川省', '天津市', '宁夏回族自治区', '安徽省', '山东省', '山西省', '广东省', '广西壮族自治区', '新疆维吾尔自治区', '江苏省', '江西省', '河北省', '河南省', '浙江省', '海南省', '湖北省', '湖南省', '甘肃省', '福建省', '贵州省', '辽宁省', '重庆市', '陕西省', '青海省', '黑龙江省']
const prices = [36, 34.85, 34.85, 35.7, 33.26, 34.9, 35.4, 36.51, 33.5, 35.4, 35.8, 35.3, 32.6, 35.8, 34.1, 35.2, 33.2, 36, 35, 35.4, 32, 33.2, 34.8, 32.86, 32.7, 33, 32.8, 33.4, 33, 35.4]
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '3%',
containLabel: true
},
xAxis: {
type: 'value',
min: 0,
max: 40,
interval: 10
},
yAxis: {
type: 'category',
data: provinces,
axisLabel: {
fontSize: 10
}
},
series: [
{
name: '饲料(元/吨)',
type: 'line',
data: historicalData.map(item => item.feed),
symbol: 'circle',
symbolSize: 8,
type: 'bar',
data: prices,
itemStyle: {
color: '#fa8c16'
color: '#4e7bff'
},
lineStyle: {
width: 3
label: {
show: true,
position: 'right',
formatter: '{c}'
}
}
]
@@ -357,40 +208,102 @@ const initChart = () => {
// 组件挂载时初始化图表
onMounted(() => {
initChart()
// 设置当前价格
currentPrice.value = '23.10'
// 初始化图表
setTimeout(() => {
initPriceChart()
initProvinceChart()
}, 100)
})
</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;
}
.market-price-container {
padding: 20px;
}
.product-tabs {
margin-bottom: 20px;
}
.content-layout {
display: flex;
gap: 20px;
}
.left-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.right-content {
flex: 1;
}
.price-info-card {
margin-bottom: 0;
}
.card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.price-date {
font-size: 14px;
color: #999;
}
.price-info {
display: flex;
align-items: flex-end;
gap: 20px;
padding: 10px 0;
}
.current-price {
display: flex;
align-items: baseline;
}
.price-value {
font-size: 32px;
font-weight: bold;
color: #1890ff;
line-height: 1;
}
.price-unit {
font-size: 14px;
color: #666;
margin-left: 5px;
}
.price-change {
display: flex;
flex-direction: column;
}
.change-value {
font-size: 16px;
color: #52c41a;
}
.change-percent {
font-size: 14px;
color: #52c41a;
}
.chart-container {
height: 300px;
}
.province-chart-container {
height: 600px;
}
</style>

View File

@@ -1,7 +1,11 @@
<template>
<div>
<h1>人员管理</h1>
<a-card style="margin-bottom: 16px;">
<!-- 添加router-view来渲染子路由内容 -->
<router-view />
<a-card style="margin-bottom: 16px;" v-if="!$route.path.includes('/personnel/')">
<a-row gutter={24}>
<a-col :span="8">
<a-button type="primary" @click="handleAddUser">添加人员</a-button>
@@ -22,7 +26,7 @@
</a-card>
<!-- 人员列表 -->
<a-card title="人员列表">
<a-card title="人员列表" v-if="!$route.path.includes('/personnel/')">
<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' ? '在职' : '离职'" />

View File

@@ -1,371 +0,0 @@
<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

@@ -1,366 +0,0 @@
<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

@@ -1,874 +0,0 @@
<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

@@ -1,504 +0,0 @@
<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

@@ -0,0 +1,11 @@
<template>
<div>
<router-view />
</div>
</template>
<script>
export default {
name: 'SmartHardware'
}
</script>

View File

@@ -0,0 +1,422 @@
<template>
<div class="smart-collar-container">
<a-card title="智能项圈管理" :bordered="false">
<a-row :gutter="16">
<a-col :span="24">
<a-form layout="inline" :model="searchForm">
<a-form-item label="项圈编号">
<a-input v-model:value="searchForm.collarId" placeholder="请输入项圈编号" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="请选择状态" style="width: 120px">
<a-select-option value="active">使用中</a-select-option>
<a-select-option value="inactive">未使用</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">重置</a-button>
</a-form-item>
</a-form>
</a-col>
</a-row>
<a-row style="margin-top: 16px">
<a-col :span="24">
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加项圈
</a-button>
</a-col>
</a-row>
<a-table
style="margin-top: 16px"
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
rowKey="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-if="column.key === 'action'">
<a-space>
<a @click="showEditModal(record)">编辑</a>
<a-divider type="vertical" />
<a @click="showDetailModal(record)">详情</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此项圈吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a class="danger-text">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 添加/编辑模态框 -->
<a-modal
:title="modalTitle"
:visible="modalVisible"
:confirm-loading="modalLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form :model="formData" :rules="rules" ref="formRef" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="项圈编号" name="collarId">
<a-input v-model:value="formData.collarId" placeholder="请输入项圈编号" />
</a-form-item>
<a-form-item label="项圈名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入项圈名称" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formData.status" placeholder="请选择状态">
<a-select-option value="active">使用中</a-select-option>
<a-select-option value="inactive">未使用</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="电池电量" name="battery">
<a-input-number v-model:value="formData.battery" :min="0" :max="100" addonAfter="%" style="width: 100%" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="4" />
</a-form-item>
</a-form>
</a-modal>
<!-- 详情模态框 -->
<a-modal
title="项圈详情"
:visible="detailModalVisible"
:footer="null"
@cancel="handleDetailModalCancel"
>
<a-descriptions bordered>
<a-descriptions-item label="项圈编号" span="3">{{ detailData.collarId }}</a-descriptions-item>
<a-descriptions-item label="项圈名称" span="3">{{ detailData.name }}</a-descriptions-item>
<a-descriptions-item label="状态" span="3">
<a-tag :color="getStatusColor(detailData.status)">
{{ getStatusText(detailData.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="电池电量" span="3">{{ detailData.battery }}%</a-descriptions-item>
<a-descriptions-item label="创建时间" span="3">{{ detailData.createdAt }}</a-descriptions-item>
<a-descriptions-item label="更新时间" span="3">{{ detailData.updatedAt }}</a-descriptions-item>
<a-descriptions-item label="备注" span="3">{{ detailData.remark || '无' }}</a-descriptions-item>
</a-descriptions>
</a-modal>
</a-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
export default {
name: 'SmartCollar',
components: {
PlusOutlined
},
setup() {
// 搜索表单
const searchForm = reactive({
collarId: '',
status: undefined
});
// 表格数据
const loading = ref(false);
const dataSource = ref([]);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`
});
// 表格列定义
const columns = [
{
title: '项圈编号',
dataIndex: 'collarId',
key: 'collarId',
sorter: true
},
{
title: '项圈名称',
dataIndex: 'name',
key: 'name'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '使用中', value: 'active' },
{ text: '未使用', value: 'inactive' },
{ text: '维护中', value: 'maintenance' }
]
},
{
title: '电池电量',
dataIndex: 'battery',
key: 'battery',
customRender: ({ text }) => `${text}%`
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
sorter: true
},
{
title: '操作',
key: 'action'
}
];
// 模态框相关
const modalVisible = ref(false);
const modalLoading = ref(false);
const modalTitle = ref('添加项圈');
const formRef = ref(null);
const formData = reactive({
id: null,
collarId: '',
name: '',
status: 'inactive',
battery: 100,
remark: ''
});
const rules = {
collarId: [{ required: true, message: '请输入项圈编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入项圈名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
};
// 详情模态框
const detailModalVisible = ref(false);
const detailData = reactive({});
// 模拟数据
const mockData = [
{
id: '1',
collarId: 'CL001',
name: '智能项圈001',
status: 'active',
battery: 85,
createdAt: '2023-09-15 10:00:00',
updatedAt: '2023-09-20 14:30:00',
remark: '用于示范的项圈'
},
{
id: '2',
collarId: 'CL002',
name: '智能项圈002',
status: 'inactive',
battery: 100,
createdAt: '2023-09-16 11:20:00',
updatedAt: '2023-09-16 11:20:00',
remark: ''
},
{
id: '3',
collarId: 'CL003',
name: '智能项圈003',
status: 'maintenance',
battery: 20,
createdAt: '2023-09-10 09:15:00',
updatedAt: '2023-09-21 16:45:00',
remark: '电池需要更换'
}
];
// 获取数据
const fetchData = () => {
loading.value = true;
// 模拟API请求
setTimeout(() => {
dataSource.value = mockData;
pagination.total = mockData.length;
loading.value = false;
}, 500);
};
// 状态相关
const getStatusColor = (status) => {
const statusMap = {
active: 'green',
inactive: 'blue',
maintenance: 'orange'
};
return statusMap[status] || 'default';
};
const getStatusText = (status) => {
const statusMap = {
active: '使用中',
inactive: '未使用',
maintenance: '维护中'
};
return statusMap[status] || '未知';
};
// 搜索相关
const handleSearch = () => {
pagination.current = 1;
fetchData();
};
const resetSearch = () => {
searchForm.collarId = '';
searchForm.status = undefined;
handleSearch();
};
// 表格相关
const handleTableChange = (pag, filters, sorter) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
fetchData();
};
// 模态框相关
const showAddModal = () => {
modalTitle.value = '添加项圈';
formData.id = null;
formData.collarId = '';
formData.name = '';
formData.status = 'inactive';
formData.battery = 100;
formData.remark = '';
modalVisible.value = true;
};
const showEditModal = (record) => {
modalTitle.value = '编辑项圈';
formData.id = record.id;
formData.collarId = record.collarId;
formData.name = record.name;
formData.status = record.status;
formData.battery = record.battery;
formData.remark = record.remark;
modalVisible.value = true;
};
const handleModalOk = () => {
formRef.value.validate().then(() => {
modalLoading.value = true;
// 模拟API请求
setTimeout(() => {
if (formData.id) {
// 编辑
const index = mockData.findIndex(item => item.id === formData.id);
if (index !== -1) {
mockData[index] = {
...mockData[index],
...formData,
updatedAt: new Date().toLocaleString()
};
}
} else {
// 添加
mockData.push({
id: String(mockData.length + 1),
...formData,
createdAt: new Date().toLocaleString(),
updatedAt: new Date().toLocaleString()
});
}
modalLoading.value = false;
modalVisible.value = false;
fetchData();
}, 500);
});
};
const handleModalCancel = () => {
modalVisible.value = false;
};
// 详情相关
const showDetailModal = (record) => {
Object.assign(detailData, record);
detailModalVisible.value = true;
};
const handleDetailModalCancel = () => {
detailModalVisible.value = false;
};
// 删除
const handleDelete = (record) => {
loading.value = true;
// 模拟API请求
setTimeout(() => {
const index = mockData.findIndex(item => item.id === record.id);
if (index !== -1) {
mockData.splice(index, 1);
}
loading.value = false;
fetchData();
}, 500);
};
onMounted(() => {
fetchData();
});
return {
searchForm,
loading,
dataSource,
pagination,
columns,
modalVisible,
modalLoading,
modalTitle,
formRef,
formData,
rules,
detailModalVisible,
detailData,
getStatusColor,
getStatusText,
handleSearch,
resetSearch,
handleTableChange,
showAddModal,
showEditModal,
handleModalOk,
handleModalCancel,
showDetailModal,
handleDetailModalCancel,
handleDelete
};
}
};
</script>
<style scoped>
.smart-collar-container {
padding: 16px;
}
.danger-text {
color: #ff4d4f;
}
</style>

View File

@@ -0,0 +1,422 @@
<template>
<div class="smart-earmark-container">
<a-card title="智能耳标管理" :bordered="false">
<a-row :gutter="16">
<a-col :span="24">
<a-form layout="inline" :model="searchForm">
<a-form-item label="耳标编号">
<a-input v-model:value="searchForm.earmarkId" placeholder="请输入耳标编号" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="请选择状态" style="width: 120px">
<a-select-option value="active">使用中</a-select-option>
<a-select-option value="inactive">未使用</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">重置</a-button>
</a-form-item>
</a-form>
</a-col>
</a-row>
<a-row style="margin-top: 16px">
<a-col :span="24">
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加耳标
</a-button>
</a-col>
</a-row>
<a-table
style="margin-top: 16px"
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
rowKey="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-if="column.key === 'action'">
<a-space>
<a @click="showEditModal(record)">编辑</a>
<a-divider type="vertical" />
<a @click="showDetailModal(record)">详情</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此耳标吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a class="danger-text">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 添加/编辑模态框 -->
<a-modal
:title="modalTitle"
:visible="modalVisible"
:confirm-loading="modalLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form :model="formData" :rules="rules" ref="formRef" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="耳标编号" name="earmarkId">
<a-input v-model:value="formData.earmarkId" placeholder="请输入耳标编号" />
</a-form-item>
<a-form-item label="耳标名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入耳标名称" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formData.status" placeholder="请选择状态">
<a-select-option value="active">使用中</a-select-option>
<a-select-option value="inactive">未使用</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="电池电量" name="battery">
<a-input-number v-model:value="formData.battery" :min="0" :max="100" addonAfter="%" style="width: 100%" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="4" />
</a-form-item>
</a-form>
</a-modal>
<!-- 详情模态框 -->
<a-modal
title="耳标详情"
:visible="detailModalVisible"
:footer="null"
@cancel="handleDetailModalCancel"
>
<a-descriptions bordered>
<a-descriptions-item label="耳标编号" span="3">{{ detailData.earmarkId }}</a-descriptions-item>
<a-descriptions-item label="耳标名称" span="3">{{ detailData.name }}</a-descriptions-item>
<a-descriptions-item label="状态" span="3">
<a-tag :color="getStatusColor(detailData.status)">
{{ getStatusText(detailData.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="电池电量" span="3">{{ detailData.battery }}%</a-descriptions-item>
<a-descriptions-item label="创建时间" span="3">{{ detailData.createdAt }}</a-descriptions-item>
<a-descriptions-item label="更新时间" span="3">{{ detailData.updatedAt }}</a-descriptions-item>
<a-descriptions-item label="备注" span="3">{{ detailData.remark || '无' }}</a-descriptions-item>
</a-descriptions>
</a-modal>
</a-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
export default {
name: 'SmartEarmark',
components: {
PlusOutlined
},
setup() {
// 搜索表单
const searchForm = reactive({
earmarkId: '',
status: undefined
});
// 表格数据
const loading = ref(false);
const dataSource = ref([]);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`
});
// 表格列定义
const columns = [
{
title: '耳标编号',
dataIndex: 'earmarkId',
key: 'earmarkId',
sorter: true
},
{
title: '耳标名称',
dataIndex: 'name',
key: 'name'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '使用中', value: 'active' },
{ text: '未使用', value: 'inactive' },
{ text: '维护中', value: 'maintenance' }
]
},
{
title: '电池电量',
dataIndex: 'battery',
key: 'battery',
customRender: ({ text }) => `${text}%`
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
sorter: true
},
{
title: '操作',
key: 'action'
}
];
// 模态框相关
const modalVisible = ref(false);
const modalLoading = ref(false);
const modalTitle = ref('添加耳标');
const formRef = ref(null);
const formData = reactive({
id: null,
earmarkId: '',
name: '',
status: 'inactive',
battery: 100,
remark: ''
});
const rules = {
earmarkId: [{ required: true, message: '请输入耳标编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入耳标名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
};
// 详情模态框
const detailModalVisible = ref(false);
const detailData = reactive({});
// 模拟数据
const mockData = [
{
id: '1',
earmarkId: 'EM001',
name: '智能耳标001',
status: 'active',
battery: 90,
createdAt: '2023-09-15 10:00:00',
updatedAt: '2023-09-20 14:30:00',
remark: '用于示范的耳标'
},
{
id: '2',
earmarkId: 'EM002',
name: '智能耳标002',
status: 'inactive',
battery: 100,
createdAt: '2023-09-16 11:20:00',
updatedAt: '2023-09-16 11:20:00',
remark: ''
},
{
id: '3',
earmarkId: 'EM003',
name: '智能耳标003',
status: 'maintenance',
battery: 15,
createdAt: '2023-09-10 09:15:00',
updatedAt: '2023-09-21 16:45:00',
remark: '电池需要更换'
}
];
// 获取数据
const fetchData = () => {
loading.value = true;
// 模拟API请求
setTimeout(() => {
dataSource.value = mockData;
pagination.total = mockData.length;
loading.value = false;
}, 500);
};
// 状态相关
const getStatusColor = (status) => {
const statusMap = {
active: 'green',
inactive: 'blue',
maintenance: 'orange'
};
return statusMap[status] || 'default';
};
const getStatusText = (status) => {
const statusMap = {
active: '使用中',
inactive: '未使用',
maintenance: '维护中'
};
return statusMap[status] || '未知';
};
// 搜索相关
const handleSearch = () => {
pagination.current = 1;
fetchData();
};
const resetSearch = () => {
searchForm.earmarkId = '';
searchForm.status = undefined;
handleSearch();
};
// 表格相关
const handleTableChange = (pag, filters, sorter) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
fetchData();
};
// 模态框相关
const showAddModal = () => {
modalTitle.value = '添加耳标';
formData.id = null;
formData.earmarkId = '';
formData.name = '';
formData.status = 'inactive';
formData.battery = 100;
formData.remark = '';
modalVisible.value = true;
};
const showEditModal = (record) => {
modalTitle.value = '编辑耳标';
formData.id = record.id;
formData.earmarkId = record.earmarkId;
formData.name = record.name;
formData.status = record.status;
formData.battery = record.battery;
formData.remark = record.remark;
modalVisible.value = true;
};
const handleModalOk = () => {
formRef.value.validate().then(() => {
modalLoading.value = true;
// 模拟API请求
setTimeout(() => {
if (formData.id) {
// 编辑
const index = mockData.findIndex(item => item.id === formData.id);
if (index !== -1) {
mockData[index] = {
...mockData[index],
...formData,
updatedAt: new Date().toLocaleString()
};
}
} else {
// 添加
mockData.push({
id: String(mockData.length + 1),
...formData,
createdAt: new Date().toLocaleString(),
updatedAt: new Date().toLocaleString()
});
}
modalLoading.value = false;
modalVisible.value = false;
fetchData();
}, 500);
});
};
const handleModalCancel = () => {
modalVisible.value = false;
};
// 详情相关
const showDetailModal = (record) => {
Object.assign(detailData, record);
detailModalVisible.value = true;
};
const handleDetailModalCancel = () => {
detailModalVisible.value = false;
};
// 删除
const handleDelete = (record) => {
loading.value = true;
// 模拟API请求
setTimeout(() => {
const index = mockData.findIndex(item => item.id === record.id);
if (index !== -1) {
mockData.splice(index, 1);
}
loading.value = false;
fetchData();
}, 500);
};
onMounted(() => {
fetchData();
});
return {
searchForm,
loading,
dataSource,
pagination,
columns,
modalVisible,
modalLoading,
modalTitle,
formRef,
formData,
rules,
detailModalVisible,
detailData,
getStatusColor,
getStatusText,
handleSearch,
resetSearch,
handleTableChange,
showAddModal,
showEditModal,
handleModalOk,
handleModalCancel,
showDetailModal,
handleDetailModalCancel,
handleDelete
};
}
};
</script>
<style scoped>
.smart-earmark-container {
padding: 16px;
}
.danger-text {
color: #ff4d4f;
}
</style>

View File

@@ -0,0 +1,422 @@
<template>
<div class="smart-host-container">
<a-card title="智能主机管理" :bordered="false">
<a-row :gutter="16">
<a-col :span="24">
<a-form layout="inline" :model="searchForm">
<a-form-item label="主机编号">
<a-input v-model:value="searchForm.hostId" placeholder="请输入主机编号" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="请选择状态" style="width: 120px">
<a-select-option value="online">在线</a-select-option>
<a-select-option value="offline">离线</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">重置</a-button>
</a-form-item>
</a-form>
</a-col>
</a-row>
<a-row style="margin-top: 16px">
<a-col :span="24">
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加主机
</a-button>
</a-col>
</a-row>
<a-table
style="margin-top: 16px"
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
rowKey="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-if="column.key === 'action'">
<a-space>
<a @click="showEditModal(record)">编辑</a>
<a-divider type="vertical" />
<a @click="showDetailModal(record)">详情</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此主机吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a class="danger-text">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 添加/编辑模态框 -->
<a-modal
:title="modalTitle"
:visible="modalVisible"
:confirm-loading="modalLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form :model="formData" :rules="rules" ref="formRef" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="主机编号" name="hostId">
<a-input v-model:value="formData.hostId" placeholder="请输入主机编号" />
</a-form-item>
<a-form-item label="主机名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入主机名称" />
</a-form-item>
<a-form-item label="IP地址" name="ipAddress">
<a-input v-model:value="formData.ipAddress" placeholder="请输入IP地址" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formData.status" placeholder="请选择状态">
<a-select-option value="online">在线</a-select-option>
<a-select-option value="offline">离线</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="formData.remark" placeholder="请输入备注" :rows="4" />
</a-form-item>
</a-form>
</a-modal>
<!-- 详情模态框 -->
<a-modal
title="主机详情"
:visible="detailModalVisible"
:footer="null"
@cancel="handleDetailModalCancel"
>
<a-descriptions bordered>
<a-descriptions-item label="主机编号" span="3">{{ detailData.hostId }}</a-descriptions-item>
<a-descriptions-item label="主机名称" span="3">{{ detailData.name }}</a-descriptions-item>
<a-descriptions-item label="IP地址" span="3">{{ detailData.ipAddress }}</a-descriptions-item>
<a-descriptions-item label="状态" span="3">
<a-tag :color="getStatusColor(detailData.status)">
{{ getStatusText(detailData.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="创建时间" span="3">{{ detailData.createdAt }}</a-descriptions-item>
<a-descriptions-item label="更新时间" span="3">{{ detailData.updatedAt }}</a-descriptions-item>
<a-descriptions-item label="备注" span="3">{{ detailData.remark || '无' }}</a-descriptions-item>
</a-descriptions>
</a-modal>
</a-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
export default {
name: 'SmartHost',
components: {
PlusOutlined
},
setup() {
// 搜索表单
const searchForm = reactive({
hostId: '',
status: undefined
});
// 表格数据
const loading = ref(false);
const dataSource = ref([]);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`
});
// 表格列定义
const columns = [
{
title: '主机编号',
dataIndex: 'hostId',
key: 'hostId',
sorter: true
},
{
title: '主机名称',
dataIndex: 'name',
key: 'name'
},
{
title: 'IP地址',
dataIndex: 'ipAddress',
key: 'ipAddress'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '在线', value: 'online' },
{ text: '离线', value: 'offline' },
{ text: '维护中', value: 'maintenance' }
]
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
sorter: true
},
{
title: '操作',
key: 'action'
}
];
// 模态框相关
const modalVisible = ref(false);
const modalLoading = ref(false);
const modalTitle = ref('添加主机');
const formRef = ref(null);
const formData = reactive({
id: null,
hostId: '',
name: '',
ipAddress: '',
status: 'offline',
remark: ''
});
const rules = {
hostId: [{ required: true, message: '请输入主机编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入主机名称', trigger: 'blur' }],
ipAddress: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
};
// 详情模态框
const detailModalVisible = ref(false);
const detailData = reactive({});
// 模拟数据
const mockData = [
{
id: '1',
hostId: 'HOST001',
name: '智能主机001',
ipAddress: '192.168.1.100',
status: 'online',
createdAt: '2023-09-15 10:00:00',
updatedAt: '2023-09-20 14:30:00',
remark: '中心控制主机'
},
{
id: '2',
hostId: 'HOST002',
name: '智能主机002',
ipAddress: '192.168.1.101',
status: 'offline',
createdAt: '2023-09-16 11:20:00',
updatedAt: '2023-09-16 11:20:00',
remark: ''
},
{
id: '3',
hostId: 'HOST003',
name: '智能主机003',
ipAddress: '192.168.1.102',
status: 'maintenance',
createdAt: '2023-09-10 09:15:00',
updatedAt: '2023-09-21 16:45:00',
remark: '系统升级中'
}
];
// 获取数据
const fetchData = () => {
loading.value = true;
// 模拟API请求
setTimeout(() => {
dataSource.value = mockData;
pagination.total = mockData.length;
loading.value = false;
}, 500);
};
// 状态相关
const getStatusColor = (status) => {
const statusMap = {
online: 'green',
offline: 'gray',
maintenance: 'orange'
};
return statusMap[status] || 'default';
};
const getStatusText = (status) => {
const statusMap = {
online: '在线',
offline: '离线',
maintenance: '维护中'
};
return statusMap[status] || '未知';
};
// 搜索相关
const handleSearch = () => {
pagination.current = 1;
fetchData();
};
const resetSearch = () => {
searchForm.hostId = '';
searchForm.status = undefined;
handleSearch();
};
// 表格相关
const handleTableChange = (pag, filters, sorter) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
fetchData();
};
// 模态框相关
const showAddModal = () => {
modalTitle.value = '添加主机';
formData.id = null;
formData.hostId = '';
formData.name = '';
formData.ipAddress = '';
formData.status = 'offline';
formData.remark = '';
modalVisible.value = true;
};
const showEditModal = (record) => {
modalTitle.value = '编辑主机';
formData.id = record.id;
formData.hostId = record.hostId;
formData.name = record.name;
formData.ipAddress = record.ipAddress;
formData.status = record.status;
formData.remark = record.remark;
modalVisible.value = true;
};
const handleModalOk = () => {
formRef.value.validate().then(() => {
modalLoading.value = true;
// 模拟API请求
setTimeout(() => {
if (formData.id) {
// 编辑
const index = mockData.findIndex(item => item.id === formData.id);
if (index !== -1) {
mockData[index] = {
...mockData[index],
...formData,
updatedAt: new Date().toLocaleString()
};
}
} else {
// 添加
mockData.push({
id: String(mockData.length + 1),
...formData,
createdAt: new Date().toLocaleString(),
updatedAt: new Date().toLocaleString()
});
}
modalLoading.value = false;
modalVisible.value = false;
fetchData();
}, 500);
});
};
const handleModalCancel = () => {
modalVisible.value = false;
};
// 详情相关
const showDetailModal = (record) => {
Object.assign(detailData, record);
detailModalVisible.value = true;
};
const handleDetailModalCancel = () => {
detailModalVisible.value = false;
};
// 删除
const handleDelete = (record) => {
loading.value = true;
// 模拟API请求
setTimeout(() => {
const index = mockData.findIndex(item => item.id === record.id);
if (index !== -1) {
mockData.splice(index, 1);
}
loading.value = false;
fetchData();
}, 500);
};
onMounted(() => {
fetchData();
});
return {
searchForm,
loading,
dataSource,
pagination,
columns,
modalVisible,
modalLoading,
modalTitle,
formRef,
formData,
rules,
detailModalVisible,
detailData,
getStatusColor,
getStatusText,
handleSearch,
resetSearch,
handleTableChange,
showAddModal,
showEditModal,
handleModalOk,
handleModalCancel,
showDetailModal,
handleDetailModalCancel,
handleDelete
};
}
};
</script>
<style scoped>
.smart-host-container {
padding: 16px;
}
.danger-text {
color: #ff4d4f;
}
</style>