Browse Source

新增申诉台账页与积分流水冲正 Tab

- 新增独立「申诉台账」页 src/views/points/appeal/index.vue:列表+详情+审批,走 completeFlow(taskId 由 getProcessDetail 补全)

- 新增 src/api/points/appeal.js、src/api/points/transaction.js

- 积分管理启用「流水冲正」Tab:新建 TransactionReversePanel.vue,列表 getListByPage + 冲正 reverse/{logId}(二次确认)

- 备注:手动调整 Tab 因 balance/adjust 参数缺失暂缓(后端计划 2026-10-16 更新)

- 同步更新 PAGES_TODO / CURRENT_STATUS / DEVELOPMENT_LOG;提交前 eslint 与 build:prod 通过
master
DU 4 days ago
parent
commit
7613386940
  1. 3
      docs/development/CURRENT_STATUS.md
  2. 17
      docs/development/DEVELOPMENT_LOG.md
  3. 15
      docs/development/PAGES_TODO.md
  4. 25
      src/api/points/appeal.js
  5. 11
      src/api/points/transaction.js
  6. 388
      src/views/points/appeal/index.vue
  7. 195
      src/views/points/management/components/TransactionReversePanel.vue
  8. 9
      src/views/points/management/index.vue

3
docs/development/CURRENT_STATUS.md

@ -37,7 +37,8 @@
| 工作台 | `src/views/workbench/index.vue` | `workbench/index` | 已开发(2026-10-15),待测试。6 项 KPI + 近 7 日趋势柱状图 + 待办与预警表 + 最新上报前 5 条;接口 `analysis/home/overview`、`todo-warning`、`trend-latest`。“平台同步失败”无接口,展示 `—`、重试禁用;风控预警“处理”目标页未开发,禁用 | | 工作台 | `src/views/workbench/index.vue` | `workbench/index` | 已开发(2026-10-15),待测试。6 项 KPI + 近 7 日趋势柱状图 + 待办与预警表 + 最新上报前 5 条;接口 `analysis/home/overview`、`todo-warning`、`trend-latest`。“平台同步失败”无接口,展示 `—`、重试禁用;风控预警“处理”目标页未开发,禁用 |
| 隐患审核 | `src/views/hazard/audit/index.vue` | `hazard/audit/index` | 已完成隐患详情/附件展示、平台字段补录、通过/驳回/等级切换,并接入 `getProcessDetail` 自动补齐审批 `taskId`;已在左侧菜单隐藏,仅由台账“去审核”等入口携带参数进入 | | 隐患审核 | `src/views/hazard/audit/index.vue` | `hazard/audit/index` | 已完成隐患详情/附件展示、平台字段补录、通过/驳回/等级切换,并接入 `getProcessDetail` 自动补齐审批 `taskId`;已在左侧菜单隐藏,仅由台账“去审核”等入口携带参数进入 |
| 隐患台账 | `src/views/hazard/ledger/index.vue` | `hazard/ledger/index` | 已完成新版 Card Table、五态筛选/Tag、分页、详情弹窗和审核入口;列表已切换为 `/flowable/hazard/todoList`;单位筛选按角色使用 `factoryList`/`myFactory`;附件九宫格支持大图预览;平台同步列暂时隐藏;同步重试等待后端接口 | | 隐患台账 | `src/views/hazard/ledger/index.vue` | `hazard/ledger/index` | 已完成新版 Card Table、五态筛选/Tag、分页、详情弹窗和审核入口;列表已切换为 `/flowable/hazard/todoList`;单位筛选按角色使用 `factoryList`/`myFactory`;附件九宫格支持大图预览;平台同步列暂时隐藏;同步重试等待后端接口 |
| 积分管理 | `src/views/points/management/index.vue` | `points/management/index` | 已完成积分规则 Tab、统一 Card 列表、厂区列、分页、详情、新增、启停和删除;超管可筛选/选择厂区,厂区管理员可管理本厂区规则;其余 Tab 暂禁用 | | 积分管理 | `src/views/points/management/index.vue` | `points/management/index` | 已完成积分规则 Tab、统一 Card 列表、厂区列、分页、详情、新增、启停和删除;超管可筛选/选择厂区,厂区管理员可管理本厂区规则。**流水冲正 Tab 已开发(2026-10-15,待测试)**;申诉处理移至独立「申诉台账」页,此 Tab 暂禁用;手动调整 Tab 因 `balance/adjust` 接口定义不完整暂缓 |
| 申诉台账 | `src/views/points/appeal/index.vue` | `points/appeal/index` | 已开发(2026-10-15),待测试。独立页(抄隐患台账 + 审批):列表 `appeal/todoList`、详情 `appeal/detail/{id}`、审批 `appeal/completeFlow`(taskId 由 `flowable/busiInfo/getProcessDetail` 补全)。待联调确认:completeFlow 的 taskId 来源、改判补分 `adjustPoints` 传参 |
| 隐患流程配置 | `src/views/flowable/hazardConfig/index.vue` | `flowable/hazardConfig/index` | 已完成列表、筛选、分页、新增、修改、状态 Tag、删除和厂区权限展示;新增“流程类型”字段(`bizType`,字典 `flow_biz_type`)并在列表以 Tag 展示;分页按真实 `data.list/data.total` 解析 | | 隐患流程配置 | `src/views/flowable/hazardConfig/index.vue` | `flowable/hazardConfig/index` | 已完成列表、筛选、分页、新增、修改、状态 Tag、删除和厂区权限展示;新增“流程类型”字段(`bizType`,字典 `flow_biz_type`)并在列表以 Tag 展示;分页按真实 `data.list/data.total` 解析 |
| 切换日志 | `src/views/flowable/switchLog/index.vue` | `flowable/switchLog/index` | 空白页面,无明确列表接口 | | 切换日志 | `src/views/flowable/switchLog/index.vue` | `flowable/switchLog/index` | 空白页面,无明确列表接口 |

