| | |
| | | <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> |
| | | </div> |
| | | <div class="operation-row"> |
| | | <div class="operation-item"> |
| | | <span class="label">年份</span> |
| | | <el-select v-model="adjustmentYear" style="width:120px" @change="loadAdjustments"> |
| | | <el-option v-for="item in availableAdjustmentYears" :key="item" :label="item + '年'" :value="item"></el-option> |
| | | </el-select> |
| | | </div> |
| | | <div v-if="operatorIsAdmin" class="operation-item"> |
| | | <el-button type="primary" icon="el-icon-plus" @click="openAdjustmentDialog()">新增调整</el-button> |
| | | </div> |
| | | </div> |
| | | <el-table :data="adjustments" border stripe size="small" style="margin-top:12px" v-loading="adjustmentLoading"> |
| | | <el-table-column prop="year" label="年份" width="90" align="center"></el-table-column> |
| | | <el-table-column prop="flowDate" label="日期" width="130" align="center"></el-table-column> |
| | | <el-table-column label="调整后国省道车流(万辆次)" width="230" align="right"> |
| | | <template slot-scope="s">{{ formatAdjustmentValue(s.row.adjustedValue) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="reason" label="调整原因" min-width="280" show-overflow-tooltip></el-table-column> |
| | | <el-table-column label="更新时间" min-width="170"><template slot-scope="s">{{ formatDateTime(s.row.updatedAt) }}</template></el-table-column> |
| | | <el-table-column v-if="operatorIsAdmin" label="操作" width="130" align="center"> |
| | | <template slot-scope="s"> |
| | | <el-button type="text" @click="openAdjustmentDialog(s.row)">编辑</el-button> |
| | | <el-button type="text" class="danger-text" @click="deleteAdjustment(s.row)">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-dialog :title="adjustmentForm.id ? '编辑调整' : '新增调整'" :visible.sync="adjustmentDialogVisible" width="560px" v-dialog-drag> |
| | | <el-form label-width="160px"> |
| | | <el-form-item label="年份"> |
| | | <el-select v-model="adjustmentForm.year" style="width:100%" @change="adjustmentForm.flowDate = ''"> |
| | | <el-option v-for="item in availableAdjustmentYears" :key="item" :label="item + '年'" :value="item"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="日期"> |
| | | <el-date-picker v-model="adjustmentForm.flowDate" type="date" value-format="yyyy-MM-dd" |
| | | style="width:100%" :picker-options="adjustmentDatePickerOptions" placeholder="选择日期"></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item label="调整后车流量(万辆次)"> |
| | | <el-input-number v-model="adjustmentForm.adjustedValue" :min="0" :precision="4" :step="0.0001" controls-position="right" style="width:100%"></el-input-number> |
| | | </el-form-item> |
| | | <el-form-item label="调整原因"> |
| | | <el-input v-model="adjustmentForm.reason" type="textarea" :rows="3" maxlength="500" show-word-limit></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div slot="footer"> |
| | | <el-button @click="adjustmentDialogVisible = false">取消</el-button> |
| | | <el-button type="primary" :loading="adjustmentSaving" @click="saveAdjustment">保存</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | </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> |
| | |
| | | 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, |
| | | adjustmentYear: new Date().getFullYear() - 1, |
| | | adjustments: [], |
| | | adjustmentLoading: false, |
| | | adjustmentDialogVisible: false, |
| | | adjustmentSaving: false, |
| | | adjustmentForm: { |
| | | id: null, |
| | | year: new Date().getFullYear() - 1, |
| | | flowDate: '', |
| | | adjustedValue: null, |
| | | reason: '' |
| | | } |
| | | } |
| | | }, |
| | | computed: { |
| | | holidayTypeLabel() { |
| | | const item = this.holidayTypeOptions.find(x => x.value === this.holidayType) |
| | | return item ? item.label : this.holidayType |
| | | }, |
| | | availableAdjustmentYears() { |
| | | const currentYear = new Date().getFullYear() |
| | | return this.years.filter(item => item < currentYear) |
| | | }, |
| | | adjustmentDatePickerOptions() { |
| | | return { |
| | | disabledDate: date => date.getFullYear() !== Number(this.adjustmentForm.year) |
| | | } |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadOverview() |
| | | this.loadObservationDates() |
| | | this.loadAdjustments() |
| | | }, |
| | | methods: { |
| | | previousDateString() { |
| | |
| | | }) |
| | | .catch(err => this.showError(err, '节假日数据加载失败')) |
| | | .finally(() => { this.loading = false }) |
| | | }, |
| | | loadAdjustments() { |
| | | this.adjustmentLoading = true |
| | | api.get('/holiday/adjustments', { params: { year: this.adjustmentYear } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || '加载失败') |
| | | this.adjustments = res.data || [] |
| | | }) |
| | | .catch(err => this.showError(err, '往年国省道数据调整加载失败')) |
| | | .finally(() => { this.adjustmentLoading = false }) |
| | | }, |
| | | openAdjustmentDialog(row) { |
| | | const source = row || {} |
| | | this.adjustmentForm = { |
| | | id: source.id || null, |
| | | year: source.year || this.adjustmentYear, |
| | | flowDate: source.flowDate || '', |
| | | adjustedValue: source.adjustedValue === undefined || source.adjustedValue === null |
| | | ? null : Number(source.adjustedValue), |
| | | reason: source.reason || '' |
| | | } |
| | | this.adjustmentDialogVisible = true |
| | | }, |
| | | saveAdjustment() { |
| | | const form = this.adjustmentForm |
| | | if (!form.year || !form.flowDate) { |
| | | this.$message.warning('请选择年份和日期') |
| | | return |
| | | } |
| | | if (form.adjustedValue === null || form.adjustedValue === undefined || Number(form.adjustedValue) < 0) { |
| | | this.$message.warning('请输入不低于 0 的调整后车流量') |
| | | return |
| | | } |
| | | if (!form.reason || !form.reason.trim()) { |
| | | this.$message.warning('请填写调整原因') |
| | | return |
| | | } |
| | | this.adjustmentSaving = true |
| | | api.post('/holiday/adjustments/save', { |
| | | id: form.id, |
| | | year: form.year, |
| | | flowDate: form.flowDate, |
| | | adjustedValue: form.adjustedValue, |
| | | reason: form.reason |
| | | }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || '保存失败') |
| | | this.adjustmentDialogVisible = false |
| | | this.adjustmentYear = form.year |
| | | this.loadAdjustments() |
| | | this.loadOverview() |
| | | this.$message.success('调整已保存') |
| | | }) |
| | | .catch(err => this.showError(err, '保存失败')) |
| | | .finally(() => { this.adjustmentSaving = false }) |
| | | }, |
| | | deleteAdjustment(row) { |
| | | this.$confirm('确定删除 ' + row.flowDate + ' 的国省道数据调整吗?', '删除确认', { type: 'warning' }) |
| | | .then(() => api.post('/holiday/adjustments/delete', null, { params: { id: row.id } })) |
| | | .then(res => { |
| | | if (res && res.code !== 200) throw new Error(res.message || '删除失败') |
| | | this.loadAdjustments() |
| | | this.loadOverview() |
| | | this.$message.success('调整已删除') |
| | | }) |
| | | .catch(err => { if (err !== 'cancel' && err !== 'close') this.showError(err, '删除失败') }) |
| | | }, |
| | | onFileChange(file) { |
| | | this.selectedFile = file.raw |
| | |
| | | .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, '删除失败') }) |
| | | }, |
| | | formatAdjustmentValue(value) { |
| | | if (value === null || value === undefined || value === '') return '—' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return number.toFixed(4) |
| | | }, |
| | | formatDateTime(value) { |
| | | if (!value) return '—' |
| | | return String(value).replace('T', ' ').slice(0, 19) |
| | | }, |
| | | 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> |