Browse Source

merge: 将master分支的代码合并到test

xieyong 1 ngày trước cách đây
mục cha
commit
722f818960

+ 12 - 0
src/api/doc-manage/index.js

@@ -287,6 +287,18 @@ export async function fileSaveAPI(data) {
   }
   return Promise.reject(new Error(res.data.message));
 }
+/**
+ * 批量检查文件名称是否已存在(未逻辑删除)
+ * @param {string[]} names 待检查的文件名称集合(不含扩展名)
+ * @returns {Promise<string[]>} 重复的名称列表(既有重名 + 本次请求内重复)
+ */
+export async function checkNameExistsAPI(names) {
+  const res = await request.post('/fm/file/checkNameExists', names);
+  if (res.data.code == 0) {
+    return res.data.data || [];
+  }
+  return Promise.reject(new Error(res.data.message));
+}
 /**
  * 修改
  * @data data

+ 83 - 35
src/components/upload/fileUpload1.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="upload-file">
     <el-upload
+      ref="upload"
       class="upload-demo"
       action="#"
       :disabled="disabled || loading"
@@ -39,10 +40,7 @@
         class="progress-item"
       >
         <div class="file-name">{{ fileNameMap[fileId] }}</div>
-        <el-progress
-          :percentage="progress"
-          :stroke-width="6"
-        ></el-progress>
+        <el-progress :percentage="progress" :stroke-width="6"></el-progress>
       </div>
     </div>
   </div>
@@ -89,6 +87,11 @@
       disabled: {
         default: false,
         type: Boolean
+      },
+      // 上传前拦截钩子:返回 false 取消上传;返回 Promise<boolean> 异步判断(resolve(false) 取消)
+      beforeUploadFile: {
+        type: Function,
+        default: null
       }
     },
     data() {
@@ -221,46 +224,91 @@
 
         if (this.limit > 0 && this.fileList.length === this.limit) {
           this.$message.error(`最多上传 ${this.limit}个文件`);
+          this._removeCanceledFile(file);
           return false;
         }
+        // 父组件可在上传前异步拦截(如查重)
+        if (this.beforeUploadFile) {
+          const result = this.beforeUploadFile(file);
+          if (result === false) {
+            this._removeCanceledFile(file);
+            return false;
+          }
+          if (result && typeof result.then === 'function') {
+            return result.then((allowed) => {
+              if (allowed === false) {
+                this._removeCanceledFile(file);
+                return false;
+              }
+              return this._doUpload(file);
+            });
+          }
+        }
+        return this._doUpload(file);
+      },
+      // 取消上传时清理进度条;file-list 由 el-upload 自己从内部 uploadFiles 移除
+      _removeCanceledFile(file) {
+        const fileId = file.uid;
+        if (
+          fileId !== undefined &&
+          this.fileProgressMap[fileId] !== undefined
+        ) {
+          this.$delete(this.fileProgressMap, fileId);
+          this.$delete(this.fileNameMap, fileId);
+          if (Object.keys(this.fileProgressMap).length === 0) {
+            this.loading = false;
+          }
+        }
+        // el-upload 的 handleStart 在 beforeUpload 之前就把文件塞进 uploadFiles,
+        // 即便 beforeUpload 返回 false 也不会自动清掉;这里手动清掉,否则 display 还会显示
+        const upload = this.$refs.upload;
+        if (upload && Array.isArray(upload.uploadFiles)) {
+          const idx = upload.uploadFiles.findIndex((f) => f.uid === fileId);
+          if (idx !== -1) {
+            upload.uploadFiles.splice(idx, 1);
+          }
+        }
+      },
+      async _doUpload(file) {
         const fileId = file.uid || Date.now();
         this.loading = true;
         this.$set(this.fileProgressMap, fileId, 0);
         this.$set(this.fileNameMap, fileId, file.name);
-        return uploadFileNew(
-          {
-            module: this.module,
-            multiPartFile: file
-          },
-          {
-            onUploadProgress: (progressEvent) => {
-              if (progressEvent.total) {
-                this.$set(
-                  this.fileProgressMap,
-                  fileId,
-                  Math.round((progressEvent.loaded * 100) / progressEvent.total)
-                );
+        try {
+          const res = await uploadFileNew(
+            {
+              module: this.module,
+              multiPartFile: file
+            },
+            {
+              onUploadProgress: (progressEvent) => {
+                if (progressEvent.total) {
+                  this.$set(
+                    this.fileProgressMap,
+                    fileId,
+                    Math.round(
+                      (progressEvent.loaded * 100) / progressEvent.total
+                    )
+                  );
+                }
               }
             }
+          );
+          if (res.data) {
+            this.$emit('input', [
+              ...(this.value || []),
+              { ...file, url: res.data.storePath, ...res.data }
+            ]);
           }
-        )
-          .then((res) => {
-            if (res.data) {
-              this.$emit('input', [
-                ...(this.value || []),
-                { ...file, url: res.data.storePath, ...res.data }
-              ]);
-            }
-            this.$emit('fileChange', res.data);
-            return res.data;
-          })
-          .finally(() => {
-            this.$delete(this.fileProgressMap, fileId);
-            this.$delete(this.fileNameMap, fileId);
-            if (Object.keys(this.fileProgressMap).length === 0) {
-              this.loading = false;
-            }
-          });
+          this.$emit('fileChange', res.data);
+          return res.data;
+        } finally {
+          this.$delete(this.fileProgressMap, fileId);
+          this.$delete(this.fileNameMap, fileId);
+          if (Object.keys(this.fileProgressMap).length === 0) {
+            this.loading = false;
+          }
+        }
       },
       handlRequest() {
         return Promise.resolve();

+ 97 - 94
src/layout/components/header-tools.vue

@@ -43,9 +43,7 @@
     <div class="ele-admin-header-tool-item">
       <el-dropdown @command="onUserDropClick">
         <div class="ele-admin-header-avatar">
-          <el-avatar
-          :src="loginUser && loginUser.avatar ? loginUser.avatar : ''"
-          />
+          <el-avatar :src="getAvatar(loginUser && loginUser.avatar)" />
           <span class="hidden-xs-only">{{
             loginUser && loginUser.nickname ? loginUser.nickname : ''
           }}</span>
@@ -85,102 +83,107 @@
 </template>
 
 <script>
-import HeaderNotice from './header-notice.vue';
-import PasswordModal from './password-modal.vue';
-import SettingDrawer from './setting-drawer.vue';
-import I18nIcon from './i18n-icon.vue';
-import { logout } from '@/utils/page-tab-util';
-import { userLogout } from '@/api/system/user';
-import router from '@/router/index';
-import { getCurrentUser,setCurrentUser } from '@/utils/token-util';
-export default {
-  components: { HeaderNotice, PasswordModal, SettingDrawer, I18nIcon },
-  props: {
-    // 是否是全屏
-    fullscreen: Boolean
-  },
-  data() {
-    return {
-      // 是否显示修改密码弹窗
-      passwordVisible: false,
-      // 是否显示主题设置抽屉
-      settingVisible: false,
-      groupId: '',
-      roleId: '',
-      currentUser:{
-        currentGroupId:'',
-        currentRoleId:''
-      }
-    };
-  },
-  created() {
-    this.currentUser = getCurrentUser()
-    this.groupId =  this.currentUser.currentGroupId;
-    this.roleId =  this.currentUser.currentRoleId;
-  },
-  computed: {
-    // 当前用户信息
-    loginUser() {
-      return this.$store.state.user.info;
+  import HeaderNotice from './header-notice.vue';
+  import PasswordModal from './password-modal.vue';
+  import SettingDrawer from './setting-drawer.vue';
+  import I18nIcon from './i18n-icon.vue';
+  import { logout } from '@/utils/page-tab-util';
+  import { userLogout } from '@/api/system/user';
+  import router from '@/router/index';
+  import { getCurrentUser, setCurrentUser } from '@/utils/token-util';
+  export default {
+    components: { HeaderNotice, PasswordModal, SettingDrawer, I18nIcon },
+    props: {
+      // 是否是全屏
+      fullscreen: Boolean
     },
-    // 部门下拉
-    loginChangeGroupVOList() {
-      return this.$store.state.user?.info?.loginChangeGroupVOList;
+    data() {
+      return {
+        // 是否显示修改密码弹窗
+        passwordVisible: false,
+        // 是否显示主题设置抽屉
+        settingVisible: false,
+        groupId: '',
+        roleId: '',
+        currentUser: {
+          currentGroupId: '',
+          currentRoleId: ''
+        }
+      };
     },
-    // 角色下拉
-    loginChangeRoleVOList() {
-      return this.$store.state.user?.info?.loginChangeGroupVOList.find(
-        (item) => item.groupId == this.groupId
-      )?.loginChangeRoleVOList;
-    }
-  },
-  methods: {
-    groupIdChange(val) {
-      this.roleChange(this.loginChangeRoleVOList[0].roleId);
+    created() {
+      this.currentUser = getCurrentUser();
+      this.groupId = this.currentUser.currentGroupId;
+      this.roleId = this.currentUser.currentRoleId;
     },
-    roleChange(val) {
-      this.roleId = val;
-      this.currentUser.currentGroupId = this.groupId;
-      this.currentUser.currentRoleId = val;
-      setCurrentUser(this.currentUser)
-      this.$store
-        .dispatch('user/fetchUserInfo')
-        .then(({ menus, homePath, authoritiesRouter }) => {
-          router.roleChange({ menus, homePath, authoritiesRouter });
-        });
+    computed: {
+      // 当前用户信息
+      loginUser() {
+        return this.$store.state.user.info;
+      },
+      // 部门下拉
+      loginChangeGroupVOList() {
+        return this.$store.state.user?.info?.loginChangeGroupVOList;
+      },
+      // 角色下拉
+      loginChangeRoleVOList() {
+        return this.$store.state.user?.info?.loginChangeGroupVOList.find(
+          (item) => item.groupId == this.groupId
+        )?.loginChangeRoleVOList;
+      }
     },
-    /* 用户信息下拉点击事件 */
-    onUserDropClick(command) {
-      if (command === 'password') {
-        this.passwordVisible = true;
-      } else if (command === 'profile') {
-        if (this.$route.fullPath !== '/user/profile') {
-          this.$router.push('/user/profile');
+    methods: {
+      /* 头像兜底:avatar 可能为字符串或字符串数组 */
+      getAvatar(avatar) {
+        if (Array.isArray(avatar)) return avatar[0] || '';
+        return avatar || '';
+      },
+      groupIdChange(val) {
+        this.roleChange(this.loginChangeRoleVOList[0].roleId);
+      },
+      roleChange(val) {
+        this.roleId = val;
+        this.currentUser.currentGroupId = this.groupId;
+        this.currentUser.currentRoleId = val;
+        setCurrentUser(this.currentUser);
+        this.$store
+          .dispatch('user/fetchUserInfo')
+          .then(({ menus, homePath, authoritiesRouter }) => {
+            router.roleChange({ menus, homePath, authoritiesRouter });
+          });
+      },
+      /* 用户信息下拉点击事件 */
+      onUserDropClick(command) {
+        if (command === 'password') {
+          this.passwordVisible = true;
+        } else if (command === 'profile') {
+          if (this.$route.fullPath !== '/user/profile') {
+            this.$router.push('/user/profile');
+          }
+        } else if (command === 'logout') {
+          // 退出登录
+          this.$confirm(
+            this.$t('layout.logout.message'),
+            this.$t('layout.logout.title'),
+            { type: 'warning' }
+          )
+            .then(() => {
+              userLogout().then((res) => {
+                localStorage.removeItem('userId');
+                logout();
+              });
+            })
+            .catch(() => {});
         }
-      } else if (command === 'logout') {
-        // 退出登录
-        this.$confirm(
-          this.$t('layout.logout.message'),
-          this.$t('layout.logout.title'),
-          { type: 'warning' }
-        )
-          .then(() => {
-            userLogout().then((res) => {
-              localStorage.removeItem('userId');
-              logout();
-            });
-          })
-          .catch(() => {});
+      },
+      /* 全屏切换 */
+      toggleFullscreen() {
+        this.$emit('fullscreen');
+      },
+      /* 打开设置抽屉 */
+      openSetting() {
+        this.settingVisible = true;
       }
-    },
-    /* 全屏切换 */
-    toggleFullscreen() {
-      this.$emit('fullscreen');
-    },
-    /* 打开设置抽屉 */
-    openSetting() {
-      this.settingVisible = true;
     }
-  }
-};
+  };
 </script>

