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

<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>