17
docs/development/DEVELOPMENT_LOG.md

@ -1,5 +1,22 @@
# 开发日志 # 开发日志
## 2026-10-15:新增 Web 积分管理「流水冲正」Tab(W4)
- 新增 `src/views/points/management/components/TransactionReversePanel.vue`:积分流水分页列表(筛选:业务类型 / 状态 / 流水号),行操作“冲正”(二次确认,仅 `status=1` 有效可冲正)。
- 新增 `src/api/points/transaction.js`:`listTransactions`(`transaction/getListByPage`)、`reverseTransaction`(`transaction/reverse/{logId}`)。
- 在 `src/views/points/management/index.vue` 启用第 4 个 Tab「流水冲正」(`v-if` 懒加载),第 3 个 Tab「手动调整」仍禁用。
- 备注:流水明细无用户姓名字段,列表“用户”列显示 `userId`。
- 验证:新文件 `eslint --no-ignore` 通过、`npm run build:prod` 通过(仅既有 warning)、页面诊断无错误。
## 2026-10-15:新增 Web 申诉台账页面(含审批)
- 新增 `src/views/points/appeal/index.vue`:独立「申诉台账」页,抄隐患台账结构(筛选/表格/分页/详情弹窗)+ 审批弹窗(通过/驳回 + 调整积分 + 意见)。
- 新增 `src/api/points/appeal.js`:`listAppealTodo`、`getAppealDetail`、`getAppealProcessDetail`、`completeAppealFlow`,对应 `appeal/todoList`、`appeal/detail/{id}`、`flowable/busiInfo/getProcessDetail`、`appeal/completeFlow`。
- 审批走新流程 `completeFlow`:taskId 由 `flowable/busiInfo/getProcessDetail`(按 `flowInstanceId`)取当前待办任务补全,与隐患审核一致;无 taskId 时提示不可提交。
- **待联调确认**:① `completeFlow` 的 taskId 取值方式;② 改判补分的 `adjustPoints` 传参(当前放入 `variables`)。
- 菜单(需后端配置):一级「积分与商城」→ 菜单「申诉台账」,路由 `points/appeal`,组件 `points/appeal/index`(`/points/appeal`)。原型积分管理内原“申诉处理 Tab”暂保持禁用。
- 验证:`eslint --no-ignore` 通过、`npm run build:prod` 通过(仅既有 warning)。
## 2026-10-15:新增 Web 工作台页面 ## 2026-10-15:新增 Web 工作台页面
- 新增 `src/views/workbench/index.vue`:6 项 KPI(待审核 / 本月已闭环 / 平台同步失败 / 今日新增上报 / 今日兑换单 / 今日发放积分)、近 7 日上报趋势柱状图、待办与预警表、最新上报(前 5 条)。 - 新增 `src/views/workbench/index.vue`:6 项 KPI(待审核 / 本月已闭环 / 平台同步失败 / 今日新增上报 / 今日兑换单 / 今日发放积分)、近 7 日上报趋势柱状图、待办与预警表、最新上报(前 5 条)。

15
docs/development/PAGES_TODO.md