+ 224 - 0
src/mixins/tableColumnsMixin.js

@@ -0,0 +1,224 @@
+import request from '@/utils/request';
+
+export default {
+  data() {
+    return {
+      newColumns: [],
+      tabMixinsInit: true //进入页面是否默认请求列配置
+    };
+  },
+  created() {
+    //从服务器获取缓存列表配置
+    if (this.tabMixinsInit) {
+      this.getTabColumns();
+    }
+    // 创建防抖函数并绑定this
+    this.debouncedHandleColumnChange = this.debounce(
+      this.handleColumnChangeImpl,
+      1000
+    );
+  },
+  methods: {
+    // 实际的列变更处理逻辑
+    handleColumnChangeImpl() {
+      try {
+        const list = this.getStorage(this.cacheKeyUrl + 'Cols');
+        if (list) {
+          this.saveColumns(list);
+        }
+      } catch (error) {
+        console.error('处理列配置出错:', error);
+      }
+    },
+
+    // 列表变化回调
+    handleColumnChange() {
+      this.debouncedHandleColumnChange();
+    },
+
+    // 获取table-column配置
+    async getTabColumns() {
+      const res = await this.getByTableId(this.cacheKeyUrl);
+      if (res?.columnConfig?.length > 0) {
+        //对比接口返回和本地columns
+        let { nlist, type } = this.columnsContrast(res.columnConfig);
+        //有更新则更新服务缓存配置
+        if (type) {
+          this.saveColumns(nlist);
+        }
+        this.setStorage(this.cacheKeyUrl + 'Cols', nlist);
+        // 更新列
+        if (this._computedWatchers && this._computedWatchers.columns) {
+          // console.log('columns 是计算属性');
+          this.columnsVersion++;
+        } else {
+          // console.log('columns 是 data 属性');
+          this.columns = [...this.columns];
+          this.newColumns = [...this.newColumns];
+        }
+      }
+    },
+    getColumns() {
+      if (typeof this.columns == 'function') {
+        return this.columns();
+      } else {
+        return this.columns;
+      }
+    },
+    //服务器和本地配置columns对比
+    columnsContrast(list) {
+      const key = 'label';
+      var updateType = 0;
+      let sList = list.filter((d, i, r) => {
+        return d[key];
+      });
+      let devColumns = this.newColumns?.length
+        ? this.newColumns
+        : this.getColumns();
+
+      let dList = devColumns.filter((d, i, r) => {
+        return d[key] && d[key] !== '序号';
+      });
+      const keysA = new Set(sList.map((item) => item[key]));
+      const keysB = new Set(dList.map((item) => item[key]));
+      // 本地 比 缓存服务端 多的对象(新增)
+      const added = dList.filter((item) => {
+        return !keysA.has(item[key]) && (item.prop || item.label === '操作');
+      });
+      // 本地 比 缓存服务端 少的对象(删除)
+      const removed = sList.filter((item) => !keysB.has(item[key]));
+      const removedPropSet = new Set(removed.map((item) => item[key]));
+      // 删除 缓存中 中被移除的对象
+      const keptA = list.filter((item) => !removedPropSet.has(item[key]));
+      added.forEach((item) => {
+        //新增columns字段prop参数为必填
+        if (item.prop) {
+          item.id = item.prop;
+        } else if (item.columnKey) {
+          item.id = item.columnKey;
+        }
+        item.checked = true;
+      });
+
+      if (added.length > 0 || removed.length > 0) {
+        updateType = 1;
+      }
+
+      // 更新项:key 存在但内容变化
+      const dMap = new Map(dList.map((item) => [item[key], item]));
+      const updated = keptA.map((sItem) => {
+        const dItem = dMap.get(sItem[key]);
+        if (dItem && dItem.prop && sItem.prop !== dItem.prop) {
+          updateType = 1;
+          // 记录旧值和新值
+          const oldValue = sItem.prop;
+          const newValue = dItem.prop;
+          // 遍历所有属性,动态替换匹配旧值的字段
+          const updatedItem = { ...sItem };
+          Object.keys(updatedItem).forEach((k) => {
+            if (updatedItem[k] === oldValue) {
+              updatedItem[k] = newValue;
+            }
+          });
+          return updatedItem;
+        }
+        return sItem;
+      });
+
+      // 合并保留的对象和新增的对象
+      return { nlist: [...updated, ...added], type: updateType };
+    },
+
+    // 提交columns配置
+    async saveColumns(e) {
+      const data = {
+        tableId: this.cacheKeyUrl,
+        columnConfig: e
+      };
+      const msg = await this.saveTableConfig(data);
+      // console.log('列配置保存成功:', msg);
+      return msg;
+    },
+
+    //获取localstorage缓存
+    setStorage(key, value) {
+      try {
+        localStorage.setItem(key, JSON.stringify(value));
+      } catch (e) {
+        console.log('LocalStorage 存储错误:', e);
+        if (e.name === 'QuotaExceededError') {
+          this.clearCacheByPrefix(); //缓存不足,清除
+          localStorage.setItem(key, JSON.stringify(value));
+        }
+      }
+    },
+
+    //缓存不足清除缓存
+    clearCacheByPrefix() {
+      const prefix = 'Cols'; // 标识后缀
+      Object.keys(localStorage).forEach((key) => {
+        if (key.endsWith(prefix)) {
+          localStorage.removeItem(key);
+          // console.log(`已清除缓存: ${key}`);
+        }
+      });
+      // console.log('缓存清除完成');
+    },
+
+    //设置localstorage缓存
+    getStorage(key) {
+      try {
+        const value = localStorage.getItem(key);
+        return value ? JSON.parse(value) : null;
+      } catch (e) {
+        console.error('LocalStorage 解析错误:', e);
+        return null;
+      }
+    },
+
+    //防抖函数
+    debounce(fn, delay) {
+      let timer = null;
+      return (...args) => {
+        clearTimeout(timer);
+        timer = setTimeout(() => {
+          fn.apply(this, args);
+        }, delay);
+      };
+    },
+
+    //获取column记录接口
+    async getByTableId(key) {
+      try {
+        const res = await request.get(
+          `/sys/table-config/getByTableId/${key}`,
+          {}
+        );
+        if (res.data.code == 0) {
+          return res.data.data;
+        }
+      } catch (error) {
+        console.error('获取列配置失败:', error);
+      }
+    },
+
+    // 添加column记录接口
+    async saveTableConfig(data) {
+      if (!data?.columnConfig?.length) {
+        return;
+      }
+      try {
+        const res = await request({
+          url: '/sys/table-config/save',
+          method: 'post',
+          data
+        });
+        if (res.data.code == 0) {
+          return res.data.data;
+        }
+      } catch (error) {
+        console.error('保存列配置失败:', error);
+      }
+    }
+  }
+};

