完善小程序

This commit is contained in:
xuqiuyun
2025-10-09 17:59:26 +08:00
parent 74b2edb510
commit f88383425f
46 changed files with 3477 additions and 185 deletions

View File

@@ -37,7 +37,7 @@ alwaysApply: true
- 运维文档.md
- 安全文档.md
- 用户手册文档.md
7. DB_DIALECT || 'mysql',
7. 养殖端的数据库信息为:DB_DIALECT || 'mysql',
DB_HOST = '129.211.213.226',
DB_PORT = 9527,
DB_DATABASE = 'nxxmdata',
@@ -51,4 +51,10 @@ DB_PASSWORD = 'aiotAiot123!',
13. 不要修改前后端端口号。发现端口占用先杀死端口再打开不要修改端口号。规定死保险端的后端端口为3000前端端口为3001.
14. 每次运行命令都要先看项目规则。
15. PowerShell不支持&&操作符,请使用;符号1. 请保持对话语言为中文
16. 开发养殖端微信小程序时后端的API接口全部请求到https://ad.ningmuyun.com/
16. 开发养殖端微信小程序时后端的API接口全部请求到https://ad.ningmuyun.com/
17. 保险端的数据库信息为DB_DIALECT || 'mysql',
DB_HOST = '129.211.213.226',
DB_PORT = 9527,
DB_DATABASE = 'insurance_data',
DB_USER = 'root',
DB_PASSWORD = 'aiotAiot123!',

View File

@@ -0,0 +1,263 @@
# 保险端导出功能修复总结
## 修复时间
2025年10月9日
## 问题描述
前端导出Excel功能虽然能收到后端返回的Excel文件数据Blob格式但文件无法正常下载或内容异常。
## 问题根源
### 1. 后端问题
- **路由顺序错误**:部分模块的 `/export` 路由放在动态路由 `/:id` 之后导致404错误
- **缺少导出方法**:部分控制器缺少 `exportToExcel` 方法
### 2. 前端问题
- **request.js 响应处理**`handleResponse` 函数不支持 blob 类型响应
- **Blob 包装错误**:前端代码使用 `new Blob([response.data])` 重复包装已经是 Blob 的数据
## 修复方案
### 后端修复insurance_backend
#### 1. 添加导出方法到控制器
为以下控制器添加了 `exportToExcel` 方法:
-`userController.js` - 用户管理导出
-`supervisoryTaskController.js` - 监管任务导出
-`installationTaskController.js` - 待安装任务导出
-`deviceAlertController.js` - 设备预警导出
-`insuranceTypeController.js` - 保险类型导出
-`policyController.js` - 保单管理导出
-`livestockClaimController.js` - 理赔管理导出
-`livestockPolicyController.js` - 生资保单管理导出
-`operationLogController.js` - 操作日志导出
#### 2. 修复路由顺序
在以下路由文件中,将 `/export``/stats` 路由移到 `/:id` 之前:
-`routes/users.js`
-`routes/supervisoryTasks.js`
-`routes/installationTasks.js`
-`routes/deviceAlerts.js`
-`routes/insuranceTypes.js`
-`routes/policies.js`
-`routes/livestockClaims.js`
-`routes/livestockPolicies.js`
-`routes/operationLogs.js`
**正确的路由顺序示例:**
```javascript
// 1. 统计接口
router.get('/stats', jwtAuth, requirePermission('xxx:read'), controller.getStats);
// 2. 导出接口
router.get('/export', jwtAuth, requirePermission('xxx:read'), controller.exportToExcel);
// 3. 列表接口
router.get('/', jwtAuth, requirePermission('xxx:read'), controller.getList);
// 4. 详情接口(动态路由放最后)
router.get('/:id', jwtAuth, requirePermission('xxx:read'), controller.getById);
```
#### 3. 导出工具类
创建了通用的导出工具 `utils/excelExport.js`,提供:
- `exportToExcel(data, columns, sheetName)` - 生成Excel文件
- `formatDate(date)` - 格式化日期
- `formatStatus(status, statusMap)` - 格式化状态
### 前端修复insurance_admin-system
#### 1. 修复 request.js
修改 `src/utils/request.js` 中的 `handleResponse` 函数,添加对 blob 类型的支持:
```javascript
const handleResponse = async (response) => {
let data
try {
const contentType = response.headers.get('content-type')
// 处理Excel文件下载
if (contentType && contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) {
data = await response.blob()
} else if (contentType && contentType.includes('application/json')) {
data = await response.json()
} else {
data = await response.text()
}
} catch (error) {
data = null
}
if (!response.ok) {
const error = new Error(data?.message || `HTTP ${response.status}: ${response.statusText}`)
error.response = {
status: response.status,
statusText: response.statusText,
data: data
}
throw error
}
return { data, status: response.status, statusText: response.statusText }
}
```
#### 2. 修复前端页面的 Blob 处理
修改以下页面中的导出方法,将:
```javascript
const url = window.URL.createObjectURL(new Blob([response.data]))
```
改为:
```javascript
const url = window.URL.createObjectURL(response.data)
```
已修复的页面:
-`UserManagement.vue` - 用户管理
-`MessageNotification.vue` - 消息通知(设备预警)
-`InsuranceTypeManagement.vue` - 保险类型管理
-`PolicyManagement.vue` - 保单管理
-`ClaimManagement.vue` - 理赔管理
-`SupervisionTaskManagement.vue` - 监管任务管理
-`CompletedTaskManagement.vue` - 监管任务结项
-`InstallationTaskManagement.vue` - 待安装任务
-`LivestockPolicyManagement.vue` - 生资保单管理
-`SystemSettings.vue` - 系统设置(操作日志)
-`ApplicationManagement.vue` - 申请管理
## 导出功能支持的筛选参数
### 用户管理
- `search` - 用户名搜索
- `status` - 状态筛选
### 监管任务
- `policyNumber` - 保单编号
- `customerName` - 客户姓名
### 待安装任务
- `policyNumber` - 保单编号
- `keyword` - 关键字搜索
### 设备预警
- `alert_level` - 预警级别
- `alert_type` - 预警类型
- `status` - 处理状态
- `is_read` - 是否已读
### 保险类型
- `name` - 险种名称
- `status` - 状态
### 保单管理
- `policy_number` - 保单编号
- `policyholder_name` - 投保人姓名
- `status` - 状态
### 理赔管理
- `claim_number` - 理赔编号
- `claimant_name` - 理赔人姓名
- `status` - 状态
### 生资保单
- `policy_no` - 保单号
- `farmer_name` - 农户姓名
- `policy_status` - 保单状态
### 操作日志
- `username` - 用户名
- `operation_type` - 操作类型
- `operation_module` - 操作模块
- `startDate` - 开始日期
- `endDate` - 结束日期
## 技术要点
### 1. Excel 文件生成
- 使用 `exceljs` 库生成 Excel 文件
- 支持自定义列宽、表头、数据格式化
- 自动设置表头样式(加粗、边框)
### 2. 数据格式化
- 日期格式:`YYYY-MM-DD HH:mm:ss`
- 状态映射:使用中文映射英文状态值
- 空值处理显示为空字符串或0
### 3. 文件下载
- Content-Type: `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
- Content-Disposition: `attachment; filename=xxx_${timestamp}.xlsx`
- 前端使用 `window.URL.createObjectURL` 创建下载链接
### 4. 权限控制
- 所有导出接口都需要 JWT 认证
- 需要相应模块的读取权限
## 测试步骤
1. **启动后端服务**
```bash
cd insurance_backend
node src/app.js
```
确保服务运行在 `http://localhost:3000`
2. **启动前端服务**
```bash
cd insurance_admin-system
npm run dev
```
3. **测试导出功能**
- 登录系统admin / 123456
- 进入各个功能模块
- 点击"导出Excel"按钮
- 验证文件能正常下载且内容正确
## 常见问题排查
### 1. 404 错误
- **原因**:导出路由在动态路由之后
- **解决**:将 `/export` 路由移到 `/:id` 之前
### 2. Excel 文件损坏或无法打开
- **原因**:前端重复包装 Blob 数据
- **解决**:直接使用 `response.data`,不要用 `new Blob([response.data])`
### 3. 导出的 Excel 为空
- **原因**:数据库查询条件有误或数据为空
- **解决**检查控制器中的查询逻辑和where条件
### 4. 权限错误
- **原因**:用户没有导出权限
- **解决**:在数据库中为用户角色添加相应的导出权限
## 注意事项
1. **不要修改端口号**后端固定3000端口前端固定3001端口
2. **测试文件清理**:所有测试文件会自动删除
3. **权限验证**:确保每个导出接口都有权限中间件
4. **数据量控制**:大量数据导出时注意性能和内存占用
5. **文件命名**:使用时间戳避免文件名冲突
## 后续优化建议
1. **分页导出**:对于大量数据,支持分批导出
2. **异步导出**:数据量大时改为异步任务,生成后通知用户下载
3. **导出模板**:提供 Excel 模板下载功能
4. **导出历史**:记录导出操作日志
5. **格式选择**:支持导出为 CSV、PDF 等其他格式
6. **数据汇总**:在 Excel 中添加汇总统计信息
## 相关文档
- 后端接口文档:`insurance_backend/docs/EXPORT_IMPLEMENTATION_GUIDE.md`
- API 文档Swagger UI `http://localhost:3000/api-docs`
- 前端环境配置:`insurance_admin-system/ENV_CONFIG.md`

View File

@@ -0,0 +1,86 @@
# 待安装任务导出字段映射说明
## 数据库字段对照表
| Excel列名 | 数据库字段 | 字段说明 | 是否必填 |
|---------|----------|---------|---------|
| 申请单号 | application_number | 申请单号 | 是 |
| 保单编号 | policy_number | 保单编号 | 是 |
| 产品名称 | product_name | 产品名称 | 是 |
| 客户姓名 | customer_name | 客户姓名 | 是 |
| 证件类型 | id_type | 证件类型 | 是 |
| 证件号码 | id_number | 证件号码 | 是 |
| 养殖生资种类 | livestock_supply_type | 养殖生资种类 | 否 |
| 安装状态 | installation_status | 安装状态 | 是 |
| 优先级 | priority | 任务优先级 | 是 |
| 安装地址 | installation_address | 安装地址 | 否 |
| 联系电话 | contact_phone | 联系电话 | 否 |
| 任务生成时间 | task_generated_time | 任务生成时间 | 否 |
| 安装完成时间 | installation_completed_time | 安装完成时间 | 否 |
| 创建时间 | created_at | 创建时间 | 是 |
| 更新时间 | updated_at | 更新时间 | 是 |
## 字段值说明
### 证件类型 (id_type)
- 身份证
- 护照
- 军官证
- 士兵证
- 港澳台居民居住证
- 其他
### 安装状态 (installation_status)
- 待安装
- 安装中
- 已安装
- 安装失败
- 已取消
### 优先级 (priority)
-
-
-
- 紧急
## 注意事项
1. **数据库字段格式**使用下划线命名snake_case
2. **模型字段格式**使用驼峰命名camelCase
3. **raw查询返回**:返回的是数据库字段名(下划线格式)
4. **空值处理**
- 字符串字段:显示为空字符串 `''`
- 日期字段:通过 `ExcelExport.formatDate()` 处理,空值显示为空字符串
5. **状态值**:直接使用数据库中的中文值,不需要额外映射
## 导出示例数据
```json
{
"applicationNumber": "APP001",
"policyNumber": "POL001",
"productName": "智能耳标监控系统",
"customerName": "张三",
"idType": "身份证",
"idNumber": "110101199001011234",
"livestockSupplyType": "牛养殖",
"installationStatus": "待安装",
"priority": "高",
"installationAddress": null,
"contactPhone": null,
"taskGeneratedTime": "2025-09-22 18:28:58",
"installationCompletedTime": null,
"createdAt": "2025-09-22 18:28:58",
"updatedAt": "2025-09-22 18:28:58"
}
```
## 修复历史
### 2025-10-09 修复内容
1. ✅ 修复字段映射错误(驼峰 → 下划线)
2. ✅ 添加缺失字段:证件类型、优先级、安装地址、联系电话、更新时间
3. ✅ 移除不存在的字段映射
4. ✅ 删除多余的状态映射逻辑
5. ✅ 统一空值处理方式

View File