@ -27,9 +27,9 @@
| 3 | 隐患治理 | 隐患审核 | ✅ | ✅ | — | [hazard/audit](../../src/views/hazard/audit/index.vue) | | 3 | 隐患治理 | 隐患审核 | ✅ | ✅ | — | [hazard/audit](../../src/views/hazard/audit/index.vue) |
| 4 | 隐患治理 | 隐患台账 | ✅ | ⚠️ | — | [hazard/ledger](../../src/views/hazard/ledger/index.vue);“重试同步”缺平台同步接口,列已隐藏 | | 4 | 隐患治理 | 隐患台账 | ✅ | ⚠️ | — | [hazard/ledger](../../src/views/hazard/ledger/index.vue);“重试同步”缺平台同步接口,列已隐藏 |
| 5 | 积分与商城 | 积分管理 · Tab1 积分规则 | ✅ | ✅ | — | [points/management](../../src/views/points/management/index.vue) | | 5 | 积分与商城 | 积分管理 · Tab1 积分规则 | ✅ | ✅ | — | [points/management](../../src/views/points/management/index.vue) |
| 6 | 积分与商城 | 积分管理 · Tab2 申诉处理 | ❌ | ✅ | ✅ | 受理/处理/审批:`appeal/todoList`、`accept/{id}`、`handle/{id}`、`completeFlow`、`detail/{id}`([积分申诉管理](../api/modules/07-points-appeal.md)) | | 6 | 积分与商城 | 申诉台账(独立页,抄隐患台账 + 审批界面) | ⚠️ 待测试 | ✅ | ✅ | **已开发(2026-10-15)**:`src/views/points/appeal/index.vue` + `src/api/points/appeal.js`。列表 `appeal/todoList`、详情 `appeal/detail/{id}`、审批 `appeal/completeFlow`(taskId 由 `flowable/busiInfo/getProcessDetail` 补全)。注:原型积分管理内原“申诉处理 Tab”暂保持禁用 |
| 7 | 积分与商城 | 积分管理 · Tab3 手动调整 | ❌ | ✅ | ✅ | `balance/adjust`(补发/扣减 + 自动生成流水与审计)、`balance/getByUserId/{userId}`([用户积分账户](../api/modules/04-points-balance.md)) | | 7 | 积分与商城 | 积分管理 · Tab3 手动调整 | ❌ | ⛔ | ⛔ | **阻塞**:`balance/adjust` 文档仅声明 form-urlencoded + 单整数,缺 `userId`/`points`/原因字段,需后端补请求结构 |
| 8 | 积分与商城 | 积分管理 · Tab4 流水冲正 | ❌ | ✅ | ✅ | `transaction/reverse/{logId}`、`transaction/list`([积分流水明细](../api/modules/05-points-transaction.md)) | | 8 | 积分与商城 | 积分管理 · Tab4 流水冲正 | ⚠️ 待测试 | ✅ | ✅ | **已开发(2026-10-15)**:`src/views/points/management/components/TransactionReversePanel.vue` + `src/api/points/transaction.js`。列表 `transaction/getListByPage`、冲正 `transaction/reverse/{logId}`(二次确认,仅 status=1 可冲正) |
| 9 | 积分与商城 | 商城运营(主页:KPI + 兑换订单表 + 订单详情时间线 + 退款/导出) | ❌ | ⚠️ | ⚠️ | 订单/追踪/物品/日志接口齐全([兑换订单](../api/modules/08-exchange-order.md)、[订单追踪](../api/modules/10-exchange-order-track.md)、[兑换物品](../api/modules/09-exchange-item.md)、[操作日志](../api/modules/12-exchange-operation-log.md));**“惠服对接状态卡”(Token 状态/同步时间/全量·增量·单品同步/刷新 Token)无接口**,该卡片需等后端 | | 9 | 积分与商城 | 商城运营(主页:KPI + 兑换订单表 + 订单详情时间线 + 退款/导出) | ❌ | ⚠️ | ⚠️ | 订单/追踪/物品/日志接口齐全([兑换订单](../api/modules/08-exchange-order.md)、[订单追踪](../api/modules/10-exchange-order-track.md)、[兑换物品](../api/modules/09-exchange-item.md)、[操作日志](../api/modules/12-exchange-operation-log.md));**“惠服对接状态卡”(Token 状态/同步时间/全量·增量·单品同步/刷新 Token)无接口**,该卡片需等后端 |
| 10 | 积分与商城 | 商城运营 › 分类管理 | ❌ | ⛔ | ⛔ | 页面本体由惠服平台提供,需甲方给出嵌入方案(URL/SSO) | | 10 | 积分与商城 | 商城运营 › 分类管理 | ❌ | ⛔ | ⛔ | 页面本体由惠服平台提供,需甲方给出嵌入方案(URL/SSO) |
| 11 | 积分与商城 | 商城运营 › 轮播图管理 | ❌ | ⛔ | ⛔ | 同上 | | 11 | 积分与商城 | 商城运营 › 轮播图管理 | ❌ | ⛔ | ⛔ | 同上 |
@ -163,6 +163,9 @@
**A. 需后端确认(接口 / 字段 / 入参)** **A. 需后端确认(接口 / 字段 / 入参)**
- [ ] `completeFlow` 请求体结构:`taskId`、通过/驳回标识、`adjustPoints` 传参位置(每节点 or 结束节点)、审批意见字段名。 - [ ] `completeFlow` 请求体结构:`taskId`、通过/驳回标识、`adjustPoints` 传参位置(每节点 or 结束节点)、审批意见字段名。
> **W2 落地假设(2026-10-15,待联调确认)**:审批 body 采用 `{ taskId, instanceId, type: 'appeal', result: '1'|'2', comment }`;`taskId` 由 `flowable/busiInfo/getProcessDetail?id=flowInstanceId` 取当前待办任务补全(沿用隐患审核做法);**改判补分的 `adjustPoints` 暂放入 `variables`**——若后端实际从申诉记录字段读取,需改为“先 `appeal/updateById` 更新记录再 `completeFlow`”。
- [ ] **手动调整 `balance/adjust` 请求结构(阻塞 W3)**:文档仅声明 form-urlencoded 单整数,缺 `userId`/`points`/原因字段与参数名,需后端补充后才能开发「手动调整」。**【后端计划 2026-10-16 更新该接口并补参数;同时需确认是直接生效还是二级审批】**
- [ ] `todoList` 筛选参数(待办/已办、类型、状态、厂区)与返回字段是否含 `canApprove`、`isInitiator`(现有 api.json 文档未列出)。 - [ ] `todoList` 筛选参数(待办/已办、类型、状态、厂区)与返回字段是否含 `canApprove`、`isInitiator`(现有 api.json 文档未列出)。
- [ ] `myList` 筛选参数(`appealType` / `status`)。 - [ ] `myList` 筛选参数(`appealType` / `status`)。
- [ ] `detail/{id}` 返回的“流程进度信息”结构(时间线需要:节点名 / 处理人 / 处理时间 / 审批意见)。 - [ ] `detail/{id}` 返回的“流程进度信息”结构(时间线需要:节点名 / 处理人 / 处理时间 / 审批意见)。
@ -196,9 +199,9 @@
| 编号 | 端 | 功能 / 页面 | 关键接口 | 建议文件(页面 / 封装) | 优先级 | 状态 | 依赖 / 备注 | | 编号 | 端 | 功能 / 页面 | 关键接口 | 建议文件(页面 / 封装) | 优先级 | 状态 | 依赖 / 备注 |
| --- | --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- | --- |
| W1 | Web | 工作台 | `analysis/home/overview`、`home/todo-warning`、`home/trend-latest` | `src/views/workbench/index.vue` + `src/api/analysis/home.js` | 高 | ◐ 待测试 | **已开发(2026-10-15)**,待测试。菜单:一级「工作台」,路由 `workbench`,组件 `workbench/index`(`/workbench`)。“平台同步失败”KPI 无接口,展示 `—` | | W1 | Web | 工作台 | `analysis/home/overview`、`home/todo-warning`、`home/trend-latest` | `src/views/workbench/index.vue` + `src/api/analysis/home.js` | 高 | ◐ 待测试 | **已开发(2026-10-15)**,待测试。菜单:一级「工作台」,路由 `workbench`,组件 `workbench/index`(`/workbench`)。“平台同步失败”KPI 无接口,展示 `—` |
| W2 | Web | 积分管理 · 申诉处理 Tab | `appeal/todoList`、`appeal/detail/{id}`、`appeal/completeFlow` | `src/views/points/management/index.vue` + `src/api/points/appeal.js` | 高 | ☐ | 默认走 `completeFlow`;开工前按第八节 A 确认入参 | | W2 | Web | 申诉台账(独立页,抄隐患台账 + 审批) | `appeal/todoList`、`appeal/detail/{id}`、`appeal/completeFlow`、`flowable/busiInfo/getProcessDetail` | `src/views/points/appeal/index.vue` + `src/api/points/appeal.js` | 高 | ◐ 待测试 | **已开发(2026-10-15)**,待测试。菜单:一级「积分与商城」→「申诉台账」,路由 `points/appeal`,组件 `points/appeal/index`(`/points/appeal`)。待联调确认:completeFlow 的 taskId 来源、改判补分 `adjustPoints` 传参(当前放 `variables`) |
| W3 | Web | 积分管理 · 手动调整 Tab | `balance/adjust`、`balance/getByUserId/{userId}` | 同上 + `src/api/points/balance.js` | 高 | ☐ | 员工选择用 `system/getOrgUsers` | | W3 | Web | 积分管理 · 手动调整 Tab | `balance/adjust`、`balance/getByUserId/{userId}` | `src/views/points/management/index.vue` + `src/api/points/balance.js` | 高 | ⛔ 需确认 | **接口定义不完整**:`balance/adjust` 文档仅声明 `application/x-www-form-urlencoded` + 单个整数,缺 `userId`/`points`/原因字段结构,无法开工;**后端计划 2026-10-16 更新该接口并补参数**,更新后即可开发 |
| W4 | Web | 积分管理 · 流水冲正 Tab | `transaction/reverse/{logId}`、`transaction/list` | 同上 + `src/api/points/transaction.js` | 中 | ☐ | 冲正需二次确认 | | W4 | Web | 积分管理 · 流水冲正 Tab | `transaction/getListByPage`、`transaction/reverse/{logId}` | `src/views/points/management/components/TransactionReversePanel.vue` + `src/api/points/transaction.js` | 中 | ◐ 待测试 | **已开发(2026-10-15)**,待测试。积分管理第 4 个 Tab;仅 status=1(有效)可冲正,二次确认。流水无用户姓名字段,列表显示 userId |
| W5 | Web | 安全兑换管理 | `exchange/config/active`·`validate`·`switch/status`·`switch/toggle`·`updateById`、`anti/farm/active`·`detectAll`·`detectSelfAudit` | `src/views/exchange/security/index.vue` + `src/api/exchange/config.js` | 中 | ☐ | 字段见 `ExchangeConfig` / `AntiFarmConfig` | | W5 | Web | 安全兑换管理 | `exchange/config/active`·`validate`·`switch/status`·`switch/toggle`·`updateById`、`anti/farm/active`·`detectAll`·`detectSelfAudit` | `src/views/exchange/security/index.vue` + `src/api/exchange/config.js` | 中 | ☐ | 字段见 `ExchangeConfig` / `AntiFarmConfig` |
| W6 | Web | 数据分析 | `analysis/stats/*`、`ranking/*`、`timeliness/level`、`report/monthly`(+`/export`)、`stats/category`·`stats/level` | `src/views/analysis/index.vue` + `src/api/analysis/index.js` | 中 | ☐ | **页面调整(2026-10-15)**:① 去掉隐患热力图(`heatmap/*` 接口暂不接);② 去掉顶部筛选列;③「处理时效漏斗」与「积分排行」改为左右布局占一行。图表用已装 `echarts` | | W6 | Web | 数据分析 | `analysis/stats/*`、`ranking/*`、`timeliness/level`、`report/monthly`(+`/export`)、`stats/category`·`stats/level` | `src/views/analysis/index.vue` + `src/api/analysis/index.js` | 中 | ☐ | **页面调整(2026-10-15)**:① 去掉隐患热力图(`heatmap/*` 接口暂不接);② 去掉顶部筛选列;③「处理时效漏斗」与「积分排行」改为左右布局占一行。图表用已装 `echarts` |
| A1 | APP | 设置(推送/声音/震动/关于/清缓存/退出) | 纯本地(AsyncStorage) | `src/screens/home/SettingsScreen.tsx` | 中 | ☐ | 参考原型 `scr-settings` | | A1 | APP | 设置(推送/声音/震动/关于/清缓存/退出) | 纯本地(AsyncStorage) | `src/screens/home/SettingsScreen.tsx` | 中 | ☐ | 参考原型 `scr-settings` |

