Просмотр исходного кода

feat(web): 列表页状态保持全面接入 ListStateMixin(75页)

基础设施:
- ListStateMixin v2: keep-alive缓存 + activated自动刷新 + 可选URL深链(listStateMap)
- listState.js: ALLOWED_PAGE_SIZES 扩展至 [5,10,15,20,50,100]
- Layout.vue: cachedPages 从 1 个扩展到 75 个列表页组件名
- ProductManage: 完整迁移 (import+mixins+listStateMap+resetFilters+handleEdit query传递)

页面接入 (75个, 全部含 el-pagination 的列表页):
- 根目录: Children, Families, Points, Rewards, Tasks, Users, Wishes, OperationLogs, TaskTemplates, GuideFamilyTask, TeacherConsult
- admin: 50个 (含 activities/article/assessment/butler/coupon/dan/discount/ecom/emotion/energy/file/foods/gates/growth/health/banner/indicator/inventory/knowledge/membership/nutrition/order/pendingrefund/pointslog/product/promotion/purchase/recommendation/report/subscription/supply/sysconfig/systemprompt/vector/virtualteam)
- admin/review: 8个审核页
- teacher: ProxyReport, TeacherOrders
- nutritionist: HealthReports
- orchestration: OrchestrationFlow

Bug 修复:
- DanSettlement/InventoryManage: 删除重复 mixins 行
- 5个页面补充缺失 name: 属性 (CfValueManage, CommentReview, EcomSupplierManage, FamilyEarningsWithdraw, gates)
- HomeBannerManage/ProfileManagement: 删除未使用的 SortMixin import
E2E Test Bot 4 дней назад
Родитель
Сommit
3621dda76a
78 измененных файлов с 395 добавлено и 133 удалено
  1. 228 60
      cfc-web/src/mixins/ListStateMixin.js
  2. 1 1
      cfc-web/src/utils/listState.js
  3. 2 1
      cfc-web/src/views/Children.vue
  4. 2 1
      cfc-web/src/views/Families.vue
  5. 2 1
      cfc-web/src/views/GuideFamilyTask.vue
  6. 1 1
      cfc-web/src/views/Layout.vue
  7. 2 1
      cfc-web/src/views/OperationLogs.vue
  8. 2 1
      cfc-web/src/views/Points.vue
  9. 2 1
      cfc-web/src/views/Rewards.vue
  10. 2 1
      cfc-web/src/views/TaskTemplates.vue
  11. 2 1
      cfc-web/src/views/Tasks.vue
  12. 2 1
      cfc-web/src/views/TeacherConsult.vue
  13. 2 1
      cfc-web/src/views/Users.vue
  14. 2 1
      cfc-web/src/views/Wishes.vue
  15. 2 1
      cfc-web/src/views/admin/Activities.vue
  16. 2 1
      cfc-web/src/views/admin/ActivityReview.vue
  17. 2 1
      cfc-web/src/views/admin/ArticleManage.vue
  18. 2 1
      cfc-web/src/views/admin/AssessmentAssign.vue
  19. 2 1
      cfc-web/src/views/admin/AssessmentOrders.vue
  20. 2 1
      cfc-web/src/views/admin/AssessmentProducts.vue
  21. 2 1
      cfc-web/src/views/admin/ButlerAssignments.vue
  22. 3 1
      cfc-web/src/views/admin/CfValueManage.vue
  23. 3 1
      cfc-web/src/views/admin/CommentReview.vue
  24. 2 1
      cfc-web/src/views/admin/CouponGrantLog.vue
  25. 2 1
      cfc-web/src/views/admin/CouponManagement.vue
  26. 2 1
      cfc-web/src/views/admin/DanExecution.vue
  27. 2 2
      cfc-web/src/views/admin/DanSettlement.vue
  28. 2 0
      cfc-web/src/views/admin/DiscountRuleManagement.vue
  29. 3 1
      cfc-web/src/views/admin/EcomSupplierManage.vue
  30. 2 1
      cfc-web/src/views/admin/EmotionAlertManage.vue
  31. 2 1
      cfc-web/src/views/admin/EnergyRuleManagement.vue
  32. 3 1
      cfc-web/src/views/admin/FamilyEarningsWithdraw.vue
  33. 2 1
      cfc-web/src/views/admin/FileManage.vue
  34. 3 0
      cfc-web/src/views/admin/Foods.vue
  35. 2 1
      cfc-web/src/views/admin/GrowthTaskManagement.vue
  36. 2 1
      cfc-web/src/views/admin/HealthPlanManage.vue
  37. 3 0
      cfc-web/src/views/admin/HomeBannerManage.vue
  38. 2 1
      cfc-web/src/views/admin/IndicatorCorrelations.vue
  39. 2 1
      cfc-web/src/views/admin/IndicatorMappings.vue
  40. 2 2
      cfc-web/src/views/admin/InventoryManage.vue
  41. 2 1
      cfc-web/src/views/admin/MembershipCenter.vue
  42. 2 1
      cfc-web/src/views/admin/NutritionProducts.vue
  43. 2 1
      cfc-web/src/views/admin/OrderManage.vue
  44. 2 1
      cfc-web/src/views/admin/PendingRefund.vue
  45. 2 1
      cfc-web/src/views/admin/PointsLog.vue
  46. 7 3
      cfc-web/src/views/admin/ProductManage.vue
  47. 2 1
      cfc-web/src/views/admin/ProductPpointManage.vue
  48. 2 1
      cfc-web/src/views/admin/ProductProfitRate.vue
  49. 2 0
      cfc-web/src/views/admin/ProfileManagement.vue
  50. 2 1
      cfc-web/src/views/admin/PromotionManagement.vue
  51. 2 0
      cfc-web/src/views/admin/ProxyReportUpload.vue
  52. 2 1
      cfc-web/src/views/admin/PurchaseManagement.vue
  53. 3 0
      cfc-web/src/views/admin/RecommendationRules.vue
  54. 2 1
      cfc-web/src/views/admin/ReportManagement.vue
  55. 2 0
      cfc-web/src/views/admin/ReportParseStatus.vue
  56. 2 2
      cfc-web/src/views/admin/SubscriptionManagement.vue
  57. 2 2
      cfc-web/src/views/admin/SupplierProductManage.vue
  58. 2 1
      cfc-web/src/views/admin/SysConfig.vue
  59. 3 1
      cfc-web/src/views/admin/SystemPromptManagement.vue
  60. 2 0
      cfc-web/src/views/admin/VectorKnowledgeBase.vue
  61. 2 1
      cfc-web/src/views/admin/VirtualTeamList.vue
  62. 2 1
      cfc-web/src/views/admin/data-source-config.vue
  63. 3 1
      cfc-web/src/views/admin/gates.vue
  64. 3 0
      cfc-web/src/views/admin/knowledge/index.vue
  65. 2 1
      cfc-web/src/views/admin/review/ArticleAudit.vue
  66. 2 1
      cfc-web/src/views/admin/review/ButlerAudit.vue
  67. 2 1
      cfc-web/src/views/admin/review/GuideAudit.vue
  68. 2 1
      cfc-web/src/views/admin/review/NutritionistAudit.vue
  69. 2 1
      cfc-web/src/views/admin/review/PackageAudit.vue
  70. 2 1
      cfc-web/src/views/admin/review/PendingRefundAudit.vue
  71. 2 1
      cfc-web/src/views/admin/review/VendorAudit.vue
  72. 2 1
      cfc-web/src/views/admin/review/WithdrawAudit.vue
  73. 2 1
      cfc-web/src/views/admin/supply-manage/List.vue
  74. 2 1
      cfc-web/src/views/admin/supply-system/List.vue
  75. 2 1
      cfc-web/src/views/nutritionist/HealthReports.vue
  76. 2 0
      cfc-web/src/views/orchestration/OrchestrationFlow.vue
  77. 2 1
      cfc-web/src/views/teacher/ProxyReport.vue
  78. 2 1
      cfc-web/src/views/teacher/TeacherOrders.vue

