From 3e069207503d4685a20ab5c695b1a17993f115ea Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期三, 09 九月 2026 15:56:45 +0800
Subject: [PATCH] fix: 09-09下午第三轮——审核结果默认按企业合并与问题范围语义收敛/导出企业合并问题列按用户4条例文话术、数据查看工具栏整理、报表整本预览分组表格化 + 测试报告与工作日结
---
traffic-audit-web/src/views/AuditResult.vue | 720 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 680 insertions(+), 40 deletions(-)
diff --git a/traffic-audit-web/src/views/AuditResult.vue b/traffic-audit-web/src/views/AuditResult.vue
index 4fc27a0..9a78adf 100644
--- a/traffic-audit-web/src/views/AuditResult.vue
+++ b/traffic-audit-web/src/views/AuditResult.vue
@@ -1,9 +1,9 @@
<template>
- <el-container>
- <el-header>浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 瀹℃牳缁撴灉</el-header>
+ <el-container direction="vertical">
+ <app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 瀹℃牳缁撴灉"></app-header>
<el-container>
<el-aside width="200px">
- <el-menu router default-active="/audit">
+ <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect">
<el-menu-item index="/dashboard">宸ヤ綔鍙�</el-menu-item>
<el-menu-item index="/import">鏁版嵁瀵煎叆</el-menu-item>
<el-menu-item index="/data">鏁版嵁鏌ョ湅</el-menu-item>
@@ -12,71 +12,356 @@
<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-menu>
</el-aside>
- <el-main>
- <h3>瀹℃牳缁撴灉</h3>
- <el-form :inline="true">
+ <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-date-picker v-model="period" type="month" placeholder="閫夋嫨鏈堜唤" value-format="yyyy-MM"></el-date-picker>
+ <el-date-picker v-model="period" type="month" placeholder="閫夋嫨鏈堜唤" value-format="yyyy-MM" @change="onPeriodChange"></el-date-picker>
</el-form-item>
+ <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>
<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>
- <el-form :inline="true" style="margin-top:12px">
+ <!-- 闂姒傝缁熻鍗� -->
+ <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>
+ <div v-if="!focusMode" class="audit-filter-bar">
+ <el-form :inline="true">
+ <el-form-item label="绛涢��"></el-form-item>
<el-form-item label="浼佷笟鍚嶇О">
- <el-input v-model="enterpriseFilter" placeholder="杈撳叆浼佷笟鍚嶇О鍏抽敭瀛�" clearable style="width:220px"></el-input>
+ <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:200px">
+ <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="issueScope" size="small" @change="updateBatchSelection">
+ <el-radio-button label="verify">浠呴渶鏍稿疄</el-radio-button>
+ <el-radio-button label="all">鍚彁閱掑瀷</el-radio-button>
+ </el-radio-group>
+ <span class="hint">鎻愰啋鍨嬩粎鎻愮ず鍙樺寲銆佷笉闇�澶勭悊锛屽鍑洪粯璁や笉鍚�</span>
+ </el-form-item>
+ <el-form-item label="鐘舵��">
+ <el-select v-model="statusScope" style="width:110px" @change="updateBatchSelection">
+ <el-option label="鍏ㄩ儴" value="all"></el-option>
+ <el-option label="寰呭鐞�" value="pending"></el-option>
+ <el-option label="宸插鐞�" value="handled"></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 }} 鏉★紝褰撳墠鏄剧ず {{ filteredResults.length }} 鏉�</span>
+ <span style="color:#303133;font-size:13px;font-weight:600">褰撳墠绛涢�� {{ filteredResults.length }} 鏉� / {{ mergedResults.length }} 瀹朵紒涓�</span>
+ <span style="color:#909399;font-size:12px;margin-left:8px">锛堝叡 {{ results.length }} 鏉¢棶棰橈級</span>
</el-form-item>
</el-form>
- <div class="table-area">
- <el-table :data="filteredResults" border v-loading="loading" height="100%">
- <el-table-column prop="enterpriseName" label="浼佷笟鍚嶇О" min-width="200" show-overflow-tooltip></el-table-column>
- <el-table-column prop="ruleName" label="瀹℃牳瑙勫垯" min-width="140"></el-table-column>
- <el-table-column prop="actualValue" label="瀹為檯鍊�" min-width="180" show-overflow-tooltip></el-table-column>
- <el-table-column prop="thresholdValue" label="闃堝��/瀵规瘮" min-width="120"></el-table-column>
- <el-table-column prop="deviation" label="鍋忓樊璇存槑" min-width="140" show-overflow-tooltip></el-table-column>
- <el-table-column label="绛夌骇" width="80">
+ </div>
+ <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">
+ <!-- 鏄庣粏瑙嗗浘锛氫竴鏉¢棶棰樹竴琛岋紙涓庡鍑烘ā鏉� Sheet鈥滃鏍哥粨鏋溾�濆悓娆撅級 -->
+ <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>
+ <el-table :data="filteredResults" border stripe height="100%"
+ :header-cell-style="{ background: '#F5F7FA', color: '#303133', fontWeight: 600 }">
+ <el-table-column width="40">
+ <template slot-scope="{ row }">
+ <el-checkbox v-model="row._selected" @change="updateBatchSelection"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column prop="reportPeriod" label="鎶ヨ〃鏈�" width="90"></el-table-column>
+ <el-table-column label="浼佷笟/甯傚窞" min-width="200" show-overflow-tooltip>
+ <template slot-scope="{ row }">{{ row.enterpriseName || row.city || '鏈煡浼佷笟' }}</template>
+ </el-table-column>
+ <el-table-column prop="ruleCode" label="瑙勫垯缂栫爜" width="100"></el-table-column>
+ <el-table-column prop="ruleName" label="瀹℃牳瑙勫垯" min-width="150" show-overflow-tooltip></el-table-column>
+ <el-table-column label="绛夌骇" width="76">
+ <template slot-scope="{ row }">
+ <el-tag :type="row.alertLevel === 'RED' ? 'danger' : 'warning'" size="mini">{{ row.alertLevel }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="actualValue" label="瀹為檯鍊�" min-width="230" show-overflow-tooltip></el-table-column>
+ <el-table-column prop="thresholdValue" label="闃堝��/鍩哄噯" min-width="140" show-overflow-tooltip></el-table-column>
+ <el-table-column prop="deviation" label="鍋忓樊璇存槑" min-width="170" show-overflow-tooltip></el-table-column>
+ <el-table-column label="鐘舵��" width="92">
+ <template slot-scope="{ row }">
+ <el-tag :type="statusType(row.status)" size="mini">
+ <i :class="statusIcon(row.status)"></i> {{ statusText(row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="185" align="right">
+ <template slot-scope="{ row }">
+ <el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="openMonthReport(row)">鏁版嵁</el-link>
+ <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>
+ </template>
+ </el-table-column>
+ <div slot="empty">
+ <el-empty description="褰撳墠绛涢�夋潯浠朵笅娌℃湁闂鏁版嵁"></el-empty>
+ </div>
+ </el-table>
+ </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">
- <el-tag :type="scope.row.alertLevel === 'RED' ? 'danger' : 'warning'">{{ scope.row.alertLevel }}</el-tag>
+ <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 prop="status" label="鐘舵��" width="100">
+ <el-table-column label="瀹℃牳闂鏄庣粏" min-width="520">
<template slot-scope="scope">
- <el-tag :type="statusType(scope.row.status)">{{ scope.row.status }}</el-tag>
+ <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="openMonthReport(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="160">
+ <el-table-column label="鎿嶄綔" width="130">
<template slot-scope="scope">
- <el-button v-if="scope.row.status === 'PENDING'" size="mini" type="success" @click="review(scope.row, 'CONFIRMED')">纭</el-button>
- <el-button v-if="scope.row.status === 'PENDING'" size="mini" type="info" @click="review(scope.row, 'IGNORED')">蹇界暐</el-button>
+ <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-dialog title="鏁版嵁鏌ョ湅锛堟湀鎶ユ牱寮忥級" :visible.sync="monthVisible" width="880px" top="4vh" custom-class="mr-dialog">
+ <div v-if="monthLoading" style="padding:40px 0;text-align:center;color:#909399">鍔犺浇涓��</div>
+ <div v-else-if="!monthRecords.length" style="padding:30px 0;text-align:center;color:#909399">
+ 鏈煡璇㈠埌璇ヤ紒涓氥�佽鎶ヨ〃鏈熺殑濉姤鏁版嵁锛堝彲鑳借繕鏈鍏ワ級
+ </div>
+ <div v-else class="mr-body">
+ <div v-for="(rec, ri) in monthRecords" :key="ri" class="mr-card">
+ <div class="mr-title">{{ monthTitle(rec) }}</div>
+ <table class="mr-meta">
+ <tbody>
+ <tr v-for="(m, mi) in monthMeta(rec)" :key="mi">
+ <td class="mr-meta-k">{{ m.label }}</td>
+ <td class="mr-meta-v">{{ fmtVal(m.value) }}</td>
+ </tr>
+ </tbody>
+ </table>
+ <div v-for="sec in monthSections(rec)" :key="sec.title" class="mr-sec">
+ <div class="mr-sec-title">{{ sec.title }}</div>
+ <table class="mr-sec-tab" border="1">
+ <thead>
+ <tr>
+ <th style="width:34%">鎸囨爣鍚嶇О</th>
+ <th v-for="(h, hi) in sec.head" :key="hi" style="width:16.5%">{{ h }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(rw, rwi) in sec.rows" :key="rwi">
+ <td>{{ rw.label }}</td>
+ <td v-for="(c, ci) in rw.cells" :key="ci">{{ fmtVal(c.v) }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div v-if="rec.verifyExplanation || rec.reportNotes" class="mr-note">
+ <div v-if="rec.verifyExplanation" class="mr-note-line"><b>鏍稿疄瑙i噴锛�</b>{{ rec.verifyExplanation }}</div>
+ <div v-if="rec.reportNotes" class="mr-note-line"><b>澶囨敞锛�</b>{{ rec.reportNotes }}</div>
+ </div>
+ </div>
+ </div>
+ <span slot="footer">
+ <el-button size="small" @click="monthVisible = false">鍏抽棴</el-button>
+ </span>
+ </el-dialog>
</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']
+
+const MONTH_CFG = {
+ h2032: {
+ meta: [['鎶ヨ〃鏈�', 'reportPeriod'], ['浼佷笟鍚嶇О', 'enterpriseName'], ['缁熶竴绀句細淇$敤浠g爜', 'unifiedCreditCode'], ['濉姤鍗曚綅', 'reportUnit']],
+ sections: [
+ { title: '涓�銆佽繍鍔涙儏鍐�', head: ['杞﹁締鏁帮紙杈嗭級', '鏍囪鍚ㄤ綅鏁帮紙鍚級'], rows: [
+ ['鎬昏', ['vehicleTotal', 'tonsTotal']],
+ ['鍏朵腑锛氱壍寮曡溅', ['vehicleTractor', null]],
+ ['鎸傝溅', ['vehicleTrailer', 'tonsTrailer']],
+ ['鍏朵腑锛氶泦瑁呯杞�', ['vehicleContainer', 'tonsContainer']],
+ ['鏁磋溅', ['vehicleWhole', 'tonsWhole']]
+ ]},
+ { title: '浜屻�佽繍杈撶敓浜ф儏鍐�', head: ['璐ц繍閲忥紙鍚級', '璐х墿鍛ㄨ浆閲忥紙鍚ㄥ叕閲岋級'], rows: [
+ ['杩愯緭閲�', ['freightTotal', 'turnoverTotal']],
+ ['鍏朵腑锛氶泦瑁呯', ['freightContainer', null]],
+ ['鍏朵腑锛氱叅鐐強鍒跺搧', ['freightCoal', null]],
+ ['鐭虫补銆佸ぉ鐒舵皵鍙婂埗鍝�', ['freightOilGas', null]],
+ ['鍏朵腑锛氬師娌�', ['freightCrudeOil', null]],
+ ['閲戝睘鐭跨煶', ['freightMetalOre', null]],
+ ['鍏朵腑锛氶搧鐭跨煶', ['freightIronOre', null]],
+ ['鐭垮缓鎬у缓绛戞潗鏂�', ['freightBuilding', null]],
+ ['绮', ['freightGrain', null]]
+ ]},
+ { title: '骞冲潎鎸囨爣', head: [], rows: [
+ ['璐ц溅骞冲潎鍚ㄤ綅', ['avgTonnage', null]],
+ ['璐ц溅骞冲潎杩愯窛', ['avgDistance', null]]
+ ]},
+ { title: '濉姤浜轰俊鎭�', head: [], rows: [
+ ['鍗曚綅璐熻矗浜�', ['unitLeader', null]], ['缁熻璐熻矗浜�', ['statsLeader', null]],
+ ['濉〃浜�', ['contactPerson', null]], ['鑱旂郴鐢佃瘽', ['contactPhone', null]], ['鎶ュ嚭鏃ユ湡', ['reportDate', null]]
+ ]}
+ ]
+ },
+ h2031: {
+ meta: [['鎶ヨ〃鏈�', 'reportPeriod'], ['浼佷笟鍚嶇О', 'enterpriseName']],
+ sections: [
+ { title: '涓�銆佽繍鍔涙儏鍐�', head: ['杞﹁締鏁帮紙杈嗭級', '鏍稿畾杞藉浣嶏紙瀹綅锛�'], rows: [
+ ['鎬昏', ['vehicleTotal', 'seatTotal']],
+ ['瀹㈣繍鐝溅锛堝惈瀹氱嚎鏃呮父瀹㈣繍瀹㈣溅锛�', ['vehicleSchedule', 'seatSchedule']],
+ ['瀹㈣繍鍖呰溅锛堝惈闈炲畾绾挎梾娓稿杩愬杞︼級', ['vehicleCharter', 'seatCharter']]
+ ]},
+ { title: '浜屻�佽繍杈撶敓浜ф儏鍐�', head: ['瀹㈣繍閲忥紙浜烘锛�', '鏃呭鍛ㄨ浆閲忥紙浜哄叕閲岋級'], rows: [
+ ['鎬昏', ['passengerTotal', 'turnoverTotal']],
+ ['涓�绫诲杩愮彮绾匡紙鍚畾绾挎梾娓革級', ['passengerClass1', 'turnoverClass1']],
+ ['浜岀被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�', ['passengerClass2', 'turnoverClass2']],
+ ['涓夌被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�', ['passengerClass3', 'turnoverClass3']],
+ ['鍥涚被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�', ['passengerClass4', 'turnoverClass4']],
+ ['瀹㈣繍鍖呰溅锛堝惈闈炲畾绾挎梾娓革級', ['passengerCharter', 'turnoverCharter']]
+ ]},
+ { title: '瀹㈣繍骞冲潎杩愯窛', head: [], rows: [
+ ['瀹㈣繍骞冲潎杩愯窛', ['avgDistanceTotal', null]],
+ ['涓�绫诲杩愮彮绾垮钩鍧囪繍璺�', ['avgDistanceClass1', null]],
+ ['浜岀被瀹㈣繍鐝嚎骞冲潎杩愯窛', ['avgDistanceClass2', null]],
+ ['涓夌被瀹㈣繍鐝嚎骞冲潎杩愯窛', ['avgDistanceClass3', null]],
+ ['鍥涚被瀹㈣繍鐝嚎骞冲潎杩愯窛', ['avgDistanceClass4', null]],
+ ['瀹㈣繍鍖呰溅骞冲潎杩愯窛', ['avgDistanceCharter', null]]
+ ]}
+ ]
+ },
+ cityBus: {
+ meta: [['鎶ヨ〃鏈�', 'reportPeriod'], ['甯傚窞', 'city'], ['浼佷笟鍚嶇О', 'enterpriseName']],
+ sections: [
+ { title: '涓�銆佸叕鍏辨苯鐢佃溅杩愯惀', head: [], rows: [
+ ['杩愯惀杞︽暟锛堣締锛�', ['opVehicles', null]], ['杩愯惀绔欑偣鎬讳釜鏁帮紙涓級', ['stopTotal', null]],
+ ['鍏朵腑锛氬煄甯傝繍钀ョ珯鐐�', ['stopCity', null]], ['瀹㈣繍閲忥紙涓囦汉娆★級', ['passengerVolume', null]],
+ ['鍏朵腑锛氬煄甯傚杩愰噺', ['passengerCity', null]], ['鏃呭鍛ㄨ浆閲忥紙涓囦汉鍏噷锛�', ['turnover', null]],
+ ['鍏朵腑锛氬煄甯傛梾瀹㈠懆杞噺', ['turnoverCity', null]]
+ ]},
+ { title: '浜屻�佹淳鐢熸寚鏍囦笌澧為��', head: [], rows: [
+ ['骞冲潎杩愯窛锛堝叕閲岋級', ['avgDistance', null]], ['鍩庡競鍐呭钩鍧囪繍璺�', ['avgDistanceCity', null]],
+ ['鍩庨檯鍩庝埂瀹㈣繍閲�', ['passengerChengxiang', null]], ['鍩庨檯鍩庝埂鍛ㄨ浆閲�', ['turnoverChengxiang', null]],
+ ['鍩庨檯鍩庝埂骞冲潎杩愯窛', ['avgDistanceChengxiang', null]], ['瀹㈣繍閲忕幆姣�', ['ratePassengerMom', null]],
+ ['鍩庡競鍐呭杩愰噺鐜瘮', ['ratePassengerCityMom', null]], ['鍩庨檯鍩庝埂瀹㈣繍閲忕幆姣�', ['rateChengxiangMom', null]],
+ ['瀹㈣繍閲忓悓姣�', ['ratePassengerYoy', null]], ['鍩庡競鍐呭杩愰噺鍗犳瘮', ['passengerCityRatio', null]],
+ ['鍩庨檯鍩庝埂瀹㈣繍閲忓崰姣�', ['chengxiangRatio', null]], ['濉〃浜�', ['contactPerson', null]],
+ ['鎶ュ嚭鏃ユ湡', ['reportDate', null]]
+ ]}
+ ]
+ },
+ cityTaxi: {
+ meta: [['鎶ヨ〃鏈�', 'reportPeriod'], ['甯傚窞', 'city'], ['浼佷笟鍚嶇О', 'enterpriseName']],
+ sections: [
+ { title: '涓�銆佸贰娓稿嚭绉熻繍钀ユ湇鍔�', head: [], rows: [
+ ['杞藉杞︽鎬绘暟锛堜竾杞︽锛�', ['tripTotal', null]], ['鍏朵腑锛氬煄甯傝浇瀹㈣溅娆�', ['tripCity', null]],
+ ['瀹㈣繍閲忥紙涓囦汉娆★級', ['passengerVolume', null]], ['鍏朵腑锛氬煄甯傚杩愰噺', ['passengerCity', null]],
+ ['鏃呭鍛ㄨ浆閲忥紙涓囦汉鍏噷锛�', ['turnover', null]], ['鍏朵腑锛氬煄甯傛梾瀹㈠懆杞噺', ['turnoverCity', null]],
+ ['杩愯惀杞﹁締鏁帮紙杈嗭級', ['opVehicles', null]]
+ ]},
+ { title: '浜屻�佹淳鐢熸寚鏍囦笌澧為��', head: [], rows: [
+ ['骞冲潎杩愯窛锛堝叕閲岋級', ['avgDistance', null]], ['鍩庡競鍐呭钩鍧囪繍璺�', ['avgDistanceCity', null]],
+ ['鍗曡溅娆¤浇瀹汉鏁�', ['passengersPerTrip', null]], ['杞藉杞︽鐜瘮', ['rateTripMom', null]],
+ ['杞藉杞︽鍚屾瘮', ['rateTripYoy', null]], ['瀹㈣繍閲忕幆姣�', ['ratePassengerMom', null]],
+ ['瀹㈣繍閲忓悓姣�', ['ratePassengerYoy', null]], ['鍩庡競鍐呭杩愰噺鍗犳瘮', ['passengerCityRatio', null]],
+ ['鍩庨檯鍩庝埂骞冲潎杩愯窛', ['avgDistanceChengxiang', null]]
+ ]}
+ ]
+ }
+}
export default {
+ components: { AppHeader },
name: 'AuditResult',
data() {
return {
- period: '', loading: false, results: [], enterpriseFilter: '', ruleFilter: ''
+ period: '', reportType: 'H2032', loading: false, marking: false, results: [], ruleStats: [], statsCollapsed: false,
+ enterpriseFilter: '', ruleFilter: '', viewMode: 'merged', issueScope: 'verify', statusScope: 'all', batchSelection: [], focusMode: false,
+ explainDrafts: {}, savingExplainId: null, expandedExplain: null,
+ monthVisible: false, monthLoading: false, monthType: '', monthPeriod: '', monthKeyword: '', monthRecords: [], monthColumns: []
}
},
computed: {
@@ -85,58 +370,413 @@
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.filteredResults.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
+ },
+ isReminderRow(r) {
+ return !!(r && r.ruleCode === 'RULE_004')
+ },
filteredResults() {
const kw = (this.enterpriseFilter || '').trim()
const rule = this.ruleFilter
- if (!kw && !rule) return this.results
+ const scope = this.issueScope || 'verify'
+ const st = this.statusScope || 'all'
return this.results.filter(r => {
if (kw && !(r.enterpriseName || '').includes(kw)) return false
if (rule && r.ruleName !== rule) return false
+ if (scope === 'verify' && this.isReminderRow(r)) return false
+ if (st === 'pending' && r.status !== 'PENDING') return false
+ if (st === 'handled' && (!r.status || r.status === 'PENDING')) 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 },
+ openMonthReport(row) {
+ const type = DATA_TYPE_MAP[this.reportType]
+ if (!type) return
+ this.monthType = type
+ this.monthPeriod = row.reportPeriod
+ this.monthKeyword = row.enterpriseName || row.city || ''
+ this.monthRecords = []
+ this.monthColumns = []
+ this.monthVisible = true
+ this.monthLoading = true
+ api.get('/data/list', {
+ params: {
+ type, period: row.reportPeriod,
+ keyword: row.enterpriseName || row.city || '',
+ page: 1, size: 50
+ }, timeout: 60000
+ }).then(res => {
+ const d = res.data || {}
+ this.monthRecords = d.records || []
+ this.monthColumns = d.columns || []
+ if (!this.monthRecords.length) this.$message.warning('鏈煡璇㈠埌璇ヤ紒涓氳鎶ヨ〃鏈熺殑濉姤鏁版嵁锛堝彲鑳藉皻鏈鍏ワ級')
+ }).catch(e => {
+ if (e && e._toast) return
+ this.$message.error('鏈堟姤鏁版嵁鏌ヨ澶辫触')
+ }).finally(() => { this.monthLoading = false })
+ },
+ monthTitle(rec) {
+ const cfg = MONTH_CFG[this.monthType] || null
+ if (!cfg) return (this.monthKeyword || '') + '锛�' + this.monthPeriod + '锛�'
+ const name = rec.enterpriseName || rec.city || this.monthKeyword || ''
+ return name + ' ' + (rec.reportPeriod || this.monthPeriod || '') + ' 鏈堟姤鏁版嵁'
+ },
+ monthMeta(rec) {
+ const cfg = MONTH_CFG[this.monthType] || null
+ if (!cfg) return []
+ return cfg.meta.map(m => ({ label: m[0], value: rec[m[1]] })).filter(m => m.value !== undefined)
+ },
+ monthSections(rec) {
+ const cfg = MONTH_CFG[this.monthType]
+ if (!cfg) return []
+ return cfg.sections.map(sec => ({
+ title: sec.title, head: sec.head || [],
+ rows: sec.rows.map(r => ({ label: r[0], cells: (r[1] || []).map(prop => ({ prop, v: prop ? rec[prop] : null })) }))
+ .filter(rw => rw.cells.some(c => c.v !== null && c.v !== undefined && c.v !== ''))
+ })).filter(sec => sec.rows.length)
+ },
+ fmtVal(v) {
+ if (v === null || v === undefined || v === '') return '鈥�'
+ if (typeof v === 'number') return String(v)
+ return String(v)
+ },
+ 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)
- api.post('/audit/execute', fd)
+ fd.append('reportType', this.reportType)
+ api.post('/audit/execute', fd, { timeout: 120000 })
.then(res => {
const count = (res.data || []).length
- this.$message.success('瀹℃牳瀹屾垚锛屽叡 ' + count + ' 鏉¢棶棰�')
- return api.get('/audit/results', { params: { period: this.period } })
+ 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(res => { this.results = res.data || [] })
- .catch(() => { this.$message.error('瀹℃牳鎵ц澶辫触') })
+ .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 } })
- .then(res => { this.results = res.data || [] })
- .catch(() => { this.$message.error('鏌ヨ澶辫触') })
+ 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('浼佷笟瑙i噴宸蹭繚瀛�')
+ 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 }
+ const q = 'period=' + this.period + '&reportType=' + this.reportType + '&onlyVerify=' + (this.issueScope !== 'all') + '&status=' + (this.statusScope || 'all')
+ window.open('/api/audit/export?' + q)
+ },
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)
- api.post('/audit/review', fd)
- .then(() => { row.status = status; this.$message.success('宸插鐞�') })
- .catch(() => { this.$message.error('鎿嶄綔澶辫触') })
+ 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; }
-</style>
\ No newline at end of file
+.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; }
+
+.mr-dialog .el-dialog__body { padding: 12px 18px; }
+.mr-body { max-height: 66vh; overflow-y: auto; }
+.mr-card { border: 1px solid #DCDFE6; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
+.mr-title { background: #409EFF; color: #fff; font-weight: 600; padding: 8px 12px; font-size: 15px; }
+.mr-meta { width: 100%; border-collapse: collapse; background: #F7F9FC; }
+.mr-meta td { border: 1px solid #EBEEF5; padding: 6px 10px; font-size: 13px; }
+.mr-meta-k { width: 120px; color: #909399; background: #F0F4FA; }
+.mr-meta-v { color: #303133; font-weight: 600; }
+.mr-sec { padding: 10px 12px 4px; }
+.mr-sec-title { font-weight: 600; color: #303133; margin: 4px 0 8px; }
+.mr-sec-tab { width: 100%; border-collapse: collapse; font-size: 13px; }
+.mr-sec-tab th, .mr-sec-tab td { border: 1px solid #DCDFE6; padding: 5px 8px; text-align: left; color: #303133; }
+.mr-sec-tab th { background: #F5F7FA; font-weight: 600; }
+.mr-note { margin: 6px 12px 12px; background: #FDF6EC; border: 1px solid #FAECD8; border-radius: 4px; padding: 8px 10px; font-size: 12px; color: #8a6d3b; white-space: pre-wrap; word-break: break-all; }
+.mr-note-line { margin: 2px 0; }
+
+.audit-filter-bar { background:#FAFAFA; border:1px solid #EBEEF5; border-radius:8px; padding:2px 12px 0; margin-top:8px; flex:none; }
+.audit-filter-bar .el-form-item { margin-bottom:6px; }
+.audit-filter-bar .el-form-item__label { color:#606266; font-weight:600; }
+.hint { color:#909399; font-size:12px; margin-left:6px; white-space:nowrap; }
+</style>
--
Gitblit v1.9.1