25
src/api/points/appeal.js

@ -0,0 +1,25 @@
import request from '@/utils/request'
// 申诉审批台账(审批人视角,待办/已办;返回 PageInfo,行内含 canApprove/isInitiator)
export function listAppealTodo(params) {
return request({ url: '/business/points/appeal/todoList', method: 'get', params })
}
// 申诉详情(含完整申诉字段与流程进度信息)
export function getAppealDetail(id) {
return request({ url: '/business/points/appeal/detail/' + id, method: 'get' })
}
// 根据流程实例 ID 获取流程详情,用于补齐当前待办任务 taskId
export function getAppealProcessDetail(flowInstanceId) {
return request({
url: '/flowable/busiInfo/getProcessDetail',
method: 'get',
params: { id: flowInstanceId }
})
}
// 审批申诉流程(通过/驳回当前节点);流程结束时后端按 adjustPoints 补/扣分并推送消息
export function completeAppealFlow(data) {
return request({ url: '/business/points/appeal/completeFlow', method: 'post', data })
}

11
src/api/points/transaction.js

@ -0,0 +1,11 @@
import request from '@/utils/request'
// 积分流水分页查询(管理端)
export function listTransactions(params) {
return request({ url: '/business/points/transaction/getListByPage', method: 'get', params })
}
// 积分冲正:按原流水 ID 冲正,原流水标记已冲正并生成反向流水
export function reverseTransaction(logId) {
return request({ url: '/business/points/transaction/reverse/' + logId, method: 'post' })
}

