| | |
| | | <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> |
| | |
| | | <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>核实解释:</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'], ['统一社会信用代码', '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: { |
| | |
| | | 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 |
| | | }, |
| | | 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: { |
| | | isReminderRow(r) { |
| | | return !!(r && r.ruleCode === 'RULE_004') |
| | | }, |
| | | 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('企业解释已保存') |
| | | 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 params = { |
| | | period: this.period, |
| | | reportType: this.reportType, |
| | | onlyVerify: this.issueScope !== 'all', |
| | | status: this.statusScope || 'all' |
| | | } |
| | | api.get('/audit/export', { params, responseType: 'blob' }) |
| | | .then(res => this.downloadBlob(res, '审核结果_' + this.period + '.xlsx')) |
| | | .catch(() => this.$message.error('导出失败:请确认该报表期已有审核结果')) |
| | | }, |
| | | downloadBlob(res, fileName) { |
| | | const blob = res instanceof Blob ? res : new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const a = document.createElement('a') |
| | | a.href = url |
| | | a.download = fileName |
| | | document.body.appendChild(a) |
| | | a.click() |
| | | window.URL.revokeObjectURL(url) |
| | | document.body.removeChild(a) |
| | | }, |
| | | 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-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> |