From 37f52097847f9025589c79034be2454432c55433 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期五, 02 十月 2026 10:12:27 +0800
Subject: [PATCH] docs(交接): 记录关机前开发服务已停止
---
traffic-audit-web/src/views/HolidayStat.vue | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 302 insertions(+), 0 deletions(-)
diff --git a/traffic-audit-web/src/views/HolidayStat.vue b/traffic-audit-web/src/views/HolidayStat.vue
new file mode 100644
index 0000000..6f7f266
--- /dev/null
+++ b/traffic-audit-web/src/views/HolidayStat.vue
@@ -0,0 +1,302 @@
+<template>
+ <el-container direction="vertical">
+ <app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 涓撻」缁熻"></app-header>
+ <el-container>
+ <el-aside width="200px">
+ <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="/explain">浼佷笟瑙i噴</el-menu-item>
+ <el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
+ <el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
+ <el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
+ <el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
+ </el-aside>
+ <el-main>
+ <div class="page-scroll">
+ <h3>涓撻」缁熻 - 鑺傚亣鏃�</h3>
+ <div class="toolbar">
+ <el-select v-model="year" style="width:120px" @change="loadOverview">
+ <el-option v-for="item in years" :key="item" :label="item + '骞�'" :value="item"></el-option>
+ </el-select>
+ <el-select v-model="holidayType" style="width:150px" @change="loadOverview">
+ <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+ </el-select>
+ <el-button icon="el-icon-refresh" @click="loadOverview">鍒锋柊</el-button>
+ <el-button icon="el-icon-download" @click="downloadTemplate">涓嬭浇妯℃澘</el-button>
+ </div>
+
+ <el-card shadow="never" class="operation-card">
+ <div class="operation-row">
+ <div class="operation-item">
+ <span class="label">瀵煎叆鏁版嵁</span>
+ <el-upload action="#" :auto-upload="false" :show-file-list="true" :limit="1"
+ accept=".xlsx,.xls" :on-change="onFileChange" :on-remove="onFileRemove" :file-list="fileList">
+ <el-button type="primary" plain icon="el-icon-folder-opened">閫夋嫨鏂囦欢</el-button>
+ </el-upload>
+ <el-button type="primary" icon="el-icon-upload2" :loading="importing" :disabled="!selectedFile" @click="importData">瀵煎叆</el-button>
+ </div>
+ <div class="operation-item">
+ <span class="label">瀵煎嚭鏁版嵁</span>
+ <el-button type="success" icon="el-icon-download" :loading="exporting" @click="exportData">瀵煎嚭 Excel</el-button>
+ </div>
+ <div class="operation-item">
+ <span class="label">瀹㈡祦蹇姤</span>
+ <el-date-picker v-model="bulletinDate" type="date" value-format="yyyy-MM-dd" style="width:160px" placeholder="閫夋嫨鏃ユ湡"></el-date-picker>
+ <el-button type="warning" icon="el-icon-document" :loading="bulletinLoading" @click="downloadBulletin">涓嬭浇蹇姤</el-button>
+ </div>
+ </div>
+ </el-card>
+
+ <div v-if="importMessage" class="import-message">
+ <pre>{{ importMessage }}</pre>
+ </div>
+
+ <el-card shadow="never" style="margin-top:14px">
+ <div slot="header">{{ year }}骞磠{ holidayTypeLabel }}鑺傚亣鏃ユ暟鎹�</div>
+ <el-table :data="rows" border stripe size="small" v-loading="loading" style="width:100%">
+ <el-table-column prop="dayIndex" label="绗嚑鏃�" width="80" align="center"></el-table-column>
+ <el-table-column prop="date" label="鏃ユ湡" width="120" align="center"></el-table-column>
+ <el-table-column label="閾佽矾锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.rail) }}</template></el-table-column>
+ <el-table-column label="姘戣埅锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.civilAviation) }}</template></el-table-column>
+ <el-table-column label="閬撹矾锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.road) }}</template></el-table-column>
+ <el-table-column label="姘磋矾锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.water) }}</template></el-table-column>
+ <el-table-column label="楂橀�熻溅娴侊紙涓囪締娆★級" width="150" align="right"><template slot-scope="s">{{ formatNumber(s.row.expressway) }}</template></el-table-column>
+ <el-table-column label="鍥界渷閬撹溅娴侊紙涓囪締娆★級" width="160" align="right"><template slot-scope="s">{{ formatNumber(s.row.nationalProvincial) }}</template></el-table-column>
+ <el-table-column label="鍥界渷閬撶幆姣斿熀鏈燂紙涓囪締娆★級" width="190" align="right"><template slot-scope="s">{{ formatNumber(s.row.comparisonBase) }}</template></el-table-column>
+ <el-table-column label="鍥界渷閬撹溅娴佺幆姣�" width="140" align="right"><template slot-scope="s">{{ formatRatio(s.row.nationalProvincialRingRatio) }}</template></el-table-column>
+ <el-table-column label="瀹㈣繍绯绘暟" width="100" align="right"><template slot-scope="s">{{ formatCoefficient(s.row.coefficient) }}</template></el-table-column>
+ <el-table-column label="闈炶惀涓氭�т汉鍛樻祦鍔ㄩ噺锛堜竾浜烘锛�" min-width="200" align="right"><template slot-scope="s">{{ formatNumber(s.row.nonBusinessPersonFlow) }}</template></el-table-column>
+ <el-table-column label="鎬讳汉鍛樻祦鍔ㄩ噺锛堜竾浜烘锛�" min-width="180" align="right"><template slot-scope="s">{{ formatNumber(s.row.totalPersonFlow) }}</template></el-table-column>
+ </el-table>
+ </el-card>
+ </div>
+ </el-main>
+ </el-container>
+ </el-container>
+</template>
+
+<script>
+import api from '@/api'
+import AppHeader from '@/components/AppHeader.vue'
+
+export default {
+ components: { AppHeader },
+ name: 'HolidayStat',
+ data() {
+ return {
+ years: [2024, 2025, 2026, 2027, 2028, 2029, 2030],
+ year: 2026,
+ holidayType: 'NATIONAL_DAY',
+ holidayTypeOptions: [],
+ rows: [],
+ loading: false,
+ importing: false,
+ exporting: false,
+ bulletinLoading: false,
+ selectedFile: null,
+ fileList: [],
+ importMessage: '',
+ allowHistoricalOverwrite: false,
+ bulletinDate: this.previousDateString()
+ }
+ },
+ computed: {
+ holidayTypeLabel() {
+ const item = this.holidayTypeOptions.find(x => x.value === this.holidayType)
+ return item ? item.label : this.holidayType
+ }
+ },
+ mounted() {
+ this.loadOptions()
+ this.loadOverview()
+ },
+ methods: {
+ previousDateString() {
+ const date = new Date()
+ date.setDate(date.getDate() - 1)
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return date.getFullYear() + '-' + month + '-' + day
+ },
+ loadOptions() {
+ api.get('/holiday/config/options').then(res => {
+ const map = (res.data && res.data.holidayTypes) || {}
+ this.holidayTypeOptions = Object.keys(map).map(key => ({ value: key, label: map[key] }))
+ })
+ },
+ loadOverview() {
+ this.loading = true
+ api.get('/holiday/overview', { params: { year: this.year, holidayType: this.holidayType } })
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '鍔犺浇澶辫触')
+ this.rows = (res.data && res.data.rows) || []
+ })
+ .catch(err => this.showError(err, '鑺傚亣鏃ユ暟鎹姞杞藉け璐�'))
+ .finally(() => { this.loading = false })
+ },
+ onFileChange(file) {
+ this.selectedFile = file.raw
+ this.fileList = [file]
+ this.importMessage = ''
+ },
+ onFileRemove() {
+ this.selectedFile = null
+ this.fileList = []
+ },
+ importData(allowHistoricalOverwrite) {
+ if (!this.selectedFile) return
+ const allowOverwrite = allowHistoricalOverwrite === true || this.allowHistoricalOverwrite === true
+ const form = new FormData()
+ form.append('file', this.selectedFile)
+ form.append('year', this.year)
+ form.append('holidayType', this.holidayType)
+ form.append('allowHistoricalOverwrite', allowOverwrite ? 'true' : 'false')
+ this.importing = true
+ this.importMessage = ''
+ api.post('/holiday/import', form, { timeout: 120000, headers: { 'Content-Type': 'multipart/form-data' } })
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '瀵煎叆澶辫触')
+ const data = res.data || {}
+ if (data.confirmationRequired) {
+ const changes = data.historicalChanges || []
+ this.importMessage = ['妫�娴嬪埌鍘嗗彶鏃ユ湡鐨勬暟鎹彂鐢熷彉鍖栵紝灏氭湭瀵煎叆銆傝纭鏄惁鍏佽瑕嗙洊锛�'].concat(changes).join('\n')
+ this.$message.warning('妫�娴嬪埌鍘嗗彶鏁版嵁鍙戠敓鍙樺寲锛岃纭鏄惁鍏佽瀵煎叆')
+ this.confirmHistoricalImport(changes)
+ return
+ }
+ const lines = [
+ '瀵煎叆瀹屾垚锛氬娴� ' + (data.passengerRows || 0) + ' 鏉★紝杞︽祦 ' + (data.vehicleRows || 0) + ' 鏉★紝璺宠繃绌虹櫧 ' + (data.skippedRows || 0) + ' 鏉°��'
+ ]
+ if (data.errors && data.errors.length) {
+ lines.push('澶辫触鏄庣粏锛�')
+ data.errors.forEach(item => lines.push(item))
+ }
+ this.importMessage = lines.join('\n')
+ this.allowHistoricalOverwrite = false
+ this.$message.success('瀵煎叆瀹屾垚')
+ this.loadOverview()
+ })
+ .catch(err => {
+ this.allowHistoricalOverwrite = false
+ this.showError(err, '瀵煎叆澶辫触')
+ })
+ .finally(() => { this.importing = false })
+ },
+ confirmHistoricalImport(changes) {
+ const rows = (changes && changes.length ? changes : ['鍘嗗彶鏁版嵁鍙戠敓鍙樺寲']).map(this.escapeHtml)
+ const html = '<div>妫�娴嬪埌宸插彂甯冩棩鏈熺殑鏁版嵁鍙戠敓鍙樺寲銆傚厑璁稿鍏ュ皢瑕嗙洊杩欎簺鍘嗗彶鏁版嵁銆�</div>'
+ + '<pre style="max-height:320px;overflow:auto;white-space:pre-wrap;margin-top:10px;background:#f7f7f7;padding:8px;border:1px solid #e4e7ed;">'
+ + rows.join('\n') + '</pre>'
+ this.$confirm(html, '鍘嗗彶鏁版嵁鍙戠敓鍙樺寲', {
+ dangerouslyUseHTMLString: true,
+ confirmButtonText: '鍏佽瀵煎叆',
+ cancelButtonText: '涓嶅厑璁�',
+ type: 'warning',
+ distinguishCancelAndClose: true
+ }).then(() => {
+ this.allowHistoricalOverwrite = true
+ this.importData(true)
+ }).catch(() => {
+ this.allowHistoricalOverwrite = false
+ })
+ },
+ escapeHtml(value) {
+ return String(value === null || value === undefined ? '' : value)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ },
+ exportData() {
+ this.exporting = true
+ api.get('/holiday/export', { params: { year: this.year, holidayType: this.holidayType }, responseType: 'blob', timeout: 120000 })
+ .then(res => { this.downloadBlob(res, this.year + '骞�' + this.holidayTypeLabel + '鏁版嵁.xlsx'); this.$message.success('瀵煎嚭鎴愬姛') })
+ .catch(err => this.handleBlobError(err, '瀵煎嚭澶辫触'))
+ .finally(() => { this.exporting = false })
+ },
+ downloadTemplate() {
+ const templatePath = '涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鏁版嵁.xlsx'
+ const encodedPath = templatePath.split('/').map(encodeURIComponent).join('/')
+ api.get('/templates/' + encodedPath, { responseType: 'blob' })
+ .then(res => { this.downloadBlob(res, '2026骞村崄涓�鏁版嵁.xlsx'); this.$message.success('妯℃澘涓嬭浇鎴愬姛') })
+ .catch(err => this.handleBlobError(err, '妯℃澘涓嬭浇澶辫触'))
+ },
+ downloadBulletin() {
+ this.bulletinLoading = true
+ api.get('/holiday/bulletin', { params: { date: this.bulletinDate }, responseType: 'blob', timeout: 120000 })
+ .then(res => {
+ const day = Number((this.bulletinDate || '').slice(-2))
+ this.downloadBlob(res, '婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ワ紙10鏈�' + day + '鏃ワ級.docx')
+ this.$message.success('蹇姤涓嬭浇鎴愬姛')
+ })
+ .catch(err => this.handleBlobError(err, '蹇姤鐢熸垚澶辫触'))
+ .finally(() => { this.bulletinLoading = false })
+ },
+ downloadBlob(data, fileName) {
+ const blob = data instanceof Blob ? data : new Blob([data])
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ window.URL.revokeObjectURL(url)
+ },
+ handleBlobError(err, fallback) {
+ if (err && err.response && err.response.data instanceof Blob) {
+ err.response.data.text().then(text => {
+ try {
+ const parsed = JSON.parse(text)
+ this.$message.error(parsed.message || fallback)
+ } catch (e) {
+ this.$message.error(fallback)
+ }
+ })
+ return
+ }
+ this.showError(err, fallback)
+ },
+ showError(err, fallback) {
+ if (err && err._toast) return
+ const data = err && err.response && err.response.data
+ this.$message.error((data && data.message) || (err && err.message) || fallback)
+ },
+ formatNumber(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ return Number(value).toFixed(2)
+ },
+ formatCoefficient(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ const number = Number(value)
+ if (!Number.isFinite(number)) return value
+ return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '')
+ },
+ formatRatio(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ const number = Number(value)
+ if (!Number.isFinite(number)) return value
+ return (number * 100).toFixed(2) + '%'
+ }
+ }
+}
+</script>
+
+<style scoped>
+.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
+.operation-card { margin-top: 12px; }
+.operation-row { display: flex; gap: 36px; align-items: flex-start; flex-wrap: wrap; }
+.operation-item { display: flex; align-items: center; gap: 8px; }
+.operation-item .label { color: #606266; font-size: 14px; }
+.import-message { margin-top: 12px; padding: 10px 12px; background: #f5f7fa; border: 1px solid #e4e7ed; border-radius: 4px; }
+.import-message pre { margin: 0; white-space: pre-wrap; font-family: inherit; color: #606266; }
+</style>
--
Gitblit v1.9.1