From 93b36c8fb52102d72b3c56cfec7d274c1378dbcf Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期一, 05 十月 2026 09:05:00 +0800
Subject: [PATCH] docs(version): record v1.0.0 release package rebuild

---
 traffic-audit-web/src/views/HolidayStat.vue |  186 ++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 184 insertions(+), 2 deletions(-)

diff --git a/traffic-audit-web/src/views/HolidayStat.vue b/traffic-audit-web/src/views/HolidayStat.vue
index 6f7f266..799267a 100644
--- a/traffic-audit-web/src/views/HolidayStat.vue
+++ b/traffic-audit-web/src/views/HolidayStat.vue
@@ -52,6 +52,10 @@
                 <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 class="operation-item">
+                <span class="label">璧板娍鍥�</span>
+                <el-button type="primary" icon="el-icon-picture-outline" :loading="trendLoading" @click="downloadTrend">涓嬭浇璧板娍鍥�</el-button>
+              </div>
             </div>
           </el-card>
 
@@ -77,6 +81,74 @@
               <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>
+
+          <el-card shadow="never" style="margin-top:14px">
+            <div slot="header">
+              <span>瑙傛祴绔欏垎鏃ヨ皟鏌ユ暟鎹�</span>
+              <span class="hint">鐢ㄤ簬鍘熷鏁版嵁鐣欏瓨锛屼笉涓庝笂鏂硅妭鍋囨棩缁熻鏁版嵁鍏宠仈</span>
+            </div>
+            <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=".xls,.xlsx" :on-change="onObservationFileChange" :on-remove="onObservationFileRemove"
+                           :file-list="observationFileList">
+                  <el-button type="primary" plain icon="el-icon-folder-opened">閫夋嫨鏂囦欢</el-button>
+                </el-upload>
+                <el-button type="primary" icon="el-icon-upload2" :loading="observationImporting"
+                           :disabled="!observationFile" @click="importObservation">瀵煎叆</el-button>
+              </div>
+            </div>
+            <div v-if="observationMessage" class="import-message">
+              <pre>{{ observationMessage }}</pre>
+            </div>
+            <el-table :data="observationDates" border stripe size="small" style="margin-top:12px" v-loading="observationLoading">
+              <el-table-column prop="obsDate" label="瑙傛祴鏃ユ湡" width="130" align="center"></el-table-column>
+              <el-table-column prop="stationCount" label="瑙傛祴绔欐暟" width="110" align="right"></el-table-column>
+              <el-table-column label="涓皬瀹㈡祦閲忓悎璁★紙涓囪締娆★級" width="210" align="right">
+                <template slot-scope="s">{{ formatWanVehicles(s.row.smallMediumPassengerTotal) }}</template>
+              </el-table-column>
+              <el-table-column prop="importedAt" label="鏈�杩戝鍏ユ椂闂�" min-width="180"></el-table-column>
+              <el-table-column label="鎿嶄綔" width="150" align="center">
+                <template slot-scope="s">
+                  <el-button type="text" @click="viewObservation(s.row.obsDate)">鏌ョ湅</el-button>
+                  <el-button type="text" class="danger-text" @click="deleteObservation(s.row.obsDate)">鍒犻櫎</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-card>
+
+          <el-dialog :title="'瑙傛祴绔欐槑缁� - ' + observationDialogDate" :visible.sync="observationDialogVisible" width="92%" top="5vh">
+            <el-table :data="observationRows" border stripe size="mini" height="540" v-loading="observationRowsLoading">
+              <el-table-column prop="obsDate" label="瑙傛祴鏃ユ湡" width="110" fixed="left" align="center"></el-table-column>
+              <el-table-column prop="stationCode" label="瑙傛祴绔欑紪鍙�" width="130" fixed="left"></el-table-column>
+              <el-table-column prop="stationName" label="瑙傛祴绔欏悕绉�" width="120"></el-table-column>
+              <el-table-column label="瑙傛祴閲岀▼" width="100" align="right"><template slot-scope="s">{{ formatPlain(s.row.mileage) }}</template></el-table-column>
+              <el-table-column prop="cityName" label="鍦板競鍚嶇О" width="140"></el-table-column>
+              <el-table-column prop="countyName" label="鍖哄幙鍚嶇О" width="110"></el-table-column>
+              <el-table-column prop="direction" label="琛岄┒鏂瑰悜" width="90"></el-table-column>
+              <el-table-column label="涓皬瀹㈡祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.smallMediumPassenger) }}</template></el-table-column>
+              <el-table-column label="澶у杞︽祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.largePassenger) }}</template></el-table-column>
+              <el-table-column label="灏忚揣杞︽祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.smallTruck) }}</template></el-table-column>
+              <el-table-column label="涓揣杞︽祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.mediumTruck) }}</template></el-table-column>
+              <el-table-column label="澶ц揣杞︽祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.largeTruck) }}</template></el-table-column>
+              <el-table-column label="鐗瑰ぇ璐ф祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.extraLargeTruck) }}</template></el-table-column>
+              <el-table-column label="闆嗚绠辨祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.containerTruck) }}</template></el-table-column>
+              <el-table-column label="鎽╂墭杞︽祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorcycle) }}</template></el-table-column>
+              <el-table-column label="鎷栨媺鏈烘祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.tractor) }}</template></el-table-column>
+              <el-table-column label="瀹㈣溅娴侀噺" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.passengerTotal) }}</template></el-table-column>
+              <el-table-column label="璐ц溅娴侀噺" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.truckTotal) }}</template></el-table-column>
+              <el-table-column label="姹借溅娴侀噺" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.carTotal) }}</template></el-table-column>
+              <el-table-column label="鏈哄姩杞︽祦閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorTotal) }}</template></el-table-column>
+              <el-table-column label="瀹㈣溅褰撻噺" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.passengerEquivalent) }}</template></el-table-column>
+              <el-table-column label="璐ц溅褰撻噺" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.truckEquivalent) }}</template></el-table-column>
+              <el-table-column label="姹借溅褰撻噺" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.carEquivalent) }}</template></el-table-column>
+              <el-table-column label="鏈哄姩杞﹀綋閲�" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorEquivalent) }}</template></el-table-column>
+              <el-table-column label="鏈哄姩杞﹂�熷害" width="110" align="right"><template slot-scope="s">{{ formatPlain(s.row.vehicleSpeed) }}</template></el-table-column>
+              <el-table-column label="鎷ユ尋搴�" width="100" align="right"><template slot-scope="s">{{ formatPlain(s.row.congestionDegree) }}</template></el-table-column>
+              <el-table-column prop="sourceFile" label="鏉ユ簮鏂囦欢鍚�" min-width="220" show-overflow-tooltip></el-table-column>
+            </el-table>
+          </el-dialog>
         </div>
       </el-main>
     </el-container>
