Browse Source

申诉处理 Tab 复用申诉台账组件

- 抽出可复用组件 src/views/points/appeal/components/AppealLedgerPanel.vue(列表/详情/审批)

- 独立页 src/views/points/appeal/index.vue 改为包裹该组件

- 积分管理启用第 2 个 Tab「申诉处理」,复用同一组件

- 同步更新 PAGES_TODO / CURRENT_STATUS / DEVELOPMENT_LOG
master
DU 4 days ago
parent
commit
1b9570aff9
  1. 2
      docs/development/CURRENT_STATUS.md
  2. 7
      docs/development/DEVELOPMENT_LOG.md
  3. 2
      docs/development/PAGES_TODO.md
  4. 379
      src/views/points/appeal/components/AppealLedgerPanel.vue
  5. 374
      src/views/points/appeal/index.vue
  6. 7
      src/views/points/management/index.vue

2
docs/development/CURRENT_STATUS.md

@ -37,7 +37,7 @@
| 工作台 | `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 已开发(2026-10-15,待测试)**;申诉处理移至独立「申诉台账」页,此 Tab 暂禁用;手动调整 Tab 因 `balance/adjust` 接口定义不完整暂缓 | | 积分管理 | `src/views/points/management/index.vue` | `points/management/index` | 已完成积分规则 Tab、统一 Card 列表、厂区列、分页、详情、新增、启停和删除;超管可筛选/选择厂区,厂区管理员可管理本厂区规则。**流水冲正 Tab 已开发(2026-10-15,待测试)**;申诉处理 Tab 已启用(复用独立「申诉台账」页同一组件 `AppealLedgerPanel`);手动调整 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/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` | 空白页面,无明确列表接口 |

7
docs/development/DEVELOPMENT_LOG.md

@ -1,5 +1,12 @@
# 开发日志 # 开发日志
## 2026-10-15:申诉处理 Tab 复用申诉台账组件
- 将申诉台账核心逻辑抽成可复用组件 `src/views/points/appeal/components/AppealLedgerPanel.vue`(列表/详情/审批)。
- 独立页 `src/views/points/appeal/index.vue` 改为仅包裹该组件。
- 积分管理 `src/views/points/management/index.vue` 启用第 2 个 Tab「申诉处理」(`v-if` 懒加载),复用同一组件。
- 验证:`eslint --no-ignore` 通过、`npm run build:prod` 通过(仅既有 warning)。
## 2026-10-15:新增 Web 积分管理「流水冲正」Tab(W4) ## 2026-10-15:新增 Web 积分管理「流水冲正」Tab(W4)
- 新增 `src/views/points/management/components/TransactionReversePanel.vue`:积分流水分页列表(筛选:业务类型 / 状态 / 流水号),行操作“冲正”(二次确认,仅 `status=1` 有效可冲正)。 - 新增 `src/views/points/management/components/TransactionReversePanel.vue`:积分流水分页列表(筛选:业务类型 / 状态 / 流水号),行操作“冲正”(二次确认,仅 `status=1` 有效可冲正)。

2
docs/development/PAGES_TODO.md

@ -27,7 +27,7 @@
| 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 | 积分与商城 | 申诉台账(独立页,抄隐患台账 + 审批界面) | ⚠️ 待测试 | ✅ | ✅ | **已开发(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”暂保持禁用 | | 6 | 积分与商城 | 申诉台账(独立页 + 积分管理内“申诉处理”Tab) | ⚠️ 待测试 | ✅ | ✅ | **已开发(2026-10-15)**:`AppealLedgerPanel.vue`(复用组件)+ 独立页 `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` 文档仅声明 form-urlencoded + 单整数,缺 `userId`/`points`/原因字段,需后端补请求结构 | | 7 | 积分与商城 | 积分管理 · Tab3 手动调整 | ❌ | ⛔ | ⛔ | **阻塞**:`balance/adjust` 文档仅声明 form-urlencoded + 单整数,缺 `userId`/`points`/原因字段,需后端补请求结构 |
| 8 | 积分与商城 | 积分管理 · Tab4 流水冲正 | ⚠️ 待测试 | ✅ | ✅ | **已开发(2026-10-15)**:`src/views/points/management/components/TransactionReversePanel.vue` + `src/api/points/transaction.js`。列表 `transaction/getListByPage`、冲正 `transaction/reverse/{logId}`(二次确认,仅 status=1 可冲正) | | 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)无接口**,该卡片需等后端 |

379
src/views/points/appeal/components/AppealLedgerPanel.vue

@ -0,0 +1,379 @@
<template>
<div class="appeal-ledger-panel">
<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: 'AppealLedgerPanel',
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) {
return String(row.status === undefined || row.status === null ? '' : row.status)
},
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>
.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;
}
</style>

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

@ -1,340 +1,15 @@
<template> <template>
<div class="app-container appeal-ledger"> <div class="app-container appeal-ledger">
<data-list-panel modern-table> <appeal-ledger-panel />
<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> </div>
</template> </template>
<script> <script>
import { listAppealTodo, getAppealDetail, getAppealProcessDetail, completeAppealFlow } from '@/api/points/appeal' import AppealLedgerPanel from './components/AppealLedgerPanel'
export default { export default {
name: 'PointsAppeal', name: 'PointsAppeal',
data() { components: { AppealLedgerPanel }
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> </script>
@ -342,47 +17,4 @@ export default {
.appeal-ledger { .appeal-ledger {
min-height: calc(100vh - 52px); 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> </style>

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

@ -164,7 +164,9 @@
</template> </template>
</data-list-panel> </data-list-panel>
</el-tab-pane> </el-tab-pane>
><el-tab-pane label="申诉处理" name="appeal" disabled /><el-tab-pane ><el-tab-pane label="申诉处理" name="appeal">
<appeal-ledger-panel v-if="activeTab === 'appeal'" />
</el-tab-pane><el-tab-pane
label="手动调整" label="手动调整"
name="adjust" name="adjust"
disabled /><el-tab-pane label="流水冲正" name="reverse"> disabled /><el-tab-pane label="流水冲正" name="reverse">
@ -300,6 +302,7 @@ import {
getMyFactoryDepartment, getMyFactoryDepartment,
} from "@/api/system/dept"; } from "@/api/system/dept";
import TransactionReversePanel from "./components/TransactionReversePanel"; import TransactionReversePanel from "./components/TransactionReversePanel";
import AppealLedgerPanel from "@/views/points/appeal/components/AppealLedgerPanel";
const LEVELS = [ const LEVELS = [
{ value: "general", label: "一般隐患" }, { value: "general", label: "一般隐患" },
{ value: "major", label: "较大隐患" }, { value: "major", label: "较大隐患" },
@ -307,7 +310,7 @@ const LEVELS = [
]; ];
export default { export default {
name: "PointsManagement", name: "PointsManagement",
components: { TransactionReversePanel }, components: { TransactionReversePanel, AppealLedgerPanel },
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