| | |
| | | <app-header title="交通统计报表审核系统 - 报表生成"></app-header> |
| | | <el-container> |
| | | <el-aside width="200px"> |
| | | <el-menu router default-active="/report"> |
| | | <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-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | | <h3>生成统计报表</h3> |
| | | <el-card style="max-width:640px"> |
| | | <el-form label-width="100px"> |
| | | <el-form-item label="报表期"> |
| | | <el-date-picker v-model="period" type="month" placeholder="选择月份" value-format="yyyy-MM" style="width:100%"></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="exportReport('cityDetail')" :loading="loading === 'cityDetail'">生成_货运量分市州明细</el-button> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="exportReport('freightRank')" :loading="loading === 'freightRank'">生成_货运量排名</el-button> |
| | | <el-button type="primary" @click="exportReport('turnoverRank')" :loading="loading === 'turnoverRank'">生成_周转量排名</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | <el-alert |
| | | title="提示:报表由系统根据已导入数据自动计算生成。货运量:合计来自模板_货运量周转量(左半1-N月)、规上来自H203-2月报、规下=合计-规上;周转量来自规上规下拆分表。" |
| | | type="info" style="margin-top:15px" :closable="false"></el-alert> |
| | | <!-- 第一行:标题 + 更新时间 --> |
| | | <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 { period: '', loading: '' } |
| | | 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: [] |
| | | } |
| | | ] |
| | | } |
| | | }, |
| | | methods: { |
| | | exportReport(type) { |
| | | if (!this.period) { this.$message.warning('请选择报表期'); return } |
| | | this.loading = type |
| | | const token = localStorage.getItem('token') || '' |
| | | api.get('/report/export/' + type, { params: { period: this.period }, responseType: 'blob' }) |
| | | .then(res => { |
| | | const blob = 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 = this.fileName(type) |
| | | document.body.appendChild(a) |
| | | a.click() |
| | | window.URL.revokeObjectURL(url) |
| | | this.$message.success('报表生成成功') |
| | | }) |
| | | .catch(() => { this.$message.error('报表生成失败,请确认已导入对应数据') }) |
| | | .finally(() => { this.loading = '' }) |
| | | computed: { |
| | | username() { return localStorage.getItem('username') || '' }, |
| | | realName() { return localStorage.getItem('realName') || '' }, |
| | | myModules() { |
| | | const s = localStorage.getItem('modules') |
| | | if (!s) return [] |
| | | return s.split(',').filter(Boolean) |
| | | }, |
| | | fileName(type) { |
| | | const map = { |
| | | cityDetail: '生成_货运量分市州明细.xlsx', |
| | | freightRank: '生成_货运量排名.xlsx', |
| | | turnoverRank: '生成_周转量排名.xlsx' |
| | | 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) })) }] |
| | | } |
| | | return map[type] || 'report.xlsx' |
| | | 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() { |
| | | localStorage.removeItem('token'); localStorage.removeItem('username'); localStorage.removeItem('realName'); localStorage.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 }) |
| | | .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 }) |
| | | .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' }) |
| | | .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' }) |
| | | .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 }) |
| | | .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) { |
| | | 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> |
| | | </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> |