+ 31 - 1
src/views/doc/components/file-edit.vue

@@ -47,6 +47,7 @@
                 v-model="form.storagePath"
                 module="main"
                 :limit="1"
+                :beforeUploadFile="checkNameBeforeUpload"
                 @fileChange="fileChange"
               >
                 <template slot="templateBtn">
@@ -148,7 +149,8 @@
     fileUpdateAPI,
     fileGetByIdAPI,
     fileVersion,
-    listCode
+    listCode,
+    checkNameExistsAPI
   } from '@/api/doc-manage';
   import FileUpload from '@/components/upload/fileUpload1.vue';
   import { setFolderList } from '../util.js';
@@ -252,6 +254,34 @@
         console.log(file, 'file');
         this.form.name = file.name.replace(/\.[^/.]+$/, '');
       },
+      // 上传前查重:返回 false 取消上传;返回 Promise<boolean> 异步判断
+      async checkNameBeforeUpload(file) {
+        try {
+          const nameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
+          const duplicates = await checkNameExistsAPI([nameWithoutExt]);
+          console.log(duplicates, 'duplicates1');
+          if (!duplicates || duplicates.length === 0) {
+            return true;
+          }
+          try {
+            await this.$confirm(
+              `以下文件名称已存在:${duplicates.join('、')},是否继续上传?`,
+              '重名提示',
+              {
+                type: 'warning',
+                confirmButtonText: '继续上传',
+                cancelButtonText: '取消上传'
+              }
+            );
+            return true;
+          } catch {
+            return false;
+          }
+        } catch (e) {
+          // 查重接口异常 → 放行
+          return true;
+        }
+      },
       cascaderChange() {
         this.getCode();
       },

