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

feat: 为多个文档组件添加 cacheKeyUrl 属性,优化列配置的本地存储和加载逻辑

xieyong 1 месяц назад
Родитель
Сommit
a023fb93a1

+ 1 - 1
src/views/doc/abolish_doc/index.vue

@@ -1,5 +1,5 @@
 <template>
-<Main :fileType="0" lcyStatus="4"></Main>
+<Main :fileType="0" lcyStatus="4" cacheKeyUrl="fm-2611241029-doc-abolish-0-4"></Main>
 </template>
 
 <script>

+ 56 - 19
src/views/doc/components/file-table-list.vue

@@ -675,6 +675,15 @@
     created() {
       this.loadColumnsFromServer();
     },
+    mounted() {
+      // mounted 之后强制触发一次 columns watcher,确保 ele-pro-table 重新读取 localStorage 缓存
+      // (覆盖 watcher immediate 在某些时序下读不到 localStorage 的情况)
+      this.$nextTick(() => {
+        if (this.cacheKeyUrl) {
+          this.columns = [...this.columns];
+        }
+      });
+    },
 
     methods: {
       // ele-pro-table 列变化 → 转换为 slim 格式后同步到后端
@@ -690,40 +699,68 @@
           const key = c.prop || c.columnKey;
           if (key) fixedMap.set(key, c.fixed);
         });
-        const visibleIds = new Set(
-          visibleColumns.map((c) => c.prop || c.columnKey)
-        );
-        const merged = this.columns
+        // 必须按 visibleColumns(用户拖动后的实际顺序)输出 merged,
+        // 否则服务端存的会是 columns 的初始顺序,下次 loadColumnsFromServer 用它
+        // 覆盖 localStorage 会把用户调整的顺序"打回原形"。
+        const sourceByKey = new Map();
+        this.columns
           .filter(
             (c) =>
               (c.prop || c.columnKey || c.label === '操作') &&
               !['selection', 'index', 'expand'].includes(c.type)
           )
