<template>
|
<el-container direction="vertical">
|
<app-header title="交通统计报表审核系统 - 报表生成"></app-header>
|
<el-container>
|
<el-aside width="200px">
|
<el-menu router :default-active="activeMenu">
|
<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>
|
<div class="page-scroll">
|
<!-- 第一行:标题 + 更新时间 -->
|
<div class="topbar">
|
<h3>生成统计报表</h3>
|
<div class="topbar-right">
|
<span v-if="myModules.length" class="focus-user">{{ realName || username }}:负责 {{ myModules.map(moduleTitle).join('、') }}</span>
|
<span class="update-time">更新时间:{{ updatedAt }}</span>
|
</div>
|
</div>
|
|
<!-- 第二行:报表类型 Tab(货运 / 旅客 / 投资 / 能耗 / 城市客运) -->
|
<el-tabs v-model="activeKey" class="module-tabs">
|
<el-tab-pane v-for="m in modules" :key="m.key" :name="m.key">
|
<span slot="label">{{ m.title }}<el-tag v-if="m.tag" size="mini" :type="m.tagType || 'info'" style="margin-left:6px">{{ m.tag }}</el-tag></span>
|
</el-tab-pane>
|
</el-tabs>
|
|
<!-- 第三行:查询条件区(周期 + 月份 + 查询) -->
|
<section class="query-panel">
|
<span class="ctrl-label">周期</span>
|
<el-radio-group v-model="cycle" size="small" @change="onCycleChange">
|
<el-radio-button v-for="opt in cycleOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-radio-button>
|
</el-radio-group>
|
<span class="ctrl-label">{{ cycle === 'year' ? '年份' : '月份' }}</span>
|
<el-select v-model="period" size="small" style="width:190px" :placeholder="periodPlaceholder" @change="onPeriodChange">
|
<el-option-group v-for="grp in periodGroups" :key="grp.label" :label="grp.label">
|
<el-option v-for="opt in grp.options" :key="opt.value" :label="opt.label" :value="opt.value"></el-option>
|
</el-option-group>
|
</el-select>
|
<el-button type="primary" size="small" icon="el-icon-search" @click="doQuery">查询</el-button>
|
</section>
|
|
<!-- 第四行:报表列表 -->
|
<section class="module-panel">
|
<div class="panel-head">
|
<div class="panel-title">报表列表<span class="muted">({{ curModule.title }})</span></div>
|
<div class="panel-controls">
|
<el-checkbox :value="allChecked" @change="toggleAll">全选</el-checkbox>
|
<span class="checked-count">已选 {{ curModule.checked.length }} 张</span>
|
<el-button size="small" icon="el-icon-view" :disabled="!curModule.checked.length" @click="previewChecked()">批量预览</el-button>
|
<el-button size="small" type="primary" icon="el-icon-download" :disabled="!curModule.checked.length" @click="exportChecked()">批量导出</el-button>
|
</div>
|
</div>
|
<el-table :data="curModule.items" size="mini" class="report-table">
|
<el-table-column width="44">
|
<template slot-scope="{ row }">
|
<el-checkbox :value="curModule.checked.indexOf(row.key) >= 0" @change="toggleChecked(row)"></el-checkbox>
|
</template>
|
</el-table-column>
|
<el-table-column label="报表名称" prop="label" min-width="220"></el-table-column>
|
<el-table-column label="数据状态" width="180">
|
<template slot-scope="{ row }">
|
<el-tag v-if="row.status === 'ready'" size="mini" type="success">数据就绪</el-tag>
|
<el-tag v-else-if="row.status === 'unaudited'" size="mini" type="warning">未审核</el-tag>
|
<el-tag v-else-if="row.status === 'missing'" size="mini" type="danger">缺数据</el-tag>
|
<el-tag v-else size="mini" type="info">—</el-tag>
|
<div v-if="row.statusNote" class="status-note">{{ row.statusNote }}</div>
|
</template>
|
</el-table-column>
|
<el-table-column label="备注" min-width="190">
|
<template slot-scope="{ row }">
|
<span class="note-text">{{ noteOf(row.key) || '点击图标添加备注' }}</span>
|
<el-button size="mini" type="text" icon="el-icon-edit" title="编辑备注" @click="openNoteDialog(row)"></el-button>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="170" align="right">
|
<template slot-scope="{ row }">
|
<el-button size="mini" icon="el-icon-view" @click="previewOne(row)">预览</el-button>
|
<el-button size="mini" type="primary" icon="el-icon-download" @click="exportOne(row)">导出</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div class="panel-footer">共 {{ curModule.items.length }} 张报表</div>
|
</section>
|
|
<el-alert title="使用提示" type="info" style="margin-top:15px" :closable="false">
|
<div class="tip-text">
|
<p>1. 选择报表类型 Tab、周期与月份后点击「查询」,列表会显示每张报表的数据状态(数据就绪 / 缺数据 / 未审核)。</p>
|
<p>2. 数据校验与审核请到「审核结果」页查看,规则可在「规则管理」页配置。</p>
|
<p>3. 单张可直接点「预览 / 导出」;多张可勾选后「批量预览 / 批量导出」,多张导出自动打包为 zip。</p>
|
<p>4. 月报:选 2026-07 生成 1~7 月数据;年报:选年份生成全年 1~12 月 + 上年同比。</p>
|
</div>
|
</el-alert>
|
</div>
|
<el-dialog :title="'预览数据 - ' + previewModule + '(' + previewPeriod + ')'" :visible.sync="previewVisible" width="760px">
|
<div class="preview-body">
|
<div v-for="it in previewItems" :key="it.key" class="preview-item">
|
<div class="preview-head">
|
<span class="preview-name">{{ it.label }}</span>
|
<span class="preview-rows">共 {{ it.rowCount || 0 }} 行</span>
|
</div>
|
<div class="preview-coverage">
|
<span class="cov-label">月度覆盖:</span>
|
<span :class="it.ready ? 'cov-ok' : 'cov-bad'">{{ coverageText(it) }}</span>
|
<span v-if="!it.ready" class="cov-bad" style="margin-left:10px">{{ it.note }}</span>
|
<span v-if="it.ready && it.audited === false" class="cov-bad" style="margin-left:10px">未审核</span>
|
</div>
|
<table class="preview-summary" v-if="it.summary && it.summary.length">
|
<tbody>
|
<tr v-for="(s, i) in it.summary" :key="i">
|
<td>{{ s.name }}</td>
|
<td><span v-if="s.value === null || s.value === ''" class="cell-empty">(空)</span>{{ fmtVal(s.value) }}</td>
|
</tr>
|
</tbody>
|
</table>
|
<div class="preview-problems" v-if="it.problems && it.problems.length">
|
<div v-for="(p, i) in it.problems" :key="i" class="prob-item">· {{ p }}</div>
|
</div>
|
<div class="preview-ok" v-else-if="it.ready">数据完整,可正常生成</div>
|
</div>
|
</div>
|
<span slot="footer">
|
<el-button size="small" @click="previewVisible = false">关闭</el-button>
|
</span>
|
</el-dialog>
|
|
<!-- 备注编辑弹窗 -->
|
<el-dialog title="编辑备注" :visible.sync="noteDialogVisible" width="480px">
|
<el-input v-model="noteDraft" type="textarea" :rows="4" placeholder="填写该报表的备注说明(保存后本地记忆)"></el-input>
|
<span slot="footer">
|
<el-button size="small" @click="noteDialogVisible = false">取消</el-button>
|
<el-button size="small" type="primary" @click="saveNote">保存</el-button>
|
</span>
|
</el-dialog>
|
</el-main>
|
</el-container>
|
</el-container>
|
</template>
|
|
<script>
|
import AppHeader from '@/components/AppHeader.vue'
|
import api from '@/api'
|
|
export default {
|
components: { AppHeader },
|
name: 'ReportExport',
|
data() {
|
return {
|
loading: '',
|
activeMenu: '/report',
|
activeKey: '',
|
updatedAt: '',
|
// 查询条件:cycle 周期(month 月报 / quarter 季报 / year 年报),period 报表期(月份 yyyy-MM 或年份 yyyy)
|
cycle: 'month',
|
period: '',
|
// 预览弹窗
|
previewVisible: false,
|
previewItems: [],
|
previewModule: '',
|
previewPeriod: '',
|
// 备注编辑弹窗
|
noteDialogVisible: false,
|
noteEditingKey: '',
|
noteDraft: '',
|
modules: [
|
{
|
key: 'freight', title: '货运报表', items: [
|
{ key: 'cityDetail', label: '货运量分市州明细', fileName: '生成_货运量分市州明细.xlsx', status: null, statusNote: '' },
|
{ key: 'freightRank', label: '货运量排名', fileName: '生成_货运量排名.xlsx', status: null, statusNote: '' },
|
{ key: 'turnoverRank', label: '周转量排名', fileName: '生成_周转量排名.xlsx', status: null, statusNote: '' }
|
], checked: []
|
},
|
{
|
key: 'passenger', title: '旅客报表', items: [
|
{ key: 'passengerCityDetail', label: '公路旅客分市州', fileName: '生成_公路旅客分市州.xlsx', status: null, statusNote: '' },
|
{ key: 'passengerMidDetail', label: '中口径明细', fileName: '生成_公路旅客中口径明细.xlsx', status: null, statusNote: '' },
|
{ key: 'passengerMidRank', label: '中口径排名', fileName: '生成_公路旅客中口径排名.xlsx', status: null, statusNote: '' },
|
{ key: 'passengerMidAnalysis', label: '中口径分析', fileName: '生成_公路旅客中口径分析.xlsx', status: null, statusNote: '' }
|
], checked: []
|
}, {
|
key: 'cityPassenger', title: '城市客运报表', tag: '公交/出租/轨道轮渡/汇总', tagType: 'success', items: [
|
{ key: 'cityBusDetail', label: '城市公交客运量分市州明细', fileName: '生成_城市公交客运量分市州明细.xlsx', status: null, statusNote: '' },
|
{ key: 'cityTaxiDetail', label: '巡游出租客运量分市州明细', fileName: '生成_巡游出租客运量分市州明细.xlsx', status: null, statusNote: '' },
|
{ key: 'cityRailFerryDetail', label: '轨道轮渡客运量分市州明细', fileName: '生成_轨道轮渡客运量分市州明细.xlsx', status: null, statusNote: '' },
|
{ key: 'cityPassengerCitySum', label: '城市客运客运量分市州累计', fileName: '生成_城市客运客运量分市州累计.xlsx', status: null, statusNote: '' },
|
{ key: 'cityPassengerDetailSum', label: '城市客运各市州明细表', fileName: '生成_城市客运各市州明细表.xlsx', status: null, statusNote: '' },
|
{ key: 'cityPassengerSummary', label: '城市客运汇总', fileName: '生成_城市客运汇总.xlsx', status: null, statusNote: '' }
|
], checked: []
|
},
|
{
|
key: 'invest', title: '投资报表', items: [
|
{ key: 'investPlan', label: '全省预安排计划进度汇总', fileName: '生成_全省预安排计划进度情况汇总.xls', status: null, statusNote: '' },
|
{ key: 'investFiveYearLogistics', label: '十五五规划物流项目进展', fileName: '生成_十五五规划物流项目进展情况.xls', status: null, statusNote: '' },
|
{ key: 'investBillion', label: '亿元投资项目', fileName: '生成_湖北省(客货站场)亿元投资项目.xls', status: null, statusNote: '' },
|
{ key: 'investCounty', label: '经济强县项目投资统计', fileName: '生成_经济强县交通物流基础设施投资统计报表.xls', status: null, statusNote: '' }
|
], checked: []
|
},
|
{
|
key: 'energy', title: '能耗报表', items: [
|
{ key: 'energySummary', label: '能运汇总表', fileName: '生成_能运汇总表.xlsx', status: null, statusNote: '' }
|
], checked: []
|
}
|
]
|
}
|
},
|
computed: {
|
username() { return sessionStorage.getItem('username') || '' },
|
realName() { return sessionStorage.getItem('realName') || '' },
|
myModules() {
|
const s = sessionStorage.getItem('modules')
|
if (!s) return []
|
return s.split(',').filter(Boolean)
|
},
|
curModule() {
|
return this.modules.find(m => m.key === this.activeKey) || this.modules[0]
|
},
|
allChecked() {
|
const m = this.curModule
|
return m.items.length > 0 && m.checked.length === m.items.length
|
},
|
// 周期选项:能耗为季报(季度末月),其余为月报 / 年报
|
cycleOptions() {
|
if (this.activeKey === 'energy') return [{ label: '季报', value: 'quarter' }]
|
return [{ label: '月报', value: 'month' }, { label: '年报', value: 'year' }]
|
},
|
periodPlaceholder() {
|
return this.cycle === 'year' ? '选择年份' : '选择月份'
|
},
|
// 报表期下拉分组:年报按年份;月报/季报按年分组列 1-12 月(能耗只列季度末月)
|
periodGroups() {
|
const year = new Date().getFullYear()
|
const years = [year - 1, year, year + 1]
|
if (this.cycle === 'year') {
|
return [{ label: '选择年份', options: years.map(y => ({ label: y + '年', value: String(y) })) }]
|
}
|
const months = this.activeKey === 'energy' ? [3, 6, 9, 12] : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
return years.map(y => ({
|
label: y + '年' + (this.activeKey === 'energy' ? '(季度末月)' : ''),
|
options: months.map(mo => ({ label: y + '年' + mo + '月', value: y + '-' + String(mo).padStart(2, '0') }))
|
}))
|
},
|
},
|
watch: {
|
activeKey() {
|
this.ensureCycle()
|
this.refreshAll()
|
}
|
},
|
created() {
|
const mine = this.myModules.filter(k => this.modules.some(m => m.key === k))
|
this.activeKey = (mine.length ? mine : this.modules.map(m => m.key))[0]
|
this.ensureCycle()
|
this.refreshAll()
|
}, methods: {
|
moduleTitle(key) {
|
const m = this.modules.find(x => x.key === key)
|
return m ? m.title : key
|
},
|
logout() {
|
sessionStorage.removeItem('token'); sessionStorage.removeItem('username'); sessionStorage.removeItem('realName'); sessionStorage.removeItem('modules')
|
this.$router.push('/login')
|
},
|
// ==================== 查询条件与状态刷新 ====================
|
buildParams() {
|
const p = { period: this.period }
|
p.mode = this.cycle === 'year' ? 'year' : 'month'
|
return p
|
},
|
// 周期与报表期匹配:切换周期后清掉不匹配的旧值
|
normalizePeriod() {
|
const isYear = /^\d{4}$/.test(this.period || '')
|
const isMonth = /^\d{4}-\d{2}$/.test(this.period || '')
|
if (this.cycle === 'year' && isMonth) this.period = ''
|
if (this.cycle !== 'year' && isYear) this.period = ''
|
},
|
ensureCycle() {
|
const allowed = this.cycleOptions.map(o => o.value)
|
if (!allowed.includes(this.cycle)) this.cycle = allowed[0]
|
this.normalizePeriod()
|
},
|
onCycleChange() {
|
this.normalizePeriod()
|
this.refreshAll()
|
},
|
onPeriodChange() {
|
this.refreshAll()
|
},
|
doQuery() {
|
if (!this.period) { this.$message.warning('请先选择周期和月份'); return }
|
this.refreshAll()
|
},
|
refreshAll() {
|
this.updatedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
this.refreshStatus()
|
},
|
refreshStatus() {
|
const m = this.curModule
|
if (!m || !this.period) return
|
const params = Object.assign(this.buildParams(), { types: m.items.map(i => i.key).join(',') })
|
api.get('/report/ready', { params, timeout: 60000 })
|
.then(res => {
|
const list = (res && res.data && res.data.items) || []
|
list.forEach(r => {
|
const it = m.items.find(x => x.key === r.key)
|
if (!it) return
|
if (!r.ready) { it.status = 'missing'; it.statusNote = r.note || '缺数据' }
|
else if (r.audited === false) { it.status = 'unaudited'; it.statusNote = '尚未审核通过' }
|
else { it.status = 'ready'; it.statusNote = '' }
|
})
|
})
|
.catch(() => {})
|
},
|
noteOf(key) {
|
return localStorage.getItem('report_note_' + key) || ''
|
},
|
openNoteDialog(row) {
|
this.noteEditingKey = row.key
|
this.noteDraft = this.noteOf(row.key)
|
this.noteDialogVisible = true
|
},
|
saveNote() {
|
localStorage.setItem('report_note_' + this.noteEditingKey, this.noteDraft.trim())
|
this.noteDialogVisible = false
|
this.$message.success('备注已保存')
|
},
|
// ==================== 勾选 / 预览 / 导出 ====================
|
toggleChecked(row) {
|
const checked = this.curModule.checked
|
const i = checked.indexOf(row.key)
|
if (i >= 0) checked.splice(i, 1)
|
else checked.push(row.key)
|
},
|
toggleAll(v) {
|
this.curModule.checked = v ? this.curModule.items.map(i => i.key) : []
|
},
|
exportOne(item) { this.exportChecked([item]) },
|
previewOne(item) { this.previewChecked([item]) },
|
previewChecked(itemList) {
|
const m = this.curModule
|
if (!this.period) { this.$message.warning('请先选择周期和月份'); return }
|
const items = itemList || m.items.filter(i => m.checked.includes(i.key))
|
if (!items.length) { this.$message.warning('请选择要预览的报表'); return }
|
this.loading = 'preview-' + m.key
|
const params = Object.assign(this.buildParams(), { types: items.map(i => i.key).join(',') })
|
api.get('/report/preview', { params, timeout: 60000 })
|
.then(res => {
|
const list = (res && res.data && res.data.items) || []
|
this.previewItems = list.map(it => {
|
const item = items.find(x => x.key === it.key)
|
return Object.assign({}, it, { label: item ? item.label : it.key })
|
})
|
this.previewModule = m.title
|
this.previewPeriod = this.period
|
this.previewVisible = true
|
this.loading = ''
|
})
|
.catch(err => this.failWith(err))
|
},
|
fmtVal(v) {
|
if (v === null || v === undefined || v === '') return '—'
|
if (typeof v === 'number') {
|
if (Number.isInteger(v)) return String(v)
|
return v.toFixed(4).replace(/\.?0+$/, '')
|
}
|
return String(v)
|
},
|
coverageText(it) {
|
const cov = it.monthCoverage || []
|
if (!cov.length) return '无数据'
|
const continuous = cov.length === cov[cov.length - 1]
|
if (continuous) return '1~' + cov[cov.length - 1] + '月均有数据'
|
return cov.map(x => x + '月').join('、') + ' 有数据'
|
},
|
exportChecked(itemList) {
|
const m = this.curModule
|
if (!this.period) { this.$message.warning('请先选择周期和月份'); return }
|
const items = itemList || m.items.filter(i => m.checked.includes(i.key))
|
if (!items.length) { this.$message.warning('请选择要导出的报表'); return }
|
this.loading = 'gen-' + m.key
|
const params = this.buildParams()
|
const doExport = () => {
|
if (items.length === 1) {
|
api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 })
|
.then(res => { this.downloadBlob(res, items[0].fileName); this.$message.success('报表导出成功'); this.loading = '' })
|
.catch(err => this.failWith(err))
|
return
|
}
|
const zipParams = Object.assign({}, params, { types: items.map(i => i.key).join(',') })
|
api.get('/report/export/selected', { params: zipParams, responseType: 'blob', timeout: 120000 })
|
.then(res => { this.downloadBlob(res, '生成_所选报表_' + this.period + '.zip'); this.$message.success('已导出 ' + items.length + ' 张报表(zip)'); this.loading = '' })
|
.catch(err => this.failWith(err))
|
}
|
const readyParams = Object.assign({}, params, { types: items.map(i => i.key).join(',') })
|
api.get('/report/ready', { params: readyParams, timeout: 60000 })
|
.then(rd => {
|
const list = (rd && rd.data && rd.data.items) || []
|
const missing = list.filter(i => !i.ready)
|
const unaudited = list.filter(i => i.ready && i.audited === false)
|
const lines = []
|
missing.forEach(i => {
|
const it = items.find(x => x.key === i.key)
|
lines.push('【缺数据】' + (it ? it.label : i.key) + '(' + i.note + ')')
|
})
|
unaudited.forEach(i => {
|
const it = items.find(x => x.key === i.key)
|
lines.push('【未审核】' + (it ? it.label : i.key))
|
})
|
if (!lines.length) { doExport(); return }
|
this.$confirm('以下报表请注意:\n' + lines.join('\n') + '\n\n仍要导出吗?', '导出前提示', {
|
confirmButtonText: '仍要导出', cancelButtonText: '取消', type: 'warning'
|
}).then(() => doExport()).catch(() => { this.loading = '' })
|
})
|
.catch(() => doExport())
|
},
|
failWith(err) {
|
if (err && err._toast) { this.loading = ''; return }
|
let msg = ''
|
const d = err && err.response && err.response.data
|
if (d && typeof d === 'object' && d.message) msg = d.message
|
this.$message.error('导出失败:' + (msg || '请确认已导入对应数据,或查看后端日志'))
|
this.loading = ''
|
},
|
downloadBlob(res, fileName) {
|
const isZip = /\.zip$/.test(fileName)
|
const isXls = /\.xls$/.test(fileName)
|
const blob = new Blob([res], { type: isZip ? 'application/zip' : (isXls ? 'application/vnd.ms-excel' : '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)
|
}
|
}
|
}
|
</script><style scoped>
|
.el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
|
.page-scroll { max-width: 1200px; }
|
.topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
.topbar h3 { margin: 0; }
|
.topbar-right { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #606266; }
|
.update-time { font-size: 12px; color: #909399; }
|
.module-tabs { margin-bottom: 6px; }
|
.query-panel { background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 12px 16px; margin-bottom: 12px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
.data-card { background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 14px 16px; margin-bottom: 12px; }
|
.card-title { font-size: 15px; font-weight: 600; color: #303133; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
.muted { font-size: 12px; color: #909399; font-weight: 400; }
|
.module-panel { background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 14px 16px; }
|
.ctrl-label { font-size: 13px; color: #606266; }
|
.panel-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
|
.panel-title { font-size: 16px; font-weight: 600; color: #303133; display: flex; align-items: center; gap: 6px; }
|
.panel-controls { display: flex; align-items: center; gap: 10px; }
|
.report-table { width: 100%; }
|
.checked-count { font-size: 12px; color: #909399; }
|
.panel-footer { margin-top: 10px; font-size: 12px; color: #909399; text-align: right; }
|
.status-note { font-size: 11px; color: #F56C6C; margin-top: 2px; line-height: 1.4; }
|
.note-text { font-size: 12px; color: #606266; margin-right: 4px; }
|
.spacer { flex: 1; }
|
.preview-body { max-height: 68vh; overflow-y: auto; }
|
.preview-item { border: 1px solid #ebeef5; border-radius: 6px; padding: 12px 14px; margin-bottom: 12px; }
|
.preview-head { display: flex; align-items: center; margin-bottom: 8px; }
|
.preview-name { font-weight: 600; font-size: 15px; margin-right: 10px; }
|
.preview-rows { margin-left: auto; color: #909399; font-size: 12px; }
|
.preview-coverage { font-size: 13px; margin-bottom: 8px; }
|
.cov-ok { color: #67C23A; }
|
.cov-bad { color: #F56C6C; }
|
.cov-label { color: #909399; }
|
.preview-summary { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 8px; }
|
.preview-summary th, .preview-summary td { border: 1px solid #ebeef5; padding: 4px 8px; text-align: left; }
|
.preview-summary th { background: #f5f7fa; }
|
.preview-summary .cell-empty { color: #F56C6C; }
|
.preview-problems { font-size: 13px; color: #F56C6C; }
|
.preview-problems .prob-item { margin: 2px 0; }
|
.preview-ok { font-size: 13px; color: #67C23A; }
|
.tip-text { font-size: 13px; line-height: 1.7; }
|
.tip-text p { margin: 2px 0; }
|
@media (max-width: 900px) {
|
.panel-head { flex-direction: column; align-items: flex-start; }
|
}
|
</style>
|