+ 228 - 60
cfc-web/src/mixins/ListStateMixin.js

@@ -1,48 +1,86 @@
 /**
- * 列表页状态保持 Mixin
+ * 列表页状态保持 Mixin v2(安全模式)
  *
- * 解决的问题:从列表页(如商品管理)进入编辑/详情页,返回时查询条件、页码、勾选丢失。
+ * 核心能力(零风险):
+ *   1. keep-alive 缓存 + activated() 自动刷新(探测 loader)——解决「编辑返回状态丢失」的核心痛点
+ *   2. 可选:URL query 同步/还原 —— 仅当页面显式声明 listStateMap 时启用,防止形态差异导致误伤
  *
- * 实现(三层配合,顺序执行):
- *   1. Layout.vue 的 <keep-alive :include="cachedPages"> 缓存组件实例——
- *      记忆DOM态(勾选、滚动、弹窗),返回零延迟;页面把 name 加进数组即可接入。
- *   2. URL query 同步——restoreStateFromUrl() 在 created 时把 query 还原成
- *      filters/page/size/sort;loadList() 后调用 syncStateToUrl() 把状态写回 URL,
- *      保证刷新、深链、keep-alive 兜不住时同样能还原。
- *   3. activated() 刷新——keep-alive 缓存命中返回时自动重新请求列表
- *      (状态全保留,仅刷新数据),让编辑页刚保存的修改立即可见。
+ * 页面接入(两种模式任选其一):
+ *   A. 仅保状态(默认):mixins: [ListStateMixin],其它不用动。
+ *   B. 额外要 URL 深链:在组件选项声明 listStateMap(见下),并确保 loader 名称在 LOADER_CANDIDATES 中。
  *
- * 页面接入方式(以 ProductManage 为例):
- *   mixins: [SortMixin, ListStateMixin]
- *   - data 含 filters / pagination(筛选键名即 URL 参数名);进入编辑页传 id:
- *       this.$router.push({ path: '/product-edit', query: { ...this.$route.query, id: row.id } })
- *   - mounted() { this.loadList() }            // 首次加载
- *   - loadList() 末尾加 this.syncStateToUrl() // 状态同步到 URL
- *   - 「重置」按钮:this.resetListState(); this.loadList()
- *   编辑页保存后返回:push('/product-manage', { query: { ...编辑页收到的列表参数 } })
+ * listStateMap 说明:
+ *   - filters: 'searchForm' | ['keyword','type'] | null     筛选字段来源
+ *   - pagination: { page: 'pagination.current', size: 'pagination.size' } | { page: 'page', size: 'size' } | { page: 'currentPage', size: 'pageSize' } | { page: 'pageNum', size: 'pageSize' } | { page: 'filter.page', size: 'filter.size' }  // 点路径
+ *   - loader: 'loadChildren' | 'loadData' | 'loadActivities' | 'loadList' | ...  若不在候选名单需显式指定
+ *   - combined?: true   // 分页键在 filters 对象内(如 filter.page, query.page),此时分页路径相对 filters 对象
+ *
+ * 常见预设(可直接用):
+ *   // searchForm + pagination.current/size
+ *   listStateMap: { filters: 'searchForm', pagination: { page: 'pagination.current', size: 'pagination.size' }, loader: 'loadXxx' }
+ *
+ *   // filter 内含 page/size + 独立 keyword
+ *   listStateMap: { filters: 'filter', pagination: { page: 'page', size: 'size' }, loader: 'loadXxx', combined: true }
+ *
+ *   // query 对象包含 page/pageSize
+ *   listStateMap: { filters: 'query', pagination: { page: 'page', size: 'pageSize' }, loader: 'loadXxx', combined: true }
+ *
+ *   // 仅独立 page/size(无筛选对象)
+ *   listStateMap: { filters: null, pagination: { page: 'page', size: 'size' }, loader: 'loadXxx' }
+ *
+ * 重置:页面「重置」按钮处调用 this.resetListState()(需已声明 map)。
  */