+ 30 - 1
src/views/doc/components/file-editAll.vue

@@ -61,6 +61,7 @@
                 module="main"
                 :limit="100"
                 :multiple="true"
+                :beforeUploadFile="checkNameBeforeUpload"
                 @fileChange="fileChange"
               >
                 <!-- <template slot="templateBtn">
@@ -132,7 +133,7 @@
 </template>
 
 <script>
-  import { fileSaveAPI, listCode } from '@/api/doc-manage';
+  import { fileSaveAPI, listCode, checkNameExistsAPI } from '@/api/doc-manage';
   import FileUpload from '@/components/upload/fileUpload1.vue';
   import { setFolderList } from '../util.js';
   // import getCode from './getCode.vue';
@@ -258,6 +259,34 @@
         console.log(file, 'file');
         this.form.name = file.name.replace(/\.[^/.]+$/, '');
       },
+      // 上传前查重:返回 false 取消上传;返回 Promise<boolean> 异步判断
+      async checkNameBeforeUpload(file) {
+        try {
+          const nameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
+          const duplicates = await checkNameExistsAPI([nameWithoutExt]);
+          console.log(duplicates, 'duplicates2');
+          if (!duplicates || duplicates.length === 0) {
+            return true; // 不重名,放行
+          }
+          try {
+            await this.$confirm(
+              `以下文件名称已存在:${duplicates.join('、')},是否继续上传?`,
+              '重名提示',
+              {
+                type: 'warning',
+                confirmButtonText: '继续上传',
+                cancelButtonText: '取消上传'
+              }
+            );
+            return true; // 用户确认,放行
+          } catch {
+            return false; // 用户取消,不上传
+          }
+        } catch (e) {
+          // 查重接口异常 → 放行,由后续 save 兜底
+          return true;
+        }
+      },
       // success(code) {
       //   this.code = code;
       //   this.$nextTick(() => {

