You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
20 KiB
406 lines
20 KiB
<template> |
|
<div class="app-container hazard-ledger"> |
|
<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="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 v-if="isSuperAdmin" label="单位" prop="unitId"> |
|
<el-select v-model="queryParams.unitId" clearable filterable placeholder="全部单位"> |
|
<el-option |
|
v-for="item in unitOptions" |
|
:key="factoryId(item)" |
|
:label="factoryName(item)" |
|
:value="factoryId(item)" |
|
/> |
|
</el-select> |
|
</el-form-item> |
|
<!-- 隐患名称筛选暂时隐藏,保留 queryParams 以便后续恢复。 --> |
|
<el-form-item> |
|
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">查询</el-button> |
|
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button> |
|
</el-form-item> |
|
</el-form> |
|
</template> |
|
|
|
<template #actions> |
|
<div class="summary-inline"> |
|
<span>当前筛选 <b>{{ total }}</b> 条</span> |
|
<span>本页待提交 <b>{{ summary.draft }}</b></span> |
|
<span>本页审核中 <b class="primary-text">{{ summary.reviewing }}</b></span> |
|
<span>本页整改中 <b class="warning-text">{{ summary.rectifying }}</b></span> |
|
<span>本页已通过 <b class="success-text">{{ summary.approved }}</b></span> |
|
<span>本页已驳回 <b class="danger-text">{{ summary.rejected }}</b></span> |
|
</div> |
|
</template> |
|
|
|
<el-table |
|
v-loading="loading" |
|
:data="ledgerList" |
|
border |
|
empty-text="暂无隐患台账数据" |
|
class="data-list-table ledger-table" |
|
> |
|
<el-table-column prop="reportNo" label="隐患编号" min-width="150" show-overflow-tooltip /> |
|
<el-table-column prop="hazardName" label="隐患名称" min-width="190" show-overflow-tooltip /> |
|
<el-table-column label="分级" width="100" align="center"> |
|
<template slot-scope="scope"> |
|
<el-tag :type="levelTagType(scope.row)" size="small">{{ levelLabel(scope.row) }}</el-tag> |
|
</template> |
|
</el-table-column> |
|
<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="积分发放" min-width="130" align="center"> |
|
<template slot-scope="scope"> |
|
<el-tag :type="pointsTagType(scope.row)" size="small">{{ pointsLabel(scope.row) }}</el-tag> |
|
</template> |
|
</el-table-column> |
|
<!-- 平台同步列暂时隐藏,后续需要时可直接恢复。 --> |
|
<!-- <el-table-column label="平台同步" min-width="120" align="center"> |
|
<template slot-scope="scope"> |
|
<el-tag :type="pushTagType(scope.row)" size="small">{{ pushLabel(scope.row) }}</el-tag> |
|
</template> |
|
</el-table-column> --> |
|
<el-table-column prop="reporterName" label="上报人" width="100" show-overflow-tooltip /> |
|
<el-table-column label="时间" width="145"> |
|
<template slot-scope="scope">{{ formatTime(scope.row.createTime || scope.row.discoverTime) }}</template> |
|
</el-table-column> |
|
<el-table-column |
|
label="操作" |
|
width="210" |
|
fixed="right" |
|
align="center" |
|
class-name="data-list-operation-column" |
|
> |
|
<template slot-scope="scope"> |
|
<el-button size="mini" @click="openDetail(scope.row)">详情</el-button> |
|
<el-button v-if="isReviewing(scope.row)" type="primary" size="mini" @click="goAudit(scope.row)">去审核</el-button> |
|
<el-button v-if="isSyncFailed(scope.row)" type="warning" size="mini" @click="handleRetrySync(scope.row)">重试同步</el-button> |
|
</template> |
|
</el-table-column> |
|
</el-table> |
|
|
|
<template #footer> |
|
<pagination |
|
v-show="total > 0" |
|
:total="total" |
|
:page.sync="queryParams.pageNum" |
|
:limit.sync="queryParams.pageSize" |
|
@pagination="getList" |
|
/> |
|
<div class="table-note"> |
|
隐患状态:待提交 → 审核中 → 整改中 → 已通过 / 已驳回。整改闭环由生产运营平台负责,不在本系统范围。 |
|
</div> |
|
</template> |
|
</data-list-panel> |
|
<el-dialog title="隐患详情" :visible.sync="detailVisible" width="980px" top="5vh" append-to-body> |
|
<div v-loading="detailLoading" class="detail-dialog"> |
|
<template v-if="detail"> |
|
<div class="detail-head"> |
|
<div> |
|
<span class="detail-no">{{ detail.reportNo || '暂无编号' }}</span> |
|
<span class="detail-name">{{ detail.hazardName || '未命名隐患' }}</span> |
|
</div> |
|
<div class="detail-tags"> |
|
<el-tag :type="levelTagType(detail)" size="small">{{ levelLabel(detail) }}</el-tag> |
|
<el-tag :type="statusTagType(detail)" size="small">{{ statusLabel(detail) }}</el-tag> |
|
</div> |
|
</div> |
|
<el-descriptions title="基础信息" :column="3" border size="small"> |
|
<el-descriptions-item v-for="item in basicFields" :key="item.label" :label="item.label">{{ displayField(detail, item) }}</el-descriptions-item> |
|
</el-descriptions> |
|
<el-descriptions title="整改与流程" :column="3" border size="small" class="detail-section"> |
|
<el-descriptions-item v-for="item in flowFields" :key="item.label" :label="item.label">{{ displayField(detail, item) }}</el-descriptions-item> |
|
</el-descriptions> |
|
<div class="text-section"> |
|
<div class="section-title">描述与处理</div> |
|
<div v-for="item in textFields" :key="item.label" class="text-row"> |
|
<span>{{ item.label }}</span> |
|
<p>{{ detail[item.key] || '—' }}</p> |
|
</div> |
|
</div> |
|
<div class="attachment-section"> |
|
<div class="section-title">附件({{ attachments.length }})</div> |
|
<div v-if="attachments.length" class="attachment-grid"> |
|
<el-image |
|
v-for="(item, index) in imageAttachments" |
|
:key="attachmentKey(item, index)" |
|
:src="attachmentUrl(item)" |
|
:preview-src-list="previewUrls" |
|
:initial-index="previewIndex(item)" |
|
fit="cover" |
|
class="attachment-image" |
|
> |
|
<div slot="error" class="attachment-placeholder"> |
|
<i class="el-icon-picture-outline" /> |
|
<span>{{ item.fileName || '图片加载失败' }}</span> |
|
</div> |
|
</el-image> |
|
<a |
|
v-for="(item, index) in otherAttachments" |
|
:key="attachmentKey(item, index)" |
|
:href="attachmentUrl(item) || undefined" |
|
:target="attachmentUrl(item) ? '_blank' : undefined" |
|
rel="noopener noreferrer" |
|
class="attachment-file" |
|
> |
|
<i class="el-icon-document" /> |
|
<span>{{ item.fileName || '附件' }}</span> |
|
</a> |
|
</div> |
|
<span v-else class="muted">暂无附件</span> |
|
</div> |
|
</template> |
|
<el-empty v-else-if="!detailLoading" description="暂无详情数据" /> |
|
</div> |
|
<span slot="footer"> |
|
<el-button @click="detailVisible = false">关闭</el-button> |
|
<el-button v-if="detail && isReviewing(detail)" type="primary" @click="goAudit(detail)">去审核</el-button> |
|
</span> |
|
</el-dialog> |
|
</div> |
|
</template> |
|
|
|
<script> |
|
/* global MyConfig */ |
|
import { getMyFactoryDepartment, listFactoryDepartments } from '@/api/system/dept' |
|
import { listHazardLedger, getHazardLedgerDetail, listHazardLedgerAttachments, retryHazardSync } from '@/api/hazard/ledger' |
|
|
|
export default { |
|
name: 'HazardLedger', |
|
data() { return { loading: false, detailLoading: false, detailVisible: false, ledgerList: [], total: 0, unitOptions: [], currentFactory: null, attachments: [], detail: null, queryParams: { pageNum: 1, pageSize: 10, status: undefined, unitId: undefined, hazardName: undefined }, statusOptions: [{ label: '待提交', value: 'draft' }, { label: '审核中', value: 'reviewing' }, { label: '整改中', value: 'rectifying' }, { label: '已通过', value: 'approved' }, { label: '已驳回', value: 'rejected' }], basicFields: [{ label: '单位', key: 'unitName' }, { label: '所属部门', key: 'departmentName', fallback: 'factoryDeptName' }, { label: '上报人', key: 'reporterName' }, { label: '隐患分类', key: 'hazardCategoryName', fallback: 'hazardCategory' }, { label: '隐患类型', key: 'hazardTypeName', fallback: 'hazardType' }, { label: '隐患来源', key: 'sourceTypeName', fallback: 'sourceType' }, { label: '专业', key: 'majorName', fallback: 'major' }, { label: '风险分析单元', key: 'riskUnit' }, { label: '风险分析对象', key: 'riskTarget' }, { label: '发现时间', key: 'discoverTime', time: true }, { label: '创建时间', key: 'createTime', time: true }, { label: '更新人', key: 'updateBy' }], flowFields: [{ label: '治理类型', key: 'governanceTypeName', fallback: 'governanceType' }, { label: '整改负责人', key: 'chargePersonName' }, { label: '限定完成时间', key: 'limitedCompleteTime', time: true }, { label: '实际完成时间', key: 'actualCompleteTime', time: true }, { label: '奖励积分', key: 'pointsAwarded', suffix: ' 分' }, { label: '积分状态', key: 'pointsStatusName', fallback: 'pointsStatus' }, { label: '平台同步', key: 'pushStatusName', fallback: 'pushStatus' }, { label: '同步时间', key: 'pushTime', time: true }, { label: '第三方记录', key: 'thirdPartyId' }, { label: '流程状态', key: 'flowStatus' }, { label: '审核人', key: 'auditUserNameName', fallback: 'auditUserName' }, { label: '审核时间', key: 'auditTime', time: true }], textFields: [{ label: '隐患描述', key: 'description' }, { label: '原因分析', key: 'causeAnalysis' }, { label: '整改方案', key: 'rectificationPlan' }, { label: '防范措施', key: 'preventiveMeasures' }, { label: '审核意见', key: 'auditRemark' }, { label: '备注', key: 'remark' }] } }, |
|
computed: { |
|
isSuperAdmin() { return (this.$store.getters.roles || []).indexOf('admin') !== -1 }, |
|
summary() { return this.ledgerList.reduce((result, row) => { const status = this.statusKey(row); if (status === 'draft') result.draft++; else if (status === 'reviewing') result.reviewing++; else if (status === 'rectifying') result.rectifying++; else if (status === 'approved') result.approved++; else if (status === 'rejected') result.rejected++; return result }, { draft: 0, reviewing: 0, rectifying: 0, approved: 0, rejected: 0 }) }, |
|
imageAttachments() { return this.attachments.filter(item => this.isImageAttachment(item)) }, |
|
otherAttachments() { return this.attachments.filter(item => !this.isImageAttachment(item)) }, |
|
previewUrls() { return this.imageAttachments.map(item => this.attachmentUrl(item)).filter(Boolean) } |
|
}, |
|
created() { this.getUnits().finally(() => this.getList()) }, |
|
methods: { |
|
getList() { this.loading = true; listHazardLedger(this.queryParams).then(response => { const page = response.data || response; this.ledgerList = page.list || page.rows || []; this.total = Number(page.total || page.totalCount || this.ledgerList.length) }).catch(() => { this.ledgerList = []; this.total = 0; this.$modal.msgError('隐患台账加载失败,请检查后端服务和登录状态') }).finally(() => { this.loading = false }) }, |
|
getUnits() { const request = this.isSuperAdmin ? listFactoryDepartments() : getMyFactoryDepartment(); return request.then(response => { const data = response.data || response; if (this.isSuperAdmin) { this.unitOptions = Array.isArray(data) ? data : (data && (data.list || data.rows)) || []; return } this.currentFactory = data || null; if (this.currentFactory) { this.queryParams.unitId = this.factoryId(this.currentFactory) } }).catch(() => { this.unitOptions = []; this.currentFactory = null }) }, factoryId(item) { const value = item && (item.deptId !== undefined ? item.deptId : item.id); return value === undefined || value === null ? '' : String(value) }, factoryName(item) { return (item && (item.deptName || item.label || item.name)) || '' }, |
|
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.attachments = []; this.detailLoading = true; Promise.all([getHazardLedgerDetail(row.id), listHazardLedgerAttachments(row.id)]).then(results => { this.detail = results[0].data || results[0]; const data = results[1].data || results[1]; this.attachments = Array.isArray(data) ? data : [] }).catch(() => { this.$modal.msgError('隐患详情加载失败') }).finally(() => { this.detailLoading = false }) }, |
|
goAudit(row) { const query = { id: String(row.id) }; if (row.canApprove === true || String(row.canApprove) === 'true') query.canApprove = '1'; if (row.flowInstanceId) query.flowInstanceId = String(row.flowInstanceId); this.detailVisible = false; this.$router.push({ path: '/hazard/audit/detail', query }) }, |
|
handleRetrySync(row) { this.$confirm('确认重试将该隐患同步到生产运营平台吗?', '重试同步', { type: 'warning' }).then(() => retryHazardSync(row.id)).then(() => { this.$modal.msgSuccess('同步重试成功'); this.getList() }).catch(error => { if (error !== 'cancel') this.$modal.msgError(error.message || '同步重试失败') }) }, |
|
statusKey(row) { const value = String(row.status || '').toLowerCase(); const name = row.statusName || ''; if (/待提交|未提交/.test(name) || value === 'draft' || value === '0') return 'draft'; if (/审核中|待审核/.test(name) || value === 'reviewing' || value === '1') return 'reviewing'; if (/整改中/.test(name) || value === 'rectifying' || value === '2') return 'rectifying'; if (/已通过|已闭环/.test(name) || value === 'approved' || value === '3') return 'approved'; if (/已驳回|驳回/.test(name) || value === 'rejected' || value === '4') return 'rejected'; return value }, isReviewing(row) { return this.statusKey(row) === 'reviewing' }, isSyncFailed(row) { return /失败|异常/.test(row.pushStatusName || row.pushStatus || '') }, levelLabel(row) { return row.hazardLevelName || ({ general: '一般', major: '较大', critical: '重大' }[row.hazardLevel] || row.hazardLevel || '—') }, statusLabel(row) { const key = this.statusKey(row); return ({ draft: '待提交', reviewing: '审核中', rectifying: '整改中', approved: '已通过', rejected: '已驳回' }[key] || row.statusName || row.status || '—') }, pointsLabel(row) { if (row.pointsStatusName) return row.pointsAwarded === undefined || row.pointsAwarded === null ? row.pointsStatusName : `${row.pointsAwarded > 0 ? '+' : ''}${row.pointsAwarded} ${row.pointsStatusName}`; return row.pointsAwarded === undefined || row.pointsAwarded === null ? '—' : `${row.pointsAwarded > 0 ? '+' : ''}${row.pointsAwarded} 分` }, pushLabel(row) { return row.pushStatusName || ({ SUCCESS: '已同步', FAIL: '同步失败', FAILED: '同步失败', WAIT: '待同步' }[String(row.pushStatus).toUpperCase()] || row.pushStatus || '—') }, statusTagType(row) { const value = this.statusKey(row); return value === 'approved' ? 'success' : value === 'rejected' ? 'danger' : value === 'rectifying' ? 'warning' : value === 'reviewing' ? 'primary' : 'info' }, levelTagType(row) { return String(row.hazardLevel || '').toLowerCase() === 'critical' ? 'danger' : String(row.hazardLevel || '').toLowerCase() === 'major' ? 'warning' : 'info' }, pointsTagType(row) { return row.pointsStatusName && /待|未/.test(row.pointsStatusName) ? 'info' : row.pointsAwarded === undefined || row.pointsAwarded === null ? 'info' : 'success' }, pushTagType(row) { return this.isSyncFailed(row) ? 'danger' : /待/.test(this.pushLabel(row)) ? 'info' : 'success' }, |
|
isImageAttachment(item) { return String(item.attachmentType || '').toUpperCase() === 'IMAGE' || /^image\//i.test(item.fileType || '') || /\.(png|jpe?g|gif|webp|bmp|svg)(?:\?.*)?$/i.test(item.fileName || item.filePath || item.fileUrl || '') }, |
|
attachmentUrl(item) { const path = item.fileUrl || item.filePath || ''; if (!path || /^https?:\/\//i.test(path) || /^data:image\//i.test(path)) return path; return MyConfig.BASE_API.replace(/\/$/, '') + '/' + path.replace(/^\//, '') }, |
|
previewIndex(item) { return this.previewUrls.indexOf(this.attachmentUrl(item)) }, |
|
attachmentKey(item, index) { return item.id || item.fileUrl || item.filePath || `attachment-${index}` }, |
|
displayField(row, item) { const value = row[item.key] || (item.fallback && row[item.fallback]); if (value === undefined || value === null || value === '') return '—'; return (item.time ? this.formatTime(value) : String(value)) + (item.suffix || '') }, formatTime(value) { return value ? String(value).replace('T', ' ').slice(0, 19) : '—' } |
|
} |
|
} |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.hazard-ledger { |
|
min-height: calc(100vh - 52px); |
|
background: #f0f2f5; |
|
} |
|
|
|
.summary-inline { |
|
display: flex; |
|
align-items: center; |
|
gap: 20px; |
|
color: var(--prototype-text-secondary, #6b7280); |
|
font-size: 12px; |
|
white-space: nowrap; |
|
} |
|
|
|
.summary-inline b { |
|
margin-left: 3px; |
|
color: var(--prototype-text, #1a1a2e); |
|
} |
|
|
|
.warning-text { |
|
color: #c97e24 !important; |
|
} |
|
|
|
.primary-text { |
|
color: #409eff !important; |
|
} |
|
|
|
.success-text { |
|
color: #2d8a4e !important; |
|
} |
|
|
|
.danger-text { |
|
color: #d64550 !important; |
|
} |
|
|
|
.table-note { |
|
margin-top: 10px; |
|
padding: 9px 12px; |
|
color: var(--prototype-text-secondary, #6b7280); |
|
font-size: 12px; |
|
line-height: 20px; |
|
} |
|
|
|
.detail-dialog { |
|
min-height: 250px; |
|
max-height: 68vh; |
|
overflow-y: auto; |
|
padding: 2px 2px 8px; |
|
} |
|
|
|
.detail-head { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
margin-bottom: 14px; |
|
} |
|
|
|
.detail-no { |
|
margin-right: 14px; |
|
color: #9898ae; |
|
font-size: 12px; |
|
} |
|
|
|
.detail-name { |
|
color: #1a1a2e; |
|
font-size: 16px; |
|
font-weight: 600; |
|
} |
|
|
|
.detail-tags { |
|
display: flex; |
|
gap: 8px; |
|
} |
|
|
|
.detail-section, |
|
.text-section, |
|
.attachment-section { |
|
margin-top: 16px; |
|
} |
|
|
|
.section-title { |
|
margin-bottom: 8px; |
|
color: #5a5a72; |
|
font-size: 13px; |
|
font-weight: 600; |
|
} |
|
|
|
.text-row { |
|
display: grid; |
|
grid-template-columns: 90px 1fr; |
|
gap: 10px; |
|
padding: 7px 0; |
|
border-bottom: 1px solid #f0f0f2; |
|
font-size: 12px; |
|
} |
|
|
|
.text-row > span { |
|
color: #9898ae; |
|
} |
|
|
|
.text-row p { |
|
margin: 0; |
|
color: #1a1a2e; |
|
white-space: pre-wrap; |
|
word-break: break-word; |
|
} |
|
|
|
.attachment-grid { |
|
display: grid; |
|
grid-template-columns: repeat(3, minmax(0, 1fr)); |
|
gap: 10px; |
|
} |
|
|
|
.attachment-image, |
|
.attachment-file { |
|
width: 100%; |
|
height: 150px; |
|
overflow: hidden; |
|
border: 1px solid #e4e7ed; |
|
border-radius: 6px; |
|
background: #f7f9fc; |
|
} |
|
|
|
.attachment-image { |
|
cursor: zoom-in; |
|
} |
|
|
|
.attachment-file, |
|
.attachment-placeholder { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
gap: 8px; |
|
padding: 12px; |
|
color: #909399; |
|
text-align: center; |
|
text-decoration: none; |
|
} |
|
|
|
.attachment-file { |
|
font-size: 13px; |
|
} |
|
|
|
.attachment-file:hover { |
|
border-color: #409eff; |
|
color: #409eff; |
|
} |
|
|
|
.attachment-file i, |
|
.attachment-placeholder i { |
|
font-size: 28px; |
|
} |
|
|
|
.attachment-file span, |
|
.attachment-placeholder span { |
|
max-width: 100%; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
} |
|
|
|
.attachment-placeholder { |
|
width: 100%; |
|
height: 100%; |
|
font-size: 12px; |
|
} |
|
|
|
.muted { |
|
color: #9898ae; |
|
font-size: 12px; |
|
} |
|
|
|
@media (max-width: 800px) { |
|
.summary-inline { |
|
width: 100%; |
|
flex-wrap: wrap; |
|
gap: 8px 16px; |
|
white-space: normal; |
|
} |
|
|
|
.attachment-grid { |
|
grid-template-columns: repeat(2, minmax(0, 1fr)); |
|
} |
|
} |
|
|
|
@media (max-width: 520px) { |
|
.attachment-grid { |
|
grid-template-columns: 1fr; |
|
} |
|
} |
|
</style>
|
|
|