@@ -0,0 +1,313 @@
# 监管任务模块问题诊断报告
## 诊断时间
2025年10月9日
## 问题描述
用户反映监管任务结项管理模块表中有数据但是前端页面没有数据后端API接口返回也没数据。
## 诊断结果
### ✅ 后端状态 - 完全正常
#### 1. 数据库层面
- **表名**`supervisory_tasks`
- **数据**存在1条测试数据
- **结构**:正确
```sql
SELECT COUNT(*) FROM supervisory_tasks;
-- 结果: 1条记录
SELECT * FROM supervisory_tasks LIMIT 1;
-- 数据正常,包含完整字段
```
#### 2. Model层面
- **模型文件**`models/SupervisoryTask.js`
- **表映射**:正确 (`tableName: 'supervisory_tasks'`)
- **查询测试**:✅ 通过
```javascript
// 直接通过Model查询成功
const tasks = await SupervisoryTask.findAll();
// 结果: 返回1条记录
```
#### 3. API层面
- **控制器**`controllers/supervisoryTaskController.js`
- **路由**`routes/supervisoryTasks.js`
- **注册**:✅ 正确注册在 `src/app.js`
- `/api/supervisory-tasks` (备用路径)
- `/api/supervision-tasks` (主要路径 - 前端使用)
- **API测试**:✅ 完全正常
**API测试结果**
```bash
GET /api/supervision-tasks?page=1&limit=10
Authorization: Bearer <token>
响应:
{
"code": 200,
"status": "success",
"message": "获取监管任务列表成功",
"data": {
"list": [
{
"id": 1,
"applicationNumber": "APP2025001",
"policyNumber": "POL2025001",
"productName": "农业保险产品",
"customerName": "张三",
"taskStatus": "待处理",
"priority": "中",
...
}
],
"total": 1,
"page": 1,
"limit": 10
}
}
```
#### 4. 后端服务状态
- **端口**3000
- **状态**:✅ 正在运行
- **PID**19340
### ✅ 前端状态
#### 1. 服务状态
- **端口**3001
- **状态**:✅ 正在运行
- **PID**18648
#### 2. 代理配置
```javascript
// vite.config.js
proxy: {
'/insurance/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/insurance/, '')
}
}
```
配置:✅ 正确
#### 3. API配置
```javascript
// src/utils/api.js
export const supervisionTaskApi = {
getList: (params) => api.get('/supervision-tasks', { params }),
getStats: () => api.get('/supervision-tasks/stats'),
export: (params) => api.get('/supervision-tasks/export', { params, responseType: 'blob' }),
...
}
```
配置:✅ 正确
#### 4. 环境配置
```javascript
// src/config/env.js
const envConfig = {
baseURL: '/insurance/api',
timeout: 10000
}
```
配置:✅ 正确
## 可能的问题原因
### 1. 认证Token问题 ⚠️
前端可能没有正确发送认证token或token已过期。
**验证方法**
```javascript
// 在浏览器控制台检查
localStorage.getItem('accessToken')
localStorage.getItem('refreshToken')
```
### 2. 请求拦截器问题 ⚠️
前端的请求拦截器可能没有正确添加Authorization头。
**检查文件**`insurance_admin-system/src/utils/request.js`
### 3. 数据响应格式解析问题 ⚠️
前端页面可能没有正确解析后端返回的数据格式。
**后端返回格式**
```javascript
{
data: {
status: 'success',
data: {
list: [...],
total: 1
}
}
}
```
**前端期望格式**:需要检查 `CompletedTaskManagement.vue``SupervisionTaskManagement.vue`
### 4. 路由路径不匹配 ⚠️
如果前端使用了错误的API路径。
### 5. 跨域问题 ⚠️
虽然后端配置了CORS但可能存在配置问题。
## 建议的解决步骤
### 步骤1检查用户登录状态
```javascript
// 在浏览器控制台执行
console.log('Token:', localStorage.getItem('accessToken'));
console.log('是否登录:', !!localStorage.getItem('accessToken'));
```
### 步骤2检查网络请求
1. 打开浏览器开发者工具 (F12)
2. 切换到 Network 标签
3. 刷新页面
4. 查找 `/supervision-tasks` 请求
5. 检查:
- 请求URL是否正确
- 请求头是否包含 `Authorization: Bearer <token>`
- 响应状态码
- 响应内容
### 步骤3检查控制台错误
1. 打开浏览器开发者工具Console标签
2. 查看是否有JavaScript错误
3. 查看是否有API请求失败的错误信息
### 步骤4强制刷新Token
```javascript
// 在前端代码中
1. 清除现有token
localStorage.clear()
2. 重新登录
使用账号: admin
密码: 123456
```
### 步骤5检查数据处理逻辑
检查前端页面中的数据响应处理:
**CompletedTaskManagement.vue**
```javascript
// 第466-476行
const response = await supervisionTaskApi.getList(params)
console.log('监管任务结项API响应:', response)
if (response.data && response.data.status === 'success') {
taskList.value = response.data.data.list || []
pagination.total = response.data.data.total || 0
}
```
**SupervisionTaskManagement.vue**
```javascript
// 第513-522行
const response = await supervisionTaskApi.getList(params)
console.log('监管任务API响应:', response)
if (response.data && response.data.status === 'success') {
tableData.value = response.data.data.list
pagination.total = response.data.data.total
}
```
## 测试命令
### 测试数据库
```bash
cd insurance_backend
node test-supervisory-table.js
```
### 测试API需要后端服务运行
```bash
cd insurance_backend
node test-full-api-flow.js
```
### 启动后端服务
```bash
cd insurance_backend
npm start
```
### 启动前端服务
```bash
cd insurance_admin-system
npm run dev
```
## 前端调试代码
在浏览器控制台执行以下代码测试API
```javascript
// 1. 获取token
const token = localStorage.getItem('accessToken');
console.log('Token:', token);
// 2. 手动测试API
fetch('/insurance/api/supervision-tasks?page=1&limit=10', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(data => {
console.log('API响应:', data);
if (data.status === 'success') {
console.log('✓ 数据获取成功');
console.log('记录数:', data.data.total);
console.log('数据列表:', data.data.list);
} else {
console.log('✗ 响应状态异常');
}
})
.catch(err => console.error('✗ 请求失败:', err));
```
## 结论
**后端完全正常**,问题很可能出在前端的以下几个方面:
1. **认证token未正确发送**(最可能)
2. **数据响应格式解析错误**
3. **页面组件渲染逻辑问题**
4. **浏览器缓存问题**
建议用户按照上述步骤逐一排查特别是先检查浏览器Network标签中的实际请求情况。
## 文件清单
### 后端文件
-`models/SupervisoryTask.js` - 模型定义正确
-`controllers/supervisoryTaskController.js` - 控制器逻辑正确
-`routes/supervisoryTasks.js` - 路由配置正确
-`src/app.js` - 路由注册正确
### 前端文件
-`src/config/env.js` - 环境配置(新增)
-`src/utils/api.js` - API封装正确
-`src/utils/request.js` - 请求工具配置正确
-`src/views/CompletedTaskManagement.vue` - 已更新使用API
-`src/views/SupervisionTaskManagement.vue` - 已使用API
-`vite.config.js` - 代理配置正确
### 测试文件(可删除)
- `insurance_backend/test-supervisory-table.js`
- `insurance_backend/test-api-response.js`
- `insurance_backend/test-api-direct.js`
- `insurance_backend/test-full-api-flow.js`

View File