+ 74 - 5
src/views/doc/components/file-table-list.vue

@@ -13,6 +13,8 @@
       :current.sync="current"
       :selection.sync="selection"
       highlight-current-row
+      :cache-key="cacheKeyUrl"
+      @columns-change="onColumnsChange"
       @row-click="rowClick"
       @selection-change="selectionClick"
       @done="done"
@@ -99,7 +101,7 @@
             })
           "
           :disabled="
-            selection.filter((item) => item.checkOutStatus != 1).length
+            !!(selection.filter((item) => item.checkOutStatus != 1).length)
           "
           v-if="lcyStatus == 1"
         >
@@ -115,7 +117,7 @@
             })
           "
           :disabled="
-            selection.filter((item) => item.checkOutStatus == 1).length
+            !!(selection.filter((item) => item.checkOutStatus == 1).length)
           "
           v-if="lcyStatus == 1"
         >
@@ -434,6 +436,7 @@
 
   import { isPower, isCheckOut, fileStatus, isCreateUserId } from '../util.js';
   import { getFile } from '@/api/system/file/index.js';
+  import tableColumnsMixin from '@/mixins/tableColumnsMixin';
   import {
     fileDeleteAPI,
     filePageAPI,
@@ -445,6 +448,7 @@
     fileUpdateAllAPI
   } from '@/api/doc-manage';
   export default {
+    mixins: [tableColumnsMixin],
     components: {
       fileSearch,
       fileEdit,
@@ -484,12 +488,21 @@
       disabledTableList: {
         //已选择列表
         default: () => []
+      },
+      // 列配置远端同步 key:仅在调用方显式传入时启用同步,其他路由不传则完全关闭
+      cacheKeyUrl: {
+        type: String,
+        default: ''
       }
     },
     //add:新建 fileEdit:修改  filePigeonhole:归档 fileUnPigeonhole:取消归档  fileIssue:发布 fileChange:变更 fileDel:删除
 
     data() {
       return {
+        // 关闭 mixin 默认的 getTabColumns(与 ele-pro-table 工具栏共享同一 localStorage key,
+        // 混用会破坏缓存结构,由本组件自行接管"远端拉取 + 应用"流程)。
+        // 仅在 cacheKeyUrl 非空时才会真正发起同步(见 loadColumnsFromServer / onColumnsChange 的守卫)
+        tabMixinsInit: false,
         isPower,
         isCheckOut,
         isCreateUserId,
@@ -506,6 +519,7 @@
             type: 'selection',
             columnKey: 'selection',
             align: 'center',
+            fixed: 'left',
             selectable: (row, index) => {
               return (
                 !this.disabledTableList
@@ -658,9 +672,64 @@
         showEditFlag: false
       };
     },
-    created() {},
+    created() {
+      this.loadColumnsFromServer();
+    },
 
     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);
+        });
+        const visibleIds = new Set(
+          visibleColumns.map((c) => c.prop || c.columnKey)
+        );
+        const merged = 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
+            };
+          });
+        this.saveColumns(merged);
+      },
+      // 拉取后端列配置,写入与 ele-pro-table 共享的 localStorage,触发表格重渲染
+      // 过滤掉 HIDE_SETTING_TYPES 的列(selection/index/expand),避免污染 ⚙️ 工具栏列表
+      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 重读缓存
+            this.columns = [...this.columns];
+          }
+        } catch (e) {
+          console.error('加载列配置失败:', e);
+        }
+      },
       /* 表格数据源 */
       datasource({ page, limit, where, order }) {
         if (this.lcyStatus) {
@@ -915,8 +984,8 @@
 
         this.selection.forEach((item) => {
           getFile(
-            { objectName: item.storagePath[0].storePath },
-            item.storagePath[0].name
+            { objectName: item.stampStoragePath[0].storePath },
+            item.stampStoragePath[0].name
           );
         });
       },

+ 6 - 0
src/views/doc/components/main.vue

@@ -128,6 +128,7 @@
             :lcyStatus="lcyStatus"
             :isPop="isPop"
             :disabledTableList="disabledTableList"
+            :cacheKeyUrl="cacheKeyUrl"
           />
         </template>
       </ele-split-layout>
@@ -179,6 +180,11 @@
       disabledTableList: {
         //已选择列表
         default: () => []
+      },
+      // 列配置远端同步 key(透传给 FileTableList):留空表示不启用列配置远端同步
+      cacheKeyUrl: {
+        type: String,
+        default: ''
       }
     },
     data() {

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

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