|
@@ -1,29 +1,31 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="page">
|
|
<div class="page">
|
|
|
<el-form :inline="true" class="query-form" ref="searchForm" :model="searchForm" @keyup.enter.native="refreshList()" @submit.native.prevent>
|
|
<el-form :inline="true" class="query-form" ref="searchForm" :model="searchForm" @keyup.enter.native="refreshList()" @submit.native.prevent>
|
|
|
-<!-- <el-form-item label="站点" prop="siteCode">-->
|
|
|
|
|
-<!-- <el-select v-model="searchForm.siteCode" placeholder="全部" clearable style="width: 150px" @change="handleSiteChange">-->
|
|
|
|
|
-<!-- <el-option v-for="item in options.sites" :key="item.siteCode" :label="item.siteName" :value="item.siteCode"></el-option>-->
|
|
|
|
|
-<!-- </el-select>-->
|
|
|
|
|
-<!-- </el-form-item>-->
|
|
|
|
|
<el-form-item label="物料" prop="materialName">
|
|
<el-form-item label="物料" prop="materialName">
|
|
|
<el-input v-model="searchForm.materialName" placeholder="输入关键字模糊查询" clearable style="width: 180px"></el-input>
|
|
<el-input v-model="searchForm.materialName" placeholder="输入关键字模糊查询" clearable style="width: 180px"></el-input>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="分类" prop="category">
|
|
<el-form-item label="分类" prop="category">
|
|
|
<!-- filterable:输入文字即可从选项里过滤,不输入时仍按下拉选择 -->
|
|
<!-- filterable:输入文字即可从选项里过滤,不输入时仍按下拉选择 -->
|
|
|
<el-select v-model="searchForm.category" placeholder="全部" clearable filterable style="width: 140px" @change="handleCategoryChange">
|
|
<el-select v-model="searchForm.category" placeholder="全部" clearable filterable style="width: 140px" @change="handleCategoryChange">
|
|
|
- <el-option v-for="item in options.categories" :key="item" :label="item" :value="item"></el-option>
|
|
|
|
|
|
|
+ <el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item"></el-option>
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="省份" prop="province">
|
|
<el-form-item label="省份" prop="province">
|
|
|
<el-select v-model="searchForm.province" placeholder="全部" clearable filterable style="width: 120px" @change="handleProvinceChange">
|
|
<el-select v-model="searchForm.province" placeholder="全部" clearable filterable style="width: 120px" @change="handleProvinceChange">
|
|
|
- <el-option v-for="item in options.provinces" :key="item" :label="item" :value="item"></el-option>
|
|
|
|
|
|
|
+ <el-option v-for="item in provinceOptions" :key="item" :label="item" :value="item"></el-option>
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
- <!-- 城市按所选省份级联;站点没有城市信息时该下拉为空 -->
|
|
|
|
|
|
|
+ <!-- 城市必须先选省份:未选省份时禁用,选项为空 -->
|
|
|
<el-form-item label="城市" prop="city">
|
|
<el-form-item label="城市" prop="city">
|
|
|
- <el-select v-model="searchForm.city" placeholder="全部" clearable filterable style="width: 120px">
|
|
|
|
|
- <el-option v-for="item in options.cities" :key="item" :label="item" :value="item"></el-option>
|
|
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="searchForm.city"
|
|
|
|
|
+ :disabled="!searchForm.province"
|
|
|
|
|
+ :placeholder="searchForm.province ? '全部' : '请先选择省份'"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ filterable
|
|
|
|
|
+ style="width: 120px"
|
|
|
|
|
+ @change="handleCityChange">
|
|
|
|
|
+ <el-option v-for="item in cityOptions" :key="item" :label="item" :value="item"></el-option>
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<!-- 数据日期区间:数组由 httpRequest 序列化成 priceDates[0]/priceDates[1] -->
|
|
<!-- 数据日期区间:数组由 httpRequest 序列化成 priceDates[0]/priceDates[1] -->
|
|
@@ -142,6 +144,16 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="range-tip">采集在后台执行,关闭弹窗不会中断;可以稍后重新打开查看进度。</div>
|
|
<div class="range-tip">采集在后台执行,关闭弹窗不会中断;可以稍后重新打开查看进度。</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <!-- 状态查询中:先占位,避免日期表单先闪出来让人以为任务没在跑 -->
|
|
|
|
|
+ <div v-else-if="rangeStatusLoading" class="range-status-loading">
|
|
|
|
|
+ <el-icon class="is-loading"><Loading /></el-icon>
|
|
|
|
|
+ <span>正在获取采集状态…</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 状态查询失败:不显示表单,否则用户可能重复提交一个正在跑的任务 -->
|
|
|
|
|
+ <div v-else-if="rangeStatusError" class="range-status-error">
|
|
|
|
|
+ <div class="range-line">{{ rangeStatusError }}</div>
|
|
|
|
|
+ <el-button type="primary" link @click="fetchRangeStatus()">重试</el-button>
|
|
|
|
|
+ </div>
|
|
|
<!-- 未在采集:选择区间后提交 -->
|
|
<!-- 未在采集:选择区间后提交 -->
|
|
|
<div v-else class="range-form">
|
|
<div v-else class="range-form">
|
|
|
<el-form label-width="80px">
|
|
<el-form label-width="80px">
|
|
@@ -174,7 +186,7 @@
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
<el-button @click="rangeDialogVisible = false">关闭</el-button>
|
|
<el-button @click="rangeDialogVisible = false">关闭</el-button>
|
|
|
<el-button
|
|
<el-button
|
|
|
- v-if="!rangeRunning"
|
|
|
|
|
|
|
+ v-if="!rangeRunning && !rangeStatusLoading && !rangeStatusError"
|
|
|
type="primary"
|
|
type="primary"
|
|
|
:loading="rangeSubmitting"
|
|
:loading="rangeSubmitting"
|
|
|
:disabled="!rangeStartDate || !rangeEndDate"
|
|
:disabled="!rangeStartDate || !rangeEndDate"
|
|
@@ -195,19 +207,21 @@
|
|
|
return {
|
|
return {
|
|
|
// 查询条件;priceDates 为空数组时后端不会按日期过滤
|
|
// 查询条件;priceDates 为空数组时后端不会按日期过滤
|
|
|
searchForm: {
|
|
searchForm: {
|
|
|
- siteCode: '',
|
|
|
|
|
materialName: '',
|
|
materialName: '',
|
|
|
category: '',
|
|
category: '',
|
|
|
province: '',
|
|
province: '',
|
|
|
city: '',
|
|
city: '',
|
|
|
priceDates: []
|
|
priceDates: []
|
|
|
},
|
|
},
|
|
|
- // 下拉选项,全部由后端 /options 返回
|
|
|
|
|
|
|
+ // 下拉选项一次拉全,切换条件只在本地做级联收窄,不再请求后端;
|
|
|
|
|
+ // 后三张关系表用于「分类↔省份」双向收窄和「分类+省份→城市」
|
|
|
options: {
|
|
options: {
|
|
|
- sites: [],
|
|
|
|
|
categories: [],
|
|
categories: [],
|
|
|
provinces: [],
|
|
provinces: [],
|
|
|
- cities: []
|
|
|
|
|
|
|
+ citiesByProvince: {},
|
|
|
|
|
+ provincesByCategory: {},
|
|
|
|
|
+ categoriesByProvince: {},
|
|
|
|
|
+ citiesByCategoryProvince: {}
|
|
|
},
|
|
},
|
|
|
dataList: [],
|
|
dataList: [],
|
|
|
tablePage: {
|
|
tablePage: {
|
|
@@ -226,6 +240,10 @@
|
|
|
rangeSubmitting: false,
|
|
rangeSubmitting: false,
|
|
|
// 后端锁或本进程标记为执行中
|
|
// 后端锁或本进程标记为执行中
|
|
|
rangeRunning: false,
|
|
rangeRunning: false,
|
|
|
|
|
+ // 弹窗打开后正在查询后端状态;查询完成前不渲染日期表单,避免误导
|
|
|
|
|
+ rangeStatusLoading: false,
|
|
|
|
|
+ // 状态查询失败的提示文案;有值时同样不渲染表单
|
|
|
|
|
+ rangeStatusError: '',
|
|
|
// 最近一次区间采集日志(material_price_sync_log)
|
|
// 最近一次区间采集日志(material_price_sync_log)
|
|
|
rangeLog: {
|
|
rangeLog: {
|
|
|
batchNo: '',
|
|
batchNo: '',
|
|
@@ -242,10 +260,55 @@
|
|
|
// 已经提示过结果的批次号
|
|
// 已经提示过结果的批次号
|
|
|
finishedBatchNo: '',
|
|
finishedBatchNo: '',
|
|
|
// 进度轮询定时器
|
|
// 进度轮询定时器
|
|
|
- pollTimer: null
|
|
|
|
|
|
|
+ pollTimer: null,
|
|
|
|
|
+ // keep-alive 下首次进入会先触发 mounted 再触发 activated,用它跳过重复加载
|
|
|
|
|
+ activatedOnce: false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
|
|
|
+ // 省份选项:选了分类后只保留该分类下有数据的省份。
|
|
|
|
|
+ // 关系表整体为空说明后端仍在跑旧版本,此时不做收窄,避免把省份列表清空
|
|
|
|
|
+ provinceOptions () {
|
|
|
|
|
+ const category = this.searchForm.category
|
|
|
|
|
+ const map = this.options.provincesByCategory || {}
|
|
|
|
|
+ if (!category || Object.keys(map).length === 0) {
|
|
|
|
|
+ return this.options.provinces
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[category] || []
|
|
|
|
|
+ },
|
|
|
|
|
+ // 分类选项:选了省份按省份收窄;省市都选了再按城市收窄。
|
|
|
|
|
+ // 城市本来就来自某个分类,所以不会把当前分类过滤掉
|
|
|
|
|
+ categoryOptions () {
|
|
|
|
|
+ const province = this.searchForm.province
|
|
|
|
|
+ const city = this.searchForm.city
|
|
|
|
|
+ const byProvinceMap = this.options.categoriesByProvince || {}
|
|
|
|
|
+ let list = this.options.categories
|
|
|
|
|
+ if (province && Object.keys(byProvinceMap).length > 0) {
|
|
|
|
|
+ list = byProvinceMap[province] || []
|
|
|
|
|
+ }
|
|
|
|
|
+ if (province && city) {
|
|
|
|
|
+ const crossMap = this.options.citiesByCategoryProvince || {}
|
|
|
|
|
+ list = list.filter((category) => {
|
|
|
|
|
+ const byProvince = crossMap[category]
|
|
|
|
|
+ const cities = byProvince && byProvince[province]
|
|
|
|
|
+ return !!cities && cities.indexOf(city) !== -1
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ return list
|
|
|
|
|
+ },
|
|
|
|
|
+ // 城市选项:未选省份时为空(下拉同时禁用);选了分类则只留该分类在该省的城市。
|
|
|
|
|
+ // 交叉关系表为空时退回省份维度,同样是为了兼容旧后端
|
|
|
|
|
+ cityOptions () {
|
|
|
|
|
+ const province = this.searchForm.province
|
|
|
|
|
+ if (!province) return []
|
|
|
|
|
+ const category = this.searchForm.category
|
|
|
|
|
+ const crossMap = this.options.citiesByCategoryProvince || {}
|
|
|
|
|
+ if (category && Object.keys(crossMap).length > 0) {
|
|
|
|
|
+ const byProvince = crossMap[category]
|
|
|
|
|
+ return (byProvince && byProvince[province]) || []
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.options.citiesByProvince[province] || []
|
|
|
|
|
+ },
|
|
|
// 进度条百分比:执行中最多显示 99%,避免没完全结束就显示 100%
|
|
// 进度条百分比:执行中最多显示 99%,避免没完全结束就显示 100%
|
|
|
rangePercent () {
|
|
rangePercent () {
|
|
|
const total = this.rangeLog.progressTotal || 0
|
|
const total = this.rangeLog.progressTotal || 0
|
|
@@ -294,8 +357,13 @@
|
|
|
// 页面刷新时可能刚好有区间任务在跑,恢复轮询
|
|
// 页面刷新时可能刚好有区间任务在跑,恢复轮询
|
|
|
this.resumeRangeWatch()
|
|
this.resumeRangeWatch()
|
|
|
},
|
|
},
|
|
|
- // 项目启用了 keep-alive,从其它页面切回来时刷新一次
|
|
|
|
|
|
|
+ // 项目启用了 keep-alive,从其它页面切回来时刷新一次;
|
|
|
|
|
+ // 首次进入时 mounted 已经加载过,这里跳过,避免同时发两遍列表请求
|
|
|
activated () {
|
|
activated () {
|
|
|
|
|
+ if (!this.activatedOnce) {
|
|
|
|
|
+ this.activatedOnce = true
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
this.refreshList()
|
|
this.refreshList()
|
|
|
this.resumeRangeWatch()
|
|
this.resumeRangeWatch()
|
|
|
},
|
|
},
|
|
@@ -308,38 +376,53 @@
|
|
|
this.stopPolling()
|
|
this.stopPolling()
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
- // 拉取下拉选项:传 siteCode/category/province 做级联收窄
|
|
|
|
|
|
|
+ // 拉取下拉选项:不带参数,一次拿全量(后端有 10 分钟缓存)
|
|
|
loadOptions () {
|
|
loadOptions () {
|
|
|
- this.materialPriceService.options({
|
|
|
|
|
- siteCode: this.searchForm.siteCode,
|
|
|
|
|
- category: this.searchForm.category,
|
|
|
|
|
- province: this.searchForm.province
|
|
|
|
|
- }).then((data) => {
|
|
|
|
|
|
|
+ this.materialPriceService.options().then((data) => {
|
|
|
this.options = {
|
|
this.options = {
|
|
|
- sites: (data && data.sites) || [],
|
|
|
|
|
categories: (data && data.categories) || [],
|
|
categories: (data && data.categories) || [],
|
|
|
provinces: (data && data.provinces) || [],
|
|
provinces: (data && data.provinces) || [],
|
|
|
- cities: (data && data.cities) || []
|
|
|
|
|
|
|
+ citiesByProvince: (data && data.citiesByProvince) || {},
|
|
|
|
|
+ provincesByCategory: (data && data.provincesByCategory) || {},
|
|
|
|
|
+ categoriesByProvince: (data && data.categoriesByProvince) || {},
|
|
|
|
|
+ citiesByCategoryProvince: (data && data.citiesByCategoryProvince) || {}
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
- // 换站点后分类、省份、城市都失效,清空后重新拉选项
|
|
|
|
|
- handleSiteChange () {
|
|
|
|
|
- this.searchForm.category = ''
|
|
|
|
|
- this.searchForm.province = ''
|
|
|
|
|
- this.searchForm.city = ''
|
|
|
|
|
- this.loadOptions()
|
|
|
|
|
- },
|
|
|
|
|
- // 换分类后省份、城市随之收窄
|
|
|
|
|
|
|
+ // 换分类:先收窄省份,当前省份在新分类下没有数据就一并清掉;
|
|
|
|
|
+ // 再检查城市是否还落在「新分类 + 当前省份」的城市集合里。
|
|
|
|
|
+ // 关系表为空(旧后端)时跳过省份校验,避免误清已选省份
|
|
|
handleCategoryChange () {
|
|
handleCategoryChange () {
|
|
|
- this.searchForm.province = ''
|
|
|
|
|
- this.searchForm.city = ''
|
|
|
|
|
- this.loadOptions()
|
|
|
|
|
|
|
+ const category = this.searchForm.category
|
|
|
|
|
+ const province = this.searchForm.province
|
|
|
|
|
+ const map = this.options.provincesByCategory || {}
|
|
|
|
|
+ if (category && province && Object.keys(map).length > 0) {
|
|
|
|
|
+ const provinces = map[category] || []
|
|
|
|
|
+ if (provinces.indexOf(province) === -1) {
|
|
|
|
|
+ this.searchForm.province = ''
|
|
|
|
|
+ this.searchForm.city = ''
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.searchForm.city && this.cityOptions.indexOf(this.searchForm.city) === -1) {
|
|
|
|
|
+ this.searchForm.city = ''
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
- // 换省份后城市列表变化,先清空已选城市
|
|
|
|
|
|
|
+ // 换省份:城市只属于原省份,必须清空;当前分类在该省没有数据也一起清掉
|
|
|
handleProvinceChange () {
|
|
handleProvinceChange () {
|
|
|
|
|
+ const category = this.searchForm.category
|
|
|
this.searchForm.city = ''
|
|
this.searchForm.city = ''
|
|
|
- this.loadOptions()
|
|
|
|
|
|
|
+ if (category && this.categoryOptions.indexOf(category) === -1) {
|
|
|
|
|
+ this.searchForm.category = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 换城市:按「省份 + 城市」反向收窄分类,当前分类不再成立就清掉
|
|
|
|
|
+ // (选了分类时城市本来就来自该分类,这里不会误清)
|
|
|
|
|
+ handleCityChange () {
|
|
|
|
|
+ const category = this.searchForm.category
|
|
|
|
|
+ if (category && this.categoryOptions.indexOf(category) === -1) {
|
|
|
|
|
+ this.searchForm.category = ''
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
// 分页查询;空值条件直接剔除,避免后端收到空串
|
|
// 分页查询;空值条件直接剔除,避免后端收到空串
|
|
|
refreshList () {
|
|
refreshList () {
|
|
@@ -367,12 +450,11 @@
|
|
|
this.tablePage.pageSize = pageSize
|
|
this.tablePage.pageSize = pageSize
|
|
|
this.refreshList()
|
|
this.refreshList()
|
|
|
},
|
|
},
|
|
|
- // 重置全部条件并回到第一页
|
|
|
|
|
|
|
+ // 重置全部条件并回到第一页(选项是静态的,不需要重新拉取)
|
|
|
resetSearch () {
|
|
resetSearch () {
|
|
|
this.$refs.searchForm.resetFields()
|
|
this.$refs.searchForm.resetFields()
|
|
|
this.searchForm.priceDates = []
|
|
this.searchForm.priceDates = []
|
|
|
this.tablePage.currentPage = 1
|
|
this.tablePage.currentPage = 1
|
|
|
- this.loadOptions()
|
|
|
|
|
this.refreshList()
|
|
this.refreshList()
|
|
|
},
|
|
},
|
|
|
// 开始日期限制:只能选昨天及更早;已选结束日期时,还要保证 start <= end 且跨度不超过 maxRangeDays
|
|
// 开始日期限制:只能选昨天及更早;已选结束日期时,还要保证 start <= end 且跨度不超过 maxRangeDays
|
|
@@ -408,6 +490,9 @@
|
|
|
this.rangeStartDate = ''
|
|
this.rangeStartDate = ''
|
|
|
this.rangeEndDate = ''
|
|
this.rangeEndDate = ''
|
|
|
this.rangeDialogVisible = true
|
|
this.rangeDialogVisible = true
|
|
|
|
|
+ // 状态没回来之前先显示「正在获取采集状态」,否则会先闪一下日期表单
|
|
|
|
|
+ this.rangeStatusLoading = true
|
|
|
|
|
+ this.rangeStatusError = ''
|
|
|
this.fetchRangeStatus()
|
|
this.fetchRangeStatus()
|
|
|
},
|
|
},
|
|
|
// 提交区间采集:接口立即返回,之后靠轮询拿进度
|
|
// 提交区间采集:接口立即返回,之后靠轮询拿进度
|
|
@@ -477,8 +562,14 @@
|
|
|
},
|
|
},
|
|
|
// 查询最近一次区间采集状态;结束后停止轮询并提示结果
|
|
// 查询最近一次区间采集状态;结束后停止轮询并提示结果
|
|
|
fetchRangeStatus () {
|
|
fetchRangeStatus () {
|
|
|
|
|
+ this.rangeStatusLoading = true
|
|
|
|
|
+ this.rangeStatusError = ''
|
|
|
this.materialPriceService.syncStatus().then((res) => {
|
|
this.materialPriceService.syncStatus().then((res) => {
|
|
|
- if (!res) return
|
|
|
|
|
|
|
+ // 没拿到数据(超时/无权限)时不能当成「没在跑」,否则会放行表单导致重复提交
|
|
|
|
|
+ if (!res) {
|
|
|
|
|
+ this.rangeStatusError = '获取采集状态失败,任务可能仍在后台执行,请重试'
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
// 天数上限以后端配置为准,失败时沿用本地默认值
|
|
// 天数上限以后端配置为准,失败时沿用本地默认值
|
|
|
if (res.maxDays) {
|
|
if (res.maxDays) {
|
|
|
this.maxRangeDays = res.maxDays
|
|
this.maxRangeDays = res.maxDays
|
|
@@ -498,7 +589,11 @@
|
|
|
this.stopPolling()
|
|
this.stopPolling()
|
|
|
this.handleRangeFinished()
|
|
this.handleRangeFinished()
|
|
|
}
|
|
}
|
|
|
- }).catch(() => {})
|
|
|
|
|
|
|
+ }).catch(() => {
|
|
|
|
|
+ this.rangeStatusError = '获取采集状态失败,任务可能仍在后台执行,请重试'
|
|
|
|
|
+ }).finally(() => {
|
|
|
|
|
+ this.rangeStatusLoading = false
|
|
|
|
|
+ })
|
|
|
},
|
|
},
|
|
|
// 页面激活时检查是否有区间任务在跑,有则恢复轮询
|
|
// 页面激活时检查是否有区间任务在跑,有则恢复轮询
|
|
|
resumeRangeWatch () {
|
|
resumeRangeWatch () {
|
|
@@ -564,4 +659,16 @@
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
line-height: 20px;
|
|
line-height: 20px;
|
|
|
}
|
|
}
|
|
|
|
|
+ /* 状态查询中的占位,高度与进度区接近,避免弹窗高度来回跳 */
|
|
|
|
|
+ .range-status-loading {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ min-height: 96px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .range-status-error {
|
|
|
|
|
+ min-height: 96px;
|
|
|
|
|
+ }
|
|
|
</style>
|
|
</style>
|