xyc
3 天以前 9ee7acd9d16e59224ee5184c89374f06d6592ad3
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>