Explorar o código

评估:物料抓取页面检索条件优化

Alex hai 2 días
pai
achega
171b4503d3

+ 4 - 4
src/api/program/MaterialPriceService.js

@@ -8,12 +8,12 @@ import { ASSESS_PATH as prefix } from '../AppPath'
  * 不直接访问 Python 抓取服务。
  */
 export default class MaterialPriceService {
-  // 筛选框下拉选项(站点/分类/省份/城市/物料),支持按站点、分类、省份级联
-  options (params) {
+  // 筛选框下拉选项(分类 / 省份 / 城市 + 分类↔省份、分类+省份→城市的级联关系),
+  // 一次全量返回,切换筛选条件时在本地收窄,不再请求后端
+  options () {
     return request({
       url: prefix + '/materialPrice/record/options',
-      method: 'get',
-      params: params
+      method: 'get'
     })
   }
 

+ 149 - 42
src/views/program/materialPrice/MaterialPriceList.vue

@@ -1,29 +1,31 @@
 <template>
   <div class="page">
     <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-input v-model="searchForm.materialName" placeholder="输入关键字模糊查询" clearable style="width: 180px"></el-input>
       </el-form-item>
       <el-form-item label="分类" prop="category">
         <!-- filterable:输入文字即可从选项里过滤,不输入时仍按下拉选择 -->
         <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-form-item>
       <el-form-item label="省份" prop="province">
         <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-form-item>
-      <!-- 城市按所选省份级联;站点没有城市信息时该下拉为空 -->
+      <!-- 城市必须先选省份:未选省份时禁用,选项为空 -->
       <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-form-item>
       <!-- 数据日期区间:数组由 httpRequest 序列化成 priceDates[0]/priceDates[1] -->
@@ -142,6 +144,16 @@
         </div>
         <div class="range-tip">采集在后台执行,关闭弹窗不会中断;可以稍后重新打开查看进度。</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">
         <el-form label-width="80px">
@@ -174,7 +186,7 @@
       <template #footer>
         <el-button @click="rangeDialogVisible = false">关闭</el-button>
         <el-button
-          v-if="!rangeRunning"
+          v-if="!rangeRunning && !rangeStatusLoading && !rangeStatusError"
           type="primary"
           :loading="rangeSubmitting"
           :disabled="!rangeStartDate || !rangeEndDate"
@@ -195,19 +207,21 @@
       return {
         // 查询条件;priceDates 为空数组时后端不会按日期过滤
         searchForm: {
-          siteCode: '',
           materialName: '',
           category: '',
           province: '',
           city: '',
           priceDates: []
         },
-        // 下拉选项,全部由后端 /options 返回
+        // 下拉选项一次拉全,切换条件只在本地做级联收窄,不再请求后端;
+        // 后三张关系表用于「分类↔省份」双向收窄和「分类+省份→城市」
         options: {
-          sites: [],
           categories: [],
           provinces: [],
-          cities: []
+          citiesByProvince: {},
+          provincesByCategory: {},
+          categoriesByProvince: {},
+          citiesByCategoryProvince: {}
         },
         dataList: [],
         tablePage: {
@@ -226,6 +240,10 @@
         rangeSubmitting: false,
         // 后端锁或本进程标记为执行中
         rangeRunning: false,
+        // 弹窗打开后正在查询后端状态;查询完成前不渲染日期表单,避免误导
+        rangeStatusLoading: false,
+        // 状态查询失败的提示文案;有值时同样不渲染表单
+        rangeStatusError: '',
         // 最近一次区间采集日志(material_price_sync_log)
         rangeLog: {
           batchNo: '',
@@ -242,10 +260,55 @@
         // 已经提示过结果的批次号
         finishedBatchNo: '',
         // 进度轮询定时器
-        pollTimer: null
+        pollTimer: null,
+        // keep-alive 下首次进入会先触发 mounted 再触发 activated,用它跳过重复加载
+        activatedOnce: false
       }
     },
     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%
       rangePercent () {
         const total = this.rangeLog.progressTotal || 0
@@ -294,8 +357,13 @@
       // 页面刷新时可能刚好有区间任务在跑,恢复轮询
       this.resumeRangeWatch()
     },
-    // 项目启用了 keep-alive,从其它页面切回来时刷新一次
+    // 项目启用了 keep-alive,从其它页面切回来时刷新一次;
+    // 首次进入时 mounted 已经加载过,这里跳过,避免同时发两遍列表请求
     activated () {
+      if (!this.activatedOnce) {
+        this.activatedOnce = true
+        return
+      }
       this.refreshList()
       this.resumeRangeWatch()
     },
@@ -308,38 +376,53 @@
       this.stopPolling()
     },
     methods: {
-      // 拉取下拉选项:传 siteCode/category/province 做级联收窄
+      // 拉取下拉选项:不带参数,一次拿全量(后端有 10 分钟缓存)
       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 = {
-            sites: (data && data.sites) || [],
             categories: (data && data.categories) || [],
             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 () {
-        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 () {
+        const category = this.searchForm.category
         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 () {
@@ -367,12 +450,11 @@
         this.tablePage.pageSize = pageSize
         this.refreshList()
       },
-      // 重置全部条件并回到第一页
+      // 重置全部条件并回到第一页(选项是静态的,不需要重新拉取)
       resetSearch () {
         this.$refs.searchForm.resetFields()
         this.searchForm.priceDates = []
         this.tablePage.currentPage = 1
-        this.loadOptions()
         this.refreshList()
       },
       // 开始日期限制:只能选昨天及更早;已选结束日期时,还要保证 start <= end 且跨度不超过 maxRangeDays
@@ -408,6 +490,9 @@
         this.rangeStartDate = ''
         this.rangeEndDate = ''
         this.rangeDialogVisible = true
+        // 状态没回来之前先显示「正在获取采集状态」,否则会先闪一下日期表单
+        this.rangeStatusLoading = true
+        this.rangeStatusError = ''
         this.fetchRangeStatus()
       },
       // 提交区间采集:接口立即返回,之后靠轮询拿进度
@@ -477,8 +562,14 @@
       },
       // 查询最近一次区间采集状态;结束后停止轮询并提示结果
       fetchRangeStatus () {
+        this.rangeStatusLoading = true
+        this.rangeStatusError = ''
         this.materialPriceService.syncStatus().then((res) => {
-          if (!res) return
+          // 没拿到数据(超时/无权限)时不能当成「没在跑」,否则会放行表单导致重复提交
+          if (!res) {
+            this.rangeStatusError = '获取采集状态失败,任务可能仍在后台执行,请重试'
+            return
+          }
           // 天数上限以后端配置为准,失败时沿用本地默认值
           if (res.maxDays) {
             this.maxRangeDays = res.maxDays
@@ -498,7 +589,11 @@
             this.stopPolling()
             this.handleRangeFinished()
           }
-        }).catch(() => {})
+        }).catch(() => {
+          this.rangeStatusError = '获取采集状态失败,任务可能仍在后台执行,请重试'
+        }).finally(() => {
+          this.rangeStatusLoading = false
+        })
       },
       // 页面激活时检查是否有区间任务在跑,有则恢复轮询
       resumeRangeWatch () {
@@ -564,4 +659,16 @@
     font-size: 13px;
     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>