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

feat: ZmTable 支持缓存列设置

- 新增列设置本地缓存,支持按 settingsCacheKey 或路由列信息恢复配置
- 支持缓存列顺序、显隐和固定状态,并在重置列设置时同步缓存
- action 列纳入表格设置列表,仅 hideInColumnSettings 控制排除
- 瑞恒日报统计表配置固定缓存 key
Zimo 1 неделя назад
Родитель
Сommit
f2eb235852

+ 27 - 1
src/components/ZmTable/README.md

@@ -75,6 +75,8 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
 | `showBorder` | `boolean` | `false` | 控制组件自己的边框显示样式。 |
 | `hoverHighlight` | `boolean` | `true` | 控制鼠标悬浮行时是否显示 hover 背景。 |
 | `showOverflowTooltip` | `boolean` | `true` | 继承自 Element Plus。想让文字换行时传 `false`,再配合页面 CSS 覆盖 `.cell` 的换行样式。 |
+| `settingsCache` | `boolean` | `true` | 是否缓存列设置。当前使用 `localStorage`,缓存列顺序、显隐和固定状态。 |
+| `settingsCacheKey` | `string` | - | 列设置缓存 key。建议业务页面传稳定唯一值,没传时组件会用当前路由和列 key 自动生成。 |
 
 组件内部默认还会给 Element Plus 表格设置这些值:
 
@@ -99,7 +101,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
 | 属性 | 类型 | 默认值 | 说明 |
 | --- | --- | --- | --- |
 | `prop` | `keyof T \| string` | - | 字段名,也会作为列设置 key 的优先来源。 |
-| `action` | `boolean` | `false` | 标记为操作列,并在表头显示列设置按钮。操作列本身不会进入列设置列表。 |
+| `action` | `boolean` | `false` | 标记为操作列,并在表头显示列设置按钮。操作列会进入列设置列表。 |
 | `hideInColumnSettings` | `boolean` | `false` | 不显示在列设置面板里,适合序号列、选择列等固定功能列。 |
 | `isParent` | `boolean` | `false` | 标记当前列是多级表头父级。需要控制子级列排序/显隐/固定时必须加。 |
 | `zmSortable` | `boolean` | `false` | 显示排序按钮。组件只维护/展示排序状态,具体排序逻辑由调用方处理。 |
