Browse Source
- 抽出可复用组件 src/views/points/appeal/components/AppealLedgerPanel.vue(列表/详情/审批) - 独立页 src/views/points/appeal/index.vue 改为包裹该组件 - 积分管理启用第 2 个 Tab「申诉处理」,复用同一组件 - 同步更新 PAGES_TODO / CURRENT_STATUS / DEVELOPMENT_LOGmaster
6 changed files with 396 additions and 375 deletions
@ -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> |
||||||
Loading…
Reference in new issue