@@ -0,0 +1,267 @@
# 监管任务结项管理API封装修复说明
## 修复日期
2025年10月9日
## 问题描述
监管任务结项管理模块(`CompletedTaskManagement.vue`中使用了模拟数据没有调用真实的后端API接口。需要将其改为使用封装的API工具类。
## 修复内容
### 1. 创建环境配置文件 `src/config/env.js`
创建了统一的环境配置文件用于管理所有API URL和环境变量
**主要功能:**
- 支持多环境配置development、production、test
- 统一管理API基础URL、超时时间等配置
- 定义了所有API端点的路径常量
**配置项:**
```javascript
{
baseURL: '/insurance/api',
timeout: 10000,
wsURL: 'ws://localhost:3000'
}
```
### 2. 更新 API 工具类 `src/utils/api.js`
`supervisionTaskApi` 中添加了两个新的API方法
```javascript
export const supervisionTaskApi = {
// ... 现有方法
archive: (id) => api.patch(`/supervision-tasks/${id}/archive`),
downloadReport: (id) => api.get(`/supervision-tasks/${id}/report`, { responseType: 'blob' })
}
```
### 3. 更新请求工具 `src/utils/request.js`
- 导入环境配置文件
- 使用 `envConfig.baseURL``envConfig.timeout` 替代硬编码的配置
**修改前:**
```javascript
const API_CONFIG = {
baseURL: '/insurance/api',
timeout: 10000
}
```
**修改后:**
```javascript
import envConfig from '@/config/env'
const API_CONFIG = {
baseURL: envConfig.baseURL,
timeout: envConfig.timeout
}
```
### 4. 更新用户状态管理 `src/stores/user.js`
- 导入环境配置文件
- 在刷新token的fetch请求中使用 `envConfig.baseURL`
**修改前:**
```javascript
const response = await fetch('/insurance/api/auth/refresh', {
// ...
})
```
**修改后:**
```javascript
import envConfig from '@/config/env'
const response = await fetch(`${envConfig.baseURL}/auth/refresh`, {
// ...
})
```
### 5. 完善监管任务结项管理 `src/views/CompletedTaskManagement.vue`
#### 5.1 获取任务列表 `fetchTaskList`
**修改前:** 使用模拟数据
**修改后:** 调用真实API
```javascript
const fetchTaskList = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
limit: pagination.pageSize,
taskStatus: 'completed' // 只获取已完成的任务
}
// 添加搜索条件
if (searchForm.taskName) {
params.taskName = searchForm.taskName
}
if (searchForm.status) {
params.status = searchForm.status
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
params.startDate = searchForm.dateRange[0].format('YYYY-MM-DD')
params.endDate = searchForm.dateRange[1].format('YYYY-MM-DD')
}
const response = await supervisionTaskApi.getList(params)
// 处理响应...
}
}
```
#### 5.2 获取统计数据 `fetchStats`
新增函数调用统计API获取实时数据
```javascript
const fetchStats = async () => {
try {
const response = await supervisionTaskApi.getStats()
if (response.data && response.data.status === 'success') {
const statsData = response.data.data
stats.total = statsData.total || 0
stats.thisMonth = statsData.thisMonth || 0
stats.archived = statsData.archived || 0
stats.avgDuration = statsData.avgDuration || 0
}
} catch (error) {
console.error('获取统计数据失败:', error)
}
}
```
#### 5.3 下载报告 `handleDownload`
**修改前:** 仅显示提示信息
**修改后:** 调用API下载报告文件
```javascript
const handleDownload = async (record) => {
try {
message.loading(`正在下载 ${record.taskName} 的报告...`, 0)
const response = await supervisionTaskApi.downloadReport(record.id)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `${record.taskName}_报告_${new Date().getTime()}.pdf`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.destroy()
message.success('报告下载成功')
} catch (error) {
message.destroy()
console.error('下载报告失败:', error)
message.error('下载报告失败,请稍后重试')
}
}
```
#### 5.4 归档任务 `handleArchive`
**修改前:** 仅显示成功提示
**修改后:** 调用API执行归档操作
```javascript
const handleArchive = async (record) => {
try {
const response = await supervisionTaskApi.archive(record.id)
if (response.data && response.data.status === 'success') {
message.success(`任务 ${record.taskName} 已归档`)
fetchTaskList()
} else {
message.error(response.data?.message || '归档失败')
}
} catch (error) {
console.error('归档任务失败:', error)
message.error('归档任务失败,请稍后重试')
}
}
```
## 修复后的优势
1. **统一配置管理**所有API URL通过env.js统一管理便于维护和切换环境
2. **代码规范**所有API调用都使用封装的工具类符合项目规范
3. **功能完整**:实现了真实的数据获取、统计、导出、归档、下载报告等功能
4. **错误处理**:完善的错误提示和异常处理
5. **用户体验**:添加了加载提示和操作反馈
## 后端需要实现的接口
为了使前端功能完整可用,后端需要实现以下接口:
### 1. 获取监管任务列表(支持已完成任务筛选)
- **路径**`GET /supervision-tasks`
- **参数**
- `page`: 页码
- `limit`: 每页数量
- `taskStatus`: 任务状态completed
- `taskName`: 任务名称(可选)
- `status`: 状态completed/archived可选
- `startDate`: 开始日期(可选)
- `endDate`: 结束日期(可选)
### 2. 获取统计数据
- **路径**`GET /supervision-tasks/stats`
- **返回数据**
```javascript
{
total: 总结项任务数,
thisMonth: 本月结项数,
archived: 已归档任务数,
avgDuration: 平均处理时长(天)
}
```
### 3. 归档任务
- **路径**`PATCH /supervision-tasks/:id/archive`
- **说明**:将已完成的任务标记为已归档
### 4. 下载任务报告
- **路径**`GET /supervision-tasks/:id/report`
- **返回**PDF文件流
- **说明**:生成并下载任务报告
### 5. 导出任务列表
- **路径**`GET /supervision-tasks/export`
- **参数**:与获取列表接口相同的筛选参数
- **返回**Excel文件流
## 测试建议
1. 测试获取已完成任务列表功能
2. 测试搜索和筛选功能
3. 测试统计数据显示
4. 测试任务归档功能
5. 测试报告下载功能
6. 测试导出Excel功能
7. 测试不同环境下的API调用开发、测试、生产
## 相关文件
- `insurance_admin-system/src/config/env.js` - 环境配置文件(新增)
- `insurance_admin-system/src/utils/api.js` - API工具类修改
- `insurance_admin-system/src/utils/request.js` - 请求工具(修改)
- `insurance_admin-system/src/stores/user.js` - 用户状态管理(修改)
- `insurance_admin-system/src/views/CompletedTaskManagement.vue` - 结项管理页面(修改)
## 注意事项
1. 所有fetch调用都已改为使用封装的API工具类
2. 环境配置文件支持多环境切换,部署时需要根据实际情况配置
3. 请确保后端API接口已实现并测试通过
4. 导出和下载功能需要后端返回正确的文件流和Content-Disposition头

View File

@@ -0,0 +1,156 @@
/**
* 环境配置文件
* 统一管理所有API URL和环境变量
*/
// 获取环境变量
const env = import.meta.env.MODE || 'development'
// 环境配置
const envConfig = {
development: {
// 开发环境配置
baseURL: '/insurance/api',
timeout: 30000,
// 其他开发环境配置
wsURL: 'ws://localhost:3000',
},
production: {
// 生产环境配置
baseURL: '/insurance/api',
timeout: 10000,
// 其他生产环境配置
wsURL: 'wss://production-domain.com',
},
test: {
// 测试环境配置
baseURL: '/insurance/api',
timeout: 15000,
// 其他测试环境配置
wsURL: 'ws://test-server:3000',
}
}
// 导出当前环境配置
export default envConfig[env] || envConfig.development
// 导出特定配置项,方便按需引入
export const { baseURL, timeout, wsURL } = envConfig[env] || envConfig.development
// API端点配置
export const API_ENDPOINTS = {
// 认证相关
AUTH: {
LOGIN: '/auth/login',
LOGOUT: '/auth/logout',
REFRESH: '/auth/refresh',
PROFILE: '/users/profile'
},
// 用户管理
USERS: {
LIST: '/users',
CREATE: '/users',
UPDATE: (id) => `/users/${id}`,
DELETE: (id) => `/users/${id}`,
EXPORT: '/users/export'
},
// 保险类型管理
INSURANCE_TYPES: {
LIST: '/insurance-types',
CREATE: '/insurance-types',
UPDATE: (id) => `/insurance-types/${id}`,
DELETE: (id) => `/insurance-types/${id}`,
EXPORT: '/insurance-types/export'
},
// 申请管理
APPLICATIONS: {
LIST: '/insurance/applications',
DETAIL: (id) => `/insurance/applications/${id}`,
CREATE: '/insurance/applications',
UPDATE: (id) => `/insurance/applications/${id}`,
REVIEW: (id) => `/insurance/applications/${id}/review`,
EXPORT: '/insurance/applications/export'
},
// 保单管理
POLICIES: {
LIST: '/policies',
DETAIL: (id) => `/policies/${id}`,
CREATE: '/policies',
UPDATE: (id) => `/policies/${id}`,
EXPORT: '/policies/export'
},
// 理赔管理
CLAIMS: {
LIST: '/claims',
DETAIL: (id) => `/claims/${id}`,
UPDATE_STATUS: (id) => `/claims/${id}/status`,
EXPORT: '/claims/export'
},
// 设备预警
DEVICE_ALERTS: {
LIST: '/device-alerts',
DETAIL: (id) => `/device-alerts/${id}`,
STATS: '/device-alerts/stats',
MARK_READ: (id) => `/device-alerts/${id}/read`,
EXPORT: '/device-alerts/export'
},
// 监管任务
SUPERVISION_TASKS: {
LIST: '/supervision-tasks',
DETAIL: (id) => `/supervision-tasks/${id}`,
CREATE: '/supervision-tasks',
UPDATE: (id) => `/supervision-tasks/${id}`,
DELETE: (id) => `/supervision-tasks/${id}`,
STATS: '/supervision-tasks/stats',
EXPORT: '/supervision-tasks/export',
ARCHIVE: (id) => `/supervision-tasks/${id}/archive`,
DOWNLOAD_REPORT: (id) => `/supervision-tasks/${id}/report`
},
// 待安装任务
INSTALLATION_TASKS: {
LIST: '/installation-tasks',
DETAIL: (id) => `/installation-tasks/${id}`,
CREATE: '/installation-tasks',
UPDATE: (id) => `/installation-tasks/${id}`,
DELETE: (id) => `/installation-tasks/${id}`,
STATS: '/installation-tasks/stats',
EXPORT: '/installation-tasks/export'
},
// 生资保险
LIVESTOCK: {
TYPES: '/livestock-types',
POLICIES: '/livestock-policies',
CLAIMS: '/livestock-claims'
},
// 操作日志
OPERATION_LOGS: {
LIST: '/operation-logs',
STATS: '/operation-logs/stats',
EXPORT: '/operation-logs/export'
},
// 权限管理
PERMISSIONS: {
LIST: '/permissions',
TREE: '/permissions/tree'
},
// 角色权限
ROLE_PERMISSIONS: {
ROLES: '/role-permissions/roles',
PERMISSIONS: '/role-permissions/permissions',
ASSIGN: (roleId) => `/role-permissions/${roleId}/assign`,
COPY: '/role-permissions/copy'
}
}

View File

@@ -20,7 +20,6 @@ import RangePickerTest from '@/views/RangePickerTest.vue'
import LoginTest from '@/views/LoginTest.vue'
import LivestockPolicyManagement from '@/views/LivestockPolicyManagement.vue'
import SystemSettings from '@/views/SystemSettings.vue'
import TokenDebug from '@/views/TokenDebug.vue'
const routes = [
{
@@ -167,7 +166,7 @@ const routes = [
{
path: 'token-debug',
name: 'TokenDebug',
component: TokenDebug,
component: () => import('@/views/TokenDebug.vue'),
meta: { title: 'Token调试' }
}
]

View File

@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import envConfig from '@/config/env'
export const useUserStore = defineStore('user', () => {
// 状态
@@ -102,7 +103,7 @@ export const useUserStore = defineStore('user', () => {
}
try {
const response = await fetch('/insurance/api/auth/refresh', {
const response = await fetch(`${envConfig.baseURL}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -20,7 +20,8 @@ export const userAPI = {
changePassword: (data) => api.put('/users/change-password', data),
uploadAvatar: (formData) => api.post('/users/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}),
export: (params) => api.get('/users/export', { params, responseType: 'blob' })
};
export const menuAPI = {
@@ -39,7 +40,8 @@ export const insuranceTypeAPI = {
create: (data) => api.post('/insurance-types', data),
update: (id, data) => api.put(`/insurance-types/${id}`, data),
delete: (id) => api.delete(`/insurance-types/${id}`),
updateStatus: (id, data) => api.patch(`/insurance-types/${id}/status`, data)
updateStatus: (id, data) => api.patch(`/insurance-types/${id}/status`, data),
export: (params) => api.get('/insurance-types/export', { params, responseType: 'blob' })
}
export const applicationAPI = {
@@ -61,14 +63,16 @@ export const policyAPI = {
create: (data) => api.post('/policies', data),
update: (id, data) => api.put(`/policies/${id}`, data),
updateStatus: (id, data) => api.put(`/policies/${id}/status`, data),
delete: (id) => api.delete(`/policies/${id}`)
delete: (id) => api.delete(`/policies/${id}`),
export: (params) => api.get('/policies/export', { params, responseType: 'blob' })
}
export const claimAPI = {
getList: (params) => api.get('/claims', { params }),
getDetail: (id) => api.get(`/claims/${id}`),
updateStatus: (id, data) => api.put(`/claims/${id}/status`, data),
delete: (id) => api.delete(`/claims/${id}`)
delete: (id) => api.delete(`/claims/${id}`),
export: (params) => api.get('/claims/export', { params, responseType: 'blob' })
}
export const dashboardAPI = {
@@ -84,7 +88,8 @@ export const deviceAlertAPI = {
getDetail: (id) => api.get(`/device-alerts/${id}`),
markAsRead: (id) => api.patch(`/device-alerts/${id}/read`),
markAllAsRead: () => api.patch('/device-alerts/read-all'),
handle: (id, data) => api.patch(`/device-alerts/${id}/handle`, data)
handle: (id, data) => api.patch(`/device-alerts/${id}/handle`, data),
export: (params) => api.get('/device-alerts/export', { params, responseType: 'blob' })
}
// 数据览仓API
@@ -104,7 +109,10 @@ export const supervisionTaskApi = {
delete: (id) => api.delete(`/supervision-tasks/${id}`),
getDetail: (id) => api.get(`/supervision-tasks/${id}`),
batchOperate: (data) => api.post('/supervision-tasks/batch/operate', data),
getStats: () => api.get('/supervision-tasks/stats')
getStats: () => api.get('/supervision-tasks/stats'),
export: (params) => api.get('/supervision-tasks/export', { params, responseType: 'blob' }),
archive: (id) => api.patch(`/supervision-tasks/${id}/archive`),
downloadReport: (id) => api.get(`/supervision-tasks/${id}/report`, { responseType: 'blob' })
}
// 待安装任务API
@@ -118,7 +126,11 @@ export const installationTaskApi = {
getStats: () => api.get('/installation-tasks/stats'),
assign: (id, data) => api.post(`/installation-tasks/${id}/assign`, data),
complete: (id, data) => api.post(`/installation-tasks/${id}/complete`, data),
getHistory: (id) => api.get(`/installation-tasks/${id}/history`)
getHistory: (id) => api.get(`/installation-tasks/${id}/history`),
export: (params) => api.get('/installation-tasks/export', { params, responseType: 'blob' }),
batchDelete: (ids) => api.post('/installation-tasks/batch-delete', { ids }),
batchUpdateStatus: (data) => api.post('/installation-tasks/batch-update-status', data),
getDetail: (id) => api.get(`/installation-tasks/${id}`)
}
// 生资保险相关API
@@ -140,7 +152,8 @@ export const livestockPolicyApi = {
getById: (id) => api.get(`/livestock-policies/${id}`),
updateStatus: (id, data) => api.patch(`/livestock-policies/${id}/status`, data),
getStats: () => api.get('/livestock-policies/stats'),
getLivestockTypes: () => api.get('/livestock-types/active')
getLivestockTypes: () => api.get('/livestock-types/active'),
export: (params) => api.get('/livestock-policies/export', { params, responseType: 'blob' })
}
export const livestockClaimApi = {
@@ -152,7 +165,8 @@ export const livestockClaimApi = {
approve: (id, data) => api.post(`/livestock-claims/${id}/approve`, data),
reject: (id, data) => api.post(`/livestock-claims/${id}/reject`, data),
updatePaymentStatus: (id, data) => api.patch(`/livestock-claims/${id}/payment`, data),
getStats: () => api.get('/livestock-claims/stats')
getStats: () => api.get('/livestock-claims/stats'),
export: (params) => api.get('/livestock-claims/export', { params, responseType: 'blob' })
}
// 操作日志API

View File

@@ -14,7 +14,7 @@ export default {
// 更新安装任务
updateInstallationTask: (id, data) => {
return installationTaskApi.update({ ...data, id })
return installationTaskApi.update(id, data)
},
// 删除安装任务

View File

@@ -1,11 +1,12 @@
import { useUserStore } from '@/stores/user'
import { message, Modal } from 'ant-design-vue'
import router from '@/router'
import envConfig from '@/config/env'
// API基础配置
const API_CONFIG = {
baseURL: '/insurance/api',
timeout: 10000
baseURL: envConfig.baseURL,
timeout: envConfig.timeout
}
// 是否正在刷新token的标志
@@ -85,7 +86,11 @@ const handleResponse = async (response) => {
try {
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
// 处理Excel文件下载
if (contentType && contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) {
data = await response.blob()
} else if (contentType && contentType.includes('application/json')) {
data = await response.json()
} else {
data = await response.text()

View File

@@ -640,7 +640,7 @@ const exportData = async () => {
exportLoading.value = true
try {
const response = await applicationAPI.export(searchForm)
const url = window.URL.createObjectURL(new Blob([response.data]))
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `申请数据_${new Date().toISOString().slice(0, 10)}.xlsx`)

View File

@@ -5,10 +5,16 @@
sub-title="管理系统所有理赔申请"
>
<template #extra>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增理赔
</a-button>
<a-space>
<a-button @click="handleExport">
<download-outlined />
导出Excel
</a-button>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增理赔
</a-button>
</a-space>
</template>
</a-page-header>
@@ -445,7 +451,8 @@ import {
PlusOutlined,
SearchOutlined,
RedoOutlined,
FileTextOutlined
FileTextOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
import { livestockClaimApi } from '@/utils/api'
import dayjs from 'dayjs'
@@ -967,6 +974,42 @@ const handleDelete = async (id) => {
}
}
const handleExport = async () => {
try {
loading.value = true
const params = {
search: searchForm.search || undefined,
status: searchForm.status || undefined
}
// 过滤掉undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await livestockClaimApi.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `理赔列表_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
onMounted(() => {
loadClaims()
})

View File

@@ -39,6 +39,10 @@
</a-col>
<a-col :span="6">
<a-space>
<a-button @click="handleExport">
<DownloadOutlined />
导出Excel
</a-button>
<a-button type="primary" @click="handleSearch">
<SearchOutlined />
搜索
@@ -233,6 +237,7 @@ import {
ExportOutlined,
DownOutlined
} from '@ant-design/icons-vue'
import { supervisionTaskApi } from '@/utils/api'
// 响应式数据
const loading = ref(false)
@@ -370,6 +375,42 @@ const handleReset = () => {
fetchTaskList()
}
const handleExport = async () => {
try {
loading.value = true
const params = {
taskName: searchForm.taskName || undefined,
status: searchForm.status || undefined
}
// 过滤掉undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await supervisionTaskApi.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `监管任务结项列表_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
// 表格变化处理
const handleTableChange = (pag) => {
pagination.current = pag.current
@@ -384,76 +425,113 @@ const handleView = (record) => {
}
// 下载报告
const handleDownload = (record) => {
message.info(`正在下载 ${record.taskName} 的报告...`)
// 这里实现下载逻辑
const handleDownload = async (record) => {
try {
message.loading(`正在下载 ${record.taskName} 的报告...`, 0)
const response = await supervisionTaskApi.downloadReport(record.id)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `${record.taskName}_报告_${new Date().getTime()}.pdf`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.destroy()
message.success('报告下载成功')
} catch (error) {
message.destroy()
console.error('下载报告失败:', error)
message.error('下载报告失败,请稍后重试')
}
}
// 归档任务
const handleArchive = (record) => {
message.success(`任务 ${record.taskName} 已归档`)
fetchTaskList()
const handleArchive = async (record) => {
try {
const response = await supervisionTaskApi.archive(record.id)
if (response.data && response.data.status === 'success') {
message.success(`任务 ${record.taskName} 已归档`)
fetchTaskList()
} else {
message.error(response.data?.message || '归档失败')
}
} catch (error) {
console.error('归档任务失败:', error)
message.error('归档任务失败,请稍后重试')
}
}
// 导出任务
const handleExport = (record) => {
message.info(`正在导出 ${record.taskName} 的数据...`)
// 这里实现导出逻辑
}
// 导出单个任务此功能已合并到handleExport
// const handleExportSingle = (record) => {
// message.info(`正在导出 ${record.taskName} 的数据...`)
// }
// 获取任务列表
const fetchTaskList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
const params = {
page: pagination.current,
limit: pagination.pageSize,
taskStatus: 'completed' // 只获取已完成的任务
}
// 模拟数据
const mockData = [
{
id: 1,
taskCode: 'RT001',
taskName: '农场设备安全检查',
priority: 'high',
status: 'completed',
assignee: '张三',
createdAt: '2024-01-15',
completedAt: '2024-01-20',
duration: 5,
description: '对农场所有设备进行安全检查,确保设备正常运行',
completionNotes: '检查完成发现3处需要维修的设备已安排维修'
},
{
id: 2,
taskCode: 'RT002',
taskName: '环境监测数据审核',
priority: 'medium',
status: 'archived',
assignee: '李四',
createdAt: '2024-01-10',
completedAt: '2024-01-18',
duration: 8,
description: '审核上月环境监测数据,确保数据准确性',
completionNotes: '数据审核完成,所有数据符合标准'
}
]
// 添加搜索条件
if (searchForm.taskName) {
params.taskName = searchForm.taskName
}
if (searchForm.status) {
params.status = searchForm.status
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
params.startDate = searchForm.dateRange[0].format('YYYY-MM-DD')
params.endDate = searchForm.dateRange[1].format('YYYY-MM-DD')
}
taskList.value = mockData
pagination.total = mockData.length
const response = await supervisionTaskApi.getList(params)
console.log('监管任务结项API响应:', response)
// 更新统计数据
stats.total = 156
stats.thisMonth = 23
stats.archived = 89
stats.avgDuration = 6.5
if (response.data && response.data.status === 'success') {
taskList.value = response.data.data.list || []
pagination.total = response.data.data.total || 0
console.log('监管任务结项数据设置成功:', taskList.value.length, '条')
} else {
console.log('监管任务结项响应格式错误:', response)
message.error(response.data?.message || '获取任务列表失败')
}
// 获取统计数据
await fetchStats()
} catch (error) {
console.error('获取任务列表失败:', error)
message.error('获取任务列表失败')
} finally {
loading.value = false
}
}
// 获取统计数据
const fetchStats = async () => {
try {
const response = await supervisionTaskApi.getStats()
if (response.data && response.data.status === 'success') {
const statsData = response.data.data
stats.total = statsData.total || 0
stats.thisMonth = statsData.thisMonth || 0
stats.archived = statsData.archived || 0
stats.avgDuration = statsData.avgDuration || 0
}
} catch (error) {
console.error('获取统计数据失败:', error)
}
}
// 组件挂载时获取数据
onMounted(() => {
fetchTaskList()

View File

@@ -110,11 +110,147 @@
<div class="pagination-info">
<span> {{ pagination.total }} 条记录</span>
</div>
<!-- 查看/编辑安装任务弹窗 -->
<a-modal
:title="modalTitle"
:open="modalVisible"
:width="900"
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirmLoading="modalLoading"
:footer="isViewMode ? null : undefined"
>
<a-form
ref="modalFormRef"
:model="modalForm"
:rules="modalRules"
layout="vertical"
:disabled="isViewMode"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="申请单号" name="applicationNumber">
<a-input v-model:value="modalForm.applicationNumber" placeholder="请输入申请单号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保单编号" name="policyNumber">
<a-input v-model:value="modalForm.policyNumber" placeholder="请输入保单编号" />
</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="modalForm.productName" placeholder="请输入产品名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="客户姓名" name="customerName">
<a-input v-model:value="modalForm.customerName" placeholder="请输入客户姓名" />
</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="modalForm.idType" placeholder="请选择证件类型">
<a-select-option value="身份证">身份证</a-select-option>
<a-select-option value="护照">护照</a-select-option>
<a-select-option value="军官证">军官证</a-select-option>
<a-select-option value="其他">其他</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="证件号码" name="idNumber">
<a-input v-model:value="modalForm.idNumber" placeholder="请输入证件号码" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="养殖生资种类" name="livestockSupplyType">
<a-input v-model:value="modalForm.livestockSupplyType" placeholder="请输入养殖生资种类" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="待安装设备" name="pendingDevices">
<a-input v-model:value="modalForm.pendingDevices" placeholder="请输入待安装设备" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="安装状态" name="installationStatus">
<a-select v-model:value="modalForm.installationStatus" placeholder="请选择安装状态">
<a-select-option value="待安装">待安装</a-select-option>
<a-select-option value="安装中">安装中</a-select-option>
<a-select-option value="已安装">已安装</a-select-option>
<a-select-option value="安装失败">安装失败</a-select-option>
<a-select-option value="已取消">已取消</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="生成安装任务时间" name="taskGeneratedTime">
<a-date-picker
v-model:value="modalForm.taskGeneratedTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择生成任务时间"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="安装完成生效时间" name="installationCompletedTime">
<a-date-picker
v-model:value="modalForm.installationCompletedTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择完成时间"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="安装人员" name="installer">
<a-input v-model:value="modalForm.installer" placeholder="请输入安装人员" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="备注" name="remarks">
<a-textarea
v-model:value="modalForm.remarks"
placeholder="请输入备注信息"
:rows="3"
/>
</a-form-item>
<!-- 查看模式下显示操作按钮 -->
<div v-if="isViewMode" style="text-align: right; margin-top: 16px;">
<a-space>
<a-button @click="handleModalCancel">关闭</a-button>
<a-button type="primary" @click="switchToEditMode">编辑</a-button>
</a-space>
</div>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined } from '@ant-design/icons-vue';
import dayjs from 'dayjs';
@@ -123,6 +259,11 @@ import installationTaskApi from '@/utils/installationTaskApi';
// 响应式数据
const loading = ref(false);
const tableData = ref([]);
const modalVisible = ref(false);
const modalLoading = ref(false);
const modalFormRef = ref();
const editingId = ref(null);
const isViewMode = ref(false);
// 搜索表单
const searchForm = reactive({
@@ -141,6 +282,48 @@ const pagination = reactive({
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
});
// 弹窗表单
const modalForm = reactive({
applicationNumber: '',
policyNumber: '',
productName: '',
customerName: '',
idType: '',
idNumber: '',
livestockSupplyType: '',
pendingDevices: '',
installationStatus: '',
taskGeneratedTime: null,
installationCompletedTime: null,
installer: '',
remarks: ''
});
// 表单验证规则
const modalRules = {
applicationNumber: [
{ required: true, message: '请输入申请单号', trigger: 'blur' }
],
policyNumber: [
{ required: true, message: '请输入保单编号', trigger: 'blur' }
],
productName: [
{ required: true, message: '请输入产品名称', trigger: 'blur' }
],
customerName: [
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
],
installationStatus: [
{ required: true, message: '请选择安装状态', trigger: 'change' }
]
};
// 计算属性
const modalTitle = computed(() => {
if (isViewMode.value) return '查看安装任务';
return editingId.value ? '编辑安装任务' : '新增安装任务';
});
// 表格列定义
const columns = [
{
@@ -279,12 +462,106 @@ const handleTableChange = (paginationInfo) => {
const handleView = (record) => {
console.log('查看记录:', record);
message.info('查看功能开发中');
isViewMode.value = true;
editingId.value = record.id;
fillModalForm(record);
modalVisible.value = true;
};
const handleEdit = (record) => {
console.log('编辑记录:', record);
message.info('编辑功能开发中');
isViewMode.value = false;
editingId.value = record.id;
fillModalForm(record);
modalVisible.value = true;
};
const fillModalForm = (record) => {
Object.assign(modalForm, {
applicationNumber: record.applicationNumber || '',
policyNumber: record.policyNumber || '',
productName: record.productName || '',
customerName: record.customerName || '',
idType: record.idType || '',
idNumber: record.idNumber || '',
livestockSupplyType: record.livestockSupplyType || '',
pendingDevices: record.pendingDevices || '',
installationStatus: record.installationStatus || '',
taskGeneratedTime: record.taskGeneratedTime ? dayjs(record.taskGeneratedTime) : null,
installationCompletedTime: record.installationCompletedTime ? dayjs(record.installationCompletedTime) : null,
installer: record.installer || '',
remarks: record.remarks || ''
});
};
const resetModalForm = () => {
Object.assign(modalForm, {
applicationNumber: '',
policyNumber: '',
productName: '',
customerName: '',
idType: '',
idNumber: '',
livestockSupplyType: '',
pendingDevices: '',
installationStatus: '',
taskGeneratedTime: null,
installationCompletedTime: null,
installer: '',
remarks: ''
});
};
const switchToEditMode = () => {
isViewMode.value = false;
};
const handleModalOk = async () => {
if (isViewMode.value) {
modalVisible.value = false;
return;
}
try {
await modalFormRef.value.validate();
modalLoading.value = true;
const submitData = {
...modalForm,
taskGeneratedTime: modalForm.taskGeneratedTime ?
dayjs(modalForm.taskGeneratedTime).format('YYYY-MM-DD HH:mm:ss') : null,
installationCompletedTime: modalForm.installationCompletedTime ?
dayjs(modalForm.installationCompletedTime).format('YYYY-MM-DD HH:mm:ss') : null
};
const response = editingId.value
? await installationTaskApi.updateInstallationTask(editingId.value, submitData)
: await installationTaskApi.createInstallationTask(submitData);
if (response.code === 200 || response.code === 201) {
message.success(editingId.value ? '更新成功' : '创建成功');
modalVisible.value = false;
fetchInstallationTasks();
} else {
message.error(response.message || '操作失败');
}
} catch (error) {
console.error('提交失败:', error);
if (error.errorFields) {
message.error('请完善表单信息');
} else {
message.error('操作失败');
}
} finally {
modalLoading.value = false;
}
};
const handleModalCancel = () => {
modalVisible.value = false;
isViewMode.value = false;
editingId.value = null;
resetModalForm();
};
const handleDelete = async (id) => {
@@ -306,16 +583,15 @@ const handleExportTasks = async () => {
try {
const response = await installationTaskApi.exportInstallationTasks(searchForm);
if (response) {
// 处理文件下载
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
if (response && response.data) {
// 处理文件下载 - response.data是Blob对象
const url = window.URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.download = `安装任务导出_${dayjs().format('YYYY-MM-DD_HH-mm-ss')}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success('导出成功');
@@ -324,7 +600,7 @@ const handleExportTasks = async () => {
}
} catch (error) {
console.error('导出失败:', error);
message.error('导出失败');
message.error('导出失败,请稍后重试');
}
};
@@ -446,4 +722,22 @@ const getInstallationStatusColor = (status) => {
color: #666;
font-size: 14px;
}
/* 查看模式下表单样式优化 */
:deep(.ant-form-item-control-input-content) {
.ant-input[disabled],
.ant-select-disabled .ant-select-selector,
.ant-picker-disabled,
.ant-input-number-disabled {
color: rgba(0, 0, 0, 0.85);
background-color: #fafafa;
border-color: #d9d9d9;
cursor: default;
}
.ant-input-disabled,
.ant-select-disabled .ant-select-selection-item {
color: rgba(0, 0, 0, 0.85);
}
}
</style>

View File

@@ -5,10 +5,16 @@
sub-title="管理系统支持的保险产品险种"
>
<template #extra>
<a-button type="primary" @click="showModal">
<PlusOutlined />
新增险种
</a-button>
<a-space>
<a-button @click="handleExport">
<DownloadOutlined />
导出Excel
</a-button>
<a-button type="primary" @click="showModal">
<PlusOutlined />
新增险种
</a-button>
</a-space>
</template>
</a-page-header>
@@ -326,7 +332,8 @@ import { insuranceTypeAPI } from '@/utils/api'
import {
PlusOutlined,
SearchOutlined,
RedoOutlined
RedoOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
const loading = ref(false)
@@ -1274,6 +1281,35 @@ const handleOnSaleStatusChange = (checked) => {
console.log(`险种销售状态${statusText},时间:${new Date().toLocaleString()}`)
}
// 导出Excel
const handleExport = async () => {
try {
loading.value = true
const params = {
...searchForm
}
const response = await insuranceTypeAPI.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `保险类型列表_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
// 组件卸载状态跟踪
const isComponentMounted = ref(true)

View File

@@ -2,10 +2,16 @@
<div class="livestock-policy-management">
<div class="page-header">
<h2>生资保单管理</h2>
<a-button type="primary" @click="showCreateModal">
<PlusOutlined />
新建保单
</a-button>
<a-space>
<a-button @click="handleExport">
<DownloadOutlined />
导出Excel
</a-button>
<a-button type="primary" @click="showCreateModal">
<PlusOutlined />
新建保单
</a-button>
</a-space>
</div>
<!-- 搜索筛选区域 -->
@@ -332,7 +338,8 @@ import dayjs from 'dayjs'
import {
PlusOutlined,
SearchOutlined,
DownOutlined
DownOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
import { livestockPolicyApi, livestockTypeApi } from '@/utils/api'
@@ -769,6 +776,44 @@ const getCurrentPremiumRate = () => {
return '请先选择牲畜类型'
}
const handleExport = async () => {
try {
loading.value = true
const params = {
policy_no: searchForm.policy_no || undefined,
policyholder_name: searchForm.policyholder_name || undefined,
policy_status: searchForm.policy_status || undefined,
payment_status: searchForm.payment_status || undefined
}
// 过滤掉undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await livestockPolicyApi.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `生资保单列表_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
fetchData()

View File

@@ -117,6 +117,10 @@
</a-col>
<a-col :span="8">
<a-space>
<a-button @click="handleExport">
<download-outlined />
导出Excel
</a-button>
<a-button type="primary" @click="markAllAsRead" :disabled="!hasUnreadAlerts">
<check-outlined />
全部标记已读
@@ -322,7 +326,8 @@ import {
FireOutlined,
CloudOutlined,
WifiOutlined,
ToolOutlined
ToolOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
import { deviceAlertAPI } from '@/utils/api'
import dayjs from 'dayjs'
@@ -605,6 +610,42 @@ const refreshData = async () => {
message.success('数据已刷新')
}
// 导出Excel
const handleExport = async () => {
try {
loading.value = true
const params = {
...filters
}
// 移除undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await deviceAlertAPI.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `消息通知_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
// 组件挂载时获取数据
onMounted(() => {
fetchStats()

View File

@@ -5,10 +5,16 @@
sub-title="管理系统所有保单信息"
>
<template #extra>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增保单
</a-button>
<a-space>
<a-button @click="handleExport">
<download-outlined />
导出Excel
</a-button>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增保单
</a-button>
</a-space>
</template>
</a-page-header>
@@ -322,7 +328,8 @@ import dayjs from 'dayjs'
import {
PlusOutlined,
SearchOutlined,
RedoOutlined
RedoOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
import { policyAPI } from '@/utils/api'
@@ -639,6 +646,44 @@ const handleDelete = async (id) => {
})
}
const handleExport = async () => {
try {
loading.value = true
const params = {
policy_number: searchForm.policy_number || undefined,
policyholder_name: searchForm.policyholder_name || undefined,
insurance_type_id: searchForm.insurance_type_id || undefined,
status: searchForm.status || undefined
}
// 过滤掉undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await policyAPI.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `保单列表_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
onMounted(() => {
loadPolicies()
})

View File

@@ -8,6 +8,10 @@
<!-- 操作按钮区 -->
<div class="action-buttons">
<a-space>
<a-button @click="handleExport">
<DownloadOutlined />
导出Excel
</a-button>
<a-button type="primary" @click="handleCreateTask">
新增监管任务
</a-button>
@@ -108,6 +112,69 @@
<span>页共 0 </span>
</div>
<!-- 查看详情弹窗 -->
<a-modal
title="查看监管任务详情"
:open="viewModalVisible"
:width="900"
@cancel="handleViewModalCancel"
:footer="null"
>
<a-descriptions bordered :column="2" size="middle">
<a-descriptions-item label="申请单号">
{{ viewRecord.applicationNumber || '-' }}
</a-descriptions-item>
<a-descriptions-item label="保单编号">
{{ viewRecord.policyNumber || '-' }}
</a-descriptions-item>
<a-descriptions-item label="产品名称">
{{ viewRecord.productName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="保险期间">
{{ viewRecord.insurancePeriod || '-' }}
</a-descriptions-item>
<a-descriptions-item label="客户姓名">
{{ viewRecord.customerName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="证件类型">
{{ viewRecord.idType || '-' }}
</a-descriptions-item>
<a-descriptions-item label="证件号码">
{{ viewRecord.idNumber || '-' }}
</a-descriptions-item>
<a-descriptions-item label="适用金额">
{{ viewRecord.applicableAmount ? formatAmount(viewRecord.applicableAmount) : '-' }}
</a-descriptions-item>
<a-descriptions-item label="监管生资数量">
{{ viewRecord.supervisorySuppliesQuantity || '-' }}
</a-descriptions-item>
<a-descriptions-item label="任务类型">
<a-tag :color="getTaskTypeColor(viewRecord.taskType)">
{{ getTaskTypeText(viewRecord.taskType) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="任务状态">
<a-tag :color="getStatusColor(viewRecord.taskStatus)">
{{ getStatusText(viewRecord.taskStatus) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="优先级">
<a-tag :color="getPriorityColor(viewRecord.priority)">
{{ getPriorityText(viewRecord.priority) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="创建时间" :span="2">
{{ viewRecord.createdAt || '-' }}
</a-descriptions-item>
<a-descriptions-item label="更新时间" :span="2">
{{ viewRecord.updatedAt || '-' }}
</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">
{{ viewRecord.remarks || '-' }}
</a-descriptions-item>
</a-descriptions>
</a-modal>
<!-- 创建/编辑监管任务弹窗 -->
<a-modal
:title="modalTitle"
@@ -223,13 +290,14 @@
<script>
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { SearchOutlined } from '@ant-design/icons-vue'
import { SearchOutlined, DownloadOutlined } from '@ant-design/icons-vue'
import { supervisionTaskApi } from '@/utils/api'
export default {
name: 'SupervisionTaskManagement',
components: {
SearchOutlined
SearchOutlined,
DownloadOutlined
},
setup() {
const loading = ref(false)
@@ -239,6 +307,8 @@ export default {
const modalLoading = ref(false)
const modalFormRef = ref()
const editingId = ref(null)
const viewModalVisible = ref(false)
const viewRecord = ref({})
// 搜索表单
const searchForm = reactive({
@@ -493,7 +563,13 @@ export default {
const handleView = (record) => {
console.log('查看详情:', record)
message.info('查看功能开发中...')
viewRecord.value = { ...record }
viewModalVisible.value = true
}
const handleViewModalCancel = () => {
viewModalVisible.value = false
viewRecord.value = {}
}
const handleEdit = (record) => {
@@ -564,6 +640,42 @@ export default {
resetModalForm()
}
const handleExport = async () => {
try {
loading.value = true
const params = {
policyNumber: searchForm.policyNumber || undefined,
customerName: searchForm.customerName || undefined
}
// 过滤掉undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await supervisionTaskApi.export(params)
// 创建下载链接 - response.data已经是Blob对象不需要再包装
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `监管任务列表_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
loadData()
@@ -583,6 +695,8 @@ export default {
modalFormRef,
modalRules,
modalTitle,
viewModalVisible,
viewRecord,
getStatusColor,
getStatusText,
getTaskTypeColor,
@@ -597,10 +711,12 @@ export default {
handleTaskGuidance,
handleBatchCreate,
handleView,
handleViewModalCancel,
handleEdit,
handleDelete,
handleModalOk,
handleModalCancel
handleModalCancel,
handleExport
}
}
}

View File

@@ -92,13 +92,19 @@
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="searchLogs">
<SearchOutlined />
搜索
</a-button>
<a-button style="margin-left: 8px" @click="resetLogFilters">
重置
</a-button>
<a-space>
<a-button @click="handleExportLogs">
<DownloadOutlined />
导出Excel
</a-button>
<a-button type="primary" @click="searchLogs">
<SearchOutlined />
搜索
</a-button>
<a-button @click="resetLogFilters">
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
@@ -170,7 +176,7 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { SearchOutlined } from '@ant-design/icons-vue'
import { SearchOutlined, DownloadOutlined } from '@ant-design/icons-vue'
import { operationLogAPI } from '@/utils/api'
import dayjs from 'dayjs'
import PermissionManagement from '@/components/PermissionManagement.vue'
@@ -329,6 +335,48 @@ const resetLogFilters = () => {
searchLogs()
}
const handleExportLogs = async () => {
try {
logLoading.value = true
const params = {
username: logFilters.username || undefined,
action: logFilters.action || undefined
}
// 处理日期范围
if (logFilters.dateRange && logFilters.dateRange.length === 2) {
params.startDate = dayjs(logFilters.dateRange[0]).format('YYYY-MM-DD')
params.endDate = dayjs(logFilters.dateRange[1]).format('YYYY-MM-DD')
}
// 过滤掉undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await operationLogAPI.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `操作日志_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
logLoading.value = false
}
}
// 表格变化处理
const handleLogTableChange = (pagination) => {
logPagination.current = pagination.current

View File

@@ -5,10 +5,16 @@
sub-title="管理系统用户和权限"
>
<template #extra>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增用户
</a-button>
<a-space>
<a-button @click="handleExport">
<download-outlined />
导出Excel
</a-button>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增用户
</a-button>
</a-space>
</template>
</a-page-header>
@@ -137,7 +143,8 @@ import { message } from 'ant-design-vue'
import {
PlusOutlined,
SearchOutlined,
RedoOutlined
RedoOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
import { userAPI } from '@/utils/api'
@@ -406,6 +413,42 @@ const handleDelete = async (id) => {
}
}
const handleExport = async () => {
try {
loading.value = true
const params = {
search: searchForm.username || undefined,
status: searchForm.status || undefined
}
// 过滤掉undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await userAPI.export(params)
// 创建下载链接
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `用户列表_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请稍后重试')
} finally {
loading.value = false
}
}
onMounted(() => {
loadUsers()
})

View File

@@ -18,10 +18,10 @@ export default defineConfig(({ mode }) => {
port: parseInt(env.VITE_PORT) || 3001,
historyApiFallback: true,
proxy: {
'/api': {
target: env.VITE_API_BASE_URL || 'https://ad.ningmuyun.com/insurace/',
'/insurance/api': {
target: env.VITE_API_BASE_URL || 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path
rewrite: (path) => path.replace(/^\/insurance/, '')
}
}
}

View File

@@ -1,6 +1,7 @@
const { DeviceAlert, Device, User } = require('../models');
const { Op } = require('sequelize');
const logger = require('../utils/logger');
const ExcelExport = require('../utils/excelExport');
/**
* 设备预警控制器
@@ -416,6 +417,103 @@ class DeviceAlertController {
});
}
}
/**
* 导出设备预警列表到Excel
*/
static async exportToExcel(req, res) {
try {
const { alert_level, alert_type, status, is_read } = req.query;
// 构建查询条件
const where = {};
if (alert_level) where.alert_level = alert_level;
if (alert_type) where.alert_type = alert_type;
if (status) where.status = status;
if (is_read !== undefined) where.is_read = is_read === 'true';
// 查询所有符合条件的数据
const alerts = await DeviceAlert.findAll({
where,
include: [{
model: Device,
as: 'Device',
attributes: ['device_name', 'device_number', 'installation_location']
}],
order: [['alert_time', 'DESC']],
raw: true,
nest: true
});
// 状态映射
const levelMap = {
info: '信息',
warning: '警告',
critical: '严重'
};
const typeMap = {
temperature: '温度异常',
humidity: '湿度异常',
offline: '设备离线',
maintenance: '设备维护'
};
const statusMap = {
pending: '待处理',
processing: '处理中',
resolved: '已解决'
};
// 准备导出数据
const exportData = alerts.map(alert => ({
alert_title: alert.alert_title || '',
alert_level: ExcelExport.formatStatus(alert.alert_level, levelMap),
alert_type: ExcelExport.formatStatus(alert.alert_type, typeMap),
alert_content: alert.alert_content || '',
device_name: alert.Device?.device_name || '',
device_number: alert.Device?.device_number || '',
installation_location: alert.Device?.installation_location || '',
status: ExcelExport.formatStatus(alert.status, statusMap),
is_read: alert.is_read ? '已读' : '未读',
alert_time: ExcelExport.formatDate(alert.alert_time),
read_time: ExcelExport.formatDate(alert.read_time)
}));
// 定义列
const columns = [
{ header: '预警标题', key: 'alert_title', width: 30 },
{ header: '预警级别', key: 'alert_level', width: 12 },
{ header: '预警类型', key: 'alert_type', width: 15 },
{ header: '预警内容', key: 'alert_content', width: 40 },
{ header: '设备名称', key: 'device_name', width: 20 },
{ header: '设备编号', key: 'device_number', width: 20 },
{ header: '安装位置', key: 'installation_location', width: 30 },
{ header: '处理状态', key: 'status', width: 12 },
{ header: '阅读状态', key: 'is_read', width: 10 },
{ header: '预警时间', key: 'alert_time', width: 20 },
{ header: '阅读时间', key: 'read_time', width: 20 }
];
// 生成Excel
const buffer = await ExcelExport.exportToExcel(exportData, columns, '设备预警列表');
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=device_alerts_${Date.now()}.xlsx`);
// 发送文件
res.send(buffer);
} catch (error) {
logger.error('导出设备预警失败:', error);
res.status(500).json({
success: false,
message: '导出失败',
error: error.message
});
}
}
}
module.exports = DeviceAlertController;

View File

@@ -2,6 +2,7 @@ const InstallationTask = require('../models/InstallationTask');
const User = require('../models/User');
const { Op, sequelize } = require('sequelize');
const logger = require('../utils/logger');
const ExcelExport = require('../utils/excelExport');
class InstallationTaskController {
@@ -472,6 +473,99 @@ class InstallationTaskController {
});
}
}
// 导出待安装任务列表到Excel
async exportToExcel(req, res) {
try {
const { policyNumber, keyword } = req.query;
// 构建查询条件使用数据库字段名因为raw:true模式
const whereConditions = {};
if (policyNumber) {
whereConditions.policy_number = { [Op.like]: `%${policyNumber}%` };
}
// 关键字搜索
if (keyword) {
whereConditions[Op.or] = [
{ application_number: { [Op.like]: `%${keyword}%` } },
{ policy_number: { [Op.like]: `%${keyword}%` } },
{ customer_name: { [Op.like]: `%${keyword}%` } },
{ installation_address: { [Op.like]: `%${keyword}%` } }
];
}
// 查询所有符合条件的数据
const tasks = await InstallationTask.findAll({
where: whereConditions,
order: [['created_at', 'DESC']]
});
console.log(`导出查询到 ${tasks.length} 条数据`);
// 准备导出数据Sequelize自动转换为驼峰格式
const exportData = tasks.map(task => {
const data = task.toJSON ? task.toJSON() : task;
return {
applicationNumber: data.applicationNumber || '',
policyNumber: data.policyNumber || '',
productName: data.productName || '',
customerName: data.customerName || '',
idType: data.idType || '',
idNumber: data.idNumber || '',
livestockSupplyType: data.livestockSupplyType || '',
installationStatus: data.installationStatus || '',
priority: data.priority || '',
installationAddress: data.installationAddress || '',
contactPhone: data.contactPhone || '',
taskGeneratedTime: ExcelExport.formatDate(data.taskGeneratedTime),
installationCompletedTime: ExcelExport.formatDate(data.installationCompletedTime),
createdAt: ExcelExport.formatDate(data.created_at || data.createdAt),
updatedAt: ExcelExport.formatDate(data.updated_at || data.updatedAt)
};
});
// 定义列
const columns = [
{ header: '申请单号', key: 'applicationNumber', width: 20 },
{ header: '保单编号', key: 'policyNumber', width: 20 },
{ header: '产品名称', key: 'productName', width: 25 },
{ header: '客户姓名', key: 'customerName', width: 15 },
{ header: '证件类型', key: 'idType', width: 12 },
{ header: '证件号码', key: 'idNumber', width: 20 },
{ header: '养殖生资种类', key: 'livestockSupplyType', width: 20 },
{ header: '安装状态', key: 'installationStatus', width: 12 },
{ header: '优先级', key: 'priority', width: 10 },
{ header: '安装地址', key: 'installationAddress', width: 30 },
{ header: '联系电话', key: 'contactPhone', width: 15 },
{ header: '任务生成时间', key: 'taskGeneratedTime', width: 20 },
{ header: '安装完成时间', key: 'installationCompletedTime', width: 20 },
{ header: '创建时间', key: 'createdAt', width: 20 },
{ header: '更新时间', key: 'updatedAt', width: 20 }
];
// 生成Excel
const buffer = await ExcelExport.exportToExcel(exportData, columns, '待安装任务列表');
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=installation_tasks_${Date.now()}.xlsx`);
// 发送文件
res.send(buffer);
} catch (error) {
console.error('导出待安装任务失败:', error);
logger.error('导出待安装任务失败:', error);
res.status(500).json({
code: 500,
status: 'error',
message: '导出失败',
error: error.message
});
}
}
}
module.exports = new InstallationTaskController();

View File

@@ -1,6 +1,7 @@
const { InsuranceType } = require('../models');
const { Op } = require('sequelize');
const responseFormat = require('../utils/response');
const ExcelExport = require('../utils/excelExport');
// 获取保险类型列表
const getInsuranceTypes = async (req, res) => {
@@ -457,11 +458,59 @@ const updateInsuranceTypeStatus = async (req, res) => {
}
};
// 导出保险类型列表到Excel
const exportToExcel = async (req, res) => {
try {
const { name, status } = req.query;
const where = {};
if (name) where.name = { [Op.like]: `%${name}%` };
if (status) where.status = status;
const types = await InsuranceType.findAll({
where,
order: [['created_at', 'DESC']],
raw: true
});
const statusMap = { active: '启用', inactive: '停用' };
const exportData = types.map(type => ({
name: type.name || '',
coverage_amount: type.coverage_amount || 0,
premium_rate: type.premium_rate || 0,
applicable_livestock: type.applicable_livestock || '',
description: type.description || '',
status: ExcelExport.formatStatus(type.status, statusMap),
created_at: ExcelExport.formatDate(type.created_at)
}));
const columns = [
{ header: '险种名称', key: 'name', width: 20 },
{ header: '保额', key: 'coverage_amount', width: 15 },
{ header: '费率', key: 'premium_rate', width: 10 },
{ header: '适用牲畜', key: 'applicable_livestock', width: 20 },
{ header: '描述', key: 'description', width: 40 },
{ header: '状态', key: 'status', width: 10 },
{ header: '创建时间', key: 'created_at', width: 20 }
];
const buffer = await ExcelExport.exportToExcel(exportData, columns, '保险类型列表');
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=insurance_types_${Date.now()}.xlsx`);
res.send(buffer);
} catch (error) {
console.error('导出保险类型失败:', error);
res.status(500).json(responseFormat.error('导出失败'));
}
};
module.exports = {
getInsuranceTypes,
getInsuranceTypeById,
createInsuranceType,
updateInsuranceType,
deleteInsuranceType,
updateInsuranceTypeStatus
updateInsuranceTypeStatus,
exportToExcel
};

View File

@@ -4,6 +4,7 @@ const LivestockType = require('../models/LivestockType');
const User = require('../models/User');
const responseFormat = require('../utils/response');
const { Op } = require('sequelize');
const ExcelExport = require('../utils/excelExport');
// 获取生资理赔列表
const getLivestockClaims = async (req, res) => {
@@ -540,6 +541,66 @@ const deleteLivestockClaim = async (req, res) => {
}
};
// 导出理赔列表到Excel
const exportToExcel = async (req, res) => {
try {
const { claim_number, claimant_name, status } = req.query;
const where = {};
if (claim_number) where.claim_number = { [Op.like]: `%${claim_number}%` };
if (claimant_name) where.claimant_name = { [Op.like]: `%${claimant_name}%` };
if (status) where.status = status;
const claims = await LivestockClaim.findAll({
where,
include: [{
model: LivestockPolicy,
as: 'policy',
attributes: ['policy_number']
}],
order: [['createdAt', 'DESC']],
raw: true,
nest: true
});
const statusMap = { pending: '待审核', approved: '已批准', rejected: '已拒绝', settled: '已理赔' };
const exportData = claims.map(claim => ({
claim_number: claim.claim_number || '',
policy_number: claim.policy?.policy_number || '',
claimant_name: claim.claimant_name || '',
claim_amount: claim.claim_amount || 0,
approved_amount: claim.approved_amount || 0,
claim_reason: claim.claim_reason || '',
status: ExcelExport.formatStatus(claim.status, statusMap),
claim_date: ExcelExport.formatDate(claim.claim_date),
settled_date: ExcelExport.formatDate(claim.settled_date),
createdAt: ExcelExport.formatDate(claim.createdAt)
}));
const columns = [
{ header: '理赔编号', key: 'claim_number', width: 20 },
{ header: '保单编号', key: 'policy_number', width: 20 },
{ header: '理赔人', key: 'claimant_name', width: 15 },
{ header: '理赔金额', key: 'claim_amount', width: 15 },
{ header: '批准金额', key: 'approved_amount', width: 15 },
{ header: '理赔原因', key: 'claim_reason', width: 30 },
{ header: '状态', key: 'status', width: 12 },
{ header: '理赔日期', key: 'claim_date', width: 15 },
{ header: '结算日期', key: 'settled_date', width: 15 },
{ header: '创建时间', key: 'createdAt', width: 20 }
];
const buffer = await ExcelExport.exportToExcel(exportData, columns, '理赔列表');
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=livestock_claims_${Date.now()}.xlsx`);
res.send(buffer);
} catch (error) {
console.error('导出理赔记录失败:', error);
res.status(500).json(responseFormat.error('导出失败'));
}
};
module.exports = {
getLivestockClaims,
createLivestockClaim,
@@ -548,5 +609,6 @@ module.exports = {
deleteLivestockClaim,
reviewLivestockClaim,
updateLivestockClaimPayment,
getLivestockClaimStats
getLivestockClaimStats,
exportToExcel
};

View File

@@ -3,6 +3,7 @@ const LivestockType = require('../models/LivestockType');
const User = require('../models/User');
const responseFormat = require('../utils/response');
const { Op } = require('sequelize');
const ExcelExport = require('../utils/excelExport');
// 获取生资保单列表
const getLivestockPolicies = async (req, res) => {
@@ -337,6 +338,69 @@ const deleteLivestockPolicy = async (req, res) => {
}
};
// 导出生资保单列表到Excel
const exportToExcel = async (req, res) => {
try {
const { policy_no, farmer_name, policy_status } = req.query;
const where = {};
if (policy_no) where.policy_no = { [Op.like]: `%${policy_no}%` };
if (farmer_name) where.farmer_name = { [Op.like]: `%${farmer_name}%` };
if (policy_status) where.policy_status = policy_status;
const policies = await LivestockPolicy.findAll({
where,
include: [{
model: LivestockType,
as: 'livestockType',
attributes: ['name']
}],
order: [['created_at', 'DESC']],
raw: true,
nest: true
});
const statusMap = { active: '生效中', pending: '待生效', expired: '已过期', cancelled: '已取消' };
const paymentMap = { unpaid: '未支付', paid: '已支付', partial: '部分支付' };
const exportData = policies.map(policy => ({
policy_no: policy.policy_no || '',
farmer_name: policy.farmer_name || '',
farmer_phone: policy.farmer_phone || '',
livestock_type: policy.livestockType?.name || '',
insured_amount: policy.insured_amount || 0,
premium_amount: policy.premium_amount || 0,
policy_status: ExcelExport.formatStatus(policy.policy_status, statusMap),
payment_status: ExcelExport.formatStatus(policy.payment_status, paymentMap),
start_date: ExcelExport.formatDate(policy.start_date),
end_date: ExcelExport.formatDate(policy.end_date),
created_at: ExcelExport.formatDate(policy.created_at)
}));
const columns = [
{ header: '保单号', key: 'policy_no', width: 20 },
{ header: '农户姓名', key: 'farmer_name', width: 15 },
{ header: '联系电话', key: 'farmer_phone', width: 15 },
{ header: '牲畜类型', key: 'livestock_type', width: 15 },
{ header: '保额', key: 'insured_amount', width: 15 },
{ header: '保费', key: 'premium_amount', width: 15 },
{ header: '保单状态', key: 'policy_status', width: 12 },
{ header: '支付状态', key: 'payment_status', width: 12 },
{ header: '起始日期', key: 'start_date', width: 15 },
{ header: '结束日期', key: 'end_date', width: 15 },
{ header: '创建时间', key: 'created_at', width: 20 }
];
const buffer = await ExcelExport.exportToExcel(exportData, columns, '生资保单列表');
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=livestock_policies_${Date.now()}.xlsx`);
res.send(buffer);
} catch (error) {
console.error('导出生资保单失败:', error);
res.status(500).json(responseFormat.error('导出失败'));
}
};
module.exports = {
getLivestockPolicies,
createLivestockPolicy,
@@ -344,5 +408,6 @@ module.exports = {
updateLivestockPolicy,
updateLivestockPolicyStatus,
deleteLivestockPolicy,
getLivestockPolicyStats
getLivestockPolicyStats,
exportToExcel
};

View File

@@ -1,6 +1,7 @@
const { OperationLog, User } = require('../models');
const { Op } = require('sequelize');
const ExcelJS = require('exceljs');
const ExcelExport = require('../utils/excelExport');
/**
* 操作日志控制器
@@ -339,6 +340,99 @@ class OperationLogController {
});
}
}
/**
* 导出操作日志列表到Excel
*/
async exportToExcel(req, res) {
try {
const { username, operation_type, operation_module, startDate, endDate } = req.query;
// 构建查询条件
const where = {};
if (username) {
where['$User.username$'] = { [Op.like]: `%${username}%` };
}
if (operation_type) where.operation_type = operation_type;
if (operation_module) where.operation_module = operation_module;
if (startDate && endDate) {
where.created_at = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
// 查询所有符合条件的数据
const logs = await OperationLog.findAll({
where,
include: [{
model: User,
as: 'User',
attributes: ['username', 'real_name']
}],
order: [['created_at', 'DESC']],
raw: true,
nest: true
});
// 操作类型映射
const typeMap = {
login: '登录',
logout: '登出',
create: '创建',
update: '更新',
delete: '删除',
view: '查看',
export: '导出'
};
const statusMap = {
success: '成功',
failure: '失败'
};
// 准备导出数据
const exportData = logs.map(log => ({
username: log.User?.username || '',
real_name: log.User?.real_name || '',
operation_type: ExcelExport.formatStatus(log.operation_type, typeMap),
operation_module: log.operation_module || '',
operation_desc: log.operation_desc || '',
ip_address: log.ip_address || '',
status: ExcelExport.formatStatus(log.status, statusMap),
created_at: ExcelExport.formatDate(log.created_at)
}));
// 定义列
const columns = [
{ header: '用户名', key: 'username', width: 15 },
{ header: '真实姓名', key: 'real_name', width: 15 },
{ header: '操作类型', key: 'operation_type', width: 12 },
{ header: '操作模块', key: 'operation_module', width: 20 },
{ header: '操作描述', key: 'operation_desc', width: 40 },
{ header: 'IP地址', key: 'ip_address', width: 20 },
{ header: '状态', key: 'status', width: 10 },
{ header: '操作时间', key: 'created_at', width: 20 }
];
// 生成Excel
const buffer = await ExcelExport.exportToExcel(exportData, columns, '操作日志');
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=operation_logs_${Date.now()}.xlsx`);
// 发送文件
res.send(buffer);
} catch (error) {
console.error('导出操作日志失败:', error);
res.status(500).json({
status: 'error',
message: '导出失败',
error: error.message
});
}
}
}
module.exports = new OperationLogController();

View File

@@ -1,6 +1,7 @@
const { Policy, InsuranceApplication, InsuranceType, User } = require('../models');
const responseFormat = require('../utils/response');
const { Op } = require('sequelize');
const ExcelExport = require('../utils/excelExport');
// 获取保单列表
const getPolicies = async (req, res) => {
@@ -363,6 +364,64 @@ const deletePolicy = async (req, res) => {
}
};
// 导出保单列表到Excel
const exportToExcel = async (req, res) => {
try {
const { policy_number, policyholder_name, status } = req.query;
const where = {};
if (policy_number) where.policy_number = { [Op.like]: `%${policy_number}%` };
if (policyholder_name) where.policyholder_name = { [Op.like]: `%${policyholder_name}%` };
if (status) where.status = status;
const policies = await Policy.findAll({
where,
include: [{
model: InsuranceType,
as: 'InsuranceType',
attributes: ['name']
}],
order: [['created_at', 'DESC']],
raw: true,
nest: true
});
const statusMap = { active: '生效中', pending: '待生效', expired: '已过期', cancelled: '已取消' };
const exportData = policies.map(policy => ({
policy_number: policy.policy_number || '',
policyholder_name: policy.policyholder_name || '',
insurance_type_name: policy.InsuranceType?.name || '',
coverage_amount: policy.coverage_amount || 0,
premium_amount: policy.premium_amount || 0,
start_date: ExcelExport.formatDate(policy.start_date),
end_date: ExcelExport.formatDate(policy.end_date),
status: ExcelExport.formatStatus(policy.status, statusMap),
created_at: ExcelExport.formatDate(policy.created_at)
}));
const columns = [
{ header: '保单编号', key: 'policy_number', width: 20 },
{ header: '投保人', key: 'policyholder_name', width: 15 },
{ header: '险种', key: 'insurance_type_name', width: 20 },
{ header: '保额', key: 'coverage_amount', width: 15 },
{ header: '保费', key: 'premium_amount', width: 15 },
{ header: '开始日期', key: 'start_date', width: 15 },
{ header: '结束日期', key: 'end_date', width: 15 },
{ header: '状态', key: 'status', width: 12 },
{ header: '创建时间', key: 'created_at', width: 20 }
];
const buffer = await ExcelExport.exportToExcel(exportData, columns, '保单列表');
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=policies_${Date.now()}.xlsx`);
res.send(buffer);
} catch (error) {
console.error('导出保单失败:', error);
res.status(500).json(responseFormat.error('导出失败'));
}
};
module.exports = {
getPolicies,
createPolicy,
@@ -370,5 +429,6 @@ module.exports = {
updatePolicy,
updatePolicyStatus,
deletePolicy,
getPolicyStats
getPolicyStats,
exportToExcel
};

View File

@@ -1,5 +1,6 @@
const { SupervisoryTask, User } = require('../models');
const { Op } = require('sequelize');
const ExcelExport = require('../utils/excelExport');
/**
* 监管任务控制器
@@ -522,6 +523,93 @@ class SupervisoryTaskController {
});
}
}
/**
* 导出监管任务列表到Excel
*/
static async exportToExcel(req, res) {
try {
const { policyNumber, customerName } = req.query;
// 构建查询条件
const where = {};
if (policyNumber) {
where.policyNumber = { [Op.like]: `%${policyNumber}%` };
}
if (customerName) {
where.customerName = { [Op.like]: `%${customerName}%` };
}
// 查询所有符合条件的数据
const tasks = await SupervisoryTask.findAll({
where,
order: [['createdAt', 'DESC']],
raw: true
});
// 状态映射
const statusMap = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
rejected: '已拒绝'
};
const priorityMap = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急'
};
// 准备导出数据
const exportData = tasks.map(task => ({
policyNumber: task.policyNumber || '',
customerName: task.customerName || '',
productName: task.productName || '',
insurancePeriod: task.insurancePeriod || '',
applicableAmount: task.applicableAmount || '',
taskStatus: ExcelExport.formatStatus(task.taskStatus, statusMap),
priority: ExcelExport.formatStatus(task.priority, priorityMap),
createdAt: ExcelExport.formatDate(task.createdAt),
updatedAt: ExcelExport.formatDate(task.updatedAt)
}));
// 定义列
const columns = [
{ header: '保单编号', key: 'policyNumber', width: 20 },
{ header: '客户姓名', key: 'customerName', width: 15 },
{ header: '产品名称', key: 'productName', width: 25 },
{ header: '保险期间', key: 'insurancePeriod', width: 25 },
{ header: '适用金额', key: 'applicableAmount', width: 15 },
{ header: '任务状态', key: 'taskStatus', width: 12 },
{ header: '优先级', key: 'priority', width: 12 },
{ header: '创建时间', key: 'createdAt', width: 20 },
{ header: '更新时间', key: 'updatedAt', width: 20 }
];
// 生成Excel
const buffer = await ExcelExport.exportToExcel(exportData, columns, '监管任务列表');
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=supervision_tasks_${Date.now()}.xlsx`);
// 发送文件
res.send(buffer);
} catch (error) {
console.error('导出监管任务失败:', error);
res.status(500).json({
code: 500,
status: 'error',
message: '导出失败',
error: error.message
});
}
}
}
module.exports = SupervisoryTaskController;

View File

@@ -2,6 +2,7 @@ const { User, Role } = require('../models');
const { Op } = require('sequelize');
const responseFormat = require('../utils/response');
const crypto = require('crypto');
const ExcelExport = require('../utils/excelExport');
// 获取用户列表
const getUsers = async (req, res) => {
@@ -486,6 +487,84 @@ const getFixedTokenInfo = async (req, res) => {
}
};
// 导出用户列表到Excel
const exportToExcel = async (req, res) => {
try {
const { search, status } = req.query;
const whereClause = {};
if (search) {
whereClause[Op.or] = [
{ username: { [Op.like]: `%${search}%` } },
{ real_name: { [Op.like]: `%${search}%` } },
{ email: { [Op.like]: `%${search}%` } },
{ phone: { [Op.like]: `%${search}%` } }
];
}
if (status) whereClause.status = status;
// 查询所有符合条件的数据
const users = await User.findAll({
where: whereClause,
include: [{
model: Role,
as: 'role',
attributes: ['name']
}],
order: [['created_at', 'DESC']],
raw: true,
nest: true
});
// 状态映射
const statusMap = {
active: '活跃',
inactive: '禁用',
suspended: '暂停'
};
// 准备导出数据
const exportData = users.map(user => ({
id: user.id || '',
username: user.username || '',
real_name: user.real_name || '',
email: user.email || '',
phone: user.phone || '',
role_name: user.role?.name || '',
status: ExcelExport.formatStatus(user.status, statusMap),
created_at: ExcelExport.formatDate(user.created_at),
updated_at: ExcelExport.formatDate(user.updated_at)
}));
// 定义列
const columns = [
{ header: 'ID', key: 'id', width: 10 },
{ header: '用户名', key: 'username', width: 15 },
{ header: '真实姓名', key: 'real_name', width: 15 },
{ header: '邮箱', key: 'email', width: 25 },
{ header: '手机号', key: 'phone', width: 15 },
{ header: '角色', key: 'role_name', width: 12 },
{ header: '状态', key: 'status', width: 10 },
{ header: '创建时间', key: 'created_at', width: 20 },
{ header: '更新时间', key: 'updated_at', width: 20 }
];
// 生成Excel
const buffer = await ExcelExport.exportToExcel(exportData, columns, '用户列表');
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=users_${Date.now()}.xlsx`);
// 发送文件
res.send(buffer);
} catch (error) {
console.error('导出用户列表失败:', error);
res.status(500).json(responseFormat.error('导出失败'));
}
};
module.exports = {
getUsers,
getUser,
@@ -500,5 +579,6 @@ module.exports = {
generateFixedToken,
regenerateFixedToken,
deleteFixedToken,
getFixedTokenInfo
getFixedTokenInfo,
exportToExcel
};

View File

@@ -2386,6 +2386,42 @@
"node": ">=0.2.0"
}
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/builtins": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/builtins/-/builtins-5.1.0.tgz",
"integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==",
"dev": true,
"peer": true,
"dependencies": {
"semver": "^7.0.0"
}
},
"node_modules/builtins/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz",
@@ -3645,6 +3681,35 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-compat-utils": {
"version": "0.5.1",
"resolved": "https://registry.npmmirror.com/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz",
"integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==",
"dev": true,
"peer": true,
"dependencies": {
"semver": "^7.5.4"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"eslint": ">=6.0.0"
}
},
"node_modules/eslint-compat-utils/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-config-standard": {
"version": "17.1.0",
"resolved": "https://registry.npmmirror.com/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz",
@@ -3739,6 +3804,28 @@
"eslint": ">=4.19.1"
}
},
"node_modules/eslint-plugin-es-x": {
"version": "7.8.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz",
"integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/ota-meshi",
"https://opencollective.com/eslint"
],
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.1.2",
"@eslint-community/regexpp": "^4.11.0",
"eslint-compat-utils": "^0.5.1"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"eslint": ">=8"
}
},
"node_modules/eslint-plugin-import": {
"version": "2.32.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
@@ -3793,6 +3880,48 @@
"node": ">=0.10.0"
}
},
"node_modules/eslint-plugin-n": {
"version": "16.6.2",
"resolved": "https://registry.npmmirror.com/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz",
"integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"builtins": "^5.0.1",
"eslint-plugin-es-x": "^7.5.0",
"get-tsconfig": "^4.7.0",
"globals": "^13.24.0",
"ignore": "^5.2.4",
"is-builtin-module": "^3.2.1",
"is-core-module": "^2.12.1",
"minimatch": "^3.1.2",
"resolve": "^1.22.2",
"semver": "^7.5.3"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-n/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-plugin-node": {
"version": "11.1.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
@@ -4637,6 +4766,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-tsconfig": {
"version": "4.12.0",
"resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.12.0.tgz",
"integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==",
"dev": true,
"peer": true,
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
@@ -5113,6 +5255,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"peer": true,
"dependencies": {
"builtin-modules": "^3.3.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz",
@@ -7225,6 +7383,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmmirror.com/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"peer": true
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
@@ -7995,6 +8159,16 @@
"node": ">=4"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"peer": true,
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/resolve.exports/-/resolve.exports-2.0.3.tgz",
@@ -10869,7 +11043,8 @@
"@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg=="
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"requires": {}
},
"@redis/client": {
"version": "1.6.1",
@@ -10891,22 +11066,26 @@
"@redis/graph": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@redis/graph/-/graph-1.1.1.tgz",
"integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw=="
"integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
"requires": {}
},
"@redis/json": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/@redis/json/-/json-1.0.7.tgz",
"integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ=="
"integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==",
"requires": {}
},
"@redis/search": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/@redis/search/-/search-1.2.0.tgz",
"integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw=="
"integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==",
"requires": {}
},
"@redis/time-series": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@redis/time-series/-/time-series-1.1.0.tgz",
"integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g=="
"integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==",
"requires": {}
},
"@rtsao/scc": {
"version": "1.1.0",
@@ -11127,7 +11306,8 @@
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true
"dev": true,
"requires": {}
},
"agent-base": {
"version": "6.0.2",
@@ -11654,6 +11834,32 @@
"resolved": "https://registry.npmmirror.com/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
},
"builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"peer": true
},
"builtins": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/builtins/-/builtins-5.1.0.tgz",
"integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==",
"dev": true,
"peer": true,
"requires": {
"semver": "^7.0.0"
},
"dependencies": {
"semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"peer": true
}
}
},
"busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz",
@@ -12129,7 +12335,8 @@
"version": "1.7.0",
"resolved": "https://registry.npmmirror.com/dedent/-/dedent-1.7.0.tgz",
"integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==",
"dev": true
"dev": true,
"requires": {}
},
"deep-is": {
"version": "0.1.4",
@@ -12593,11 +12800,31 @@
"text-table": "^0.2.0"
}
},
"eslint-compat-utils": {
"version": "0.5.1",
"resolved": "https://registry.npmmirror.com/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz",
"integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==",
"dev": true,
"peer": true,
"requires": {
"semver": "^7.5.4"
},
"dependencies": {
"semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"peer": true
}
}
},
"eslint-config-standard": {
"version": "17.1.0",
"resolved": "https://registry.npmmirror.com/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz",
"integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==",
"dev": true
"dev": true,
"requires": {}
},
"eslint-import-resolver-node": {
"version": "0.3.9",
@@ -12651,6 +12878,18 @@
"regexpp": "^3.0.0"
}
},
"eslint-plugin-es-x": {
"version": "7.8.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz",
"integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==",
"dev": true,
"peer": true,
"requires": {
"@eslint-community/eslint-utils": "^4.1.2",
"@eslint-community/regexpp": "^4.11.0",
"eslint-compat-utils": "^0.5.1"
}
},
"eslint-plugin-import": {
"version": "2.32.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
@@ -12698,6 +12937,35 @@
}
}
},
"eslint-plugin-n": {
"version": "16.6.2",
"resolved": "https://registry.npmmirror.com/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz",
"integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==",
"dev": true,
"peer": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"builtins": "^5.0.1",
"eslint-plugin-es-x": "^7.5.0",
"get-tsconfig": "^4.7.0",
"globals": "^13.24.0",
"ignore": "^5.2.4",
"is-builtin-module": "^3.2.1",
"is-core-module": "^2.12.1",
"minimatch": "^3.1.2",
"resolve": "^1.22.2",
"semver": "^7.5.3"
},
"dependencies": {
"semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"peer": true
}
}
},
"eslint-plugin-node": {
"version": "11.1.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
@@ -12716,7 +12984,8 @@
"version": "6.6.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz",
"integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==",
"dev": true
"dev": true,
"requires": {}
},
"eslint-scope": {
"version": "7.2.2",
@@ -13314,6 +13583,16 @@
"get-intrinsic": "^1.2.6"
}
},
"get-tsconfig": {
"version": "4.12.0",
"resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.12.0.tgz",
"integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==",
"dev": true,
"peer": true,
"requires": {
"resolve-pkg-maps": "^1.0.0"
}
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
@@ -13632,6 +13911,16 @@
"has-tostringtag": "^1.0.2"
}
},
"is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"peer": true,
"requires": {
"builtin-modules": "^3.3.0"
}
},
"is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz",
@@ -14156,7 +14445,8 @@
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
"integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
"dev": true
"dev": true,
"requires": {}
},
"jest-regex-util": {
"version": "29.6.3",
@@ -15219,6 +15509,12 @@
"mimic-fn": "^2.1.0"
}
},
"openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmmirror.com/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"peer": true
},
"optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
@@ -15768,6 +16064,13 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"peer": true
},
"resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/resolve.exports/-/resolve.exports-2.0.3.tgz",
@@ -17020,7 +17323,8 @@
"version": "8.13.0",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"dev": true
"dev": true,
"requires": {}
},
"xmlchars": {
"version": "2.2.0",

View File

@@ -83,6 +83,47 @@ const { jwtAuth } = require('../middleware/auth');
*/
router.get('/stats', jwtAuth, deviceAlertController.getAlertStats);
/**
* @swagger
* /api/device-alerts/export:
* get:
* tags:
* - 设备预警
* summary: 导出设备预警列表到Excel
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: alert_level
* schema:
* type: string
* description: 预警级别
* - in: query
* name: alert_type
* schema:
* type: string
* description: 预警类型
* - in: query
* name: status
* schema:
* type: string
* description: 处理状态
* - in: query
* name: is_read
* schema:
* type: boolean
* description: 是否已读
* responses:
* 200:
* description: 导出成功
* content:
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
*/
router.get('/export', jwtAuth, deviceAlertController.exportToExcel);
/**
* @swagger
* /api/device-alerts:

View File

@@ -10,6 +10,50 @@ const { jwtAuth, requirePermission } = require('../middleware/auth');
* description: 待安装任务管理
*/
/**
* @swagger
* /api/installation-tasks/stats:
* get:
* summary: 获取安装任务统计数据
* tags: [InstallationTasks]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
*/
router.get('/stats', jwtAuth, requirePermission('installation_tasks:read'), installationTaskController.getInstallationTaskStats);
/**
* @swagger
* /api/installation-tasks/export:
* get:
* summary: 导出待安装任务到Excel
* tags: [InstallationTasks]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: policyNumber
* schema:
* type: string
* description: 保单编号
* - in: query
* name: keyword
* schema:
* type: string
* description: 关键字搜索
* responses:
* 200:
* description: 导出成功
* content:
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
*/
router.get('/export', jwtAuth, requirePermission('installation_tasks:read'), installationTaskController.exportToExcel);
/**
* @swagger
* /api/installation-tasks:
@@ -230,38 +274,4 @@ router.delete('/:id', jwtAuth, requirePermission('installation_tasks:delete'), i
*/
router.post('/batch/operate', jwtAuth, requirePermission('installation_tasks:update'), installationTaskController.batchOperateInstallationTasks);
/**
* @swagger
* /api/installation-tasks/export:
* get:
* summary: 导出待安装任务数据
* tags: [InstallationTasks]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: ids
* schema:
* type: string
* description: 任务ID列表逗号分隔
* responses:
* 200:
* description: 导出成功
*/
router.get('/export', jwtAuth, requirePermission('installation_tasks:read'), installationTaskController.exportInstallationTasks);
/**
* @swagger
* /api/installation-tasks/stats:
* get:
* summary: 获取安装任务统计数据
* tags: [InstallationTasks]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
*/
router.get('/stats', jwtAuth, requirePermission('installation_tasks:read'), installationTaskController.getInstallationTaskStats);
module.exports = router;

View File

@@ -174,6 +174,9 @@ router.get('/', jwtAuth, checkPermission('insurance_type', 'read'),
* '500':
* description: 服务器内部错误
*/
// 导出险种列表到Excel
router.get('/export', jwtAuth, checkPermission('insurance_type', 'read'), insuranceTypeController.exportToExcel);
// 获取单个险种详情
router.get('/:id', jwtAuth, checkPermission('insurance_type', 'read'),
insuranceTypeController.getInsuranceTypeById

View File

@@ -8,7 +8,8 @@ const {
updateLivestockClaimPayment,
getLivestockClaimStats,
updateLivestockClaim,
deleteLivestockClaim
deleteLivestockClaim,
exportToExcel
} = require('../controllers/livestockClaimController');
const { authenticateToken, requirePermission } = require('../middleware/auth');
@@ -33,6 +34,9 @@ router.get('/', authenticateToken, requirePermission('livestock_claim:read'), ge
// 获取生资理赔统计
router.get('/stats', authenticateToken, requirePermission('livestock_claim:read'), getLivestockClaimStats);
// 导出理赔列表到Excel
router.get('/export', authenticateToken, requirePermission('livestock_claim:read'), exportToExcel);
// 获取单个生资理赔详情
router.get('/:id', authenticateToken, requirePermission('livestock_claim:read'), getLivestockClaimById);

View File

@@ -7,7 +7,8 @@ const {
updateLivestockPolicy,
updateLivestockPolicyStatus,
deleteLivestockPolicy,
getLivestockPolicyStats
getLivestockPolicyStats,
exportToExcel
} = require('../controllers/livestockPolicyController');
const { jwtAuth, checkPermission } = require('../middleware/auth');
@@ -17,6 +18,9 @@ router.get('/', jwtAuth, checkPermission('insurance:policy', 'view'), getLivesto
// 获取生资保单统计
router.get('/stats', jwtAuth, checkPermission('insurance:policy', 'view'), getLivestockPolicyStats);
// 导出生资保单列表到Excel
router.get('/export', jwtAuth, checkPermission('insurance:policy', 'view'), exportToExcel);
// 获取单个生资保单详情
router.get('/:id', jwtAuth, checkPermission('insurance:policy', 'view'), getLivestockPolicyById);

View File

@@ -169,6 +169,9 @@ router.get('/', jwtAuth, checkPermission('system', 'read'), operationLogControll
*/
router.get('/stats', jwtAuth, checkPermission('system', 'read'), operationLogController.getOperationStats);
// 导出操作日志到Excel
router.get('/export', jwtAuth, checkPermission('system', 'read'), operationLogController.exportToExcel);
/**
* @swagger
* /api/operation-logs/{id}:

View File

@@ -18,6 +18,9 @@ router.post('/', jwtAuth, checkPermission('insurance:policy', 'create'),
policyController.createPolicy
);
// 导出保单列表到Excel
router.get('/export', jwtAuth, checkPermission('insurance:policy', 'view'), policyController.exportToExcel);
// 获取单个保单详情
router.get('/:id', jwtAuth, checkPermission('insurance:policy', 'view'),
policyController.getPolicyById

View File

@@ -10,6 +10,50 @@ const { jwtAuth, requirePermission } = require('../middleware/auth');
* description: 监管任务管理
*/
/**
* @swagger
* /api/supervision-tasks/stats:
* get:
* summary: 获取监管任务统计信息
* tags: [SupervisionTasks]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
*/
router.get('/stats', jwtAuth, requirePermission('supervision_tasks:read'), SupervisoryTaskController.getStatistics);
/**
* @swagger
* /api/supervision-tasks/export:
* get:
* summary: 导出监管任务列表到Excel
* tags: [SupervisionTasks]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: policyNumber
* schema:
* type: string
* description: 保单编号
* - in: query
* name: customerName
* schema:
* type: string
* description: 客户姓名
* responses:
* 200:
* description: 导出成功
* content:
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
*/
router.get('/export', jwtAuth, requirePermission('supervision_tasks:read'), SupervisoryTaskController.exportToExcel);
/**
* @swagger
* /api/supervision-tasks:
@@ -253,18 +297,4 @@ router.delete('/:id', jwtAuth, requirePermission('supervision_tasks:delete'), Su
*/
router.post('/batch/operate', jwtAuth, requirePermission('supervision_tasks:create'), SupervisoryTaskController.bulkCreate);
/**
* @swagger
* /api/supervision-tasks/stats:
* get:
* summary: 获取监管任务统计信息
* tags: [SupervisionTasks]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
*/
router.get('/stats', jwtAuth, requirePermission('supervision_tasks:read'), SupervisoryTaskController.getStatistics);
module.exports = router;

View File

@@ -13,6 +13,9 @@ router.get('/profile', jwtAuth, userController.getProfile);
// 更新个人资料(不需要特殊权限,用户可以更新自己的资料)
router.put('/profile', jwtAuth, userController.updateProfile);
// 导出用户列表(必须在 /:id 之前)
router.get('/export', jwtAuth, checkPermission('user', 'read'), userController.exportToExcel);
// 获取单个用户信息
router.get('/:id', jwtAuth, checkPermission('user', 'read'), userController.getUser);

View File

@@ -0,0 +1,121 @@
/**
* 批量为各个控制器添加导出路由的脚本
* 使用说明node scripts/add-export-routes.js
*/
const fs = require('fs');
const path = require('path');
// 需要添加导出功能的路由配置
const routeConfigs = [
{
routeFile: 'routes/users.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'users:read',
description: '导出用户列表到Excel'
},
{
routeFile: 'routes/deviceAlerts.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'device_alerts:read',
description: '导出设备预警列表到Excel'
},
{
routeFile: 'routes/insuranceTypes.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'insurance_types:read',
description: '导出保险类型列表到Excel'
},
{
routeFile: 'routes/policies.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'policies:read',
description: '导出保单列表到Excel'
},
{
routeFile: 'routes/livestockClaims.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'livestock_claims:read',
description: '导出理赔列表到Excel'
},
{
routeFile: 'routes/installationTasks.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'installation_tasks:read',
description: '导出待安装任务列表到Excel'
},
{
routeFile: 'routes/livestockPolicies.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'livestock_policies:read',
description: '导出生资保单列表到Excel'
},
{
routeFile: 'routes/operationLogs.js',
exportPath: '/export',
controllerMethod: 'exportToExcel',
permission: 'operation_logs:read',
description: '导出操作日志到Excel'
}
];
console.log('🚀 开始为各个路由添加导出功能...\n');
routeConfigs.forEach(config => {
const filePath = path.join(__dirname, '..', config.routeFile);
try {
// 读取路由文件
let content = fs.readFileSync(filePath, 'utf8');
// 检查是否已经存在导出路由
if (content.includes(`router.get('${config.exportPath}'`) || content.includes(`router.get("${config.exportPath}"`)) {
console.log(`⏭️ ${config.routeFile} - 导出路由已存在,跳过`);
return;
}
// 在module.exports之前添加导出路由
const exportRoute = `
/**
* @swagger
* ${config.exportPath}:
* get:
* summary: ${config.description}
* tags: [Export]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 导出成功
* content:
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
*/
router.get('${config.exportPath}', jwtAuth, requirePermission('${config.permission}'), controller.${config.controllerMethod});
`;
// 在module.exports之前插入
content = content.replace(/module\.exports\s*=\s*router;/, exportRoute + 'module.exports = router;');
// 写回文件
fs.writeFileSync(filePath, content, 'utf8');
console.log(`${config.routeFile} - 导出路由添加成功`);
} catch (error) {
console.error(`${config.routeFile} - 添加失败: ${error.message}`);
}
});
console.log('\n✨ 路由添加完成!');
console.log('⚠️ 注意:还需要为各个控制器添加 exportToExcel 方法');

View File

@@ -0,0 +1,31 @@
require('dotenv').config();
const { sequelize, testConnection } = require('./config/database');
async function test() {
console.log('测试数据库连接...');
console.log('数据库配置:');
console.log('- Host:', process.env.DB_HOST || '129.211.213.226');
console.log('- Port:', process.env.DB_PORT || 9527);
console.log('- Database:', process.env.DB_DATABASE || 'insurance_data');
console.log('- User:', process.env.DB_USER || 'root');
console.log('');
try {
await sequelize.authenticate();
console.log('✅ 数据库连接成功!');
// 测试查询
const [results] = await sequelize.query('SELECT 1 as test');
console.log('✅ 测试查询成功:', results);
process.exit(0);
} catch (error) {
console.error('❌ 数据库连接失败:');
console.error('错误信息:', error.message);
console.error('错误详情:', error);
process.exit(1);
}
}
test();

View File

@@ -0,0 +1,52 @@
const { Sequelize } = require('sequelize');
// 测试连接到 nxxmdata 数据库
const sequelize = new Sequelize({
dialect: 'mysql',
host: '129.211.213.226',
port: 9527,
database: 'nxxmdata',
username: 'root',
password: 'aiotAiot123!',
logging: console.log,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
});
async function test() {
console.log('测试连接到 nxxmdata 数据库...');
try {
await sequelize.authenticate();
console.log('✅ 连接 nxxmdata 数据库成功!');
// 列出所有表
const [tables] = await sequelize.query('SHOW TABLES');
console.log('数据库中的表:', tables.length);
// 检查是否有 users 表
const [userTables] = await sequelize.query("SHOW TABLES LIKE 'users'");
if (userTables.length > 0) {
console.log('✅ 找到 users 表');
// 查询用户数据
const [users] = await sequelize.query('SELECT id, username, email FROM users LIMIT 5');
console.log('用户数据:', users);
} else {
console.log('❌ 没有找到 users 表');
}
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('❌ 连接失败:', error.message);
process.exit(1);
}
}
test();

View File

@@ -0,0 +1,94 @@
const ExcelJS = require('exceljs');
/**
* Excel导出工具类
*/
class ExcelExport {
/**
* 创建Excel工作簿并导出数据
* @param {Array} data - 要导出的数据
* @param {Array} columns - 列定义 [{ header: '列名', key: '字段名', width: 宽度 }]
* @param {String} sheetName - 工作表名称
* @returns {Promise<Buffer>} Excel文件Buffer
*/
static async exportToExcel(data, columns, sheetName = 'Sheet1') {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(sheetName);
// 设置列
worksheet.columns = columns;
// 设置表头样式
worksheet.getRow(1).eachCell((cell) => {
cell.font = { bold: true, color: { argb: 'FFFFFFFF' } };
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF4472C4' }
};
cell.alignment = { vertical: 'middle', horizontal: 'center' };
});
// 添加数据
data.forEach((item) => {
worksheet.addRow(item);
});
// 自动调整列宽
worksheet.columns.forEach((column) => {
if (!column.width) {
const maxLength = Math.max(
column.header.length,
...worksheet.getColumn(column.key).values.map(v =>
v ? String(v).length : 0
)
);
column.width = Math.min(Math.max(maxLength + 2, 10), 50);
}
});
// 生成Excel文件Buffer
const buffer = await workbook.xlsx.writeBuffer();
return buffer;
}
/**
* 格式化日期
* @param {Date|String} date - 日期
* @returns {String} 格式化后的日期字符串
*/
static formatDate(date) {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
/**
* 格式化金额
* @param {Number} amount - 金额
* @returns {String} 格式化后的金额字符串
*/
static formatAmount(amount) {
if (!amount && amount !== 0) return '';
return `¥${Number(amount).toFixed(2)}`;
}
/**
* 格式化状态
* @param {String} status - 状态值
* @param {Object} statusMap - 状态映射表
* @returns {String} 状态文本
*/
static formatStatus(status, statusMap) {
return statusMap[status] || status || '';
}
}
module.exports = ExcelExport;