<template>
|
<el-container direction="vertical">
|
<app-header title="交通统计报表审核系统 - 审核结果"></app-header>
|
<el-container>
|
<el-aside width="200px">
|
<el-menu router default-active="/audit">
|
<el-menu-item index="/dashboard">工作台</el-menu-item>
|
<el-menu-item index="/import">数据导入</el-menu-item>
|
<el-menu-item index="/data">数据查看</el-menu-item>
|
<el-menu-item index="/explain">企业解释</el-menu-item>
|
<el-menu-item index="/audit">审核结果</el-menu-item>
|
<el-menu-item index="/report">报表生成</el-menu-item>
|
<el-menu-item index="/rules">规则管理</el-menu-item>
|
<el-menu-item index="/users">用户管理</el-menu-item>
|
</el-menu>
|
</el-aside>
|
<el-main :class="{ 'main-focus': focusMode }" style="display:flex;flex-direction:column;min-height:0">
|
<h3 v-show="!focusMode" style="flex:none;margin:0 0 10px">审核结果</h3>
|
<el-form :inline="true" style="flex:none">
|
<el-form-item label="报表类型">
|
<el-radio-group v-model="reportType" @change="onTypeChange">
|
<el-radio label="H2032">货运月报</el-radio>
|
<el-radio label="H2031">旅客月报</el-radio>
|
<el-radio label="CITY_BUS">城市公交月报</el-radio>
|
<el-radio label="CITY_TAXI">巡游出租月报</el-radio>
|
<el-radio label="INVEST">投资月报</el-radio>
|
<el-radio label="H204">能耗季报</el-radio>
|
|
|
|
</el-radio-group>
|
</el-form-item>
|
<el-form-item label="报表期">
|
<el-date-picker v-model="period" type="month" placeholder="选择月份" value-format="yyyy-MM" @change="onPeriodChange"></el-date-picker>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="executeAudit" :loading="loading">执行审核</el-button>
|
<el-button type="success" plain @click="markAudited" :loading="marking">标记已审核</el-button>
|
<el-button @click="queryResults">查询结果</el-button>
|
<el-button type="success" icon="el-icon-download" @click="exportExcel">导出 Excel</el-button>
|
<el-button v-if="!focusMode" size="small" icon="el-icon-full-screen" @click="focusMode = true">聚焦查看</el-button>
|
<el-button v-else type="primary" size="small" icon="el-icon-crop" @click="focusMode = false">退出聚焦</el-button>
|
</el-form-item>
|
</el-form>
|
<!-- 问题概览统计卡 -->
|
<div v-if="results.length && !focusMode" class="sev-cards" style="flex:none">
|
<div class="sev-card sev-red"><b>{{ severityStats.red }}</b> 红色问题</div>
|
<div class="sev-card sev-yellow"><b>{{ severityStats.yellow }}</b> 黄色问题</div>
|
<div class="sev-card sev-pending"><b>{{ severityStats.pending }}</b> 待处理</div>
|
<div class="sev-card sev-done"><b>{{ severityStats.done }}</b> 已处理</div>
|
</div>
|
<el-form v-if="!focusMode" :inline="true" style="margin-top:8px;flex:none">
|
<el-form-item label="企业名称">
|
<el-input v-model="enterpriseFilter" placeholder="输入企业名称关键字" clearable style="width:200px" @keyup.enter.native="queryResults"></el-input>
|
</el-form-item>
|
<el-form-item label="审核规则">
|
<el-select v-model="ruleFilter" placeholder="全部规则" clearable style="width:180px">
|
<el-option v-for="rn in ruleOptions" :key="rn" :label="rn" :value="rn"></el-option>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="视图">
|
<el-radio-group v-model="viewMode">
|
<el-radio label="detail">按条明细</el-radio>
|
<el-radio label="merged">按企业合并</el-radio>
|
</el-radio-group>
|
</el-form-item>
|
<el-form-item v-if="viewMode === 'detail'">
|
<el-button size="small" type="success" :disabled="!batchPending.length" @click="reviewBatch('CONFIRMED')">批量确认({{ batchPending.length }})</el-button>
|
<el-button size="small" type="info" :disabled="!batchPending.length" @click="reviewBatch('IGNORED')">批量忽略({{ batchPending.length }})</el-button>
|
</el-form-item>
|
<el-form-item>
|
<el-button size="small" @click="clearFilters">清空过滤</el-button>
|
<el-button size="small" plain @click="statsCollapsed = !statsCollapsed">{{ statsCollapsed ? '展开' : '收起' }}规则统计</el-button>
|
</el-form-item>
|
<el-form-item>
|
<span style="color:#909399;font-size:12px">共 {{ results.length }} 条问题,{{ mergedResults.length }} 家企业</span>
|
</el-form-item>
|
</el-form>
|
<div v-if="ruleStats.length && !statsCollapsed && !focusMode" class="rule-stats" style="flex:none">
|
<div class="stats-cards">
|
<div v-for="rs in sortedRuleStats" :key="rs.ruleId" class="stat-card"
|
:class="{ 'has-hit': rs.hit > 0, 'is-skipped': rs.skipped, active: ruleFilter === rs.ruleName }"
|
:title="rs.ruleName + '(' + rs.ruleCode + ')检查' + rs.checked + ' 命中' + rs.hit + '(点击过滤)'"
|
@click="toggleRuleFilter(rs)">
|
<template v-if="rs.skipped">
|
<span class="stat-name">{{ rs.ruleName }}</span>
|
<span class="skip-label">基准缺失,已跳过</span>
|
</template>
|
<template v-else>
|
<span class="stat-name">{{ rs.ruleName }}</span>
|
<span class="stat-num"><b :class="{ 'hit-num': rs.hit > 0 }">{{ rs.hit }}</b> / {{ rs.checked }}</span>
|
</template>
|
</div>
|
</div>
|
</div>
|
<div v-if="focusMode" style="flex:none;padding:0 2px 6px;color:#909399;font-size:12px">
|
共 {{ results.length }} 条问题,{{ mergedResults.length }} 家企业
|
</div>
|
<div class="table-area" style="flex:1;min-height:0">
|
<!-- 明细视图:一条问题一张卡片 -->
|
<div v-if="viewMode === 'detail'" class="detail-cards-wrap" v-loading="loading">
|
<div class="detail-toolbar">
|
<el-checkbox :value="allSelected" :indeterminate="someSelected" @change="toggleSelectAll">全选当前 {{ filteredResults.length }} 条</el-checkbox>
|
<span class="toolbar-tip">勾选后可批量确认 / 忽略</span>
|
</div>
|
<div v-for="row in filteredResults" :key="row.id" class="detail-card"
|
:class="{ 'card-red': row.alertLevel === 'RED', 'card-yellow': row.alertLevel === 'YELLOW', 'card-done': row.status !== 'PENDING' }">
|
<div class="card-head">
|
<el-checkbox v-model="row._selected" @change="updateBatchSelection"></el-checkbox>
|
<div class="card-ent">
|
<div class="ent-name">{{ row.enterpriseName || row.city || '未知企业' }}</div>
|
<div class="ent-sub">{{ row.reportPeriod }}</div>
|
</div>
|
<el-tag :type="row.alertLevel === 'RED' ? 'danger' : 'warning'" size="small" effect="dark">{{ row.alertLevel }}</el-tag>
|
<el-tag :type="statusType(row.status)" size="small" effect="plain" class="status-tag">
|
<i :class="statusIcon(row.status)"></i> {{ statusText(row.status) }}
|
</el-tag>
|
<span class="spacer"></span>
|
<el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="goData(row)">数据</el-link>
|
<el-tag v-if="explainType(reportType)" size="mini" :type="row.verifyExplanation ? 'success' : 'warning'"
|
style="cursor:pointer" :title="expandedExplain === row.reportId ? '收起解释' : '展开填写/查看解释'"
|
@click="toggleExplain(row.reportId)">
|
{{ row.verifyExplanation ? '解释:已填写' : '解释:未填写' }}
|
</el-tag>
|
<el-button v-if="row.status === 'PENDING'" size="mini" type="success" icon="el-icon-check" round @click="review(row, 'CONFIRMED')">确认</el-button>
|
<el-button v-if="row.status === 'PENDING'" size="mini" type="info" icon="el-icon-close" round plain @click="review(row, 'IGNORED')">忽略</el-button>
|
<span v-else class="done-text">已处理</span>
|
</div>
|
<div class="card-body">
|
<div class="rule-title">
|
<b>{{ row.ruleName }}</b>
|
<span class="rule-code">{{ row.ruleCode }}</span>
|
</div>
|
<div class="card-details">
|
<div class="detail-line"><span class="dt-label">实际</span><span class="dt-val">{{ row.actualValue }}</span></div>
|
<div class="detail-line"><span class="dt-label">基准</span><span class="dt-val">{{ row.thresholdValue }}</span></div>
|
<div class="detail-line"><span class="dt-label">差异</span><span class="dt-val dt-warn">{{ row.deviation }}</span></div>
|
</div>
|
</div>
|
<div v-if="explainType(reportType) && expandedExplain === row.reportId" class="explain-side" style="margin:0 12px 10px">
|
<div class="explain-head">
|
<span class="explain-title">企业解释</span>
|
<el-tag size="mini" :type="row.verifyExplanation ? 'success' : 'warning'">{{ row.verifyExplanation ? '已填写' : '未填写' }}</el-tag>
|
<span style="flex:1"></span>
|
<el-button size="mini" text @click="expandedExplain = null">收起</el-button>
|
</div>
|
<span v-if="row.verifyExplanation" class="explain-cur">{{ row.verifyExplanation }}</span>
|
<el-input v-model="explainDrafts[row.reportId]" type="textarea" :rows="2" resize="none"
|
placeholder="填写/修改该企业的解释说明…"></el-input>
|
<div class="explain-actions">
|
<el-button size="mini" type="primary" plain :loading="savingExplainId === row.reportId" @click="saveExplain(row)">保存解释</el-button>
|
<el-button v-if="(explainDrafts[row.reportId] || '') !== (row.verifyExplanation || '')" size="mini" text @click="resetDraft(row.reportId)">重置</el-button>
|
</div>
|
</div>
|
</div>
|
<div v-if="!filteredResults.length" class="empty-tip">
|
<el-empty description="当前筛选条件下没有问题数据"></el-empty>
|
</div>
|
</div>
|
<!-- 合并视图 -->
|
<el-table v-else :data="mergedResults" border stripe v-loading="loading" height="100%"
|
:header-cell-style="{ background: '#F5F7FA', color: '#303133', fontWeight: 600 }">
|
<el-table-column label="企业" min-width="190">
|
<template slot-scope="scope">
|
<div style="font-weight:600">{{ scope.row.enterpriseName }}</div>
|
<div style="color:#909399;font-size:12px;margin-top:2px">共 {{ scope.row.items.length }} 条问题</div>
|
</template>
|
</el-table-column>
|
<el-table-column label="审核问题明细" min-width="520">
|
<template slot-scope="scope">
|
<div v-for="(item, i) in scope.row.items" :key="item.id"
|
style="padding:6px 0;border-bottom:1px dashed #EBEEF5"
|
:class="{ 'no-border': i === scope.row.items.length - 1 }">
|
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:6px">
|
<b>{{ item.ruleName }}</b>
|
<el-tag :type="item.alertLevel === 'RED' ? 'danger' : 'warning'" size="mini">{{ item.alertLevel }}</el-tag>
|
<el-tag :type="statusType(item.status)" size="mini">{{ statusText(item.status) }}</el-tag>
|
<el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="goData(item)">数据</el-link>
|
<span style="flex:1"></span>
|
<el-button v-if="item.status === 'PENDING'" size="mini" type="success" @click="review(item, 'CONFIRMED')">确认</el-button>
|
<el-button v-if="item.status === 'PENDING'" size="mini" type="info" @click="review(item, 'IGNORED')">忽略</el-button>
|
</div>
|
<div style="color:#606266;font-size:12px;line-height:1.6;margin-top:2px">
|
<span>实际:{{ item.actualValue }}</span>
|
<span style="margin:0 8px">基准:{{ item.thresholdValue }}</span>
|
<span>差异:{{ item.deviation }}</span>
|
</div>
|
</div>
|
</template>
|
</el-table-column>
|
<el-table-column label="企业解释" width="340">
|
<template slot-scope="scope">
|
<div v-if="explainType(reportType) && scope.row.items[0]" class="explain-cell">
|
<div v-if="expandedExplain !== scope.row.items[0].reportId" class="explain-compact">
|
<el-tag size="mini" :type="scope.row.items[0].verifyExplanation ? 'success' : 'warning'">{{ scope.row.items[0].verifyExplanation ? '已填写' : '未填写' }}</el-tag>
|
<div v-if="scope.row.items[0].verifyExplanation" class="explain-preview">{{ scope.row.items[0].verifyExplanation }}</div>
|
<el-button size="mini" text type="primary" @click="expandedExplain = scope.row.items[0].reportId">填写/修改</el-button>
|
</div>
|
<div v-else class="explain-side">
|
<div class="explain-head">
|
<span class="explain-title">企业解释</span>
|
<span style="flex:1"></span>
|
<el-button size="mini" text @click="expandedExplain = null">收起</el-button>
|
</div>
|
<span v-if="scope.row.items[0].verifyExplanation" class="explain-cur">{{ scope.row.items[0].verifyExplanation }}</span>
|
<el-input v-model="explainDrafts[scope.row.items[0].reportId]" type="textarea" :rows="2" resize="none"
|
placeholder="填写/修改该企业的解释说明…"></el-input>
|
<div class="explain-actions">
|
<el-button size="mini" type="primary" plain :loading="savingExplainId === scope.row.items[0].reportId" @click="saveExplain(scope.row.items[0])">保存解释</el-button>
|
<el-button v-if="(explainDrafts[scope.row.items[0].reportId] || '') !== (scope.row.items[0].verifyExplanation || '')" size="mini" text @click="resetDraft(scope.row.items[0].reportId)">重置</el-button>
|
</div>
|
</div>
|
</div>
|
<span v-else style="color:#C0C4CC;font-size:12px">—</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="130">
|
<template slot-scope="scope">
|
<el-button v-if="hasPending(scope.row)" size="mini" type="success" plain @click="reviewAll(scope.row, 'CONFIRMED')">全部确认</el-button>
|
<el-button v-if="hasPending(scope.row)" size="mini" type="info" plain @click="reviewAll(scope.row, 'IGNORED')">全部忽略</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</el-main>
|
</el-container>
|
</el-container>
|
</template>
|
<script>
|
import AppHeader from '@/components/AppHeader.vue'
|
import api from '@/api'
|
const DATA_TYPE_MAP = { H2032: 'h2032', H2031: 'h2031', CITY_BUS: 'cityBus', CITY_TAXI: 'cityTaxi' }
|
const EXPLAIN_TYPES = ['H2032', 'H2031', 'CITY_BUS', 'CITY_TAXI']
|
export default {
|
components: { AppHeader },
|
name: 'AuditResult',
|
data() {
|
return {
|
period: '', reportType: 'H2032', loading: false, marking: false, results: [], ruleStats: [], statsCollapsed: false,
|
enterpriseFilter: '', ruleFilter: '', viewMode: 'merged', batchSelection: [], focusMode: false,
|
explainDrafts: {}, savingExplainId: null, expandedExplain: null
|
}
|
},
|
computed: {
|
ruleOptions() {
|
const names = []
|
this.results.forEach(r => { if (r.ruleName && names.indexOf(r.ruleName) < 0) names.push(r.ruleName) })
|
return names
|
},
|
severityStats() {
|
const s = { red: 0, yellow: 0, pending: 0, done: 0 }
|
this.results.forEach(r => {
|
if (r.alertLevel === 'RED') s.red++
|
else if (r.alertLevel === 'YELLOW') s.yellow++
|
if (r.status === 'PENDING') s.pending++
|
else s.done++
|
})
|
return s
|
},
|
batchPending() { return this.batchSelection.filter(x => x.status === 'PENDING') },
|
allSelected() {
|
const list = this.filteredResults
|
return list.length > 0 && list.every(r => r._selected)
|
},
|
someSelected() {
|
const list = this.filteredResults
|
return list.some(r => r._selected) && !this.allSelected
|
},
|
filteredResults() {
|
const kw = (this.enterpriseFilter || '').trim()
|
const rule = this.ruleFilter
|
if (!kw && !rule) return this.results
|
return this.results.filter(r => {
|
if (kw && !(r.enterpriseName || '').includes(kw)) return false
|
if (rule && r.ruleName !== rule) return false
|
return true
|
})
|
},
|
sortedRuleStats() {
|
const arr = (this.ruleStats || []).slice()
|
arr.sort((a, b) => {
|
const ah = a.hit > 0 ? 1 : 0
|
const bh = b.hit > 0 ? 1 : 0
|
if (ah !== bh) return bh - ah
|
return (a.ruleId || 0) - (b.ruleId || 0)
|
})
|
return arr
|
},
|
mergedResults() {
|
const map = new Map()
|
this.filteredResults.forEach(r => {
|
const key = r.enterpriseName || r.actualValue || '未知企业'
|
if (!map.has(key)) map.set(key, [])
|
map.get(key).push(r)
|
})
|
return Array.from(map.entries())
|
.map(([name, items]) => ({ enterpriseName: name, items }))
|
.sort((a, b) => b.items.length - a.items.length)
|
}
|
},
|
methods: {
|
dataType(t) { return DATA_TYPE_MAP[t] || '' },
|
explainType(t) { return EXPLAIN_TYPES.indexOf(t) >= 0 },
|
goData(row) {
|
const type = DATA_TYPE_MAP[this.reportType]
|
if (!type) return
|
this.$router.push({ path: '/data', query: { type, period: row.reportPeriod, keyword: row.enterpriseName || row.city || '' } })
|
},
|
goExplain(row) {
|
this.$router.push({ path: '/explain', query: { reportType: this.reportType, keyword: row.enterpriseName || row.city || '', period: row.reportPeriod } })
|
},
|
onTypeChange() {
|
this.ruleFilter = ''; this.batchSelection = []; this.results = []; this.ruleStats = []
|
this.period = ''
|
this.loadLatestPeriod(true)
|
},
|
onPeriodChange() {
|
if (this.period) this.queryResults(); else { this.results = []; this.ruleStats = [] }
|
},
|
loadLatestPeriod(autoQuery) {
|
api.get('/audit/latestPeriod', { params: { reportType: this.reportType } })
|
.then(res => {
|
const p = res.data
|
if (p && !this.period) {
|
this.period = p
|
if (autoQuery) this.queryResults()
|
} else if (autoQuery && !p) {
|
this.results = []; this.ruleStats = []
|
}
|
})
|
.catch(() => {})
|
},
|
executeAudit() {
|
if (!this.period) { this.$message.warning('请选择报表期'); return }
|
this.loading = true
|
const fd = new FormData()
|
fd.append('period', this.period)
|
fd.append('reportType', this.reportType)
|
api.post('/audit/execute', fd, { timeout: 120000 })
|
.then(res => {
|
const count = (res.data || []).length
|
return api.get('/audit/ruleStats', { params: { period: this.period, reportType: this.reportType } })
|
.then(stats => {
|
const list = stats.data || []
|
const checked = list.reduce((s, r) => Math.max(s, Number(r.checked) || 0), 0)
|
if (list.length === 0) {
|
this.$message.warning('该报表类型没有启用审核规则,未执行审核')
|
} else if (checked === 0) {
|
this.$message.warning('该报表期没有数据,未执行审核(请先导入数据)')
|
} else {
|
const skipped = list.filter(x => x.skipped)
|
if (skipped.length > 0) {
|
const names = skipped.map(x => x.ruleCode).join('、')
|
this.$message.warning('审核完成,共 ' + count + ' 条问题;' + names + ' 因基准数据(运政/轨迹/上月等)缺失已跳过')
|
} else {
|
this.$message.success('审核完成,共 ' + count + ' 条问题')
|
}
|
}
|
})
|
})
|
.then(() => api.get('/audit/results', { params: { period: this.period, reportType: this.reportType } }))
|
.then(res => { this.applyResults(res.data) })
|
.then(() => this.loadRuleStats())
|
.catch(e => { if (e && e._toast) return; this.$message.error('审核执行失败') })
|
.finally(() => { this.loading = false })
|
},
|
markAudited() {
|
if (!this.period) { this.$message.warning('请选择报表期'); return }
|
this.marking = true
|
const fd = new FormData()
|
fd.append('period', this.period)
|
fd.append('reportType', this.reportType)
|
api.post('/audit/markAudited', fd, { timeout: 60000 })
|
.then(res => {
|
if (res.data) {
|
this.$message.success('已标记该期「' + this.reportType + '」为已审核,报表导出不再提示未审核')
|
} else {
|
this.$message.warning('该类型没有启用规则或该期没有数据,未写入标记')
|
}
|
return api.get('/audit/results', { params: { period: this.period, reportType: this.reportType } })
|
})
|
.then(res => { this.applyResults(res.data) })
|
.catch(e => { if (e && e._toast) return; this.$message.error('标记失败') })
|
.finally(() => { this.marking = false })
|
},
|
queryResults() {
|
if (!this.period) { this.$message.warning('请选择报表期'); return }
|
this.loading = true
|
api.get('/audit/results', { params: { period: this.period, reportType: this.reportType } })
|
.then(res => { this.applyResults(res.data) })
|
.then(() => this.loadRuleStats())
|
.catch(e => { if (e && e._toast) return; this.$message.error('查询失败') })
|
.finally(() => { this.loading = false })
|
},
|
toggleExplain(reportId) {
|
this.expandedExplain = this.expandedExplain === reportId ? null : reportId
|
},
|
applyResults(list) {
|
this.results = (list || []).map(r => Object.assign({}, r, { _selected: false }))
|
this.results.forEach(r => {
|
if (r.reportId && this.explainDrafts[r.reportId] === undefined) {
|
this.$set(this.explainDrafts, r.reportId, r.verifyExplanation || '')
|
}
|
})
|
},
|
saveExplain(row) {
|
const reportId = row.reportId
|
if (!reportId) { this.$message.warning('无法定位该企业的数据行'); return }
|
const explanation = (this.explainDrafts[reportId] || '').trim()
|
this.savingExplainId = reportId
|
const fd = new FormData()
|
fd.append('reportType', this.reportType)
|
fd.append('reportId', reportId)
|
fd.append('explanation', explanation)
|
api.post('/explain/save', fd, { timeout: 60000 })
|
.then(() => {
|
this.$message.success('企业解释已保存')
|
this.$set(this.explainDrafts, reportId, explanation)
|
this.results.forEach(r => { if (r.reportId === reportId) r.verifyExplanation = explanation })
|
})
|
.catch(e => { if (e && e._toast) return; this.$message.error((e.response && e.response.data && e.response.data.message) || '保存失败') })
|
.finally(() => { this.savingExplainId = null })
|
},
|
resetDraft(reportId) {
|
const row = this.results.find(r => r.reportId === reportId)
|
if (row) this.$set(this.explainDrafts, reportId, row.verifyExplanation || '')
|
},
|
loadRuleStats() {
|
if (!this.period) return
|
api.get('/audit/ruleStats', { params: { period: this.period, reportType: this.reportType } })
|
.then(res => { this.ruleStats = res.data || [] })
|
.catch(() => { this.ruleStats = [] })
|
},
|
exportExcel() {
|
if (!this.period) { this.$message.warning('请选择报表期'); return }
|
window.open('/api/audit/export?period=' + this.period + '&reportType=' + this.reportType)
|
},
|
review(row, status) {
|
if (status === 'IGNORED') {
|
this.$prompt('请输入忽略原因(方便事后追溯)', '忽略问题', {
|
inputPlaceholder: '原因(可选)', inputType: 'textarea', confirmButtonText: '确定忽略', cancelButtonText: '取消'
|
}).then(({ value }) => this.doReview(row, status, (value || '').trim()))
|
.catch(() => {})
|
} else {
|
this.doReview(row, status)
|
}
|
},
|
doReview(row, status, comment) {
|
const fd = new FormData()
|
fd.append('id', row.id)
|
fd.append('status', status)
|
if (comment) fd.append('comment', comment)
|
api.post('/audit/review', fd, { timeout: 60000 })
|
.then(() => { row.status = status; this.$message.success(status === 'CONFIRMED' ? '已确认' : '已忽略') })
|
.catch(e => { if (e && e._toast) return; this.$message.error('操作失败') })
|
},
|
updateBatchSelection() { this.batchSelection = this.filteredResults.filter(r => r._selected) },
|
toggleSelectAll(value) {
|
this.filteredResults.forEach(r => { r._selected = value })
|
this.updateBatchSelection()
|
},
|
reviewBatch(status) {
|
const list = this.batchPending.slice()
|
if (list.length === 0) return
|
if (status === 'IGNORED') {
|
this.$prompt('请输入忽略原因(应用于选中的 ' + list.length + ' 条)', '批量忽略', {
|
inputPlaceholder: '原因(可选)', inputType: 'textarea', confirmButtonText: '确定忽略', cancelButtonText: '取消'
|
}).then(({ value }) => this.doReviewBatch(list, status, (value || '').trim()))
|
.catch(() => {})
|
} else {
|
this.doReviewBatch(list, status)
|
}
|
},
|
doReviewBatch(list, status, comment) {
|
let done = 0
|
list.forEach(item => {
|
const fd = new FormData()
|
fd.append('id', item.id)
|
fd.append('status', status)
|
if (comment) fd.append('comment', comment)
|
api.post('/audit/review', fd, { timeout: 60000 })
|
.then(() => { item.status = status; done++; if (done === list.length) this.$message.success('已' + (status === 'CONFIRMED' ? '确认' : '忽略') + ' ' + done + ' 条') })
|
.catch(e => { if (e && e._toast) return; this.$message.error('部分操作失败') })
|
})
|
},
|
clearFilters() { this.enterpriseFilter = ''; this.ruleFilter = '' },
|
toggleRuleFilter(rs) {
|
this.ruleFilter = this.ruleFilter === rs.ruleName ? '' : rs.ruleName
|
},
|
hasPending(group) { return group.items.some(x => x.status === 'PENDING') },
|
reviewAll(group, status) {
|
const pending = group.items.filter(x => x.status === 'PENDING')
|
if (pending.length === 0) return
|
if (status === 'IGNORED') {
|
this.$prompt('请输入忽略原因(应用于该企业 ' + pending.length + ' 条问题)', '忽略', {
|
inputPlaceholder: '原因(可选)', inputType: 'textarea', confirmButtonText: '确定忽略', cancelButtonText: '取消'
|
}).then(({ value }) => this.doReviewBatch(pending, status, (value || '').trim()))
|
.catch(() => {})
|
} else {
|
this.doReviewBatch(pending, status)
|
}
|
},
|
statusType(status) {
|
if (status === 'CONFIRMED') return 'success'
|
if (status === 'IGNORED') return 'info'
|
return 'warning'
|
},
|
statusText(status) {
|
if (status === 'CONFIRMED') return '已确认'
|
if (status === 'IGNORED') return '已忽略'
|
return '待处理'
|
},
|
statusIcon(status) {
|
if (status === 'CONFIRMED') return 'el-icon-circle-check'
|
if (status === 'IGNORED') return 'el-icon-circle-close'
|
return 'el-icon-time'
|
}
|
},
|
mounted() {
|
this.loadLatestPeriod(true)
|
}
|
}
|
</script>
|
<style scoped>
|
.el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
|
.no-border { border-bottom: none !important; }
|
.sev-cards { display: flex; gap: 10px; }
|
.sev-card { flex: 1; max-width: 180px; border-radius: 6px; padding: 6px 12px; font-size: 13px; color: #606266; }
|
.sev-card b { font-size: 20px; margin-right: 4px; }
|
.sev-red { background: #FEF0F0; border: 1px solid #FDE2E2; }
|
.sev-red b { color: #F56C6C; }
|
.sev-yellow { background: #FDF6EC; border: 1px solid #FAECD8; }
|
.sev-yellow b { color: #E6A23C; }
|
.sev-pending { background: #F4F4F5; border: 1px solid #E9E9EB; }
|
.sev-pending b { color: #606266; }
|
.sev-done { background: #F0F9EB; border: 1px solid #E1F3D8; }
|
.sev-done b { color: #67C23A; }
|
.ent-name { font-weight: 600; color: #303133; }
|
.ent-sub { color: #909399; font-size: 12px; margin-top: 2px; }
|
.rule-name { font-weight: 500; color: #303133; }
|
.rule-code { color: #909399; font-size: 12px; margin-top: 2px; }
|
.detail-line { display: flex; align-items: baseline; line-height: 1.7; }
|
.dt-label { flex: none; width: 34px; color: #909399; font-size: 12px; }
|
.dt-val { flex: 1; color: #606266; word-break: break-all; }
|
.dt-warn { color: #E6A23C; }
|
.status-tag i { margin-right: 3px; }
|
.done-text { color: #C0C4CC; font-size: 12px; }
|
.explain-side { background: #FAFAFA; border: 1px solid #EBEEF5; border-radius: 6px; padding: 8px 10px; }
|
.explain-cell { min-height: 40px; }
|
.explain-compact .explain-preview { color: #909399; font-size: 12px; line-height: 1.5; margin: 4px 0; max-height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; word-break: break-all; }
|
.explain-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
.explain-title { font-weight: 600; color: #303133; font-size: 13px; }
|
.explain-cur { display: block; color: #909399; font-size: 12px; white-space: pre-wrap; word-break: break-all; line-height: 1.5; margin-bottom: 6px; }
|
.explain-actions { margin-top: 6px; }
|
.main-focus { padding: 8px 12px !important; }
|
.detail-cards-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 4px 16px; }
|
.detail-toolbar { display: flex; align-items: center; gap: 10px; padding: 0 2px 8px; }
|
.toolbar-tip { color: #909399; font-size: 12px; }
|
.detail-card { background: #FFF; border: 1px solid #EBEEF5; border-left: 3px solid #E6A23C; border-radius: 6px; margin-bottom: 10px; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
|
.detail-card.card-red { border-left-color: #F56C6C; }
|
.detail-card.card-yellow { border-left-color: #E6A23C; }
|
.detail-card.card-done { opacity: .72; background: #FAFAFA; }
|
.card-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px 4px; }
|
.card-ent { flex: 1; min-width: 0; }
|
.spacer { flex: 1; }
|
.card-body { padding: 2px 12px 10px; }
|
.rule-title { display: flex; align-items: baseline; gap: 8px; margin: 2px 0 4px; }
|
.rule-title b { font-size: 14px; color: #303133; }
|
.card-details { display: flex; flex-wrap: wrap; gap: 4px 24px; background: #F8F9FB; border: 1px solid #F0F2F5; border-radius: 4px; padding: 6px 10px; }
|
.card-details .detail-line { display: flex; align-items: baseline; line-height: 1.8; min-width: 200px; }
|
.empty-tip { padding: 30px 0; }
|
.rule-stats { margin: 0 0 8px; }
|
.stats-cards { display: flex; overflow-x: auto; gap: 6px; padding: 4px 0; }
|
.stat-card { flex: none; display: inline-flex; align-items: center; gap: 6px; border: 1px solid #EBEEF5; border-radius: 14px; padding: 3px 10px; background: #FAFAFA; font-size: 12px; white-space: nowrap; cursor: pointer; }
|
.stat-card:hover { border-color: #C0C4CC; }
|
.stat-card.has-hit { background: #FDF6EC; border-color: #E6A23C; }
|
.stat-card.is-skipped { background: #F2F6FC; border-color: #DCDFE6; opacity: 0.85; }
|
.stat-card.active { border-color: #409EFF; box-shadow: 0 0 0 1px #409EFF; }
|
.skip-label { color: #909399; font-size: 12px; }
|
.stat-name { color: #606266; max-width: 150px; overflow: hidden; text-overflow: ellipsis; }
|
.stat-num { color: #909399; }
|
.stat-num b { font-weight: 700; }
|
.hit-num { color: #E6A23C; }
|
</style>
|