From 35d098a89613c96179391178b7f8143dec393ada Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期日, 20 九月 2026 15:52:36 +0800
Subject: [PATCH] feat(报表): 汇总大表导出异步化 + 结果缓存 + 进度弹窗
---
traffic-audit-web/src/views/ReportExport.vue | 776 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 730 insertions(+), 46 deletions(-)
diff --git a/traffic-audit-web/src/views/ReportExport.vue b/traffic-audit-web/src/views/ReportExport.vue
index 3fddd46..22e12cf 100644
--- a/traffic-audit-web/src/views/ReportExport.vue
+++ b/traffic-audit-web/src/views/ReportExport.vue
@@ -3,7 +3,7 @@
<app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 鎶ヨ〃鐢熸垚"></app-header>
<el-container>
<el-aside width="200px">
- <el-menu router default-active="/report">
+ <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>
@@ -16,68 +16,752 @@
</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鏈堬級銆佽涓婃潵鑷狧203-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>
+
+ <!-- 绗簩琛岋細鎶ヨ〃绫诲瀷鍒囨崲锛堣揣杩� / 鏃呭 / 鎶曡祫 / 鑳借�� / 鍩庡競瀹㈣繍锛夛紱el-radio-group 鏇夸唬 el-tabs锛岃閬� el-tabs 甯冨眬鏃犻檺寰幆锛堜笌 /import 淇涓�鑷达級 -->
+ <el-radio-group v-model="activeKey" class="module-tabs">
+ <el-radio-button v-for="m in visibleModules" :key="m.key" :label="m.key">
+ {{ m.title }}<el-tag v-if="m.tag" size="mini" :type="m.tagType || 'info'" style="margin-left:6px">{{ m.tag }}</el-tag>
+ </el-radio-button>
+ </el-radio-group>
+
+ <!-- 绗笁琛岋細鏌ヨ鏉′欢鍖猴紙鍛ㄦ湡 + 鏈堜唤 + 鏌ヨ锛� -->
+ <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"
+ top="5vh" custom-class="pv-dialog" v-dialog-drag>
+ <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" v-if="it.key === 'summaryWorkbook'">鎸囨爣 {{ (it.summary || []).length }} 椤�</span>
+ <span class="preview-rows" v-else>鍏� {{ 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 wb-table" v-if="it.key === 'summaryWorkbook' && it.summary && it.summary.length">
+ <thead>
+ <tr><th style="width:12%">鍒嗙被</th><th style="width:40%">鎸囨爣</th><th style="width:28%">鏁版嵁</th><th>鍚屾瘮</th></tr>
+ </thead>
+ <tbody>
+ <template v-for="(sec, si) in wbRows(it.summary)">
+ <tr v-for="(row, ri) in sec.rows" :key="si + '-' + ri">
+ <td v-if="ri === 0" :rowspan="sec.rows.length" class="wb-sec">{{ sec.name }}</td>
+ <td class="wb-label">{{ row.label }}</td>
+ <td class="wb-value">{{ row.value === null ? '鈥�' : fmtVal(row.value) }}</td>
+ <td class="wb-yoy" :class="{ 'yoy-empty': row.yoy === null }">{{ row.yoy === null ? '鈥�' : row.yoy }}</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ <div v-if="it.key === 'summaryWorkbook' && wbMissing(it.summary)" class="wb-note">
+ 閮ㄥ垎鎸囨爣缂哄皯涓婂勾鍚屾湡锛坽{ wbPrevYear() }}锛夊熀鏈熸暟鎹紝鍚屾瘮鏆傛湭璁$畻
+ </div>
+ <table class="preview-summary" v-if="it.key !== 'summaryWorkbook' && it.summary && it.summary.length">
+ <tbody>
+ <tr v-for="(s, i) in it.summary" :key="i">
+ <td>{{ s.label }}</td>
+ <td><span v-if="s.value === null || s.value === ''" class="cell-empty">锛堢┖锛�</span>{{ fmtVal(s.value) }}</td>
+ <td style="white-space:nowrap;color:#909399">{{ s.unit || '' }}</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">鏁版嵁瀹屾暣锛屽彲姝e父鐢熸垚</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>
+
+ <!-- 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愯繘搴︼紙鐢熸垚涓�娆$害 10~20 绉掞紝閬垮厤鐐瑰嚮鍚庨暱鏃堕棿鏃犲搷搴旓級 -->
+ <el-dialog title="姝e湪鐢熸垚姹囨�诲ぇ琛�" :visible.sync="taskVisible" width="460px"
+ :close-on-click-modal="false" :show-close="taskFinished">
+ <div class="task-body">
+ <el-progress :percentage="taskInfo.percent || 0" :status="taskInfo.status === 'FAILED' ? 'exception' : (taskDone ? 'success' : null)"
+ :stroke-width="14"></el-progress>
+ <p class="task-step">{{ taskInfo.step || '鎺掗槦涓�' }}</p>
+ <p class="task-meta" v-if="taskInfo.period">鎶ヨ〃鏈� {{ taskInfo.period }}銆�{{ taskInfo.fromCache ? '涓婃鐢熸垚鐢ㄦ椂' : '宸茬敤' }} {{ (taskInfo.elapsedMs / 1000).toFixed(1) }} 绉�</p>
+ <p class="task-meta" v-if="taskInfo.fromCache">鏈鐩存帴浣跨敤 {{ taskCacheTime }} 鐢熸垚鐨勭粨鏋滐紙10 鍒嗛挓鍐呬笖鏈熼棿娌℃湁鏂板鍏ユ暟鎹級銆傚闇�鎸夋渶鏂版暟鎹噸绠楋紝璇风偣銆岄噸鏂扮敓鎴愩�嶃��</p>
+ <p class="task-error" v-if="taskInfo.status === 'FAILED'">{{ taskInfo.error }}</p>
+ </div>
+ <span slot="footer">
+ <el-button size="small" type="primary" :disabled="!taskDone" @click="downloadTaskFile">{{ taskInfo.fromCache ? '涓嬭浇' : '淇濆瓨鏂囦欢' }}</el-button>
+ <el-button size="small" :disabled="taskRunning" @click="restartTask">閲嶆柊鐢熸垚</el-button>
+ <el-button size="small" v-if="taskRunning" @click="minimizeTask">鍚庡彴杩愯</el-button>
+ <el-button size="small" v-else @click="closeTask">鍏抽棴</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: '',
+ activeKey: '',
+ updatedAt: '',
+ // 鏌ヨ鏉′欢锛歝ycle 鍛ㄦ湡锛坢onth 鏈堟姤 / quarter 瀛f姤 / year 骞存姤锛夛紝period 鎶ヨ〃鏈燂紙鏈堜唤 yyyy-MM 鎴栧勾浠� yyyy锛�
+ cycle: 'month',
+ period: '',
+ // 棰勮寮圭獥
+ previewVisible: false,
+ previewItems: [],
+ previewModule: '',
+ previewPeriod: '',
+ // 澶囨敞缂栬緫寮圭獥
+ noteDialogVisible: false,
+ noteEditingKey: '',
+ noteDraft: '',
+ // 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愪换鍔�
+ taskVisible: false,
+ taskInfo: {},
+ taskTimer: null,
+ taskKey: 'summaryWorkbook',
+ modules: [
+ {
+ key: 'summary', title: '姹囨�诲ぇ琛�', items: [
+ { key: 'summaryWorkbook', label: '閬撹矾杩愯緭閲忔眹鎬昏〃锛堟暣鏈級', fileName: '鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx', status: null, statusNote: '' }
+ ], checked: []
+ },
+ {
+ key: 'freight', title: '璐ц繍鎶ヨ〃', hidden: true, 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: '鏃呭鎶ヨ〃', hidden: true, 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: '鍩庡競瀹㈣繍鎶ヨ〃', hidden: true, 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: '' },
+ { key: 'wycSplit', 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: '' },
+ { key: 'investPassengerDetail', label: '瀹㈣繍绔欐姇璧勬槑缁嗚〃', fileName: '鐢熸垚_X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx', status: null, statusNote: '' },
+ { key: 'investLogisticsDetail', label: '鐗╂祦绔欏満鎶曡祫鏄庣粏', fileName: '鐢熸垚_X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.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)
+ },
+ visibleModules() {
+ return this.modules.filter(m => !m.hidden)
+ },
+ 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
+ },
+ // 鍛ㄦ湡閫夐」锛氳兘鑰椾负瀛f姤锛堝搴︽湯鏈堬級锛屽叾浣欎负鏈堟姤 / 骞存姤
+ cycleOptions() {
+ if (this.activeKey === 'energy') return [{ label: '瀛f姤', value: 'quarter' }]
+ return [{ label: '鏈堟姤', value: 'month' }, { label: '骞存姤', value: 'year' }]
+ },
+ periodPlaceholder() {
+ return this.cycle === 'year' ? '閫夋嫨骞翠唤' : '閫夋嫨鏈堜唤'
+ },
+ // 鎶ヨ〃鏈熶笅鎷夊垎缁勶細骞存姤鎸夊勾浠斤紱鏈堟姤/瀛f姤鎸夊勾鍒嗙粍鍒� 1-12 鏈堬紙鑳借�楀彧鍒楀搴︽湯鏈堬級
+ periodGroups() {
+ const year = new Date().getFullYear()
+ const years = [year, year - 1, 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') }))
+ }))
+ },
+ // ===== 浠ヤ笅涓烘眹鎬诲ぇ琛ㄥ紓姝ョ敓鎴愬脊绐楃殑娲剧敓鐘舵�侊紙蹇呴』骞跺叆鍚屼竴涓� computed锛屽彟璧峰悓鍚� key 浼氳鐩栨帀涓婇潰鐨勮绠楀睘鎬э級=====
+ taskRunning() {
+ const s = this.taskInfo && this.taskInfo.status
+ return s === 'PENDING' || s === 'RUNNING'
+ },
+ taskDone() {
+ return this.taskInfo && this.taskInfo.status === 'DONE'
+ },
+ // 鐢熸垚缁撴潫锛堟垚鍔熸垨澶辫触锛夊悗鎵嶅厑璁告敹璧峰脊绐�
+ taskFinished() {
+ const s = this.taskInfo && this.taskInfo.status
+ return s === 'DONE' || s === 'FAILED'
+ },
+ taskCacheTime() {
+ const t = this.taskInfo && this.taskInfo.finishedAt
+ if (!t) return '涓婃'
+ const d = new Date(t)
+ const p = (n) => String(n).padStart(2, '0')
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
+ }
+ },
+ watch: {
+ activeKey() {
+ this.ensureCycle()
+ this.refreshAll()
+ }
+ },
+ created() {
+ const mine = this.myModules.filter(k => this.visibleModules.some(m => m.key === k))
+ this.activeKey = (mine.length ? mine : this.visibleModules.map(m => m.key))[0]
+ this.ensureCycle()
+ this.refreshAll()
+ },
+ beforeDestroy() {
+ this.stopTaskPolling()
},
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 = '' })
+ moduleTitle(key) {
+ const m = this.modules.find(x => x.key === key)
+ return m ? m.title : key
},
- fileName(type) {
- const map = {
- cityDetail: '鐢熸垚_璐ц繍閲忓垎甯傚窞鏄庣粏.xlsx',
- freightRank: '鐢熸垚_璐ц繍閲忔帓鍚�.xlsx',
- turnoverRank: '鐢熸垚_鍛ㄨ浆閲忔帓鍚�.xlsx'
+ 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 map[type] || 'report.xlsx'
+ return String(v)
+ },
+ wbRows(summary) {
+ const get = (key) => {
+ const hit = (summary || []).find(x => x.label === key)
+ return hit && hit.value !== null && hit.value !== undefined && hit.value !== '' ? hit.value : null
+ }
+ const pct = (key) => {
+ const v = get(key)
+ return v === null ? null : this.fmtVal(v) + '%'
+ }
+ const mk = (defs) => defs.map(d => ({
+ label: d[0],
+ value: get(d[1]),
+ yoy: d[2] ? pct(d[2]) : null
+ }))
+ return [
+ {
+ name: '閬撹矾璐ц繍',
+ rows: mk([
+ ['褰撴湀璐ц繍閲�(涓囧惃)', '鍏ㄧ渷璐ц繍閲徛峰綋鏈�', '鍏ㄧ渷璐ц繍閲徛峰綋鏈堝悓姣�'],
+ ['褰撴湀璐х墿鍛ㄨ浆閲�(涓囧惃鍏噷)', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛峰綋鏈�', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛峰綋鏈堝悓姣�'],
+ ['绱璐ц繍閲�(涓囧惃)', '鍏ㄧ渷璐ц繍閲徛风疮璁�', '鍏ㄧ渷璐ц繍閲徛风疮璁″悓姣�'],
+ ['绱璐х墿鍛ㄨ浆閲�(涓囧惃鍏噷)', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛风疮璁�', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛风疮璁″悓姣�']
+ ])
+ },
+ {
+ name: '鍩庡競瀹㈣繍',
+ rows: mk([
+ ['褰撴湀鍩庡競瀹㈣繍閲�', '鍩庡競瀹㈣繍閲徛峰綋鏈�', '鍩庡競瀹㈣繍閲徛峰綋鏈堝悓姣�'],
+ ['褰撴湀鍩庡競瀹㈣繍鍛ㄨ浆閲�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛峰綋鏈�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛峰綋鏈堝悓姣�'],
+ ['绱鍩庡競瀹㈣繍閲�', '鍩庡競瀹㈣繍閲徛风疮璁�', '鍩庡競瀹㈣繍閲徛风疮璁″悓姣�'],
+ ['绱鍩庡競瀹㈣繍鍛ㄨ浆閲�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛风疮璁�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛风疮璁″悓姣�']
+ ])
+ },
+ {
+ name: '涓彛寰�',
+ rows: mk([
+ ['褰撴湀涓彛寰勫杩愰噺', '涓彛寰勫杩愰噺路褰撴湀', '涓彛寰勫杩愰噺路褰撴湀鍚屾瘮'],
+ ['褰撴湀涓彛寰勫懆杞噺', '涓彛寰勫懆杞噺路褰撴湀', '涓彛寰勫懆杞噺路褰撴湀鍚屾瘮'],
+ ['绱涓彛寰勫杩愰噺', '涓彛寰勫杩愰噺路绱', '涓彛寰勫杩愰噺路绱鍚屾瘮'],
+ ['绱涓彛寰勫懆杞噺', '涓彛寰勫懆杞噺路绱', '涓彛寰勫懆杞噺路绱鍚屾瘮']
+ ])
+ }
+ ]
+ },
+ wbMissing(summary) {
+ let total = 0
+ let have = 0
+ ;(summary || []).forEach(x => {
+ if (String(x.label).indexOf('鍚屾瘮') >= 0) return
+ total++
+ if (x.value !== null && x.value !== undefined && x.value !== '') have++
+ })
+ return total > 0 && have < total
+ },
+ wbPrevYear() {
+ const m = /^(\d{4})-(\d{2})$/.exec(this.previewPeriod || '')
+ if (!m) return '涓婂勾鍚屾湡'
+ return (Number(m[1]) - 1) + '-' + m[2]
+ },
+ 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('銆�') + ' 鏈夋暟鎹�'
+ },
+ // ==================== 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愶紙杩涘害寮圭獥锛� ====================
+ // 鍚庣 ReportExportTaskService锛氭彁浜や换鍔� -> 杞杩涘害 -> 瀹屾垚鍚庝笅杞芥枃浠躲��
+ // 鍚屾姤琛ㄦ湡 10 鍒嗛挓鍐呬笖鏈熼棿娌℃湁鏂板鍏ユ暟鎹椂锛屽悗绔洿鎺ヨ繑鍥炰笂娆$粨鏋滐紙寮圭獥鍐呬細璇存槑鏉ユ簮鏃堕棿锛夈��
+ startSummaryTask(refresh) {
+ if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鍛ㄦ湡鍜屾湀浠�'); return }
+ this.stopTaskPolling()
+ this.taskInfo = { status: 'PENDING', percent: 0, step: '鎻愪氦浠诲姟', period: this.period, elapsedMs: 0 }
+ this.taskVisible = true
+ this.loading = 'gen-' + this.curModule.key
+ const params = Object.assign({}, this.buildParams(), { refresh: refresh ? 'true' : 'false' })
+ api.get('/report/exportTask/summaryWorkbook', { params, timeout: 60000 })
+ .then(res => {
+ // 鍚庣缁熶竴杩斿洖 Result锛歝ode 闈� 200 鏃� data 涓虹┖锛屽繀椤绘彁绀鸿�屼笉鏄┖杞�
+ if (!res || res.code !== 200 || !res.data || !res.data.taskId) {
+ this.taskInfo = { status: 'FAILED', percent: 0, step: '鎻愪氦澶辫触', period: this.period, elapsedMs: 0,
+ error: (res && res.message) || '鍚庣鏈繑鍥炰换鍔″彿' }
+ this.loading = ''
+ return
+ }
+ this.taskInfo = res.data
+ if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(); return }
+ if (this.taskInfo.status === 'FAILED') { this.loading = ''; return }
+ this.pollTask()
+ })
+ .catch(err => {
+ this.stopTaskPolling()
+ this.loading = ''
+ this.taskVisible = false
+ this.failWith(err, '鐢熸垚')
+ })
+ },
+ pollTask() {
+ this.stopTaskPolling()
+ const id = this.taskInfo && this.taskInfo.taskId
+ if (!id) return
+ this.taskTimer = setInterval(() => {
+ api.get('/report/exportTask/' + id, { timeout: 30000 })
+ .then(res => {
+ if (!res || res.code !== 200 || !res.data) {
+ // 浠诲姟琚竻鐞�/杩囨湡锛氬仠姝㈣疆璇㈠苟缁欏嚭鍘熷洜锛屼笉璁╁脊绐椾竴鐩寸┖杞�
+ this.stopTaskPolling()
+ this.loading = ''
+ this.taskInfo = Object.assign({}, this.taskInfo, {
+ status: 'FAILED', step: '浠诲姟宸插け鏁�',
+ error: (res && res.message) || '瀵煎嚭浠诲姟宸插け鏁堬紝璇烽噸鏂扮敓鎴�'
+ })
+ return
+ }
+ this.taskInfo = res.data
+ if (this.taskInfo.status === 'DONE') {
+ this.stopTaskPolling()
+ this.loading = ''
+ this.downloadTaskFile()
+ } else if (this.taskInfo.status === 'FAILED') {
+ this.stopTaskPolling()
+ this.loading = ''
+ if (!this.taskVisible) this.$message.error('姹囨�诲ぇ琛ㄧ敓鎴愬け璐ワ細' + (this.taskInfo.error || '鏈煡鍘熷洜'))
+ }
+ })
+ .catch(() => { /* 鍗曟杞澶辫触蹇界暐锛屼笅涓�杞户缁� */ })
+ }, 700)
+ },
+ stopTaskPolling() {
+ if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null }
+ },
+ downloadTaskFile() {
+ const id = this.taskInfo && this.taskInfo.taskId
+ if (!id) return
+ const name = this.taskInfo.fileName || ('鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_' + this.period + '.xlsx')
+ api.get('/report/exportTask/' + id + '/file', { responseType: 'blob', timeout: 120000 })
+ .then(res => { this.downloadBlob(res, name); this.$message.success('鎶ヨ〃瀵煎嚭鎴愬姛') })
+ .catch(err => this.failWith(err))
+ },
+ restartTask() { this.startSummaryTask(true) },
+ closeTask() { this.taskVisible = false; this.stopTaskPolling(); this.loading = '' },
+ // 鐢熸垚涓敹璧峰脊绐楋細浠诲姟缁х画鍦ㄥ悗鍙版墽琛岋紝瀹屾垚鍚庝粛浼氳嚜鍔ㄤ笅杞斤紙涓庡師鏉ュ悓姝ュ鍑虹殑銆岀偣瀹屽嵆涓嬭浇銆嶄竴鑷达級
+ minimizeTask() {
+ this.taskVisible = false
+ this.$message.info('宸茶浆鍏ュ悗鍙扮敓鎴愶紝瀹屾垚鍚庝細鑷姩涓嬭浇')
+ },
+ 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) {
+ // 姹囨�诲ぇ琛ㄧ敓鎴愪竴娆$害 10~20 绉掞紝璧板紓姝ヤ换鍔★紙杩涘害寮圭獥锛夛紝閬垮厤鐐瑰嚮鍚庨暱鏃堕棿鏃犲搷搴�
+ if (items[0].key === 'summaryWorkbook') { this.startSummaryTask(false); return }
+ api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 })
+ .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); 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())
+ },
+ // 棰勮/瀵煎嚭澶辫触缁熶竴鎻愮ず锛氬尯鍒嗏�滆繛涓嶄笂鏈嶅姟鍣ㄢ�濅笌鈥滃悗绔繑鍥炵殑閿欒鍘熷洜鈥濄��
+ // 瀵煎嚭鎺ュ彛鐢� responseType: 'blob'锛屾姤閿欐椂 data 涔熸槸 Blob锛屽繀椤诲厛璇绘枃鏈啀瑙f瀽锛�
+ // 鍚﹀垯鎷夸笉鍒板悗绔湡瀹炲師鍥狅紝鍙細鏄剧ず绗肩粺鐨勨�滆纭宸插鍏ュ搴旀暟鎹�濄��
+ failWith(err, action) {
+ if (err && err._toast) { this.loading = ''; return }
+ const act = action || '瀵煎嚭'
+ const finish = (reason) => {
+ this.$message.error(act + '澶辫触锛�' + reason)
+ this.loading = ''
+ }
+ if (!err || !err.response) {
+ if (err && err.code === 'ECONNABORTED') { finish('璇锋眰瓒呮椂锛岃绋嶅悗閲嶈瘯'); return }
+ finish('鏃犳硶杩炴帴鏈嶅姟鍣紝璇锋鏌ョ綉缁滆繛鎺ワ紝鎴栫‘璁ゅ悗绔湇鍔℃槸鍚﹀凡鍚姩')
+ return
+ }
+ const d = err.response.data
+ const fallback = '璇风‘璁ゅ凡瀵煎叆瀵瑰簲鏁版嵁锛屾垨鏌ョ湅鍚庣鏃ュ織'
+ if (d && typeof d.text === 'function') {
+ Promise.resolve(d.text()).then(txt => finish(this.errReason(txt) || fallback)).catch(() => finish(fallback))
+ return
+ }
+ finish(this.errReason(typeof d === 'string' ? d : (d && d.message)) || fallback)
+ },
+ // 鍚庣寮傚父鍝嶅簲鍙兘鏄� JSON/HTML/绾枃鏈紝灏介噺鎻愬彇鍑哄彲璇荤殑鍘熷洜
+ errReason(raw) {
+ if (!raw) return ''
+ let text = String(raw).trim()
+ if (text.charAt(0) === '{' || text.charAt(0) === '[') {
+ try {
+ const j = JSON.parse(text)
+ if (j && j.message) return j.message
+ } catch (e) { /* 闈� JSON锛屾寜绾枃鏈鐞� */ }
+ }
+ text = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
+ // 寮�鍙戜唬鐞�/缃戠粶灞傛姤閿欒浆鎴愪汉璇濓紝閬垮厤鐩存帴鎶� ECONNREFUSED 涔嬬被鎶涚粰鐢ㄦ埛
+ if (/ECONNREFUSED|ECONNRESET|Proxy error|socket hang up|Network Error|Failed to fetch/i.test(text)) {
+ return '鏃犳硶杩炴帴鏈嶅姟鍣紝璇锋鏌ョ綉缁滆繛鎺ワ紝鎴栫‘璁ゅ悗绔湇鍔℃槸鍚﹀凡鍚姩'
+ }
+ return text.length > 200 ? text.slice(0, 200) + '鈥�' : text
+ },
+ // 鏂囦欢鍚嶅崰浣嶇 X鏈� -> 褰撳墠鎶ヨ〃鏈熸湀浠斤紙濡傘�岀敓鎴恄X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx銆�->銆岀敓鎴恄8鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx銆嶏級
+ fileNameOf(item) {
+ const name = (item && item.fileName) || ''
+ const mm = parseInt((this.period || '').split('-')[1], 10)
+ if (!mm) return name
+ return name.replace(/X鏈�/g, mm + '鏈�')
+ },
+ 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; }
+}
+.wb-table th { background:#f5f7fa; }
+.wb-sec { background:#F7F9FC; font-weight:600; text-align:center; color:#303133; }
+.wb-label { color:#303133; }
+.wb-value { text-align:right; font-variant-numeric: tabular-nums; }
+.wb-yoy { text-align:right; color:#606266; }
+.wb-yoy.yoy-empty { color:#C0C4CC; }
+.wb-note { color:#E6A23C; font-size:12px; margin:2px 0 8px; }
+/* 寮傛鐢熸垚杩涘害寮圭獥 */
+.task-body { padding: 4px 2px; }
+.task-step { margin: 12px 0 4px; font-size: 14px; color:#303133; }
+.task-meta { margin: 2px 0; font-size: 12px; color:#909399; line-height: 1.6; }
+.task-error { margin: 8px 0 0; font-size: 13px; color:#F56C6C; word-break: break-all; }
+</style>
+
+<style>
+/* 棰勮鏁版嵁寮圭獥锛氬渾瑙� + 鍙�氳繃鏍囬鏍忔嫋鍔紙v-dialog-drag 鎸囦护锛夈��
+ 涓嶈兘鍐欒繘 scoped锛歟l-dialog 榛樿 append-to-body锛孌OM 鎸傚湪 body 涓婏紝scoped 閫夋嫨鍣ㄥ尮閰嶄笉鍒般�� */
+.pv-dialog { border-radius: 10px; overflow: hidden; }
+.pv-dialog .el-dialog__header { cursor: move; user-select: none; -webkit-user-select: none; }
+.pv-dialog .el-dialog__headerbtn { cursor: pointer; }
</style>
--
Gitblit v1.9.1