瀏覽代碼

1.0.5 更新
操作列对应字段 - 且固定;
富文本编辑器图片增加尺寸调整悬框;
curd 发起审批样式修改。

liuyanpeng 10 月之前
父節點
當前提交
cdc20d3190
共有 6 個文件被更改,包括 761 次插入237 次删除
  1. 1 1
      package.json
  2. 3 0
      src/App.vue
  3. 538 28
      src/widget/EditorWidget.vue
  4. 31 1
      src/window1/tabGridView/GridBody.vue
  5. 16 3
      src/window1/tabGridView/GridHeader.vue
  6. 172 204
      src/workflow/WorkflowSelectUser.vue

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "name": "client-base-v5",
   "description": "Leanwo Prodog Client",
-  "version": "1.0.4",
+  "version": "1.0.5",
   "author": "yangzhijie1488 <yangzhijie1488@163.com>",
   "scripts": {
     "ins": "npm install --registry http://wuzhixin.vip:4873",

+ 3 - 0
src/App.vue

@@ -295,6 +295,9 @@ export default {
         if (path.indexOf('/wms/checkProfit/') >= 0) {
           count = 1;
         }
+        if (path.indexOf('/wms/generatePosition/') >= 0) {
+          count = 1;
+        }
 
         if (window.top !== window.self) {
           count = 1;

+ 538 - 28
src/widget/EditorWidget.vue

@@ -1,15 +1,53 @@
 <template>
-  <div id="editor" />
-  <!-- eslint-disable-next-line -->
-  <!-- <div id="content-editor-view" class="view-box" v-html="modelValue"/> -->
-  <Loading v-if="loading" />
+  <div class="editor-container">
+    <div id="editor" />
+    <!-- eslint-disable-next-line -->
+    <!-- <div id="content-editor-view" class="view-box" v-html="modelValue"/> -->
+    <Loading v-if="loading" />
+
+    <!-- 图片调整模态框 -->
+    <div
+      v-if="showImageModal" class="image-resize-modal" :style="{
+        top: modalPosition.top + 'px',
+        left: modalPosition.left + 'px'
+      }"
+    >
+      <div class="modal-content">
+        <!-- 一行显示:缩放控制、宽度、高度 -->
+        <div class="controls-row">
+          <div class="zoom-controls">
+            <button :disabled="currentZoom <= minZoom" class="zoom-btn" title="缩小 (10%)" @click="zoomOut">
+              <span>-</span>
+            </button>
+            <span class="zoom-display">{{ currentZoom && Math.round(currentZoom * 100) }}%</span>
+            <button :disabled="currentZoom >= maxZoom" class="zoom-btn" title="放大 (10%)" @click="zoomIn">
+              <span>+</span>
+            </button>
+          </div>
+
+          <div class="size-input-group">
+            <label>宽度:</label>
+            <input v-model="imageSize.width" type="number" @input="updateImageSize" @keydown.enter.prevent />
+            <span>px</span>
+          </div>
+
+          <div class="size-input-group">
+            <label>高度:</label>
+            <input v-model="imageSize.height" type="number" @input="updateImageSize" @keydown.enter.prevent />
+            <span>px</span>
+          </div>
+        </div>
+        <button class="close-btn" title="关闭" @click="closeImageModal">×</button>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script>
 import Common from '../common/Common';
 import { message } from 'ant-design-vue';
 import { ajaxApiFile } from '../common/utils.js';
-import { DownloadService } from 'pc-component-v3';
+import { DownloadService, Notify } from 'pc-component-v3';
 
 export default {
   components: {
@@ -24,16 +62,22 @@ export default {
       type: String,
       default: '',
     },
-    traceId: {
-      type: String,
-      default: null,
-    },
   },
   emits: ['update:modelValue'],
 
   data: function () {
     return {
       loading: false,
+      // 图片调整模态框相关数据
+      showImageModal: false,
+      currentImage: null,
+      originalImageSize: { width: 0, height: 0 },
+      imageSize: { width: 0, height: 0 },
+      modalPosition: { top: 0, left: 0 },
+      currentZoom: 1,
+      minZoom: 0.1, // 最小10%
+      maxZoom: 5.0, // 最大500%
+      contentChangeTimer: null, // 添加防抖定时器
     };
   },
 
@@ -161,34 +205,36 @@ export default {
       const clipboardData = e.originalEvent.clipboardData;
       if (!clipboardData) return;
       let handled = false;
-      
+
       // 获取 trumbowyg 实例
       const trumbowyg = $('#editor').data('trumbowyg');
-      
+
       for (let i = 0; i < clipboardData.items.length; i++) {
         const item = clipboardData.items[i];
         if (item.type.indexOf('image') !== -1) {
           const file = item.getAsFile();
           if (file) {
             handled = true;
-            
+
             // 插入临时文本作为标记
             const tempId = 'img-placeholder-' + Date.now();
             if (trumbowyg) {
               trumbowyg.execCmd('insertText', `[图片上传中${tempId}]`);
             }
-            
+
             // 上传图片
             _self.uploadImg(file, function (imagUrl) {
               // 上传完成后,替换临时文本为图片
               const html = $('#editor').trumbowyg('html');
               const newHtml = html.replace(`[图片上传中${tempId}]`, `<img src="${imagUrl}" alt="图片" />`);
               $('#editor').trumbowyg('html', newHtml);
+              // 将光标定位到图片后面
+              _self.setCursorPosition(imagUrl);
             });
           }
         }
       }
-      
+
       // 如果粘贴的是图片,阻止默认行为
       if (handled) {
         e.preventDefault();
@@ -210,12 +256,41 @@ export default {
     $('#editor').on('tbwchange', () => {
       const html = $('#editor').trumbowyg('html');
       this.$emit('update:modelValue', html);
+
+      // 检查当前选中的图片是否还存在,如果不存在则关闭模态框
+      this.checkImageExistence();
     });
 
-    // Common.downloadByA();
+    // 通过 Trumbowyg 事件系统监听编辑器变化
+    $('#editor').on('tbwchange tbwinit', () => {
+      // 每次内容变化后重新绑定图片点击事件
+      setTimeout(() => {
+        this.bindImageClickEvents();
+      }, 100);
+    });
+
+    // 点击其他地方关闭模态框,同时作为图片点击的备用监听器
+    $(document).on('click', e => {
+      // 如果点击的是编辑器内的图片,显示调整模态框
+      if (e.target.tagName === 'IMG' && $(e.target).closest('#editor').length) {
+        e.preventDefault();
+        e.stopPropagation();
+        this.showImageResizeModal(e.target);
+        return;
+      }
 
+      // 如果点击的不是模态框或图片,关闭模态框
+      if (!$(e.target).closest('.image-resize-modal, .trumbowyg-editor img, #editor img').length) {
+        this.closeImageModal();
+      }
+    });
+
+    // Common.downloadByA();
   },
   unmounted() {
+    if (this.contentChangeTimer) {
+      clearTimeout(this.contentChangeTimer);
+    }
     $('#editor').trumbowyg('destroy');
   },
   methods: {
@@ -227,7 +302,6 @@ export default {
 
     // 上传图片
     async uploadImg(file, insertFn) {
-      console.log(file);
       const _self = this;
       const formData = new FormData();
       try {
@@ -260,10 +334,6 @@ export default {
       if (selectedFile == undefined) {
         return;
       }
-      //附件大小时默认8m
-      var size = 8;
-
-      // if (selectedFile.size / 1024 <= 1024 * size) {
       var formData = new FormData();
       formData.append('files', selectedFile);
       formData.append('className', _self.className);
@@ -293,21 +363,342 @@ export default {
           Common.processException(XMLHttpRequest, textStatus, errorThrown);
         },
       });
-      // } else {
-      //   _self.loading = false;
-      //   Notify.error(
-      //     '提示',
-      //     '文件大小不能超过' + size,
-      //   );
-      // }
     },
-  },
 
+    // 获取图片缩放配置
+    showImageResizeModal(imgElement) {
+      this.currentImage = imgElement;
+
+      // 获取图片设置的样式尺寸(用户实际设置的值,不受容器限制影响)
+      const setWidth = parseInt(imgElement.style.width) || imgElement.naturalWidth;
+      const setHeight = parseInt(imgElement.style.height) || imgElement.naturalHeight;
+
+      // 如果没有设置样式,使用计算样式作为备用
+      const computedStyle = window.getComputedStyle(imgElement);
+      const currentWidth = setWidth || parseInt(computedStyle.width) || imgElement.naturalWidth;
+      const currentHeight = setHeight || parseInt(computedStyle.height) || imgElement.naturalHeight;
+
+      // 保存原始尺寸(如果是第一次点击)
+      if (!imgElement.dataset.originalWidth) {
+        imgElement.dataset.originalWidth = imgElement.naturalWidth;
+        imgElement.dataset.originalHeight = imgElement.naturalHeight;
+      }
+
+      this.originalImageSize = {
+        width: parseInt(imgElement.dataset.originalWidth),
+        height: parseInt(imgElement.dataset.originalHeight),
+      };
+
+      this.imageSize = {
+        width: currentWidth,
+        height: currentHeight,
+      };
+
+      // 计算当前缩放比例
+      if (this.originalImageSize.width == 0) {
+        this.currentZoom = 1;
+      } else {
+        this.currentZoom = currentWidth / this.originalImageSize.width;
+      }
+
+      // 计算模态框位置
+      this.calculateModalPosition(imgElement);
+
+      this.showImageModal = true;
+    },
+
+    // 计算模态框位置的独立方法
+    calculateModalPosition(imgElement) {
+      const imgRect = imgElement.getBoundingClientRect();
+      const editorContainer = document.querySelector('.editor-container');
+      const containerRect = editorContainer.getBoundingClientRect();
+
+      // 获取Trumbowyg编辑器的box容器(包含工具栏和编辑区域)
+      const trumbowygBox = document.querySelector('.trumbowyg-box');
+      const trumbowygBoxRect = trumbowygBox ? trumbowygBox.getBoundingClientRect() : null;
+
+      // 模态框宽度
+      const modalWidth = 420;
+
+      // 编辑器固定高度
+      const editorHeight = 368;
+
+      let top, left;
+
+      // 判断是否需要将模态框显示在编辑器下方
+      const shouldShowBelowEditor = this.shouldShowModalBelowEditor(imgRect, trumbowygBoxRect, editorHeight);
+
+      if (shouldShowBelowEditor) {
+        // 模态框显示在编辑器容器的正下方
+        if (trumbowygBoxRect) {
+          top = trumbowygBoxRect.bottom - containerRect.top + 5;
+        } else {
+          // 备用方案:使用固定位置
+          top = editorHeight + 40; // 工具栏高度约40px
+        }
+
+        // 水平位置:居中显示在编辑器下方
+        const centerX = trumbowygBoxRect ? (trumbowygBoxRect.left + trumbowygBoxRect.right) / 2 : containerRect.left + containerRect.width / 2;
+        left = centerX - containerRect.left - modalWidth * 2;
+
+        // 确保不超出容器边界
+        left = Math.max(0, Math.min(left, containerRect.width - modalWidth));
+      } else {
+        // 小图片且在可视区域内:模态框显示在图片正下方
+        top = imgRect.bottom - containerRect.top + 5;
+        left = imgRect.left - containerRect.left;
+
+        // 确保不超出容器边界
+        left = Math.max(0, Math.min(left, containerRect.width - modalWidth));
+      }
+
+      this.modalPosition = { top, left };
+    },
+
+    // 判断是否应该将模态框显示在编辑器下方
+    shouldShowModalBelowEditor(imgRect, trumbowygBoxRect, editorHeight) {
+      // 情况1:图片本身高度超过编辑器高度
+      if (this.imageSize.height > editorHeight) {
+        return true;
+      }
+
+      // 情况2:图片底部超出了编辑器的可视区域
+      if (trumbowygBoxRect) {
+        // 获取编辑器内容区域的底部位置(不包括工具栏)
+        const editorContentBottom = trumbowygBoxRect.bottom;
+
+        // 如果图片底部超出了编辑器内容区域,说明图片有部分在滚动区域下方
+        if (imgRect.bottom > editorContentBottom) {
+          return true;
+        }
+      }
+
+      // 情况3:图片顶部在编辑器可视区域上方(图片被向上滚动了)
+      if (trumbowygBoxRect) {
+        // 获取编辑器内容区域的顶部位置(工具栏下方)
+        const trumbowyg = $('#editor').data('trumbowyg');
+        const toolbarHeight = trumbowyg && trumbowyg.$btnPane ? trumbowyg.$btnPane.height() : 40;
+        const editorContentTop = trumbowygBoxRect.top + toolbarHeight;
+
+        // 如果图片顶部在编辑器内容区域上方,说明图片被部分滚动到上方了
+        if (imgRect.top < editorContentTop) {
+          return true;
+        }
+      }
+
+      return false;
+    },
+
+    // 关闭图片模态框
+    closeImageModal() {
+      this.showImageModal = false;
+      this.currentImage = null;
+    },
+
+    // 更新图片尺寸
+    updateImageSize() {
+      if (!this.currentImage) return;
+
+      const width = parseInt(this.imageSize.width);
+      const height = parseInt(this.imageSize.height);
+
+      if (width > 0 && height > 0) {
+        this.currentImage.style.width = width + 'px';
+        this.currentImage.style.height = height + 'px';
+
+        // 更新缩放比例
+        this.currentZoom = width / this.originalImageSize.width;
+
+        // 强制同步编辑器内容
+        this.syncEditorContent();
+      }
+    },
+
+    // 放大
+    zoomIn() {
+      if (this.currentZoom >= this.maxZoom) return;
+
+      this.currentZoom = Math.min(this.currentZoom + 0.1, this.maxZoom);
+      this.applyZoom();
+    },
+
+    // 缩小
+    zoomOut() {
+      if (this.currentZoom <= this.minZoom) return;
+
+      this.currentZoom = Math.max(this.currentZoom - 0.1, this.minZoom);
+      this.applyZoom();
+    },
+
+    // 触发缩放事件
+    applyZoom() {
+      if (!this.currentImage) return;
+
+      const newWidth = Math.round(this.originalImageSize.width * this.currentZoom);
+      const newHeight = Math.round(this.originalImageSize.height * this.currentZoom);
+
+      // 直接设置模态框显示的尺寸(用户设置的目标尺寸)
+      this.imageSize.width = newWidth;
+      this.imageSize.height = newHeight;
+
+      // 设置图片的样式尺寸
+      this.currentImage.style.width = newWidth + 'px';
+      this.currentImage.style.height = newHeight + 'px';
+
+      // 强制同步编辑器内容
+      this.syncEditorContent();
+    },
+
+    // 更新模态框中显示的尺寸值(获取图片设置的样式尺寸)
+    updateDisplayedSize() {
+      if (!this.currentImage) return;
+
+      // 获取图片设置的样式尺寸(用户实际设置的值)
+      const setWidth = parseInt(this.currentImage.style.width) || this.currentImage.naturalWidth;
+      const setHeight = parseInt(this.currentImage.style.height) || this.currentImage.naturalHeight;
 
+      // 更新模态框中显示的尺寸值(显示用户设置的值,不是被限制后的显示值)
+      this.imageSize = {
+        width: setWidth,
+        height: setHeight,
+      };
+
+      // 更新缩放比例(基于用户设置的尺寸)
+      if (this.originalImageSize.width > 0) {
+        this.currentZoom = setWidth / this.originalImageSize.width;
+      }
+    },
+    // 同步编辑器内容
+    syncEditorContent() {
+      const trumbowyg = $('#editor').data('trumbowyg');
+      if (trumbowyg) {
+        // 强制同步DOM到HTML
+        trumbowyg.syncCode();
+
+        // 延迟触发内容变化事件
+        this.debouncedTriggerContentChange();
+      }
+    },
+    // 防抖的内容变化触发器
+    debouncedTriggerContentChange() {
+      if (this.contentChangeTimer) {
+        clearTimeout(this.contentChangeTimer);
+      }
+      this.contentChangeTimer = setTimeout(() => {
+        this.triggerContentChange();
+      }, 300); // 300ms 防抖
+    },
+    // 触发编辑器内容变化事件
+    triggerContentChange() {
+      const html = $('#editor').trumbowyg('html');
+      this.$emit('update:modelValue', html);
+    },
+
+
+    // 给所有图片绑定点击事件
+    bindImageClickEvents() {
+      const _self = this;
+      const trumbowyg = $('#editor').data('trumbowyg');
+
+      if (!trumbowyg || !trumbowyg.$ed) {
+        console.log('bindImageClickEvents: Trumbowyg 编辑器未初始化');
+        return;
+      }
+
+      // 查找所有图片
+      const images = trumbowyg.$ed.find('img');
+
+      // 先移除所有已有的图片点击事件,避免重复绑定
+      images.off('click.imageResize');
+
+      // 为每个图片添加点击事件
+      images.each(function () {
+        const img = $(this);
+
+        img.on('click.imageResize', function (e) {
+          e.preventDefault();
+          e.stopPropagation();
+          _self.showImageResizeModal(this);
+        });
+      });
+    },
+
+    // 粘贴后光标位于图片后
+    setCursorPosition(imagUrl) {
+      setTimeout(() => {
+        try {
+          const $editor = $('#editor');
+          const editorElement = $editor.data('trumbowyg').$ed[0];
+
+          // 找到刚插入的图片元素
+          const imgElements = editorElement.querySelectorAll(`img[src="${imagUrl}"]`);
+          const imgElement = imgElements[imgElements.length - 1]; // 获取最后一个(最新插入的)
+
+          if (imgElement) {
+            const range = document.createRange();
+            const selection = window.getSelection();
+
+            // 直接将光标定位在图片元素后面
+            range.setStartAfter(imgElement);
+            range.setEndAfter(imgElement);
+
+            selection.removeAllRanges();
+            selection.addRange(range);
+
+            // 确保编辑器获得焦点
+            editorElement.focus();
+          }
+        } catch (error) {
+          console.error('设置光标位置失败:', error);
+        }
+      }, 100); // 延迟确保DOM更新完成
+    },
+
+    // 检查当前选中的图片是否还存在于编辑器中
+    checkImageExistence() {
+      // 如果没有显示模态框或没有当前图片,直接返回
+      if (!this.showImageModal || !this.currentImage) {
+        return;
+      }
+
+      try {
+        const trumbowyg = $('#editor').data('trumbowyg');
+        if (!trumbowyg || !trumbowyg.$ed) {
+          return;
+        }
+
+        // 获取编辑器中所有的图片元素
+        const editorImages = trumbowyg.$ed.find('img');
+
+        // 检查当前选中的图片是否还在编辑器中
+        let imageExists = false;
+        editorImages.each((_, img) => {
+          if (img === this.currentImage) {
+            imageExists = true;
+            return false; // 跳出each循环
+          }
+        });
+
+        // 如果图片不存在了,关闭模态框
+        if (!imageExists) {
+          console.log('检测到图片已被删除,关闭模态框');
+          this.closeImageModal();
+        }
+      } catch (error) {
+        console.error('检查图片存在性时出错:', error);
+        // 出错时也关闭模态框,避免显示无效的模态框
+        this.closeImageModal();
+      }
+    },
+  },
 };
 </script>
 
 <style>
+.editor-container {
+  position: relative;
+}
+
 .trumbowyg-box .trumbowyg-editor {
   height: 368px !important;
 }
@@ -326,4 +717,123 @@ export default {
   min-height: 200px;
   padding: 8px;
 }
+
+/* 图片调整模态框样式 */
+.image-resize-modal {
+  position: absolute;
+  z-index: 1000;
+  background: white;
+  border: 1px solid #ddd;
+  border-radius: 6px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  padding: 0;
+  min-width: 420px;
+  max-width: 500px;
+}
+
+.modal-content {
+  position: relative;
+  padding: 4px 12px;
+}
+
+.controls-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin: 12px;
+  justify-content: space-between;
+}
+
+.size-input-group {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.size-input-group label {
+  font-size: 12px;
+  color: #666;
+  min-width: 32px;
+}
+
+.size-input-group input {
+  width: 60px;
+  padding: 4px 6px;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+  font-size: 12px;
+  text-align: center;
+}
+
+.size-input-group span {
+  font-size: 12px;
+  color: #999;
+}
+
+.zoom-controls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.zoom-btn {
+  width: 28px;
+  height: 28px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background: white;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: bold;
+  transition: all 0.2s;
+}
+
+.zoom-btn:hover:not(:disabled) {
+  background: #f5f5f5;
+  border-color: #999;
+}
+
+.zoom-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.zoom-display {
+  font-size: 12px;
+  color: #666;
+  min-width: 40px;
+  text-align: center;
+}
+
+.close-btn {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  width: 20px;
+  height: 20px;
+  border: none;
+  background: none;
+  cursor: pointer;
+  font-size: 24px;
+  color: #999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 2px;
+}
+
+.close-btn:hover {
+  background: #f5f5f5;
+  color: #666;
+}
+
+/* 图片选中状态 */
+.trumbowyg-editor img:hover {
+  outline: 2px solid #007bff;
+  outline-offset: 2px;
+  cursor: pointer;
+}
 </style>

+ 31 - 1
src/window1/tabGridView/GridBody.vue

@@ -69,7 +69,14 @@
       <template
         v-if="gridFieldItem.groupNames == null || gridFieldItem.groupNames.length == 0 || (nowTab != null && (gridFieldItem.groupNames.indexOf(nowTab) >= 0))"
       >
-        <td v-if="!modelData.editMode" v-show="gridFieldItem.visible" @click="clickRecord">
+        <td
+          v-if="!modelData.editMode" v-show="gridFieldItem.visible" :class="{
+            'text-center sticky-col': gridFieldItem.fieldName === '-',
+            'sticky-left': gridFieldItem.fieldName === '-' && index === 0,
+            'sticky-right': gridFieldItem.fieldName === '-' && index === tabGridFields.length - 1
+          }"
+          @click="clickRecord"
+        >
           <div>
             <CellTextItem
               ref="cellTextItem" :window="window" :grid-field-item="gridFieldItem" :model-data="modelData"
@@ -543,7 +550,30 @@ table>tbody>tr:nth-of-type(odd) {
   background: #f7f7f7;
 }
 
+
 .warning {
   background-color: #fcf8e3 !important;
 }
+
+/* 选中行时固定列的背景色也要变化 */
+.warning .sticky-col {
+  background-color: #fcf8e3 !important;
+}
+.sticky-left {
+  left: 80px;
+}
+
+.sticky-right{
+  right: 0;
+}
+
+/* 鼠标悬浮行效果 */
+tr:hover {
+  background-color: #e6f4ff !important;
+}
+
+/* 鼠标悬浮时固定列的背景色也要变化 */
+tr:hover .sticky-col {
+  background-color: #e6f4ff !important;
+}
 </style>

+ 16 - 3
src/window1/tabGridView/GridHeader.vue

@@ -40,7 +40,7 @@
       </div>
     </th>
     <template v-if="tabGridFields != null && tabGridFields.length > 0">
-      <template v-for="tabGridField in tabGridFields">
+      <template v-for="(tabGridField, index) in tabGridFields">
         <th
           v-show="tabGridField.visible"
           v-if="tabGridField.groupNames == null || tabGridField.groupNames.length == 0 || (nowTab != null && (tabGridField.groupNames.indexOf(nowTab) >= 0))"
@@ -49,6 +49,11 @@
           class="text-center"
           :width="tabGridField.width + 'px'"
           style="position: relative;"
+          :class="{
+            'text-center sticky-col': tabGridField.fieldName === '-',
+            'sticky-left': tabGridField.fieldName === '-' && index === 0,
+            'sticky-right': tabGridField.fieldName === '-' && index === tabGridFields.length - 1
+          }"
           @dragover="ondragover($event, tabGridField)"
           @click="onSort(tabGridField)"
         >
@@ -348,7 +353,7 @@ table th {
 /* 固定列 */
 .sticky-col {
   position: -webkit-sticky; /* Safari */
-  position: sticky;
+  position: sticky !important;
   left: 0;
   background: #fafafa;
   z-index: 1; /* 确保固定列在其他内容之上 */
@@ -358,9 +363,17 @@ table th {
 /* 固定行 */
 .sticky-row {
   position: -webkit-sticky; /* Safari */
-  position: sticky;
+  position: sticky !important;
   top: 0;
   background: #fafafa;
   z-index: 2; /* 确保固定列在其他内容之上 */
 }
+
+.sticky-left {
+  left: 80px;
+}
+
+.sticky-right{
+  right: 0;
+}
 </style>

+ 172 - 204
src/workflow/WorkflowSelectUser.vue

@@ -1,156 +1,83 @@
 <template>
-  <div>
-    <div class="panel panel-default">
-      <div class="panel-heading">
-        {{ $t("lang.workflowSelectUser.approve") }}
-      </div>
-      <div class="panel-body">
-        <div class="row">
-          <div class="form-horizontal">
-            <div
-              v-if="userTaskDtos.length==0 && workFlow.workflowType != 'Auto'"
-              class="form-group"
-            >
-              <label class="col-xs-5 col-sm-4 col-md-3 col-lg-2 control-label img-label">
-                {{ $t("lang.workflowSelectUser.approveUser") }}
-              </label>
-              <div class="col-xs-7 col-sm-8 col-md-9 col-lg-10">
-                <div
-                  v-for="applyUser in applyUsers"
-                  :key="applyUser.id"
-                  class="img-box"
-                >
-                  <div>
-                    <span
-                      class="glyphicon glyphicon-trash remove-icon"
-                      @click="removeApplyUser(applyUser)"
-                    />
-
-                    <AuthImage
-                      :auth-src="getImgSrc(applyUser.imageName)"
-                      class="img-circle"
-                    />
-
-                    {{ applyUser.name }}
-                  </div>
-                </div>
-                <div
-                  class="add-box"
-                  @click="addApplyUser()"
-                >
-                  <span
-                    class="glyphicon glyphicon-plus add-icon"
-                    aria-hidden="true"
-                  />
-                </div>
-              </div>
-            </div>
+  <div class="workflow-select-user">
+    <a-card class="workflow-card">
+      <template #title>
+        <a-typography-title :level="5" style="margin: 0">
+          {{ $t("lang.workflowSelectUser.approve") }}
+        </a-typography-title>
+      </template>
 
-            <div
-              v-for="userTaskDto in userTaskDtos"
-              :key="userTaskDto.id"
-              class="form-group"
-            >
-              <label class="col-xs-5 col-sm-4 col-md-3 col-lg-2 control-label img-label">
-                {{ userTaskDto.name }}
-              </label>
-              <div class="col-xs-7 col-sm-8 col-md-9 col-lg-10">
-                <div
-                  v-for="applyUser in userTaskDto.assignees"
-                  :key="applyUser.id"
-                  class="img-box"
-                >
-                  <div>
-                    <span
-                      class="glyphicon glyphicon-trash remove-icon"
-                      @click="removeApplyUser(applyUser,userTaskDto)"
-                    />
-                    <AuthImage
-                      :auth-src="getImgSrc(applyUser.imageName)"
-                      class="img-circle"
-                    />
-                    {{ applyUser.name }}
-                  </div>
-                </div>
-                <div
-                  v-if="userTaskDto.multipleInstance || userTaskDto.assignees.length < 1"
-                  class="add-box"
-                  @click="addApplyUser(userTaskDto)"
-                >
-                  <span
-                    class="glyphicon glyphicon-plus add-icon"
-                    aria-hidden="true"
-                  />
+      <a-form :label-col="{ span: 2 }" :wrapper-col="{ span: 22 }" class="workflow-form">
+        <div v-if="userTaskDtos.length === 0 && workFlow.workflowType !== 'Auto'">
+          <a-form-item :label="$t('lang.workflowSelectUser.approveUser')">
+            <div class="user-list">
+              <div v-for="applyUser in applyUsers" :key="applyUser.id" class="user-item">
+                <div class="user-content">
+                  <a-button shape="circle" class="remove-btn" danger @click="removeApplyUser(applyUser)">
+                    <delete-outlined />
+                  </a-button>
+                  <AuthImage :auth-src="getImgSrc(applyUser.imageName)" class="user-avatar" />
+                  <a-typography-text strong>{{ applyUser.name }}</a-typography-text>
                 </div>
               </div>
+              <div class="add-box" @click="addApplyUser()">
+                <a-button shape="circle" size="large" class="add-btn">
+                  <plus-outlined />
+                </a-button>
+              </div>
             </div>
+          </a-form-item>
+        </div>
 
-            <div
-              class="form-group"
-              style="margin-top:20px;"
-            >
-              <label class="col-xs-5 col-sm-4 col-md-3 col-lg-2 control-label img-label">
-                {{ $t("lang.workflowSelectUser.copyUser") }}
-              </label>
-              <div class="col-xs-7 col-sm-8 col-md-9 col-lg-10">
-                <div
-                  v-for="copyUser in copyUsers"
-                  :key="copyUser.id"
-                  class="img-box"
-                >
-                  <div>
-                    <span
-                      class="glyphicon glyphicon-trash remove-icon"
-                      @click="removeCopyUser(copyUser)"
-                    />
-                    <AuthImage
-                      :auth-src="getImgSrc(copyUser.imageName)"
-                      class="img-circle"
-                    />
-                    {{ copyUser.name }}
-                  </div>
-                </div>
-                <div
-                  class="add-box"
-                  @click="addCopyUser"
-                >
-                  <span
-                    class="glyphicon glyphicon-plus add-icon"
-                    aria-hidden="true"
-                  />
+        <div v-for="userTaskDto in userTaskDtos" :key="userTaskDto.id">
+          <a-form-item :label="userTaskDto.name">
+            <div class="user-list">
+              <div v-for="applyUser in userTaskDto.assignees" :key="applyUser.id" class="user-item">
+                <div class="user-content">
+                  <a-button shape="circle" class="remove-btn" danger @click="removeApplyUser(applyUser, userTaskDto)">
+                    <delete-outlined />
+                  </a-button>
+                  <AuthImage :auth-src="getImgSrc(applyUser.imageName)" class="user-avatar" />
+                  <a-typography-text strong>{{ applyUser.name }}</a-typography-text>
                 </div>
               </div>
+              <div v-if="userTaskDto.multipleInstance || userTaskDto.assignees.length < 1" class="add-box">
+                <a-button shape="circle" size="large" class="add-btn" @click="addApplyUser(userTaskDto)">
+                  <plus-outlined />
+                </a-button>
+              </div>
             </div>
+          </a-form-item>
+        </div>
 
-            <div
-              class="form-group"
-              style="margin-top:20px;"
-            >
-              <label class="col-xs-5 col-sm-4 col-md-3 col-lg-2 control-label" />
-              <div class="col-xs-7 col-sm-8 col-md-9 col-lg-10">
-                <button
-                  type="button"
-                  class="btn btn-primary"
-                  @click="apply"
-                >
-                  {{ $t("lang.workflowSelectUser.submit") }}
-                </button>
+        <a-form-item :label="$t('lang.workflowSelectUser.copyUser')">
+          <div class="user-list">
+            <div v-for="copyUser in copyUsers" :key="copyUser.id" class="user-item">
+              <div class="user-content">
+                <a-button shape="circle" class="remove-btn" danger @click="removeCopyUser(copyUser)">
+                  <delete-outlined />
+                </a-button>
+                <AuthImage :auth-src="getImgSrc(copyUser.imageName)" class="user-avatar" />
+                <a-typography-text strong>{{ copyUser.name }}</a-typography-text>
               </div>
             </div>
+            <div class="add-box" @click="addCopyUser">
+              <a-button shape="circle" size="large" class="add-btn">
+                <plus-outlined />
+              </a-button>
+            </div>
           </div>
-        </div>
-      </div>
-    </div>
-    <Modal
-      v-model:show="modal"
-      :full="true"
-      @ok="searchDialogOk"
-      @cancel="searchDialogCancel"
-    >
+        </a-form-item>
+      </a-form>
+      <a-button type="primary" style="margin:0 0 8px 16px;" @click="apply">
+        <save-outlined />
+        {{ $t("lang.workflowSelectUser.submit") }}
+      </a-button>
+    </a-card>
+
+    <Modal v-model:show="modal" :full="true" @ok="searchDialogOk" @cancel="searchDialogCancel">
       <InfoWindow
-        ref="info"
-        :info-window-no="infoWindowNo"
-        :where-clause-source="whereClauseSource"
+        ref="info" :info-window-no="infoWindowNo" :where-clause-source="whereClauseSource"
         @data-selected="dataSelected"
       />
       <template #header>
@@ -158,10 +85,7 @@
       </template>
     </Modal>
 
-    <Modal
-      v-model:show="modal2"
-      :small="true"
-    >
+    <Modal v-model:show="modal2" :small="true">
       <div class="row box">
         <div class="col-md-12">
           <div class="table-box">
@@ -173,11 +97,7 @@
                 </tr>
               </thead>
               <tbody>
-                <tr
-                  v-for="item in currentUsers"
-                  :key="item.id"
-                  @click="selectCurrentUser(item)"
-                >
+                <tr v-for="item in currentUsers" :key="item.id" @click="selectCurrentUser(item)">
                   <td>{{ item.name }}</td>
                   <td>{{ item.no }}</td>
                 </tr>
@@ -193,17 +113,25 @@
     <Loading v-if="loading" />
   </div>
 </template>
-<script>
 
+<script>
 import Common from '../common/Common.js';
-
-
 import AuthImage from '../widget/AuthImage.vue';
-
-export default {
-
+import { defineComponent } from 'vue';
+import {
+  DeleteOutlined,
+  PlusOutlined,
+  SaveOutlined,
+  UserOutlined,
+} from '@ant-design/icons-vue';
+
+export default defineComponent({
   components: {
-    AuthImage, 
+    AuthImage,
+    DeleteOutlined,
+    PlusOutlined,
+    SaveOutlined,
+    UserOutlined,
   },
 
   props: {
@@ -214,7 +142,7 @@ export default {
     },
     workFlow: {
       type: Object,
-      default : function(){
+      default: function () {
         return null;
       },
     },
@@ -230,7 +158,7 @@ export default {
       copyUsers: [],
       currentUser: '',
       whereClauseSource: {
-        customerDataDimensions:[{
+        customerDataDimensions: [{
           fieldName: 'client.id',
           dataDimensionTypeNo: '202201191757',
           defaultDataDimensionTypeValueNo: '4',
@@ -327,7 +255,7 @@ export default {
       var _self = this;
       this.modal = false;
       var userId = modelData.id;
-      _self.loading=true;
+      _self.loading = true;
       $.ajax({
         url: Common.getApiURL('userResource/getUser'),
         type: 'get',
@@ -338,7 +266,7 @@ export default {
           userId: userId,
         },
         success: function (data) {
-          _self.loading=false;
+          _self.loading = false;
           if (_self.currentUser == 'applyUser') {
             for (var i = 0; i < _self.userTaskDtos.length; i++) {
               if (_self.userTaskDtos[i] == _self.selectedUserTaskDto) {
@@ -359,7 +287,7 @@ export default {
           }
           _self.className = 'com.leanwo.prodog.base.model.User';
           _self.whereClauseSource = {
-            customerDataDimensions:[{
+            customerDataDimensions: [{
               fieldName: 'client.id',
               dataDimensionTypeNo: '202201191757',
               defaultDataDimensionTypeValueNo: '4',
@@ -367,10 +295,10 @@ export default {
           };
         },
         error: function (XMLHttpRequest, textStatus, errorThrown) {
-          _self.loading=false;
+          _self.loading = false;
           _self.className = 'com.leanwo.prodog.base.model.User';
           _self.whereClauseSource = {
-            customerDataDimensions:[{
+            customerDataDimensions: [{
               fieldName: 'client.id',
               dataDimensionTypeNo: '202201191757',
               defaultDataDimensionTypeValueNo: '4',
@@ -443,7 +371,7 @@ export default {
         _self.applyUsers.splice(k, 1);
       }
 
-      _self.userTaskDtos[i]=userTaskDto;
+      _self.userTaskDtos[i] = userTaskDto;
     },
 
     /**
@@ -534,7 +462,7 @@ export default {
         },
         success: function (data) {
           if (data && data.length > 0) {
-            userTaskDto['users']=data;
+            userTaskDto['users'] = data;
             if (data.length == 1) {
               userTaskDto.assignees.push(data[0]);
               userTaskDto.assigneeIds.push(data[0].id);
@@ -547,64 +475,104 @@ export default {
       });
     },
   },
-};
+});
 </script>
 
 <style scoped>
-.img-label {
-    height: 80px;
-    line-height: 80px;
-    padding-top: 0px !important;
+.workflow-select-user {
+  padding: 4px 0;
+  background-color: #f8f8f8;
+}
+
+.workflow-card {
+  margin: 0 auto;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.workflow-form {
+  margin-top: 16px;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  margin-bottom: 6px;
+}
+
+.user-item {
+  position: relative;
+  width: 100px;
+  text-align: center;
+  background: #fff;
+  border-radius: 8px;
+  padding: 12px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  transition: transform 0.2s;
+}
+
+.user-item:hover {
+  transform: translateY(-4px);
+}
+
+.user-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
 }
 
-.img-box {
-    width: 60px;
-    height: 80px;
-    float: left;
-    text-align: center;
-    margin-right: 20px;
+.remove-btn {
+  position: absolute;
+  top: -8px;
+  right: -8px;
+  z-index: 1;
+  width: 28px;
+  height: 28px;
+  padding: 0;
 }
 
-.img-box div img {
-    width: 60px;
-    height: 60px;
+.user-avatar {
+  width: 72px;
+  height: 72px;
+  border-radius: 50%;
+  margin-bottom: 8px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
 }
 
 .add-box {
-    width: 60px;
-    height: 60px;
-    float: left;
-    border-radius: 30px;
-    border: 1px #999 dashed;
-    text-align: center;
-    margin-top: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100px;
+  height: 100px;
+  border: 2px dashed #d9d9d9;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.3s;
+  position: relative;
 }
 
 .add-box:hover {
-    cursor: pointer;
-    background-color: #ccc;
+  border-color: #4096ff;
+  background-color: #e6f4ff;
 }
 
-.add-icon {
-    width: 60px;
-    height: 60px;
-    color: #aaa;
-    font-size: 40px;
-    line-height: 60px;
-    position: relative;
-    top: -1px;
+.add-btn {
+  width: 60px;
+  height: 60px;
+  font-size: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
-.remove-icon {
-    color: red;
-    position: relative;
-    top: 10px;
-    right: -30px;
-    cursor: pointer;
+.table-box {
+  margin: 16px 0;
 }
 
-.table-box td,
-th {
-    text-align: center;
+:deep(.ant-card-body) {
+  padding: 0 !important;
 }
 </style>