-          .map((c) => {
-            const key = c.prop || c.columnKey;
-            return {
-              id: key,
-              prop: c.prop,
-              columnKey: c.columnKey,
-              label: c.label,
-              checked: visibleIds.has(key),
-              fixed: fixedMap.has(key) ? fixedMap.get(key) : c.fixed
-            };
+          .forEach((c) => {
+            sourceByKey.set(c.prop || c.columnKey, c);
+          });
+        const merged = [];
+        const seen = new Set();
+        visibleColumns.forEach((vc) => {
+          const key = vc.prop || vc.columnKey;
+          if (!key || seen.has(key)) return;
+          const src = sourceByKey.get(key);
+          if (!src) return;
+          seen.add(key);
+          merged.push({
+            id: key,
+            prop: src.prop,
+            columnKey: src.columnKey,
+            label: src.label,
+            checked: true,
+            fixed: fixedMap.has(key) ? fixedMap.get(key) : src.fixed
           });
+        });
+        // visibleColumns 里没出现的列(被取消勾选的),按 source 原顺序追加,checked=false
+        sourceByKey.forEach((c, key) => {
+          if (seen.has(key)) return;
+          merged.push({
+            id: key,
+            prop: c.prop,
+            columnKey: c.columnKey,
+            label: c.label,
+            checked: false,
+            fixed: c.fixed
+          });
+        });
         this.saveColumns(merged);
       },
       // 拉取后端列配置,写入与 ele-pro-table 共享的 localStorage,触发表格重渲染
       // 过滤掉 HIDE_SETTING_TYPES 的列(selection/index/expand),避免污染 ⚙️ 工具栏列表
+      // 注意:localStorage 已有数据时不覆盖(保留用户刚刚调整的列顺序,
+      // 避免服务端旧数据覆盖本地最新调整)
       async loadColumnsFromServer() {
         if (!this.cacheKeyUrl) return;
         try {
           const res = await this.getByTableId(this.cacheKeyUrl);
           if (res?.columnConfig?.length > 0) {
-            const sanitized = res.columnConfig.filter(
-              (c) => !['selection', 'index', 'expand'].includes(c.type)
-            );
-            this.setStorage(this.cacheKeyUrl + 'Cols', sanitized);
-            // 触发 ele-pro-table 的 columns watcher 重读缓存
+            const existing = this.getStorage(this.cacheKeyUrl + 'Cols');
+            if (!Array.isArray(existing) || existing.length === 0) {
+              // localStorage 没缓存(首次进入或清缓存后)→ 用服务端配置填充
+              const sanitized = res.columnConfig.filter(
+                (c) => !['selection', 'index', 'expand'].includes(c.type)
+              );
+              this.setStorage(this.cacheKeyUrl + 'Cols', sanitized);
+            }
+            // 无论是否填充,都触发 watcher 重读 localStorage
             this.columns = [...this.columns];
           }
         } catch (e) {

+ 1 - 1
src/views/doc/issue_doc/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <Main :fileType="0" lcyStatus="3"></Main>
+  <Main :fileType="0" lcyStatus="3" cacheKeyUrl="fm-2611241029-doc-issue-0-3"></Main>
 </template>
 
 <script>

+ 1 - 1
src/views/doc/personal_doc/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <Main :fileType="1" lcyStatus="1" ></Main>
+  <Main :fileType="1" lcyStatus="1" cacheKeyUrl="fm-2611241029-doc-personal-1-1"></Main>
   </template>
   
   <script>

+ 1 - 1
src/views/doc/pigeonhole_doc/index.vue

@@ -1,5 +1,5 @@
 <template>
-<Main :fileType="0" lcyStatus="2"></Main>
+<Main :fileType="0" lcyStatus="2" cacheKeyUrl="fm-2611241029-doc-pigeonhole-0-2"></Main>
 </template>
 
 <script>

+ 103 - 2
src/views/doc/recycleBin/index.vue

@@ -6,6 +6,10 @@
         ref="table"
         :columns="columns"
         :datasource="datasource"
+        :selection.sync="selection"
+        :cache-key="cacheKeyUrl"
+        @selection-change="onSelectionChange"
+        @columns-change="onColumnsChange"
         height="calc(100vh - 380px)"
         full-height="calc(100vh - 116px)"
       >
@@ -58,6 +62,7 @@
   import search from './components/search.vue';
   import { mapGetters } from 'vuex';
   import dictMixins from '@/mixins/dictMixins';
+  import tableColumnsMixin from '@/mixins/tableColumnsMixin';
   import {
     recycleBinPage,
     getDocTreeListAPI,
@@ -65,12 +70,17 @@
     recycleDeleteAPI
   } from '@/api/doc-manage';
   export default {
-    mixins: [dictMixins],
+    mixins: [dictMixins, tableColumnsMixin],
     components: {
       search
     },
     data() {
       return {
+        // 关闭 mixin 默认的 getTabColumns(与 ele-pro-table 工具栏共享同一 localStorage key,
+        // 混用会破坏缓存结构,由本组件自行接管"远端拉取 + 应用"流程)。
+        // 仅在 cacheKeyUrl 非空时才会真正发起同步(见 loadColumnsFromServer / onColumnsChange 的守卫)
+        tabMixinsInit: false,
+        cacheKeyUrl: 'fm-2611241029-doc-recycleBin',
         folderList: [],
         columns: [
           {
@@ -144,11 +154,102 @@
     },
     created() {
       this.getDocTreeListAPI();
+      this.loadColumnsFromServer();
+    },
+    mounted() {
+      // mounted 之后强制触发一次 columns watcher,确保 ele-pro-table 重新读取 localStorage 缓存
+      // (覆盖 watcher immediate 在某些时序下读不到 localStorage 的情况)
+      this.$nextTick(() => {
+        if (this.cacheKeyUrl) {
+          this.columns = [...this.columns];
+        }
+      });
     },
     computed: {
       ...mapGetters(['user'])
     },
     methods: {
+      // ele-pro-table 列变化 → 转换为 slim 格式后同步到后端
+      // slim 格式与 ele-pro-table 工具栏写入 localStorage 的结构一致,避免缓存互相覆盖
+      // 注意:type 为 selection/index/expand 的列被 ele-pro-table 排除在 ⚙️ 工具栏外(见 HIDE_SETTING_TYPES),
+      // 不应写入后端和 localStorage,否则下次加载会被 getSettingCols 当作缓存返回,污染工具栏列表
+      onColumnsChange(visibleColumns) {
+        if (!this.cacheKeyUrl) return;
+        // 用户在 ⚙️ 工具栏里改最新 fixed 值(按 columnKey / prop 索引)
+        // 源 columns 里的 fixed 是页面初始值,必须以这里为准,否则刷新后丢失
+        const fixedMap = new Map();
+        visibleColumns.forEach((c) => {
+          const key = c.prop || c.columnKey;
+          if (key) fixedMap.set(key, c.fixed);
+        });
+        // 必须按 visibleColumns(用户拖动后的实际顺序)输出 merged,
+        // 否则服务端存的会是 columns 的初始顺序,下次 loadColumnsFromServer 用它
+        // 覆盖 localStorage 会把用户调整的顺序"打回原形"。
+        const sourceByKey = new Map();
+        this.columns
+          .filter(
+            (c) =>
+              (c.prop || c.columnKey || c.label === '操作') &&
+              !['selection', 'index', 'expand'].includes(c.type)
+          )
+          .forEach((c) => {
+            sourceByKey.set(c.prop || c.columnKey, c);
+          });
+        const merged = [];
+        const seen = new Set();
+        visibleColumns.forEach((vc) => {
+          const key = vc.prop || vc.columnKey;
+          if (!key || seen.has(key)) return;
+          const src = sourceByKey.get(key);
+          if (!src) return;
+          seen.add(key);
+          merged.push({
+            id: key,
+            prop: src.prop,
+            columnKey: src.columnKey,
+            label: src.label,
+            checked: true,
+            fixed: fixedMap.has(key) ? fixedMap.get(key) : src.fixed
+          });
+        });
+        // visibleColumns 里没出现的列(被取消勾选的),按 source 原顺序追加,checked=false
+        sourceByKey.forEach((c, key) => {
+          if (seen.has(key)) return;
+          merged.push({
+            id: key,
+            prop: c.prop,
+            columnKey: c.columnKey,
+            label: c.label,
+            checked: false,
+            fixed: c.fixed
+          });
+        });
+        this.saveColumns(merged);
+      },
+      // 拉取后端列配置,写入与 ele-pro-table 共享的 localStorage,触发表格重渲染
+      // 过滤掉 HIDE_SETTING_TYPES 的列(selection/index/expand),避免污染 ⚙️ 工具栏列表
+      // 注意:localStorage 已有数据时不覆盖(保留用户刚刚调整的列顺序,
+      // 避免服务端旧数据覆盖本地最新调整)
+      async loadColumnsFromServer() {
+        if (!this.cacheKeyUrl) return;
+        try {
+          const res = await this.getByTableId(this.cacheKeyUrl);
+          if (res?.columnConfig?.length > 0) {
+            const existing = this.getStorage(this.cacheKeyUrl + 'Cols');
+            if (!Array.isArray(existing) || existing.length === 0) {
+              // localStorage 没缓存(首次进入或清缓存后)→ 用服务端配置填充
+              const sanitized = res.columnConfig.filter(
+                (c) => !['selection', 'index', 'expand'].includes(c.type)
+              );
+              this.setStorage(this.cacheKeyUrl + 'Cols', sanitized);
+            }
+            // 无论是否填充,都触发 watcher 重读 localStorage
+            this.columns = [...this.columns];
+          }
+        } catch (e) {
+          console.error('加载列配置失败:', e);
+        }
+      },
       async getDocTreeListAPI() {
         let query = {
           type: 0,
@@ -205,4 +306,4 @@
   :deep(.el-input__suffix) {
     display: none;
   }
-</style>
+</style>

+ 100 - 1
src/views/doc/sendReceive/receive/myReceive.vue

@@ -6,8 +6,10 @@
       :selection.sync="selection"
       :columns="columns"
       :datasource="datasource"
+      :cache-key="cacheKeyUrl"
       height="calc(100vh - 420px)"
       full-height="calc(100vh - 116px)"
+      @columns-change="onColumnsChange"
     >
       <!-- 表头工具栏 -->
       <!-- <template v-slot:toolbar>
@@ -25,16 +27,22 @@
 import search from './components/search.vue';
 import { mapGetters } from 'vuex';
 import dictMixins from '@/mixins/dictMixins';
+import tableColumnsMixin from '@/mixins/tableColumnsMixin';
 import {
   recyclePage,
 } from '@/api/doc-manage';
 export default {
-  mixins: [dictMixins],
+  mixins: [dictMixins, tableColumnsMixin],
   components: {
     search
   },
   data() {
     return {
+      // 关闭 mixin 默认的 getTabColumns(与 ele-pro-table 工具栏共享同一 localStorage key,
+      // 混用会破坏缓存结构,由本组件自行接管"远端拉取 + 应用"流程)。
+      // 仅在 cacheKeyUrl 非空时才会真正发起同步(见 loadColumnsFromServer / onColumnsChange 的守卫)
+      tabMixinsInit: false,
+      cacheKeyUrl: 'fm-2611241029-doc-receive-received',
       selection: [],
       columns: [
       {
@@ -92,11 +100,102 @@ export default {
     };
   },
   created() {
+    this.loadColumnsFromServer();
+  },
+  mounted() {
+    // mounted 之后强制触发一次 columns watcher,确保 ele-pro-table 重新读取 localStorage 缓存
+    // (覆盖 watcher immediate 在某些时序下读不到 localStorage 的情况)
+    this.$nextTick(() => {
+      if (this.cacheKeyUrl) {
+        this.columns = [...this.columns];
+      }
+    });
   },
   computed: {
     ...mapGetters(['user'])
   },
   methods: {
+    // ele-pro-table 列变化 → 转换为 slim 格式后同步到后端
+    // slim 格式与 ele-pro-table 工具栏写入 localStorage 的结构一致,避免缓存互相覆盖
+    // 注意:type 为 selection/index/expand 的列被 ele-pro-table 排除在 ⚙️ 工具栏外(见 HIDE_SETTING_TYPES),
+    // 不应写入后端和 localStorage,否则下次加载会被 getSettingCols 当作缓存返回,污染工具栏列表
+    onColumnsChange(visibleColumns) {
+      if (!this.cacheKeyUrl) return;
+      // 用户在 ⚙️ 工具栏里改最新 fixed 值(按 columnKey / prop 索引)
+      // 源 columns 里的 fixed 是页面初始值,必须以这里为准,否则刷新后丢失
+      const fixedMap = new Map();
+      visibleColumns.forEach((c) => {
+        const key = c.prop || c.columnKey;
+        if (key) fixedMap.set(key, c.fixed);
+      });
+      // 必须按 visibleColumns(用户拖动后的实际顺序)输出 merged,
+      // 否则服务端存的会是 columns 的初始顺序,下次 loadColumnsFromServer 用它
+      // 覆盖 localStorage 会把用户调整的顺序"打回原形"。
+      const sourceByKey = new Map();
+      this.columns
+        .filter(
+          (c) =>
+            (c.prop || c.columnKey || c.label === '操作') &&
+            !['selection', 'index', 'expand'].includes(c.type)
+        )
+        .forEach((c) => {
+          sourceByKey.set(c.prop || c.columnKey, c);
+        });
+      const merged = [];
+      const seen = new Set();
+      visibleColumns.forEach((vc) => {
+        const key = vc.prop || vc.columnKey;
+        if (!key || seen.has(key)) return;
+        const src = sourceByKey.get(key);
+        if (!src) return;
+        seen.add(key);
+        merged.push({
+          id: key,
+          prop: src.prop,
+          columnKey: src.columnKey,
+          label: src.label,
+          checked: true,
+          fixed: fixedMap.has(key) ? fixedMap.get(key) : src.fixed
+        });
+      });
+      // visibleColumns 里没出现的列(被取消勾选的),按 source 原顺序追加,checked=false
+      sourceByKey.forEach((c, key) => {
+        if (seen.has(key)) return;
+        merged.push({
+          id: key,
+          prop: c.prop,
+          columnKey: c.columnKey,
+          label: c.label,
+          checked: false,
+          fixed: c.fixed
+        });
+      });
+      this.saveColumns(merged);
+    },
+    // 拉取后端列配置,写入与 ele-pro-table 共享的 localStorage,触发表格重渲染
+    // 过滤掉 HIDE_SETTING_TYPES 的列(selection/index/expand),避免污染 ⚙️ 工具栏列表
+    // 注意:localStorage 已有数据时不覆盖(保留用户刚刚调整的列顺序,
+    // 避免服务端旧数据覆盖本地最新调整)
+    async loadColumnsFromServer() {
+      if (!this.cacheKeyUrl) return;
+      try {
+        const res = await this.getByTableId(this.cacheKeyUrl);
+        if (res?.columnConfig?.length > 0) {
+          const existing = this.getStorage(this.cacheKeyUrl + 'Cols');
+          if (!Array.isArray(existing) || existing.length === 0) {
+            // localStorage 没缓存(首次进入或清缓存后)→ 用服务端配置填充
+            const sanitized = res.columnConfig.filter(
+              (c) => !['selection', 'index', 'expand'].includes(c.type)
+            );
+            this.setStorage(this.cacheKeyUrl + 'Cols', sanitized);
+          }
+          // 无论是否填充,都触发 watcher 重读 localStorage
+          this.columns = [...this.columns];
+        }
+      } catch (e) {
+        console.error('加载列配置失败:', e);
+      }
+    },
     openEdit(type, row) {
       this.$refs.editRef.open(type, row);
     },

+ 102 - 2
src/views/doc/sendReceive/receive/receive.vue

@@ -6,8 +6,10 @@
       :selection.sync="selection"
       :columns="columns"
       :datasource="datasource"
+      :cache-key="cacheKeyUrl"
       height="calc(100vh - 420px)"
       full-height="calc(100vh - 116px)"
+      @columns-change="onColumnsChange"
     >
       <!-- 表头工具栏 -->
       <template v-slot:toolbar>
@@ -57,11 +59,12 @@ import search from './components/search.vue';
 import edit from './components/edit.vue';
 import { mapGetters } from 'vuex';
 import dictMixins from '@/mixins/dictMixins';
+import tableColumnsMixin from '@/mixins/tableColumnsMixin';
 import { receivePage, revoke, recycleDeleteAPI } from '@/api/doc-manage';
 import processSubmitDialog from '@/components/processSubmitDialog/processSubmitDialog.vue';
 
 export default {
-  mixins: [dictMixins],
+  mixins: [dictMixins, tableColumnsMixin],
   components: {
     search,
     processSubmitDialog,
@@ -69,6 +72,11 @@ export default {
   },
   data() {
     return {
+      // 关闭 mixin 默认的 getTabColumns(与 ele-pro-table 工具栏共享同一 localStorage key,
+      // 混用会破坏缓存结构,由本组件自行接管"远端拉取 + 应用"流程)。
+      // 仅在 cacheKeyUrl 非空时才会真正发起同步(见 loadColumnsFromServer / onColumnsChange 的守卫)
+      tabMixinsInit: false,
+      cacheKeyUrl: 'fm-2611241029-doc-receive-mine',
       processSubmitDialogFlag: false,
       selection: [],
       columns: [
@@ -146,11 +154,103 @@ export default {
       ]
     };
   },
-  created() {},
+  created() {
+    this.loadColumnsFromServer();
+  },
+  mounted() {
+    // mounted 之后强制触发一次 columns watcher,确保 ele-pro-table 重新读取 localStorage 缓存
+    // (覆盖 watcher immediate 在某些时序下读不到 localStorage 的情况)
+    this.$nextTick(() => {
+      if (this.cacheKeyUrl) {
+        this.columns = [...this.columns];
+      }
+    });
+  },
   computed: {
     ...mapGetters(['user'])
   },
   methods: {
+    // ele-pro-table 列变化 → 转换为 slim 格式后同步到后端
+    // slim 格式与 ele-pro-table 工具栏写入 localStorage 的结构一致,避免缓存互相覆盖
+    // 注意:type 为 selection/index/expand 的列被 ele-pro-table 排除在 ⚙️ 工具栏外(见 HIDE_SETTING_TYPES),
+    // 不应写入后端和 localStorage,否则下次加载会被 getSettingCols 当作缓存返回,污染工具栏列表
+    onColumnsChange(visibleColumns) {
+      if (!this.cacheKeyUrl) return;
+      // 用户在 ⚙️ 工具栏里改最新 fixed 值(按 columnKey / prop 索引)
+      // 源 columns 里的 fixed 是页面初始值,必须以这里为准,否则刷新后丢失
+      const fixedMap = new Map();
+      visibleColumns.forEach((c) => {
+        const key = c.prop || c.columnKey;
+        if (key) fixedMap.set(key, c.fixed);
+      });
+      // 必须按 visibleColumns(用户拖动后的实际顺序)输出 merged,
+      // 否则服务端存的会是 columns 的初始顺序,下次 loadColumnsFromServer 用它
+      // 覆盖 localStorage 会把用户调整的顺序"打回原形"。
+      const sourceByKey = new Map();
+      this.columns
+        .filter(
+          (c) =>
+            (c.prop || c.columnKey || c.label === '操作') &&
+            !['selection', 'index', 'expand'].includes(c.type)
+        )
+        .forEach((c) => {
+          sourceByKey.set(c.prop || c.columnKey, c);
+        });
+      const merged = [];
+      const seen = new Set();
+      visibleColumns.forEach((vc) => {
+        const key = vc.prop || vc.columnKey;
+        if (!key || seen.has(key)) return;
+        const src = sourceByKey.get(key);
+        if (!src) return;
+        seen.add(key);
+        merged.push({
+          id: key,
+          prop: src.prop,
+          columnKey: src.columnKey,
+          label: src.label,
+          checked: true,
+          fixed: fixedMap.has(key) ? fixedMap.get(key) : src.fixed
+        });
+      });
+      // visibleColumns 里没出现的列(被取消勾选的),按 source 原顺序追加,checked=false
+      sourceByKey.forEach((c, key) => {
+        if (seen.has(key)) return;
+        merged.push({
+          id: key,
+          prop: c.prop,
+          columnKey: c.columnKey,
+          label: c.label,
+          checked: false,
+          fixed: c.fixed
+        });
+      });
+      this.saveColumns(merged);
+    },
+    // 拉取后端列配置,写入与 ele-pro-table 共享的 localStorage,触发表格重渲染
+    // 过滤掉 HIDE_SETTING_TYPES 的列(selection/index/expand),避免污染 ⚙️ 工具栏列表
+    // 注意:localStorage 已有数据时不覆盖(保留用户刚刚调整的列顺序,
+    // 避免服务端旧数据覆盖本地最新调整)
+    async loadColumnsFromServer() {
+      if (!this.cacheKeyUrl) return;
+      try {
+        const res = await this.getByTableId(this.cacheKeyUrl);
+        if (res?.columnConfig?.length > 0) {
+          const existing = this.getStorage(this.cacheKeyUrl + 'Cols');
+          if (!Array.isArray(existing) || existing.length === 0) {
+            // localStorage 没缓存(首次进入或清缓存后)→ 用服务端配置填充
+            const sanitized = res.columnConfig.filter(
+              (c) => !['selection', 'index', 'expand'].includes(c.type)
+            );
+            this.setStorage(this.cacheKeyUrl + 'Cols', sanitized);
+          }
+          // 无论是否填充,都触发 watcher 重读 localStorage
+          this.columns = [...this.columns];
+        }
+      } catch (e) {
+        console.error('加载列配置失败:', e);
+      }
+    },
     openEdit(type, row) {
       this.$refs.editRef.open(type, row);
     },

+ 102 - 2
src/views/doc/sendReceive/send/mySend.vue

@@ -6,8 +6,10 @@
       :selection.sync="selection"
       :columns="columns"
       :datasource="datasource"
+      :cache-key="cacheKeyUrl"
       height="calc(100vh - 420px)"
       full-height="calc(100vh - 116px)"
+      @columns-change="onColumnsChange"
     >
       <template v-slot:name="{ row }">
         <el-link
@@ -42,15 +44,21 @@
 
   import { mapGetters } from 'vuex';
   import dictMixins from '@/mixins/dictMixins';
+  import tableColumnsMixin from '@/mixins/tableColumnsMixin';
   import { releasePage, signFor, allSignFor } from '@/api/doc-manage';
   export default {
-    mixins: [dictMixins],
+    mixins: [dictMixins, tableColumnsMixin],
     components: {
       search,
       edit
     },
     data() {
       return {
+        // 关闭 mixin 默认的 getTabColumns(与 ele-pro-table 工具栏共享同一 localStorage key,
+        // 混用会破坏缓存结构,由本组件自行接管"远端拉取 + 应用"流程)。
+        // 仅在 cacheKeyUrl 非空时才会真正发起同步(见 loadColumnsFromServer / onColumnsChange 的守卫)
+        tabMixinsInit: false,
+        cacheKeyUrl: 'fm-2611241029-doc-send-received',
         selection: [],
         columns: [
           {
@@ -131,11 +139,103 @@
         ]
       };
     },
-    created() {},
+    created() {
+      this.loadColumnsFromServer();
+    },
+    mounted() {
+      // mounted 之后强制触发一次 columns watcher,确保 ele-pro-table 重新读取 localStorage 缓存
+      // (覆盖 watcher immediate 在某些时序下读不到 localStorage 的情况)
+      this.$nextTick(() => {
+        if (this.cacheKeyUrl) {
+          this.columns = [...this.columns];
+        }
+      });
+    },
     computed: {
       ...mapGetters(['user'])
     },
     methods: {
+      // ele-pro-table 列变化 → 转换为 slim 格式后同步到后端
+      // slim 格式与 ele-pro-table 工具栏写入 localStorage 的结构一致,避免缓存互相覆盖
+      // 注意:type 为 selection/index/expand 的列被 ele-pro-table 排除在 ⚙️ 工具栏外(见 HIDE_SETTING_TYPES),
+      // 不应写入后端和 localStorage,否则下次加载会被 getSettingCols 当作缓存返回,污染工具栏列表
+      onColumnsChange(visibleColumns) {
+        if (!this.cacheKeyUrl) return;
+        // 用户在 ⚙️ 工具栏里改最新 fixed 值(按 columnKey / prop 索引)
+        // 源 columns 里的 fixed 是页面初始值,必须以这里为准,否则刷新后丢失
+        const fixedMap = new Map();
+        visibleColumns.forEach((c) => {
+          const key = c.prop || c.columnKey;
+          if (key) fixedMap.set(key, c.fixed);
+        });
+        // 必须按 visibleColumns(用户拖动后的实际顺序)输出 merged,
+        // 否则服务端存的会是 columns 的初始顺序,下次 loadColumnsFromServer 用它
+        // 覆盖 localStorage 会把用户调整的顺序"打回原形"。
+        const sourceByKey = new Map();
+        this.columns
+          .filter(
+            (c) =>
+              (c.prop || c.columnKey || c.label === '操作') &&
+              !['selection', 'index', 'expand'].includes(c.type)
+          )
+          .forEach((c) => {
+            sourceByKey.set(c.prop || c.columnKey, c);
+          });
+        const merged = [];
+        const seen = new Set();
+        visibleColumns.forEach((vc) => {
+          const key = vc.prop || vc.columnKey;
+          if (!key || seen.has(key)) return;
+          const src = sourceByKey.get(key);
+          if (!src) return;
+          seen.add(key);
+          merged.push({
+            id: key,
+            prop: src.prop,
+            columnKey: src.columnKey,
+            label: src.label,
+            checked: true,
+            fixed: fixedMap.has(key) ? fixedMap.get(key) : src.fixed
+          });
+        });
+        // visibleColumns 里没出现的列(被取消勾选的),按 source 原顺序追加,checked=false
+        sourceByKey.forEach((c, key) => {
+          if (seen.has(key)) return;
+          merged.push({
+            id: key,
+            prop: c.prop,
+            columnKey: c.columnKey,
+            label: c.label,
+            checked: false,
+            fixed: c.fixed
+          });
+        });
+        this.saveColumns(merged);
+      },
+      // 拉取后端列配置,写入与 ele-pro-table 共享的 localStorage,触发表格重渲染
+      // 过滤掉 HIDE_SETTING_TYPES 的列(selection/index/expand),避免污染 ⚙️ 工具栏列表
+      // 注意:localStorage 已有数据时不覆盖(保留用户刚刚调整的列顺序,
+      // 避免服务端旧数据覆盖本地最新调整)
+      async loadColumnsFromServer() {
+        if (!this.cacheKeyUrl) return;
+        try {
+          const res = await this.getByTableId(this.cacheKeyUrl);
+          if (res?.columnConfig?.length > 0) {
+            const existing = this.getStorage(this.cacheKeyUrl + 'Cols');
+            if (!Array.isArray(existing) || existing.length === 0) {
+              // localStorage 没缓存(首次进入或清缓存后)→ 用服务端配置填充
+              const sanitized = res.columnConfig.filter(
+                (c) => !['selection', 'index', 'expand'].includes(c.type)
+              );
+              this.setStorage(this.cacheKeyUrl + 'Cols', sanitized);
+            }
+            // 无论是否填充,都触发 watcher 重读 localStorage
+            this.columns = [...this.columns];
+          }
+        } catch (e) {
+          console.error('加载列配置失败:', e);
+        }
+      },
       datasource({ page, where, limit }) {
         return releasePage({
           ...where,

+ 102 - 2
src/views/doc/sendReceive/send/send.vue

@@ -6,8 +6,10 @@
       :selection.sync="selection"
       :columns="columns"
       :datasource="datasource"
+      :cache-key="cacheKeyUrl"
       height="calc(100vh - 420px)"
       full-height="calc(100vh - 116px)"
+      @columns-change="onColumnsChange"
     >
       <!-- 表头工具栏 -->
       <template v-slot:toolbar v-if="!isPop">
@@ -91,6 +93,7 @@
   import receiveEdit from '@/views/doc/sendReceive/receive/components/edit.vue';
   import { mapGetters } from 'vuex';
   import dictMixins from '@/mixins/dictMixins';
+  import tableColumnsMixin from '@/mixins/tableColumnsMixin';
   import {
     sendPage,
     sendDeleteAPI,
@@ -100,7 +103,7 @@
   import processSubmitDialog from '@/components/processSubmitDialog/processSubmitDialog.vue';
 
   export default {
-    mixins: [dictMixins],
+    mixins: [dictMixins, tableColumnsMixin],
     components: {
       search,
       edit,
@@ -118,6 +121,11 @@
     },
     data() {
       return {
+        // 关闭 mixin 默认的 getTabColumns(与 ele-pro-table 工具栏共享同一 localStorage key,
+        // 混用会破坏缓存结构,由本组件自行接管"远端拉取 + 应用"流程)。
+        // 仅在 cacheKeyUrl 非空时才会真正发起同步(见 loadColumnsFromServer / onColumnsChange 的守卫)
+        tabMixinsInit: false,
+        cacheKeyUrl: 'fm-2611241029-doc-send-mine',
         currnetId: '',
         selection: [],
         processSubmitDialogFlag: false,
@@ -222,11 +230,103 @@
         ]
       };
     },
-    created() {},
+    created() {
+      this.loadColumnsFromServer();
+    },
+    mounted() {
+      // mounted 之后强制触发一次 columns watcher,确保 ele-pro-table 重新读取 localStorage 缓存
+      // (覆盖 watcher immediate 在某些时序下读不到 localStorage 的情况)
+      this.$nextTick(() => {
+        if (this.cacheKeyUrl) {
+          this.columns = [...this.columns];
+        }
+      });
+    },
     computed: {
       ...mapGetters(['user'])
     },
     methods: {
+      // ele-pro-table 列变化 → 转换为 slim 格式后同步到后端
+      // slim 格式与 ele-pro-table 工具栏写入 localStorage 的结构一致,避免缓存互相覆盖
+      // 注意:type 为 selection/index/expand 的列被 ele-pro-table 排除在 ⚙️ 工具栏外(见 HIDE_SETTING_TYPES),
+      // 不应写入后端和 localStorage,否则下次加载会被 getSettingCols 当作缓存返回,污染工具栏列表
+      onColumnsChange(visibleColumns) {
+        if (!this.cacheKeyUrl) return;
+        // 用户在 ⚙️ 工具栏里改最新 fixed 值(按 columnKey / prop 索引)
+        // 源 columns 里的 fixed 是页面初始值,必须以这里为准,否则刷新后丢失
+        const fixedMap = new Map();
+        visibleColumns.forEach((c) => {
+          const key = c.prop || c.columnKey;
+          if (key) fixedMap.set(key, c.fixed);
+        });
+        // 必须按 visibleColumns(用户拖动后的实际顺序)输出 merged,
+        // 否则服务端存的会是 columns 的初始顺序,下次 loadColumnsFromServer 用它
+        // 覆盖 localStorage 会把用户调整的顺序"打回原形"。
+        const sourceByKey = new Map();
+        this.columns
+          .filter(
+            (c) =>
+              (c.prop || c.columnKey || c.label === '操作') &&
+              !['selection', 'index', 'expand'].includes(c.type)
+          )
+          .forEach((c) => {
+            sourceByKey.set(c.prop || c.columnKey, c);
+          });
+        const merged = [];
+        const seen = new Set();
+        visibleColumns.forEach((vc) => {
+          const key = vc.prop || vc.columnKey;
+          if (!key || seen.has(key)) return;
+          const src = sourceByKey.get(key);
+          if (!src) return;
+          seen.add(key);
+          merged.push({
+            id: key,
+            prop: src.prop,
+            columnKey: src.columnKey,
+            label: src.label,
+            checked: true,
+            fixed: fixedMap.has(key) ? fixedMap.get(key) : src.fixed
+          });
+        });
+        // visibleColumns 里没出现的列(被取消勾选的),按 source 原顺序追加,checked=false
+        sourceByKey.forEach((c, key) => {
+          if (seen.has(key)) return;
+          merged.push({
+            id: key,
+            prop: c.prop,
+            columnKey: c.columnKey,
+            label: c.label,
+            checked: false,
+            fixed: c.fixed
+          });
+        });
+        this.saveColumns(merged);
+      },
+      // 拉取后端列配置,写入与 ele-pro-table 共享的 localStorage,触发表格重渲染
+      // 过滤掉 HIDE_SETTING_TYPES 的列(selection/index/expand),避免污染 ⚙️ 工具栏列表
+      // 注意:localStorage 已有数据时不覆盖(保留用户刚刚调整的列顺序,
+      // 避免服务端旧数据覆盖本地最新调整)
+      async loadColumnsFromServer() {
+        if (!this.cacheKeyUrl) return;
+        try {
+          const res = await this.getByTableId(this.cacheKeyUrl);
+          if (res?.columnConfig?.length > 0) {
+            const existing = this.getStorage(this.cacheKeyUrl + 'Cols');
+            if (!Array.isArray(existing) || existing.length === 0) {
+              // localStorage 没缓存(首次进入或清缓存后)→ 用服务端配置填充
+              const sanitized = res.columnConfig.filter(
+                (c) => !['selection', 'index', 'expand'].includes(c.type)
+              );
+              this.setStorage(this.cacheKeyUrl + 'Cols', sanitized);
+            }
+            // 无论是否填充,都触发 watcher 重读 localStorage
+            this.columns = [...this.columns];
+          }
+        } catch (e) {
+          console.error('加载列配置失败:', e);
+        }
+      },
       openEdit(type, row) {
         console.log(row, 'row');
         this.$refs.editRef.open(type, row);