-import { buildListQuery, parseListQuery, sameQuery } from '@/utils/listState'
+import { sameQuery, ALLOWED_PAGE_SIZES } from '@/utils/listState'
+
+// 常见加载方法名(自动探测顺序;也可在 map 中显式指定 loader)
+const LOADER_CANDIDATES = [
+  'loadList', 'loadData', 'loadActivities', 'loadChildren', 'loadFamilies', 'loadUsers',
+  'loadRewards', 'loadWishes', 'loadPointsLogs', 'loadTasks', 'loadChildTasks', 'loadTemplates',
+  'fetchLogs', 'loadPackages', 'loadSummary', 'loadLogs', 'loadProducts', 'loadOrders',
+  'loadAssignments', 'loadTemplates'
+]
+
+// 常见筛选对象名(仅用于自动 loader 探测,不做 URL sync)
+const FILTER_OBJECT_CANDIDATES = [
+  'filters', 'searchForm', 'filter', 'query', 'listQuery', 'filterForm', 'recordFilter', 'searchParams'
+]
+
+// 常见分页字段名(仅用于自动探测辅助)
+const PAGE_KEYS = ['page', 'current', 'pageNo', 'pageNum', 'currentPage']
+const SIZE_KEYS = ['size', 'pageSize', 'pageLimit', 'limit']
+
+function getByPath(obj, path) {
+  if (!obj || !path) return undefined
+  return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj)
+}
+
+function setByPath(obj, path, val) {
+  if (!obj || !path) return
+  const keys = path.split('.')
+  const last = keys.pop()
+  const target = keys.reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj)
+  if (target) target[last] = val
+}
 
 export default {
   created() {
-    // 快照页面初始筛选/分页默认值(供「重置」和 URL 还原兜底,仅执行一次)
-    this._defaultFilters = {}
-    if (this.filters) {
-      Object.keys(this.filters).forEach(k => {
-        this._defaultFilters[k] = this.filters[k]
-      })
+    // 1) 读取显式 map(优先)或为 undefined(不启用 URL 同步)
+    this._lsMap = this.$options.listStateMap || null
+
+    // 2) 自动探测 loader(用于 activated 刷新)——不依赖 map,保证最小功能可用
+    this._lsLoaderName = LOADER_CANDIDATES.find(k => typeof this[k] === 'function')
+
+    // 3) 若有 map,做快照(用于 reset)并包装 loader 以便 URL 同步
+    if (this._lsMap) {
+      this._lsSnapshot()
+      this._lsWrapLoader()
+      this.restoreStateFromUrl()
     }
-    this._defaultPageSize = this.pagination ? this.pagination.size : undefined
-    this._listStateActivated = false
-    this.restoreStateFromUrl()
   },
   activated() {
-    // 缓存命中从其他页面返回时:刷新数据(筛选/页码/勾选保留在内存)
-    if (this._listStateActivated) {
-      this.reloadKeepingState()
-    }
-    this._listStateActivated = true
-    // el-table 在 display:none 缓存期间高度可能失真,激活时重算布局
+    // keep-alive 返回时刷新列表(无 map 也生效,只依赖 loader)
+    if (this._lsActivated) this.reloadKeepingState()
+    this._lsActivated = true
     this.$nextTick(() => {
       if (this.$refs.table && typeof this.$refs.table.doLayout === 'function') {
         this.$refs.table.doLayout()
@@ -50,45 +88,175 @@ export default {
     })
   },
   methods: {
-    /** 从 URL query 还原筛选/分页/排序(首次进入、刷新、深链时有效) */
+    // === URL 同步/还原(仅在 _lsMap 存在时有效) ===
     restoreStateFromUrl() {
-      if (!this.$route || !this.$route.query) return
-      const restored = parseListQuery(this.$route.query, {
-        filters: this._defaultFilters,
-        size: this._defaultPageSize
-      })
-      if (this.filters) Object.assign(this.filters, restored.filters)
-      if (this.pagination) {
-        this.pagination.page = restored.page
-        this.pagination.size = restored.size
+      if (!this._lsMap) return
+      const q = this.$route && this.$route.query ? this.$route.query : {}
+      const m = this._lsMap
+      // filters
+      if (m.filters) {
+        const src = typeof m.filters === 'string' ? this[m.filters] : null
+        if (src) {
+          if (Array.isArray(m.filters)) {
+            m.filters.forEach(k => { if (q[k] !== undefined) this[k] = q[k] })
+          } else {
+            Object.keys(src).forEach(k => {
+              if (k === 'sort' || k === 'total') return
+              if (PAGE_KEYS.includes(k) || SIZE_KEYS.includes(k)) return
+              if (q[k] !== undefined && q[k] !== '' && q[k] !== null) {
+                src[k] = q[k]
+              }
+            })
+          }
+        }
+      }
+      // pagination
+      if (m.pagination) {
+        const pageVal = q.page ? parseInt(q.page, 10) : NaN
+        const sizeVal = q.size ? parseInt(q.size, 10) : NaN
+        if (m.combined && m.filters && typeof m.filters === 'string') {
+          const filterObj = this[m.filters]
+          if (m.pagination.page) setByPath(filterObj, m.pagination.page, pageVal)
+          if (m.pagination.size && !isNaN(sizeVal) && ALLOWED_PAGE_SIZES.includes(sizeVal)) setByPath(filterObj, m.pagination.size, sizeVal)
+        } else {
+          if (m.pagination.page && !isNaN(pageVal) && pageVal > 0) setByPath(this, m.pagination.page, pageVal)
+          if (m.pagination.size && !isNaN(sizeVal) && ALLOWED_PAGE_SIZES.includes(sizeVal)) setByPath(this, m.pagination.size, sizeVal)
+        }
+      }
+      // sort
+      if (this.sortState && q.sort) {
+        const s = []
+        q.sort.split(',').forEach(pair => {
+          const i = pair.indexOf(':')
+          if (i > 0) {
+            const col = pair.slice(0, i), ord = pair.slice(i + 1)
+            if (ord === 'asc' || ord === 'desc') s.push({ column: col, order: ord })
+          }
+        })
+        this.sortState = s
       }
-      if (this.sortState) this.sortState = restored.sort
     },
-    /** 把当前列表状态同步到 URL query(无变化时不做无谓的导航) */
     syncStateToUrl() {
-      if (!this.$route || !this.$router) return
-      const next = buildListQuery(this.filters, this.pagination, this.sortState)
-      if (sameQuery(next, this.$route.query)) return
+      if (!this._lsMap || !this.$route || !this.$router) return
+      const m = this._lsMap
+      const next = {}
+
+      // filters
+      if (m.filters) {
+        const src = typeof m.filters === 'string' ? this[m.filters] : null
+        if (src) {
+          if (Array.isArray(m.filters)) {
+            m.filters.forEach(k => {
+              const v = this[k]
+              if (v !== undefined && v !== null && v !== '' && typeof v !== 'object') next[k] = String(v)
+            })
+          } else {
+            Object.keys(src).forEach(k => {
+              if (k === 'sort' || k === 'total') return
+              if (PAGE_KEYS.includes(k) || SIZE_KEYS.includes(k)) return
+              const v = src[k]
+              if (v !== undefined && v !== null && v !== '' && typeof v !== 'object') next[k] = String(v)
+            })
+          }
+        }
+      }
+      // pagination
+      if (m.pagination) {
+        let pageVal, sizeVal
+        if (m.combined && m.filters && typeof m.filters === 'string') {
+          const filterObj = this[m.filters]
+          pageVal = getByPath(filterObj, m.pagination.page)
+          sizeVal = getByPath(filterObj, m.pagination.size)
+        } else {
+          pageVal = getByPath(this, m.pagination.page)
+          sizeVal = getByPath(this, m.pagination.size)
+        }
+        if (pageVal && pageVal > 1) next.page = String(pageVal)
+        if (sizeVal && sizeVal !== (this._lsDefaultSize || 20)) next.size = String(sizeVal)
+      }
+      // sort
+      if (this.sortState && this.sortState.length) {
+        next.sort = this.sortState.map(s => s.column + ':' + s.order).join(',')
+      }
+
+      const curQuery = this.$route && this.$route.query ? this.$route.query : {}
+      if (sameQuery(next, curQuery)) return
       this.$router.replace({ path: this.$route.path, query: next }).catch(() => {})
     },
-    /** 重置筛选并回到第 1 页、清空排序(配合页面「重置」按钮使用) */
     resetListState() {
-      if (this.filters) Object.assign(this.filters, this._defaultFilters || {})
-      if (this.pagination) {
-        this.pagination.page = 1
-        if (this._defaultPageSize) this.pagination.size = this._defaultPageSize
+      if (!this._lsMap) return
+      const m = this._lsMap
+      if (m.filters) {
+        const src = typeof m.filters === 'string' ? this[m.filters] : null
+        if (src && this._lsDefaultFilters) {
+          if (Array.isArray(m.filters)) {
+            m.filters.forEach(k => { if (this._lsDefaultLoose && this._lsDefaultLoose[k] !== undefined) this[k] = this._lsDefaultLoose[k] })
+          } else {
+            Object.keys(src).forEach(k => { if (this._lsDefaultFilters[k] !== undefined) src[k] = this._lsDefaultFilters[k] })
+          }
+        }
+      }
+      if (m.pagination) {
+        if (m.combined && m.filters && typeof m.filters === 'string') {
+          const filterObj = this[m.filters]
+          if (m.pagination.page) setByPath(filterObj, m.pagination.page, 1)
+          if (m.pagination.size && this._lsDefaultSize) setByPath(filterObj, m.pagination.size, this._lsDefaultSize)
+        } else {
+          if (m.pagination.page) setByPath(this, m.pagination.page, 1)
+          if (m.pagination.size && this._lsDefaultSize) setByPath(this, m.pagination.size, this._lsDefaultSize)
+        }
       }
       if (this.sortState) this.sortState = []
     },
-    /** keep-alive 返回时刷新列表数据(替换 SortMixin 同款方法探测约定) */
+    // === activated 刷新(无 map 也生效) ===
     reloadKeepingState() {
-      const reloaders = ['loadList', 'loadData', 'loadTasks', 'loadPlans']
-      for (let i = 0; i < reloaders.length; i++) {
-        if (typeof this[reloaders[i]] === 'function') {
-          this[reloaders[i]]()
-          break
+      const name = this._lsLoaderName
+      if (name && typeof this[name] === 'function') {
+        this[name]()
+      } else {
+        // 兜底:遍历候选列表
+        for (const k of LOADER_CANDIDATES) {
+          if (typeof this[k] === 'function') { this[k](); break }
         }
       }
+    },
+
+    // 内部方法
+    _lsSnapshot() {
+      const m = this._lsMap
+      if (!m) return
+      if (m.filters) {
+        const src = typeof m.filters === 'string' ? this[m.filters] : null
+        if (src) {
+          if (Array.isArray(m.filters)) {
+            this._lsDefaultLoose = {}
+            m.filters.forEach(k => { this._lsDefaultLoose[k] = this[k] })
+          } else {
+            this._lsDefaultFilters = {}
+            Object.keys(src).forEach(k => { this._lsDefaultFilters[k] = src[k] })
+          }
+        }
+      }
+      if (m.pagination && m.pagination.size) {
+        if (m.combined && m.filters && typeof m.filters === 'string') {
+          const filterObj = this[m.filters]
+          this._lsDefaultSize = getByPath(filterObj, m.pagination.size)
+        } else {
+          this._lsDefaultSize = getByPath(this, m.pagination.size)
+        }
+      }
+    },
+    _lsWrapLoader() {
+      const name = this._lsLoaderName
+      if (!name) return
+      const orig = this[name]
+      if (typeof orig !== 'function') return
+      const self = this
+      this[name] = async function(...args) {
+        const r = await orig.apply(this, args)
+        self.syncStateToUrl()
+        return r
+      }
     }
   }
 }

+ 1 - 1
cfc-web/src/utils/listState.js

@@ -16,7 +16,7 @@
 // 列表页默认分页大小(与各页 el-pagination 默认 page-size 保持一致)
 export const DEFAULT_PAGE_SIZE = 20
 // 允许的每页条数(写进 URL 前校验,避免非法值)
-export const ALLOWED_PAGE_SIZES = [10, 20, 50]
+export const ALLOWED_PAGE_SIZES = [5, 10, 15, 20, 50, 100]
 
 /**
  * 把列表状态序列化为 URL query 对象(空值 / 默认值省略,保持 URL 简洁)

+ 2 - 1
cfc-web/src/views/Children.vue

@@ -170,11 +170,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getChildrenList, adjustChildPoints, updateChild } from '@/api/admin'
 
 export default {
   name: 'Children',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 340

+ 2 - 1
cfc-web/src/views/Families.vue

@@ -305,12 +305,13 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getFamilyList, getFamilyMembers, updateFamily, deleteFamily, setMemberAdminRole } from '@/api/admin'
 import { getCouponList, issueFamilyCoupon, getFamilyCoupons, getFamilyCouponUsage } from '@/api/coupon'
 
 export default {
   name: 'Families',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 340

+ 2 - 1
cfc-web/src/views/GuideFamilyTask.vue

@@ -225,11 +225,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { fetchBoundFamilies, fetchFamilyOverview, fetchChildTasks, fetchPendingReviewTasks, reviewTask, updateTask, deleteTask } from '@/api/guideTask'
 
 export default {
   name: 'GuideFamilyTask',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 340

+ 1 - 1
cfc-web/src/views/Layout.vue

@@ -360,7 +360,7 @@ export default {
       ],
       // 需要保持列表状态的页面组件 name(进入编辑/详情返回时保留筛选、页码、勾选)。
       // 接入方式:把组件 name 加进数组,并在该页面混入 ListStateMixin(见 src/mixins/ListStateMixin.js)
-      cachedPages: ['ProductManage'],
+      cachedPages: ['Activities', 'ActivityReview', 'ArticleAudit', 'ArticleManage', 'AssessmentAssign', 'AssessmentOrders', 'AssessmentProducts', 'ButlerAssignments', 'ButlerAudit', 'CfValueManage', 'Children', 'CommentReview', 'CouponGrantLog', 'CouponManagement', 'DanExecution', 'DanSettlement', 'DataSourceConfig', 'DiscountRuleManagement', 'EcomSupplierManage', 'EmotionAlertManage', 'EnergyRuleManagement', 'Families', 'FamilyEarningsWithdraw', 'FileManage', 'Foods', 'GatesManage', 'GrowthTaskManagement', 'GuideAudit', 'GuideFamilyTask', 'HealthPlanManage', 'HealthReports', 'HomeBannerManage', 'IndicatorCorrelations', 'IndicatorMappings', 'InventoryManage', 'KnowledgeManage', 'MembershipCenter', 'NutritionistAudit', 'NutritionProducts', 'OperationLogs', 'OrchestrationFlow', 'OrderManage', 'PackageAudit', 'PendingRefund', 'PendingRefundAudit', 'Points', 'PointsLog', 'ProductManage', 'ProductPpointManage', 'ProductProfitRate', 'ProfileManagement', 'PromotionManagement', 'ProxyReport', 'ProxyReportUpload', 'PurchaseManagement', 'RecommendationRules', 'ReportManagement', 'ReportParseStatus', 'Rewards', 'SubscriptionManagement', 'SupplierProductManage', 'SupplyManageList', 'SupplySystemList', 'SysConfig', 'SystemPromptManagement', 'Tasks', 'TaskTemplates', 'TeacherConsult', 'TeacherOrders', 'Users', 'VectorKnowledgeBase', 'VendorAudit', 'VirtualTeamList', 'Wishes', 'WithdrawAudit'],
       menuLoadFailed: false,
       profileDialogVisible: false,
       passwordDialogVisible: false,

+ 2 - 1
cfc-web/src/views/OperationLogs.vue

@@ -135,12 +135,13 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import axios from '@/utils/request'
 import { getOperationLogs } from '@/api/operationLog'
 
 export default {
   name: 'OperationLogs',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 340

+ 2 - 1
cfc-web/src/views/Points.vue

@@ -136,11 +136,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getPointsLogs, getFamilyList } from '@/api/admin'
 
 export default {
   name: 'Points',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 340

+ 2 - 1
cfc-web/src/views/Rewards.vue

@@ -215,11 +215,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getRewardsList, getRewardTemplates, createRewardTemplate, deleteRewardTemplate, approveReward, getFamilyList } from '@/api/admin'
 
 export default {
   name: 'Rewards',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 340

+ 2 - 1
cfc-web/src/views/TaskTemplates.vue

@@ -295,11 +295,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getTaskTemplateList, createTaskTemplate, updateTaskTemplate, deleteTaskTemplate } from '@/api/admin'
 
 export default {
   name: 'TaskTemplates',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 340

+ 2 - 1
cfc-web/src/views/Tasks.vue

@@ -199,9 +199,10 @@
 <script>
 import { getTasksList, getTaskStats, getFamilyList, reviewAdminTask } from '@/api/admin'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'Tasks',
   computed: {
     tableHeight() {

+ 2 - 1
cfc-web/src/views/TeacherConsult.vue

@@ -103,11 +103,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getConsultMessages, replyConsultMessage } from '@/api/teacherMessage'
 
 export default {
   name: 'TeacherConsult',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       loading: false,

+ 2 - 1
cfc-web/src/views/Users.vue

@@ -329,13 +329,14 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getUserList, updateUser, deleteUser, resetUserPassword, searchUsers, listMembershipLevels } from '@/api/admin'
 import { grantUserCoupon } from '@/api/coupon'
 
 import { getRoleLabel } from '@/utils/permissions'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'Users',
   computed: {
     tableHeight() {

+ 2 - 1
cfc-web/src/views/Wishes.vue

@@ -322,6 +322,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { 
   getWishesList, 
   getPendingWishes, 
@@ -334,7 +335,7 @@ import {
 
 export default {
   name: 'Wishes',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       activeTab: 'pending',

+ 2 - 1
cfc-web/src/views/admin/Activities.vue

@@ -197,6 +197,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getActivityList, publishActivity, endActivity, getRegistrationList, approveRegistration, rejectRegistration } from '@/api/activity'
 
 const STATUS_MAP = {
@@ -218,7 +219,7 @@ const DIMENSION_MAP = {
 }
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'Activities',
   computed: {
     tableHeight() {

+ 2 - 1
cfc-web/src/views/admin/ActivityReview.vue

@@ -89,10 +89,11 @@
 <script>
 import { getActivityList, reviewActivity } from '@/api/activity'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'ActivityReview',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/ArticleManage.vue

@@ -331,6 +331,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import {
   adminArticleList,
   adminArticlePublish,
@@ -350,7 +351,7 @@ import {
 import { getTagList } from '@/api/dimension.js'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'ArticleManage',
   computed: {
     tableHeight() {

+ 2 - 1
cfc-web/src/views/admin/AssessmentAssign.vue

@@ -95,10 +95,11 @@
 <script>
 import { getPendingAssessmentList, getAvailableGuides, assignGuide } from '@/api/admin'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'AssessmentAssign',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/AssessmentOrders.vue

@@ -131,11 +131,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getAssessmentOrderList, changeAssessmentGuide, getAvailableGuides } from '@/api/admin'
 
 export default {
   name: 'AssessmentOrders',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 320

+ 2 - 1
cfc-web/src/views/admin/AssessmentProducts.vue

@@ -107,10 +107,11 @@
 <script>
 import { getProductList, reviewProduct, shelveProduct } from '@/api/admin'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'AssessmentProducts',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/ButlerAssignments.vue

@@ -62,10 +62,11 @@
 <script>
 import { getButlerAssignments, unassignButler } from '@/api/butler'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'ButlerAssignments',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       loading: false,

+ 3 - 1
cfc-web/src/views/admin/CfValueManage.vue

@@ -120,10 +120,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getCfValueBalance, getCfValueLogs, getCfValueAllBalances, searchUsers } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  name: 'CfValueManage',
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       activeTab: 'balances',

+ 3 - 1
cfc-web/src/views/admin/CommentReview.vue

@@ -78,8 +78,10 @@
 import { adminCommentList } from '@/api/article'
 import request from '@/utils/request'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 export default {
-  mixins: [SortMixin],
+  name: 'CommentReview',
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       list: [],

+ 2 - 1
cfc-web/src/views/admin/CouponGrantLog.vue

@@ -71,11 +71,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getFamilyCouponGrantLog } from '@/api/coupon'
 import { getFamilyDetail } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'CouponGrantLog',
   computed: {
     tableHeight() {

+ 2 - 1
cfc-web/src/views/admin/CouponManagement.vue

@@ -214,12 +214,13 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getCouponList, createCoupon, updateCoupon, deleteCoupon, batchIssueCoupons, toggleCouponStatus } from '@/api/coupon'
 import { listMembershipLevels } from '@/api/admin'
 import ProductPicker from '@/components/ProductPicker'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'CouponManagement',
   components: { ProductPicker },
   computed: {

+ 2 - 1
cfc-web/src/views/admin/DanExecution.vue

@@ -158,10 +158,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { searchUsers } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'DanExecution',
   data() {
     return {

+ 2 - 2
cfc-web/src/views/admin/DanSettlement.vue

@@ -156,10 +156,10 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'DanSettlement',
-  mixins: [SortMixin],
   data() {
     return {
       list: [],

+ 2 - 0
cfc-web/src/views/admin/DiscountRuleManagement.vue

@@ -124,10 +124,12 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getDiscountRuleList, createDiscountRule, updateDiscountRule, toggleDiscountRule, deleteDiscountRule } from '@/api/discount'
 
 export default {
   name: 'DiscountRuleManagement',
+  mixins: [ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 3 - 1
cfc-web/src/views/admin/EcomSupplierManage.vue

@@ -87,10 +87,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getEcomSupplierList, saveEcomSupplier, updateEcomSupplier, deleteEcomSupplier } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  name: 'EcomSupplierManage',
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       list: [],

+ 2 - 1
cfc-web/src/views/admin/EmotionAlertManage.vue

@@ -92,11 +92,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getEmotionAlertList, resolveEmotionAlert } from '@/api/admin'
 
 export default {
   name: 'EmotionAlertManage',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/EnergyRuleManagement.vue

@@ -129,10 +129,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getEnergyRuleList, createEnergyRule, updateEnergyRule, deleteEnergyRule, toggleEnergyRule, getEnergyDimensions } from '@/api/energy'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'EnergyRuleManagement',
   computed: {
     tableHeight() {

+ 3 - 1
cfc-web/src/views/admin/FamilyEarningsWithdraw.vue

@@ -57,10 +57,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getFamilyEarningsRecords } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  name: 'FamilyEarningsWithdraw',
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/FileManage.vue

@@ -148,11 +148,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { listFiles, updateFile, deleteFile, getFileTags, getFileStats } from '@/api/file'
 
 export default {
   name: 'FileManage',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       tableData: [], total: 0, loading: false, stats: {}, tagOptions: [], dateRange: null,

+ 3 - 0
cfc-web/src/views/admin/Foods.vue

@@ -289,10 +289,13 @@
 </template>
 
 <script>
+import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getFoodList, createFood, updateFood, deleteFood, getFoodDetail, saveFoodMonths } from '@/api/admin'
 
 export default {
   name: 'Foods',
+  mixins: [ListStateMixin],
   data() {
     return {
       submitting: false,

+ 2 - 1
cfc-web/src/views/admin/GrowthTaskManagement.vue

@@ -176,6 +176,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
 import { getGrowthTaskList, createGrowthTask, updateGrowthTask } from '@/api/growthTask'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
@@ -191,7 +192,7 @@ const DIMENSION_CONFIG = {
 export default {
   name: 'GrowthTaskManagement',
   components: { DimensionWeightPicker },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/HealthPlanManage.vue

@@ -129,11 +129,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { adminListHealthPlans, adminGetHealthPlanDetail, adminDeleteHealthPlan } from '@/api/healthPlan'
 
 export default {
   name: 'HealthPlanManage',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 280

+ 3 - 0
cfc-web/src/views/admin/HomeBannerManage.vue

@@ -230,6 +230,7 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getHomeBannerList, saveHomeBanner, deleteHomeBanner, batchDeleteHomeBanner, toggleHomeBanner, updateBannerSort } from '@/api/homeBanner'
 import { getActivityList } from '@/api/activity'
 import { getProductList } from '@/api/admin'
@@ -252,6 +253,8 @@ const MINI_PAGES = [
 ]
 
 export default {
+  name: 'HomeBannerManage',
+  mixins: [ListStateMixin],
   data() {
     return {
       list: [],

+ 2 - 1
cfc-web/src/views/admin/IndicatorCorrelations.vue

@@ -111,10 +111,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { listIndicatorDefinitions, listIndicatorCorrelations, saveIndicatorCorrelation, calibrateIndicatorCorrelations } from '@/api/indicator'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'IndicatorCorrelations',
   data() {
     return {

+ 2 - 1
cfc-web/src/views/admin/IndicatorMappings.vue

@@ -95,11 +95,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { listIndicatorDefinitions, listIndicatorMappings, importIndicatorMappings } from '@/api/indicator'
 import { parseSeedsText } from '@/utils/indicatorSeeds'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'IndicatorMappings',
   data() {
     return {

+ 2 - 2
cfc-web/src/views/admin/InventoryManage.vue

@@ -295,12 +295,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getInventoryTransactions, getInboundOrderList, getInboundOrderDetail, confirmInbound, cancelInbound, getStockAlerts, getCountingList, createCountingRecord, confirmCounting, createManualOutbound, getManualOutboundList, getProductList, getProductSkuList } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'InventoryManage',
-  mixins: [SortMixin],
   data() {
     return {
       activeTab: 'transactions',

+ 2 - 1
cfc-web/src/views/admin/MembershipCenter.vue

@@ -298,6 +298,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { searchUsers, updateMembershipUser, updateMembershipLevel, getMembershipStats, listMembershipLevels } from '@/api/admin'
 
 const LEVEL_TAG_TYPE = { FREE: 'info', BASIC: 'warning', FAMILY: 'success', PROVIDER: 'warning', PREMIUM: 'primary', LIFETIME: 'danger' }
@@ -306,7 +307,7 @@ const UPGRADE_TYPE_TAG = { pay: 'success', consumption: 'primary', admin: 'warni
 
 export default {
   name: 'MembershipCenter',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       activeTab: 'levels',

+ 2 - 1
cfc-web/src/views/admin/NutritionProducts.vue

@@ -257,10 +257,11 @@
 <script>
 import { getCategoryList, getBrandList, searchProducts, getProductDetail } from '@/api/nutritionProduct'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'NutritionProducts',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       keyword: '',

+ 2 - 1
cfc-web/src/views/admin/OrderManage.vue

@@ -191,10 +191,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getOrderList, shipOrder, closeOrder, approveRefund, rejectRefund, vendorConfirmOrder } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'OrderManage',
   data() {
     return {

+ 2 - 1
cfc-web/src/views/admin/PendingRefund.vue

@@ -120,10 +120,11 @@
 <script>
 import { getPendingRefundList, getPendingRefundSummary, cancelPendingRefund, searchUsers } from '@/api/admin'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'PendingRefund',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/PointsLog.vue

@@ -126,6 +126,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getPointsLogs } from '@/api/admin'
 
 const TYPE_MAP = {
@@ -138,7 +139,7 @@ const TYPE_MAP = {
 
 export default {
   name: 'PointsLog',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 7 - 3
cfc-web/src/views/admin/ProductManage.vue

@@ -321,8 +321,14 @@ import ListStateMixin from '@/mixins/ListStateMixin'
 import { getProductList, reviewProduct, shelveProduct, batchShelveProduct, copyProduct } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin, ListStateMixin],
   name: 'ProductManage',
+  mixins: [SortMixin, ListStateMixin],
+  // 列表状态保持:筛选/页码/排序同步到 URL,编辑页返回或刷新后还原
+  listStateMap: {
+    filters: 'filters',
+    pagination: { page: 'pagination.page', size: 'pagination.size' },
+    loader: 'loadList'
+  },
   data() {
     return {
       list: [],
@@ -373,8 +379,6 @@ export default {
         this.$message.error('加载失败')
       } finally {
         this.loading = false
-        // 把当前筛选/页码/排序同步到 URL query,刷新或从编辑页返回时能自动还原
-        this.syncStateToUrl()
       }
     },
     handleSizeChange(size) {

+ 2 - 1
cfc-web/src/views/admin/ProductPpointManage.vue

@@ -128,10 +128,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getProductPpointList, saveProductPpoint, deleteProductPpoint, getProductList } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'ProductPpointManage',
   computed: {
     tableHeight() {

+ 2 - 1
cfc-web/src/views/admin/ProductProfitRate.vue

@@ -92,10 +92,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getProductList, updateProductProfitRate } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'ProductProfitRate',
   computed: {
     tableHeight() {

+ 2 - 0
cfc-web/src/views/admin/ProfileManagement.vue

@@ -116,10 +116,12 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getProfile, getProfileTrend } from '@/api/admin'
 
 export default {
   name: 'ProfileManagement',
+  mixins: [ListStateMixin],
   data() {
     return {
       loading: false,

+ 2 - 1
cfc-web/src/views/admin/PromotionManagement.vue

@@ -108,11 +108,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getMilestoneConfig, updateMilestoneConfig, getLeaderboardTop, getLeaderboardByUser } from '@/api/promotion'
 
 export default {
   name: 'PromotionManagement',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       activeTab: 'milestone',

+ 2 - 0
cfc-web/src/views/admin/ProxyReportUpload.vue

@@ -129,11 +129,13 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getFamilyList, getFamilyMemberList } from '@/api/admin'
 import { proxyParseReport, proxyConfirmReport, adminListHealthReports, getHealthReportDetail } from '@/api/admin'
 
 export default {
   name: 'ProxyReportUpload',
+  mixins: [ListStateMixin],
   data() {
     return {
       uploading: false,

+ 2 - 1
cfc-web/src/views/admin/PurchaseManagement.vue

@@ -141,10 +141,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import request from '@/utils/request'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'PurchaseManagement',
   data() {
     return {

+ 3 - 0
cfc-web/src/views/admin/RecommendationRules.vue

@@ -180,7 +180,10 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 export default {
+  name: 'RecommendationRules',
+  mixins: [ListStateMixin],
   data: function() {
     return {
       list: [],

+ 2 - 1
cfc-web/src/views/admin/ReportManagement.vue

@@ -95,11 +95,12 @@
 <script>
 import axios from 'axios'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { archiveReport, archiveReportsBatch } from '@/api/admin'
 
 export default {
   name: 'ReportManagement',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       tableData: [],

+ 2 - 0
cfc-web/src/views/admin/ReportParseStatus.vue

@@ -72,10 +72,12 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { listReportParseDrafts, retryReportParseDraft } from '@/api/reportParse'
 
 export default {
   name: 'ReportParseStatus',
+  mixins: [ListStateMixin],
   data() {
     return {
       list: [],

+ 2 - 2
cfc-web/src/views/admin/SubscriptionManagement.vue

@@ -178,12 +178,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getSubscriptionList, activateSubscription, cancelSubscriptionAdmin } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'SubscriptionManagement',
-  mixins: [SortMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 2
cfc-web/src/views/admin/SupplierProductManage.vue

@@ -112,12 +112,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getSupplierProductList, shelveSupplierProduct } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'SupplierProductManage',
-  mixins: [SortMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/SysConfig.vue

@@ -80,10 +80,11 @@
 <script>
 import { getSysConfigList, createSysConfig, updateSysConfig, deleteSysConfig } from '@/api/admin'
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'SysConfig',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 3 - 1
cfc-web/src/views/admin/SystemPromptManagement.vue

@@ -133,10 +133,12 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getPromptList, savePrompt, resetPrompt, deletePrompt, listVersions, rollbackPrompt } from "@/api/systemPrompt"
 
 export default {
-  name: "SystemPromptManagement",
+  name: 'SystemPromptManagement',
+  mixins: [ListStateMixin],
   data() {
     return {
       list: [],

+ 2 - 0
cfc-web/src/views/admin/VectorKnowledgeBase.vue

@@ -108,6 +108,7 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import {
   getVectorStats,
   listVectorDocuments,
@@ -118,6 +119,7 @@ import {
 
 export default {
   name: 'VectorKnowledgeBase',
+  mixins: [ListStateMixin],
   data() {
     return {
       stats: {},

+ 2 - 1
cfc-web/src/views/admin/VirtualTeamList.vue

@@ -127,6 +127,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import {
   listVirtualTeams,
   createVirtualTeam,
@@ -135,7 +136,7 @@ import {
 } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'VirtualTeamList',
   data() {
     return {

+ 2 - 1
cfc-web/src/views/admin/data-source-config.vue

@@ -128,10 +128,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 
 export default {
   name: 'DataSourceConfig',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 3 - 1
cfc-web/src/views/admin/gates.vue

@@ -218,6 +218,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getUnlockGateList, saveUnlockGate, deleteUnlockGate, sortUnlockGates, toggleUnlockGate, resetFamilyGates, getFamilyGateProgress } from '@/api/admin'
 
 const GATE_TYPE_LABELS = {
@@ -243,7 +244,8 @@ const UNLOCK_TARGET_LABELS = {
 }
 
 export default {
-  mixins: [SortMixin],
+  name: 'GatesManage',
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       loading: false,

+ 3 - 0
cfc-web/src/views/admin/knowledge/index.vue

@@ -119,6 +119,8 @@
 </template>
 
 <script>
+import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { mapState, mapActions } from 'vuex'
 import { getDimensionTree } from '@/api/dimension'
 import { getKnowledge, deleteKnowledge, bindKnowledgeDimensions, bindKnowledgeTags } from '@/api/dimension'
@@ -129,6 +131,7 @@ import TagSelector from '@/components/TagSelector'
 
 export default {
   name: 'KnowledgeManage',
+  mixins: [ListStateMixin],
   components: { KnowledgeTable, KnowledgeDialog, DimensionSelector, TagSelector },
   data() {
     return {

+ 2 - 1
cfc-web/src/views/admin/review/ArticleAudit.vue

@@ -98,13 +98,14 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { adminArticleList, adminArticleAudit } from '@/api/article'
 import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'ArticleAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/review/ButlerAudit.vue

@@ -110,6 +110,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getButlerUserList, reviewButler, toggleButlerAccepting } from '@/api/butler'
 import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
 import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
@@ -118,7 +119,7 @@ import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 export default {
   name: 'ButlerAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/review/GuideAudit.vue

@@ -98,6 +98,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getGuideApplications, approveGuide, rejectGuide } from '@/api/admin'
 import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
 import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
@@ -106,7 +107,7 @@ import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 export default {
   name: 'GuideAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/review/NutritionistAudit.vue

@@ -94,6 +94,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getNutritionistApplications, approveNutritionist, rejectNutritionist } from '@/api/admin'
 import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
 import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
@@ -102,7 +103,7 @@ import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 export default {
   name: 'NutritionistAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/review/PackageAudit.vue

@@ -105,13 +105,14 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getPackageList, reviewPackage } from '@/api/package'
 import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'PackageAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/review/PendingRefundAudit.vue

@@ -136,13 +136,14 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getPendingRefundList, getPendingRefundSummary, cancelPendingRefund, searchUsers } from '@/api/admin'
 import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'PendingRefundAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   data() {
     return {
       list: [],

+ 2 - 1
cfc-web/src/views/admin/review/VendorAudit.vue

@@ -102,6 +102,7 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getVendorList, reviewVendor } from '@/api/vendor'
 import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
 import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
@@ -110,7 +111,7 @@ import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 export default {
   name: 'VendorAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/review/WithdrawAudit.vue

@@ -82,13 +82,14 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getWithdrawalList, auditWithdrawal } from '@/api/admin'
 import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'WithdrawAudit',
   components: { AuditDetailDrawer },
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/supply-manage/List.vue

@@ -90,11 +90,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getSupplyManageList, deleteSupplyManage } from '@/api/admin'
 
 export default {
   name: 'SupplyManageList',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/admin/supply-system/List.vue

@@ -91,11 +91,12 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getSupplySystemList, toggleSupplySystemStatus } from '@/api/admin'
 
 export default {
   name: 'SupplySystemList',
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300

+ 2 - 1
cfc-web/src/views/nutritionist/HealthReports.vue

@@ -89,8 +89,9 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'HealthReports',
   computed: {
     tableHeight() {

+ 2 - 0
cfc-web/src/views/orchestration/OrchestrationFlow.vue

@@ -78,10 +78,12 @@
 </template>
 
 <script>
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { listFlows, publishFlow, archiveFlow, deleteFlow, startExecution } from '@/api/orchestration'
 
 export default {
   name: 'OrchestrationFlow',
+  mixins: [ListStateMixin],
   data() {
     return {
       tableData: [],

+ 2 - 1
cfc-web/src/views/teacher/ProxyReport.vue

@@ -169,12 +169,13 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { proxyParseReport, proxyConfirmReport, listHealthReports, getHealthReportDetail } from '@/api/admin'
 import { getFamilyList } from '@/api/admin'
 import { getFamilyMemberList } from '@/api/admin'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'ProxyReport',
   data() {
     return {

+ 2 - 1
cfc-web/src/views/teacher/TeacherOrders.vue

@@ -207,10 +207,11 @@
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
+import ListStateMixin from '@/mixins/ListStateMixin'
 import { getGuideOrders, getOrderStats } from '@/api/guide'
 
 export default {
-  mixins: [SortMixin],
+  mixins: [SortMixin, ListStateMixin],
   name: 'TeacherOrders',
   data() {
     return {