Procházet zdrojové kódy

refactor(filing): 使用SpreadJS原生编辑替代自定义输入框重命名

- 禁用 SpreadJS 的 undo 功能,名称修改逐次保存到服务端
- 新增 _bindFilingNameEditing 方法绑定 EditStarting/EditEnded 事件
- 新增 _canEditFilingName 权限校验方法,按列设置 readOnly
- renameFiling 改为同步调用并添加 _nameSaving 状态锁防止并发保存
- 移除原双击触发的自定义 DOM 输入框重命名逻辑
- 在删除、粘贴、新增等操作中增加 _nameSaving 互斥检查
- 刷新树后恢复整行选中背景色并支持 Ctrl+Enter 确认编辑
- 禁止在编辑状态下响应 Delete/Backspace/Ctrl+X 快捷键
caipin před 6 dny
rodič
revize
5630dcd1c7
1 změnil soubory, kde provedl 93 přidání a 88 odebrání
  1. 93 88
      app/public/js/filesjs.js

+ 93 - 88
app/public/js/filesjs.js

@@ -11,6 +11,7 @@ $(document).ready(function() {
             // 初始化 SpreadJS
             this.spread = SpreadJsObj.createNewSpread($('#filing-spread')[0]);
             this.spread.options.showHorizontalScrollbar = false; // 隐藏水平滚动条
+            this.spread.options.allowUndo = false; // 名称逐次保存到服务端,不使用仅修改单元格的撤销。
             this.sheet = this.spread.getActiveSheet();
             this.treeIcons = {
                 closed: new Image(),
@@ -54,8 +55,9 @@ $(document).ready(function() {
                             };
                         },
                         getTip: function(data) { return _.escape(data.name || ''); },
+                        readOnly: data => !this._canEditFilingName(data),
                     },
-                    { title: '文件数', colSpan: '1', rowSpan: '1', field: 'total_file_count', hAlign: 1, width: 70, formatter: '@' },
+                    { title: '文件数', colSpan: '1', rowSpan: '1', field: 'total_file_count', hAlign: 1, width: 70, formatter: '@', readOnly: true },
                 ],
                 emptyRows: 0,
                 headRows: 1,
@@ -64,7 +66,7 @@ $(document).ready(function() {
                 headColWidth: [25],
                 headerFont: '12px 微软雅黑',
                 font: '12px 微软雅黑',
-                readOnly: true,
+                readOnly: false,
                 selectedBackColor: '#e8f4fd',
             };
 
@@ -82,6 +84,8 @@ $(document).ready(function() {
             this.fileOrder = getLocalCache(this.fileOrderKey) || 'create_time|desc';
             this.curFilingKey = 'cur-filing-sjs-' + window.location.pathname.split('/')[2];
             this._isRenaming = false;
+            this._nameEdit = null;
+            this._nameSaving = false;
 
             this._bindEvents();
             this._initNameClipboard();
@@ -285,66 +289,7 @@ $(document).ready(function() {
                 self.nextPage();
             });
 
-            // 双击行内重命名分类
-            $('#filing-spread').dblclick(function() {
-                if (!canFiling) return;
-                if (self._isRenaming || self._directorySaving) return;
-                const node = SpreadJsObj.getSelectObject(self.sheet);
-                if (!node || node.is_fixed) return;
-
-                self._isRenaming = true;
-                const rowIndex = self.filingTree.nodes.indexOf(node);
-                const cellRect = self.sheet.getCellRect(rowIndex, 0);
-                const containerRect = $('#filing-spread')[0].getBoundingClientRect();
-
-                const inputLeft = containerRect.left + window.scrollX + cellRect.x + 2;
-                const inputTop = containerRect.top + window.scrollY + cellRect.y + 2;
-                const inputWidth = cellRect.width - 4;
-                const inputHeight = cellRect.height - 4;
-
-                const $input = $('<input type="text" class="form-control form-control-sm">')
-                    .val(node.name)
-                    .css({
-                        position: 'absolute',
-                        left: inputLeft,
-                        top: inputTop,
-                        width: inputWidth,
-                        height: inputHeight,
-                        zIndex: 1000,
-                        fontSize: '12px',
-                        padding: '0 4px',
-                        border: '2px solid #4a90d9',
-                        borderRadius: '2px',
-                        outline: 'none',
-                        boxShadow: '0 0 4px rgba(74,144,217,0.4)',
-                    });
-
-                let handled = false;
-                const finishEdit = async (save) => {
-                    if (handled) return;
-                    handled = true;
-                    const newName = $input.val().trim();
-                    $input.remove();
-                    self._isRenaming = false;
-                    if (save && newName && newName !== node.name) {
-                        await self.renameFiling(node, newName);
-                    }
-                };
-
-                $input.on('keydown', function(e) {
-                    if (e.keyCode === 13) { // Enter
-                        e.preventDefault();
-                        finishEdit(true);
-                    } else if (e.keyCode === 27) { // Escape
-                        e.preventDefault();
-                        finishEdit(false);
-                    }
-                });
-                $input.on('blur', function() { finishEdit(true); });
-
-                $('body').append($input);
-                $input.focus().select();
-            });
+            this._bindFilingNameEditing();
 
             // 分类右键菜单
             $.contextMenu({
@@ -360,7 +305,7 @@ $(document).ready(function() {
                         name: '粘贴名称',
                         icon: 'fa-paste',
                         callback: function() { self._showNamePastePreview(); },
-                        disabled: function() { return !self._referenceNameClipboard || self._namePasteSaving; },
+                        disabled: function() { return !self._referenceNameClipboard || self._namePasteSaving || self._nameSaving; },
                     },
                     'delete': {
                         name: '删除',
@@ -370,7 +315,7 @@ $(document).ready(function() {
                         },
                         disabled: function () {
                             const nodes = self._getSelectedFilings();
-                            return self._directorySaving || projectFileLocked || !nodes.length || nodes.some(node => {
+                            return self._directorySaving || self._nameSaving || projectFileLocked || !nodes.length || nodes.some(node => {
                                 const permission = self._getFilingPermission(node.id);
                                 return Number(node.is_fixed) || !permission.can_view || !permission.can_edit_dir;
                             });
@@ -636,8 +581,19 @@ $(document).ready(function() {
                 this.filingTree.refreshNodeVisible();
                 SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.filingTree);
                 this.resizeSpreadColumns();
+                const highlightedRows = this.sheet.getSelections().map(selection => selection.row)
+                    .filter(row => row >= 0 && row < this.sheet.getRowCount());
                 this.sheet.clearSelection();
-                if (currentNode) this.sheet.setSelection(this.filingTree.nodes.indexOf(currentNode), 0, 1, 1);
+                if (currentNode) {
+                    const selectedRow = this.filingTree.nodes.indexOf(currentNode);
+                    this.sheet.setSelection(selectedRow, 0, 1, 1);
+                    // 程序定位选区后,单独刷新组件绘制的整行选中背景。
+                    SpreadJsObj.reloadRowsBackColor(this.sheet, [...new Set(highlightedRows.concat(selectedRow))]);
+                } else {
+                    highlightedRows.forEach(row => {
+                        this.sheet.getRange(row, 0, 1, this.sheet.getColumnCount()).backColor(this.sheet.getDefaultStyle().backColor);
+                    });
+                }
                 const targetRow = focusId && currentNode ? this.filingTree.nodes.indexOf(currentNode)
                     : this.filingTree.nodes.findIndex(node => topNode && node.id === topNode.id);
                 if (targetRow >= 0) this.sheet.showRow(targetRow, focusId ? spreadNS.VerticalPosition.center : spreadNS.VerticalPosition.top);
@@ -648,6 +604,7 @@ $(document).ready(function() {
             this.curFiling = null;
             if (currentNode) {
                 this.setCurFiling(currentNode).catch(() => { $('#file-view').hide(); });
+                if (focusId) this.spread.focus();
             } else {
                 canFiling = false;
                 $('.js-can-edit-dir').hide();
@@ -659,7 +616,7 @@ $(document).ready(function() {
          * 删除分类
          */
         delFiling(nodes, callback) {
-            if (this._directorySaving || !nodes.length) return;
+            if (this._directorySaving || this._nameSaving || !nodes.length) return;
             const self = this;
             const modal = $('#filesjs-del-filing');
             this._directorySaving = true;
@@ -697,24 +654,20 @@ $(document).ready(function() {
         /**
          * 重命名分类
          */
-        async renameFiling(node, newName) {
-            const result = await postDataAsync('filing/save', { id: node.id, name: newName });
-            const srcNode = filing.find(x => x.id === node.id || x.id == node.id);
-            if (srcNode) srcNode.name = newName;
-            node.name = newName;
-            const countHint = node.total_file_count > 0 ? `(${node.total_file_count})` : '';
-            node.name = newName;
-            const rowIndex = this.filingTree.nodes.indexOf(node);
-            if (rowIndex >= 0) {
-                SpreadJsObj.reLoadRowsData(this.sheet, [rowIndex]);
-            }
-            let parent = this.filingTree.getParent(node);
-            while (parent) {
-                const pIdx = this.filingTree.nodes.indexOf(parent);
-                if (pIdx >= 0) SpreadJsObj.reLoadRowsData(this.sheet, [pIdx]);
-                parent = this.filingTree.getParent(parent);
-            }
-            return result;
+        renameFiling(node, newName) {
+            this._nameSaving = true;
+            const finishSave = () => {
+                this._nameSaving = false;
+                const row = this.filingTree.nodes.findIndex(item => item.id === node.id);
+                if (row >= 0) SpreadJsObj.reLoadRowsData(this.sheet, [row]);
+            };
+            postData('filing/save', { id: node.id, name: newName }, () => {
+                const source = filing.find(item => item.id === node.id);
+                if (source) source.name = newName;
+                const currentNode = this.filingTree.nodes.find(item => item.id === node.id);
+                if (currentNode) currentNode.name = newName;
+                finishSave();
+            }, finishSave, false);
         }
 
         /**
@@ -913,6 +866,9 @@ $(document).ready(function() {
             const differ = count - (node.file_count || 0);
             node.file_count = count;
             node.total_file_count = count;
+            // 分类增删后会从 filing 重建树,同步原始数据以保留最新文件数。
+            const source = filing.find(item => item.id === node.id);
+            if (source) source.file_count = count;
             // 更新 SpreadJS 显示
             const rowIndex = this.filingTree.nodes.indexOf(node);
             if (rowIndex >= 0) {
@@ -1054,6 +1010,7 @@ $(document).ready(function() {
                 this._pasteReferenceText(text);
             }, true);
             this.sheet.bind(spreadNS.Events.ClipboardPasting, (e, info) => {
+                if (this._isRenaming || this.sheet.isEditing()) return;
                 info.cancel = true;
                 this._pasteReferenceText(info.pasteData ? info.pasteData.text : '');
             });
@@ -1171,7 +1128,7 @@ $(document).ready(function() {
         }
 
         _showNamePastePreview() {
-            if (this._namePasteSaving || $('#filing-name-paste').hasClass('show')) return;
+            if (this._namePasteSaving || this._nameSaving || $('#filing-name-paste').hasClass('show')) return;
             const clipboard = this._referenceNameClipboard;
             if (!clipboard) { toastr.warning('请先从右侧参考文件复制名称'); return; }
             if (this._isRenaming) return;
@@ -1203,7 +1160,7 @@ $(document).ready(function() {
         }
 
         _savePastedNames() {
-            if (this._namePasteSaving || !this._namePasteItems.length || this._namePasteItems.some(item => item.errors.length)) return;
+            if (this._namePasteSaving || this._nameSaving || !this._namePasteItems.length || this._namePasteItems.some(item => item.errors.length)) return;
             const items = this._namePasteItems.map(item => ({ id: item.id, old_name: item.old_name, name: item.name }));
             if (!items.some(item => item.name !== item.old_name)) return;
             const modal = $('#filing-name-paste');
@@ -1234,7 +1191,7 @@ $(document).ready(function() {
         }
 
         _addFiling(data) {
-            if (this._directorySaving || !canFiling || this._isRenaming) return;
+            if (this._directorySaving || this._nameSaving || !canFiling || this._isRenaming) return;
             this._directorySaving = true;
             postData('filing/add', data, result => {
                 for (const update of result.update || []) {
@@ -1265,7 +1222,7 @@ $(document).ready(function() {
         }
 
         _showDeletePreview() {
-            if (this._directorySaving || projectFileLocked) return;
+            if (this._directorySaving || this._nameSaving || this._isRenaming || projectFileLocked) return;
             const nodes = this._getSelectedFilings();
             if (!nodes.length) return;
             const included = new Set(nodes.map(node => node.id));
@@ -1297,6 +1254,54 @@ $(document).ready(function() {
             $('#filesjs-del-filing-error').text('');
             $('#filesjs-del-filing').modal('show');
         }
+
+        _canEditFilingName(node) {
+            if (!node || projectFileLocked || Number(node.is_fixed)) return false;
+            const permission = this._getFilingPermission(node.id);
+            return Boolean(Number(permission.can_view) && Number(permission.can_edit_dir));
+        }
+
+        _bindFilingNameEditing() {
+            this.sheet.bind(spreadNS.Events.EditStarting, (e, info) => {
+                const node = this.filingTree.nodes[info.row];
+                if (info.col !== 0 || !this._canEditFilingName(node) || this._directorySaving
+                    || this._nameSaving || this._namePasteSaving || this._reloadingFiling) {
+                    info.cancel = true;
+                    return;
+                }
+                this._isRenaming = true;
+                this._nameEdit = { node, row: info.row, oldName: String(node.name || '') };
+            });
+            this.sheet.bind(spreadNS.Events.EditEnded, (e, info) => { this._finishFilingNameEdit(info); });
+            $('#filing-spread')[0].addEventListener('keydown', e => {
+                if (e.isComposing || e.keyCode === 229) return;
+                // 不允许快捷键绕过单个名称保存,批量替换继续走粘贴确认弹窗。
+                if (!this.sheet.isEditing() && (e.keyCode === 46 || e.keyCode === 8 || ((e.ctrlKey || e.metaKey) && e.keyCode === 88))) {
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                } else if ((e.ctrlKey || e.metaKey) && e.keyCode === 13) {
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                    if (this.sheet.isEditing()) this.sheet.endEdit();
+                }
+            }, true);
+        }
+
+        _finishFilingNameEdit(info) {
+            const editing = this._nameEdit;
+            this._nameEdit = null;
+            this._isRenaming = false;
+            if (!editing || info.col !== 0 || info.row !== editing.row) return;
+            // Esc 取消时 editingText 仍是输入内容;读取实际单元格值,避免误保存。
+            const value = String(this.sheet.getValue(info.row, 0) || '');
+            if (value === editing.oldName) return;
+            const newName = value.trim();
+            if (!newName || newName === editing.oldName) {
+                this.sheet.setValue(info.row, 0, editing.oldName);
+                return;
+            }
+            this.renameFiling(editing.node, newName);
+        }
     }
 
     // 初始化