@@ -243,6 +245,30 @@ function handleSortChange(payload: { prop: string; order: 'asc' | 'desc' | null
 - 显示 / 隐藏列。
 - 固定到左侧 / 固定到右侧 / 取消固定。
 - 重置为初始列配置。
+- 自动缓存列设置,下次进入同一张表时恢复。
+
+缓存默认开启,当前先写入 `localStorage`。后续接接口时,建议继续使用稳定的 `settingsCacheKey` 作为后端保存和读取配置的业务标识。
+
+```vue
+<ZmTable :data="list" :loading="loading" settings-cache-key="device-monitor-list">
+  <ZmTableColumn prop="name" label="名称" />
+  <ZmTableColumn prop="status" label="状态" />
+
+  <ZmTableColumn label="操作" width="120" fixed="right" action>
+    <template #default="{ row }">
+      <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+    </template>
+  </ZmTableColumn>
+</ZmTable>
+```
+
+不希望某张表缓存时,可以关闭:
+
+```vue
+<ZmTable :data="list" :loading="loading" :settings-cache="false">
+  ...
+</ZmTable>
+```
 
 列设置 key 的优先级:
 

+ 120 - 4
src/components/ZmTable/index.vue

@@ -10,8 +10,10 @@ import {
   provide,
   ref,
   useAttrs,
-  useSlots
+  useSlots,
+  watch
 } from 'vue'
+import { useRoute } from 'vue-router'
 import { TableContextKey } from './token'
 import type { ColumnAlign, ColumnFixed, ColumnSettingItem } from './token'
 import type { DefaultRow } from 'element-plus/es/components/table/src/table/defaults'
@@ -38,15 +40,21 @@ interface Props
   hoverHighlight?: boolean
   align?: ColumnAlign
   columnMaxWidth?: number
+  settingsCache?: boolean
+  settingsCacheKey?: string
 }
 
 const props = defineProps<Props>()
 const attrs = useAttrs()
 const slots = useSlots()
+const route = useRoute()
 const tableRef = ref<TableInstance>()
 const columnSettings = ref<ColumnSettingItem[]>([])
 const columnDefaultSettings = ref<ColumnSettingItem[]>([])
 const slotColumnSignature = ref('')
+const columnSettingsStorageKey = ref('')
+const isSyncingColumnSettings = ref(false)
+const columnSettingsStoragePrefix = 'zm-table:column-settings:'
 
 const defaultOptions: Partial<Props> = {
   size: 'default',
@@ -71,6 +79,8 @@ const bindProps = computed(() => {
     hoverHighlight: _hoverHighlight,
     align: _align,
     columnMaxWidth,
+    settingsCache: _settingsCache,
+    settingsCacheKey: _settingsCacheKey,
     ...otherProps
   } = props
 
@@ -93,6 +103,7 @@ const safeColumnMaxWidth = computed(() => {
   const maxWidth = Number(props.columnMaxWidth)
   return Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : 360
 })
+const isColumnSettingsCacheEnabled = computed(() => props.settingsCache !== false)
 
 const forwardedSlots = computed(() => {
   const { default: _default, ...restSlots } = slots
@@ -169,7 +180,7 @@ const getColumnMeta = (node: VNode, index: number, parentKey?: string): ColumnMe
     label: getColumnLabel(node, index, parentKey),
     visible: getPropValue(node.props, 'visible'),
     action,
-    configurable: !action && !hideInColumnSettings,
+    configurable: !hideInColumnSettings,
     fixed: normalizeFixed(getPropValue(node.props, 'fixed')),
     children: getColumnChildren(node)
       .map((child, childIndex) => getColumnMeta(child, childIndex, key))
@@ -197,8 +208,11 @@ const syncColumnSettings = (nodes: VNode[]) => {
   slotColumnSignature.value = signature
   nextTick(() => {
     const defaults = metas.map(createDefaultColumnSetting)
+    const storageKey = resolveColumnSettingsStorageKey(defaults)
+    const cachedSettings = loadColumnSettings(storageKey)
     columnDefaultSettings.value = defaults
-    columnSettings.value = mergeColumnSettings(defaults, columnSettings.value)
+    columnSettingsStorageKey.value = storageKey
+    setColumnSettings(mergeColumnSettings(defaults, cachedSettings || columnSettings.value))
   })
 }
 
@@ -291,9 +305,111 @@ const cloneColumnSettings = (items: ColumnSettingItem[]): ColumnSettingItem[] =>
 }
 
 const resetColumnSettings = () => {
-  columnSettings.value = cloneColumnSettings(columnDefaultSettings.value)
+  setColumnSettings(cloneColumnSettings(columnDefaultSettings.value))
 }
 
+const flattenColumnSettingKeys = (items: ColumnSettingItem[]): string[] => {
+  return items.flatMap((item) => [
+    item.key,
+    ...(item.children ? flattenColumnSettingKeys(item.children) : [])
+  ])
+}
+
+const encodeStorageKeyPart = (value: string) => {
+  try {
+    return encodeURIComponent(value)
+  } catch {
+    return value
+  }
+}
+
+const resolveColumnSettingsStorageKey = (defaults: ColumnSettingItem[]) => {
+  if (!isColumnSettingsCacheEnabled.value) return ''
+
+  const userKey = props.settingsCacheKey?.trim()
+  if (userKey) return `${columnSettingsStoragePrefix}${encodeStorageKeyPart(userKey)}`
+
+  const routeKey = String(route.name || route.path || 'global')
+  const columnKey = flattenColumnSettingKeys(defaults).sort().join('|')
+  return `${columnSettingsStoragePrefix}${encodeStorageKeyPart(`${routeKey}:${columnKey}`)}`
+}
+
+const isColumnSettingItem = (item: unknown): item is ColumnSettingItem => {
+  if (!item || typeof item !== 'object') return false
+  const setting = item as ColumnSettingItem
+  const validFixed =
+    setting.fixed === false || setting.fixed === 'left' || setting.fixed === 'right'
+  return typeof setting.key === 'string' && typeof setting.visible === 'boolean' && validFixed
+}
+
+const normalizeCachedColumnSettings = (items: unknown): ColumnSettingItem[] => {
+  if (!Array.isArray(items)) return []
+
+  return items.filter(isColumnSettingItem).map((item) => ({
+    key: item.key,
+    label: typeof item.label === 'string' ? item.label : item.key,
+    visible: item.visible,
+    fixed: item.fixed,
+    children: item.children ? normalizeCachedColumnSettings(item.children) : undefined
+  }))
+}
+
+const loadColumnSettings = (storageKey: string) => {
+  if (!storageKey || typeof window === 'undefined') return null
+
+  try {
+    const rawValue = window.localStorage.getItem(storageKey)
+    if (!rawValue) return null
+
+    const cachedValue = JSON.parse(rawValue)
+    const settings = normalizeCachedColumnSettings(cachedValue?.settings)
+    return settings.length ? settings : null
+  } catch {
+    return null
+  }
+}
+
+const saveColumnSettings = (storageKey: string, settings: ColumnSettingItem[]) => {
+  if (!storageKey || typeof window === 'undefined') return
+
+  try {
+    window.localStorage.setItem(
+      storageKey,
+      JSON.stringify({
+        version: 1,
+        settings
+      })
+    )
+  } catch {
+    // localStorage 可能被浏览器策略禁用,失败时不影响表格正常使用。
+  }
+}
+
+const setColumnSettings = (settings: ColumnSettingItem[]) => {
+  isSyncingColumnSettings.value = true
+  columnSettings.value = settings
+  nextTick(() => {
+    isSyncingColumnSettings.value = false
+    saveColumnSettings(columnSettingsStorageKey.value, columnSettings.value)
+  })
+}
+
+watch(
+  columnSettings,
+  (settings) => {
+    if (isSyncingColumnSettings.value) return
+    saveColumnSettings(columnSettingsStorageKey.value, settings)
+  },
+  { deep: true }
+)
+
+watch(
+  [() => props.settingsCache, () => props.settingsCacheKey, () => route.name, () => route.path],
+  () => {
+    slotColumnSignature.value = ''
+  }
+)
+
 const applyColumnSetting = (node: VNode, setting: ColumnSettingItem) => {
   const clonedNode = cloneVNode(
     node,

+ 2 - 0
src/views/pms/iotrhdailyreport/components/DailyStatistics.vue

@@ -852,6 +852,8 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
                       :loading="listLoading"
                       :data="list"
                       :height="height"
+                      :settings-cache="true"
+                      settings-cache-key="rhSummary"
                       show-border>
                       <template v-for="item in columns(type)" :key="item.prop">
                         <zm-table-column