388
src/views/points/appeal/index.vue

@ -0,0 +1,388 @@
<template>
<div class="app-container appeal-ledger">
<data-list-panel modern-table>
<template #filters>
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" class="data-list-filter-form">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="全部状态">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="申诉类型" prop="appealType">
<el-select v-model="queryParams.appealType" clearable placeholder="全部类型">
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</template>
<template #actions>
<el-button icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
</template>
<el-table v-loading="loading" :data="list" size="mini" class="data-list-table">
<el-table-column label="申诉编号" prop="appealNo" min-width="150" show-overflow-tooltip />
<el-table-column label="申诉人" prop="userName" width="100" show-overflow-tooltip />
<el-table-column label="关联隐患" prop="reportNo" width="130" show-overflow-tooltip />
<el-table-column label="申诉类型" width="130">
<template slot-scope="scope">{{ typeLabel(scope.row) }}</template>
</el-table-column>
<el-table-column label="申诉理由" prop="appealReason" min-width="220" show-overflow-tooltip />
<el-table-column label="状态" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="statusTagType(scope.row)" size="small">{{ statusLabel(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="提交时间" width="160">
<template slot-scope="scope">{{ formatTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center" fixed="right" class-name="data-list-operation-column">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openDetail(scope.row)">详情</el-button>
<el-button v-if="canApprove(scope.row)" type="text" size="mini" @click="openApprove(scope.row)">去审批</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:current-page.sync="queryParams.pageNum"
:page-size.sync="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
@size-change="getList"
@current-change="getList"
/>
</template>
</data-list-panel>
<!-- 详情 -->
<el-dialog title="申诉详情" :visible.sync="detailVisible" width="640px" append-to-body>
<div v-loading="detailLoading" class="appeal-detail">
<el-descriptions v-if="detail" :column="2" border size="small">
<el-descriptions-item label="申诉编号">{{ detail.appealNo || '—' }}</el-descriptions-item>
<el-descriptions-item label="申诉类型">{{ typeLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="申诉人">{{ detail.userName || '—' }}</el-descriptions-item>
<el-descriptions-item label="所属厂区">{{ detail.factoryDeptName || '—' }}</el-descriptions-item>
<el-descriptions-item label="关联隐患">{{ detail.reportNo || '—' }}</el-descriptions-item>
<el-descriptions-item label="当前状态">{{ statusLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatTime(detail.createTime) }}</el-descriptions-item>
<el-descriptions-item label="处理时间">{{ formatTime(detail.handleTime) }}</el-descriptions-item>
<el-descriptions-item label="处理人">{{ detail.handleUserName || '—' }}</el-descriptions-item>
<el-descriptions-item label="调整积分">{{ formatAdjust(detail.adjustPoints) }}</el-descriptions-item>
<el-descriptions-item label="申诉理由" :span="2">{{ detail.appealReason || '—' }}</el-descriptions-item>
<el-descriptions-item label="处理意见" :span="2">{{ detail.handleRemark || '—' }}</el-descriptions-item>
</el-descriptions>
<div v-if="attachmentList.length" class="appeal-attachments">
<div class="appeal-attachments__title">附件</div>
<a v-for="(url, index) in attachmentList" :key="index" class="appeal-attachments__item" :href="url" target="_blank" rel="noopener">{{ url }}</a>
</div>
<el-empty v-if="!detailLoading && !detail" description="暂无详情" />
</div>
</el-dialog>
<!-- 审批 -->
<el-dialog title="审批申诉" :visible.sync="approveVisible" width="560px" append-to-body>
<div class="approve-summary">
<div><span class="label">申诉编号:</span>{{ approveForm.appealNo || '—' }}</div>
<div><span class="label">申诉人:</span>{{ approveForm.userName || '—' }}</div>
<div><span class="label">关联隐患:</span>{{ approveForm.reportNo || '—' }}</div>
<div><span class="label">申诉理由:</span>{{ approveForm.appealReason || '—' }}</div>
</div>
<el-alert v-if="approveTaskMissing" title="未获取到审批任务 ID,暂无法提交审批。" type="warning" :closable="false" show-icon style="margin-bottom:12px" />
<el-form ref="approveForm" :model="approveForm" :rules="approveRules" label-width="90px" size="small">
<el-form-item label="审批结果" prop="result">
<el-radio-group v-model="approveForm.result">
<el-radio label="1">通过</el-radio>
<el-radio label="2">驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="approveForm.result === '1'" label="调整积分">
<el-input-number v-model="approveForm.adjustPoints" :controls="false" placeholder="正数补发/负数扣减,可选" />
<span class="field-tip">正数补发、负数扣减;不填则按原积分处理</span>
</el-form-item>
<el-form-item label="审批意见" prop="comment">
<el-input v-model="approveForm.comment" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请填写审批意见" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="approveVisible = false">取消</el-button>
<el-button type="primary" :loading="approveSubmitting" @click="submitApprove">提交审批</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listAppealTodo, getAppealDetail, getAppealProcessDetail, completeAppealFlow } from '@/api/points/appeal'
export default {
name: 'PointsAppeal',
data() {
return {
loading: false,
showSearch: true,
list: [],
total: 0,
queryParams: { pageNum: 1, pageSize: 10, status: undefined, appealType: undefined },
statusOptions: [
{ label: '待受理', value: '0' },
{ label: '处理中', value: '1' },
{ label: '已通过', value: '2' },
{ label: '已驳回', value: '3' },
{ label: '已撤回', value: '4' }
],
typeOptions: [
{ label: '审核驳回申诉', value: 'REJECT' },
{ label: '积分异议', value: 'POINTS' },
{ label: '其他', value: 'OTHER' }
],
detailVisible: false,
detailLoading: false,
detail: null,
approveVisible: false,
approveSubmitting: false,
approveTaskMissing: false,
approveRow: null,
approveForm: { appealNo: '', userName: '', reportNo: '', appealReason: '', result: '1', adjustPoints: undefined, comment: '' },
approveRules: {
result: [{ required: true, message: '请选择审批结果', trigger: 'change' }],
comment: [{ required: true, message: '请填写审批意见', trigger: 'blur' }]
}
}
},
computed: {
attachmentList() {
const raw = this.detail && this.detail.attachmentUrls
if (!raw) return []
try {
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? parsed.map(item => (typeof item === 'string' ? item : item.url)).filter(Boolean) : []
} catch (error) {
return String(raw).split(',').map(item => item.trim()).filter(Boolean)
}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
listAppealTodo(this.queryParams)
.then(response => {
const page = response.data || response
this.list = page.list || page.rows || page.records || []
this.total = Number(page.total || page.totalCount || this.list.length)
})
.catch(() => {
this.list = []
this.total = 0
this.$modal.msgError('申诉台账加载失败,请检查后端服务和登录状态')
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.$refs.queryForm.resetFields()
this.queryParams.pageNum = 1
this.getList()
},
openDetail(row) {
this.detailVisible = true
this.detail = null
this.detailLoading = true
getAppealDetail(row.id)
.then(response => {
this.detail = response.data || response
})
.catch(() => {
this.$modal.msgError('申诉详情加载失败')
})
.finally(() => {
this.detailLoading = false
})
},
openApprove(row) {
this.approveRow = row
this.approveTaskMissing = false
this.approveForm = {
appealNo: row.appealNo,
userName: row.userName,
reportNo: row.reportNo,
appealReason: row.appealReason,
result: '1',
adjustPoints: undefined,
comment: ''
}
this.approveVisible = true
this.resolveTaskId(row)
},
// 通过流程实例补全当前待办 taskId(与隐患审核一致)
async resolveTaskId(row) {
if (row.canApprove !== true && String(row.canApprove) !== 'true') return
if (!row.flowInstanceId) {
this.approveTaskMissing = true
return
}
try {
const response = await getAppealProcessDetail(row.flowInstanceId)
const task = this.findCurrentTask(response.data)
this.approveRow = Object.assign({}, row, { taskId: task ? String(task.taskId || '') : '' })
this.approveTaskMissing = !this.approveRow.taskId
} catch (error) {
this.approveTaskMissing = true
}
},
findCurrentTask(data) {
const tasks = this.normalizeTaskList(data)
if (!tasks.length) return null
return tasks.find(task => task.taskId && !task.endTime && !task.finishTime) || tasks.find(task => task.taskId) || null
},
normalizeTaskList(data) {
const tasks = []
const visit = value => {
if (Array.isArray(value)) {
value.forEach(visit)
return
}
if (!value || typeof value !== 'object') return
if (Object.prototype.hasOwnProperty.call(value, 'taskId')) tasks.push(value)
Object.keys(value).forEach(key => {
if (key !== 'taskId') visit(value[key])
})
}
visit(data)
return tasks
},
submitApprove() {
this.$refs.approveForm.validate(valid => {
if (!valid) return
const row = this.approveRow || {}
if (!row.taskId) {
this.$modal.msgWarning('未获取到审批任务 ID,无法提交审批')
return
}
const payload = {
taskId: row.taskId,
instanceId: row.flowInstanceId,
type: 'appeal',
result: this.approveForm.result,
comment: this.approveForm.comment
}
// 改判补分:adjustPoints 随流程变量下发(联调确认后端读取方式)
if (this.approveForm.result === '1' && this.approveForm.adjustPoints !== undefined && this.approveForm.adjustPoints !== null) {
payload.variables = { adjustPoints: this.approveForm.adjustPoints }
}
this.approveSubmitting = true
completeAppealFlow(payload)
.then(() => {
this.$modal.msgSuccess(this.approveForm.result === '1' ? '申诉已通过' : '申诉已驳回')
this.approveVisible = false
this.getList()
})
.catch(() => {
this.$modal.msgError('审批提交失败,请稍后重试')
})
.finally(() => {
this.approveSubmitting = false
})
})
},
canApprove(row) {
const can = row.canApprove === true || String(row.canApprove) === 'true'
const status = this.statusKey(row)
return can && (status === '0' || status === '1')
},
statusKey(row) {
const value = String(row.status === undefined || row.status === null ? '' : row.status)
return value
},
statusLabel(row) {
const name = row.statusName
if (name) return name
return ({ '0': '待受理', '1': '处理中', '2': '已通过', '3': '已驳回', '4': '已撤回' }[this.statusKey(row)]) || '—'
},
statusTagType(row) {
const key = this.statusKey(row)
if (key === '2') return 'success'
if (key === '3') return 'danger'
if (key === '1') return 'primary'
if (key === '4') return 'info'
return 'warning'
},
typeLabel(row) {
return ({ REJECT: '审核驳回申诉', POINTS: '积分异议', OTHER: '其他' }[row.appealType]) || row.appealType || '—'
},
formatAdjust(value) {
if (value === undefined || value === null || value === '') return '—'
const n = Number(value)
return Number.isNaN(n) ? String(value) : `${n > 0 ? '+' : ''}${n}`
},
formatTime(value) {
if (!value) return '—'
return String(value).replace('T', ' ').slice(0, 19)
}
}
}
</script>
<style lang="scss" scoped>
.appeal-ledger {
min-height: calc(100vh - 52px);
}
.approve-summary {
margin-bottom: 14px;
padding: 12px 14px;
border-radius: 8px;
background: var(--prototype-panel-soft, #fafbfc);
color: var(--prototype-text-secondary, #5a5a72);
font-size: 13px;
line-height: 22px;
}
.approve-summary .label {
color: var(--prototype-text-muted, #9898ae);
}
.field-tip {
margin-left: 10px;
color: var(--prototype-text-muted, #9898ae);
font-size: 12px;
}
.appeal-attachments {
margin-top: 14px;
}
.appeal-attachments__title {
margin-bottom: 6px;
color: var(--prototype-text-secondary, #5a5a72);
font-size: 13px;
font-weight: 600;
}
.appeal-attachments__item {
display: block;
margin-bottom: 4px;
color: #1890ff;
font-size: 12px;
word-break: break-all;
}
.approve-summary .el-input-number {
width: 100%;
}
</style>

195
src/views/points/management/components/TransactionReversePanel.vue

@ -0,0 +1,195 @@
<template>
<data-list-panel modern-table>
<template #filters>
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" class="data-list-filter-form">
<el-form-item label="业务类型" prop="bizType">
<el-select v-model="queryParams.bizType" clearable placeholder="全部类型">
<el-option v-for="item in bizTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="全部状态">
<el-option label="有效" :value="1" />
<el-option label="已冲正" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="流水号" prop="txnNo">
<el-input v-model="queryParams.txnNo" clearable placeholder="流水号" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</template>
<template #actions>
<el-button icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
</template>
<el-table v-loading="loading" :data="list" size="mini" class="data-list-table">
<el-table-column label="流水号" prop="txnNo" min-width="160" show-overflow-tooltip />
<el-table-column label="用户ID" prop="userId" width="90" />
<el-table-column label="业务类型" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="bizTypeTag(scope.row)" size="small">{{ bizTypeLabel(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="变动" width="90" align="right">
<template slot-scope="scope">
<span :style="{ color: amountColor(scope.row) }">{{ amountLabel(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="变动前 → 后" width="150">
<template slot-scope="scope">{{ balanceText(scope.row) }}</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="statusTagType(scope.row)" size="small">{{ statusLabel(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="时间" width="160">
<template slot-scope="scope">{{ formatTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right" class-name="data-list-operation-column">
<template slot-scope="scope">
<el-button v-if="canReverse(scope.row)" type="text" size="mini" @click="handleReverse(scope.row)">冲正</el-button>
<span v-else class="txn-muted">—</span>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:current-page.sync="queryParams.pageNum"
:page-size.sync="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
@size-change="getList"
@current-change="getList"
/>
</template>
</data-list-panel>
</template>
<script>
import { listTransactions, reverseTransaction } from '@/api/points/transaction'
const BIZ_TYPES = [
{ value: 'REPORT', label: '上报奖励' },
{ value: 'EXCHANGE', label: '兑换扣减' },
{ value: 'REFUND', label: '退款回滚' },
{ value: 'FREEZE', label: '冻结' },
{ value: 'UNFREEZE', label: '解冻' },
{ value: 'APPEAL_ADD', label: '申诉补发' },
{ value: 'APPEAL_DEDUCT', label: '申诉扣减' },
{ value: 'MALL_DEDUCT', label: '商城扣减' },
{ value: 'MALL_REFUND', label: '商城退款' },
{ value: 'MANUAL_ADD', label: '手动补发' },
{ value: 'MANUAL_DEDUCT', label: '手动扣减' }
]
export default {
name: 'TransactionReversePanel',
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: { pageNum: 1, pageSize: 10, bizType: undefined, status: undefined, txnNo: undefined },
bizTypeOptions: BIZ_TYPES
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
listTransactions(this.queryParams)
.then(response => {
const page = response.data || response
this.list = page.list || page.rows || page.records || []
this.total = Number(page.total || page.totalCount || this.list.length)
})
.catch(() => {
this.list = []
this.total = 0
this.$modal.msgError('积分流水加载失败,请检查后端服务和登录状态')
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.$refs.queryForm.resetFields()
this.queryParams.pageNum = 1
this.getList()
},
handleReverse(row) {
const label = `${row.txnNo || row.id}(${this.amountLabel(row)})`
this.$confirm(`确认对流水 ${label} 执行冲正?将生成反向流水且原流水不可删除。`, '积分冲正', { type: 'warning' })
.then(() => reverseTransaction(row.id))
.then(() => {
this.$modal.msgSuccess('冲正成功')
this.getList()
})
.catch(error => {
if (error !== 'cancel') this.$modal.msgError('冲正失败,请稍后重试')
})
},
canReverse(row) {
return String(row.status) === '1'
},
statusLabel(row) {
const value = String(row.status)
return ({ '1': '有效', '2': '已冲正' }[value]) || (row.status === undefined || row.status === null ? '—' : value)
},
statusTagType(row) {
return String(row.status) === '2' ? 'info' : 'success'
},
bizTypeLabel(row) {
const item = BIZ_TYPES.find(option => option.value === row.bizType)
return item ? item.label : (row.bizType || '—')
},
bizTypeTag(row) {
const map = { REPORT: 'success', EXCHANGE: 'primary', REFUND: 'warning' }
return map[row.bizType] || 'info'
},
amountLabel(row) {
const value = row.amount
if (value === undefined || value === null || value === '') return '—'
const n = Number(value)
if (Number.isNaN(n)) return String(value)
return `${n > 0 ? '+' : ''}${n}`
},
amountColor(row) {
const n = Number(row.amount)
if (Number.isNaN(n) || n === 0) return 'var(--prototype-text, #1a1a2e)'
return n > 0 ? '#2d8a4e' : '#d64550'
},
balanceText(row) {
const before = row.beforeBalance
const after = row.afterBalance
if ((before === undefined || before === null) && (after === undefined || after === null)) return '—'
return `${before === undefined || before === null ? '—' : before} → ${after === undefined || after === null ? '—' : after}`
},
formatTime(value) {
if (!value) return '—'
return String(value).replace('T', ' ').slice(0, 19)
}
}
}
</script>
<style lang="scss" scoped>
.txn-muted {
color: var(--prototype-text-muted, #9898ae);
}
</style>

9
src/views/points/management/index.vue

@ -167,10 +167,9 @@
><el-tab-pane label="申诉处理" name="appeal" disabled /><el-tab-pane ><el-tab-pane label="申诉处理" name="appeal" disabled /><el-tab-pane
label="手动调整" label="手动调整"
name="adjust" name="adjust"
disabled /><el-tab-pane disabled /><el-tab-pane label="流水冲正" name="reverse">
label="流水冲正" <transaction-reverse-panel v-if="activeTab === 'reverse'" />
name="reverse" </el-tab-pane></el-tabs
disabled /></el-tabs
><el-dialog ><el-dialog
title="积分规则详情" title="积分规则详情"
:visible.sync="detailVisible" :visible.sync="detailVisible"
@ -300,6 +299,7 @@ import {
listFactoryDepartments, listFactoryDepartments,
getMyFactoryDepartment, getMyFactoryDepartment,
} from "@/api/system/dept"; } from "@/api/system/dept";
import TransactionReversePanel from "./components/TransactionReversePanel";
const LEVELS = [ const LEVELS = [
{ value: "general", label: "一般隐患" }, { value: "general", label: "一般隐患" },
{ value: "major", label: "较大隐患" }, { value: "major", label: "较大隐患" },
@ -307,6 +307,7 @@ const LEVELS = [
]; ];
export default { export default {
name: "PointsManagement", name: "PointsManagement",
components: { TransactionReversePanel },
data() { data() {
const deptRequired = (rule, value, callback) => { const deptRequired = (rule, value, callback) => {
if (!value) callback(new Error("请选择所属厂区")); if (!value) callback(new Error("请选择所属厂区"));

Loading…
Cancel
Save