@@ -92,7 +164,7 @@
   name: 'HolidayStat',
   data() {
     return {
-      years: [2024, 2025, 2026, 2027, 2028, 2029, 2030],
+      years: [2019, 2024, 2025, 2026, 2027, 2028, 2029, 2030],
       year: 2026,
       holidayType: 'NATIONAL_DAY',
       holidayTypeOptions: [],
@@ -101,11 +173,22 @@
       importing: false,
       exporting: false,
       bulletinLoading: false,
+      trendLoading: false,
       selectedFile: null,
       fileList: [],
       importMessage: '',
       allowHistoricalOverwrite: false,
-      bulletinDate: this.previousDateString()
+      bulletinDate: this.previousDateString(),
+      observationFile: null,
+      observationFileList: [],
+      observationImporting: false,
+      observationLoading: false,
+      observationMessage: '',
+      observationDates: [],
+      observationDialogVisible: false,
+      observationDialogDate: '',
+      observationRows: [],
+      observationRowsLoading: false
     }
   },
   computed: {
@@ -117,6 +200,7 @@
   mounted() {
     this.loadOptions()
     this.loadOverview()
+    this.loadObservationDates()
   },
   methods: {
     previousDateString() {
@@ -241,6 +325,16 @@
         .catch(err => this.handleBlobError(err, '蹇姤鐢熸垚澶辫触'))
         .finally(() => { this.bulletinLoading = false })
     },
+    downloadTrend() {
+      this.trendLoading = true
+      api.get('/holiday/trend', { params: { year: this.year, holidayType: this.holidayType }, responseType: 'blob', timeout: 180000 })
+        .then(res => {
+          this.downloadBlob(res, this.year + '骞�' + this.holidayTypeLabel + '鍋囨湡鍏ㄧ渷缁煎悎杩愯緭瀹㈡祦閲忚蛋鍔垮浘.docx')
+          this.$message.success('璧板娍鍥句笅杞芥垚鍔�')
+        })
+        .catch(err => this.handleBlobError(err, '璧板娍鍥剧敓鎴愬け璐�'))
+        .finally(() => { this.trendLoading = false })
+    },
     downloadBlob(data, fileName) {
       const blob = data instanceof Blob ? data : new Blob([data])
       const url = window.URL.createObjectURL(blob)
@@ -271,9 +365,95 @@
       const data = err && err.response && err.response.data
       this.$message.error((data && data.message) || (err && err.message) || fallback)
     },
+    onObservationFileChange(file) {
+      this.observationFile = file.raw
+      this.observationFileList = [file]
+      this.observationMessage = ''
+    },
+    onObservationFileRemove() {
+      this.observationFile = null
+      this.observationFileList = []
+    },
+    loadObservationDates() {
+      this.observationLoading = true
+      api.get('/observation/dates')
+        .then(res => {
+          if (res.code !== 200) throw new Error(res.message || '鍔犺浇澶辫触')
+          this.observationDates = res.data || []
+        })
+        .catch(err => this.showError(err, '瑙傛祴绔欐暟鎹姞杞藉け璐�'))
+        .finally(() => { this.observationLoading = false })
+    },
+    importObservation() {
+      if (!this.observationFile) return
+      const form = new FormData()
+      form.append('file', this.observationFile)
+      this.observationImporting = true
+      this.observationMessage = ''
+      api.post('/observation/import', form, { timeout: 300000, headers: { 'Content-Type': 'multipart/form-data' } })
+        .then(res => {
+          if (res.code !== 200) throw new Error(res.message || '瀵煎叆澶辫触')
+          const data = res.data || {}
+          const dates = (data.dates && data.dates.length) ? data.dates.join('銆�') : '鈥�'
+          const lines = ['瀵煎叆瀹屾垚锛�' + (data.totalRows || 0) + ' 鏉★紝瑙傛祴绔� ' + (data.stationCount || 0)
+            + ' 涓紝鏃ユ湡 ' + dates + '锛岃烦杩囩┖鐧� ' + (data.skippedRows || 0) + ' 鏉°��']
+          if (data.errors && data.errors.length) {
+            lines.push('澶辫触鏄庣粏锛�')
+            data.errors.forEach(item => lines.push(item))
+          }
+          this.observationMessage = lines.join('\n')
+          this.$message.success('瑙傛祴绔欐暟鎹鍏ュ畬鎴�')
+          this.observationFile = null
+          this.observationFileList = []
+          this.loadObservationDates()
+        })
+        .catch(err => this.showError(err, '瑙傛祴绔欐暟鎹鍏ュけ璐�'))
+        .finally(() => { this.observationImporting = false })
+    },
+    viewObservation(date) {
+      this.observationDialogDate = date
+      this.observationDialogVisible = true
+      this.observationRowsLoading = true
+      this.observationRows = []
+      api.get('/observation/rows', { params: { date: date, limit: 2000 } })
+        .then(res => {
+          if (res.code !== 200) throw new Error(res.message || '鍔犺浇澶辫触')
+          this.observationRows = res.data || []
+        })
+        .catch(err => this.showError(err, '瑙傛祴绔欐槑缁嗗姞杞藉け璐�'))
+        .finally(() => { this.observationRowsLoading = false })
+    },
+    deleteObservation(date) {
+      this.$confirm('纭畾鍒犻櫎 ' + date + ' 鐨勮娴嬬珯鏁版嵁鍚楋紵', '鍒犻櫎纭', { type: 'warning' })
+        .then(() => api.post('/observation/delete', null, { params: { date: date } }))
+        .then(res => {
+          if (res && res.code !== 200) throw new Error(res.message || '鍒犻櫎澶辫触')
+          this.$message.success('宸插垹闄�')
+          this.loadObservationDates()
+        })
+        .catch(err => { if (err !== 'cancel' && err !== 'close') this.showError(err, '鍒犻櫎澶辫触') })
+    },
     formatNumber(value) {
       if (value === null || value === undefined || value === '') return '鈥�'
       return Number(value).toFixed(2)
+    },
+    formatWanVehicles(value) {
+      if (value === null || value === undefined || value === '') return '鈥�'
+      const number = Number(value)
+      if (!Number.isFinite(number)) return value
+      return (number / 10000).toFixed(4)
+    },
+    formatInteger(value) {
+      if (value === null || value === undefined || value === '') return '鈥�'
+      const number = Number(value)
+      if (!Number.isFinite(number)) return value
+      return String(Math.round(number))
+    },
+    formatPlain(value) {
+      if (value === null || value === undefined || value === '') return '鈥�'
+      const number = Number(value)
+      if (!Number.isFinite(number)) return value
+      return String(number)
     },
     formatCoefficient(value) {
       if (value === null || value === undefined || value === '') return '鈥�'
@@ -299,4 +479,6 @@
 .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; }
+.hint { margin-left: 10px; color: #909399; font-size: 12px; }
+.danger-text { color: #f56c6c; }
 </style>

--
Gitblit v1.9.1