| | |
| | | <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> |
| | | |
| | |
| | | <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> |
| | |
| | | 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: [], |
| | |
| | | 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: { |
| | |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadOverview() |
| | | this.loadObservationDates() |
| | | }, |
| | | methods: { |
| | | previousDateString() { |
| | |
| | | .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) |
| | |
| | | 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 '—' |
| | |
| | | .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> |