2
0

3 Коммиты 484a271053 ... d607c1429e

Автор SHA1 Сообщение Дата
  xieyong d607c1429e feat: 锁定作业类型字段,确保其值由申请时选择的作业类型决定 23 часов назад
  xieyong 6333fc250a refactor: 移除地图位置标记相关组件和逻辑,简化作业票表单 23 часов назад
  xieyong 438f01bd8a feat: 添加部门选择弹框功能,优化部门字段交互体验 23 часов назад

+ 150 - 0
src/components/CommomSelect/dept-select-dialog.vue

@@ -0,0 +1,150 @@
+<!--
+  部门选择弹框(单选)
+  用于把「部门」字段的下拉选择改为弹框选择:
+  点击输入框打开弹框,弹框内可按部门名称搜索,选中节点后点「确定」返回所选部门。
+-->
+<template>
+  <el-dialog
+    :title="title"
+    :visible.sync="visible"
+    :before-close="handleClose"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    top="8vh"
+    width="520px"
+  >
+    <el-input
+      v-model="keyword"
+      placeholder="请输入部门名称"
+      clearable
+      prefix-icon="el-icon-search"
+      class="dept-search"
+      @input="handleSearch"
+    />
+    <el-tree
+      ref="tree"
+      v-loading="treeLoading"
+      :data="treeData"
+      :props="treeProps"
+      node-key="id"
+      highlight-current
+      default-expand-all
+      :expand-on-click-node="false"
+      :filter-node-method="filterNode"
+      class="dept-tree"
+      @node-click="handleNodeClick"
+    />
+    <div slot="footer" class="dialog-footer">
+      <el-button type="primary" size="small" @click="handleConfirm"
+        >确定</el-button
+      >
+      <el-button size="small" @click="handleClose">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+  import { listOrganizations } from '@/api/system/organization';
+
+  export default {
+    props: {
+      // 弹框标题
+      title: {
+        type: String,
+        default: '选择部门'
+      }
+    },
+    data() {
+      return {
+        visible: false,
+        treeLoading: false,
+        // 搜索关键字
+        keyword: '',
+        // 部门树数据
+        treeData: [],
+        // 当前选中的部门节点
+        currentNode: null,
+        treeProps: {
+          children: 'children',
+          label: 'name'
+        }
+      };
+    },
+    methods: {
+      /**
+       * 打开弹框
+       * @param current 当前已选部门 { id, name },用于回显高亮
+       */
+      async open(current) {
+        this.visible = true;
+        this.keyword = '';
+        this.currentNode = current ? { ...current } : null;
+        if (!this.treeData.length) {
+          await this.loadData();
+        }
+        this.$nextTick(() => {
+          if (current && current.id && this.$refs.tree) {
+            this.$refs.tree.setCurrentKey(current.id);
+          }
+        });
+      },
+      // 加载部门树数据
+      async loadData() {
+        this.treeLoading = true;
+        try {
+          const data = await listOrganizations({});
+          this.treeData = this.$util.toTreeData({
+            data: data || [],
+            idField: 'id',
+            parentIdField: 'parentId'
+          });
+        } catch (e) {
+          console.error('查询部门列表失败', e);
+          this.treeData = [];
+        } finally {
+          this.treeLoading = false;
+        }
+      },
+      // 部门名称模糊搜索
+      handleSearch(value) {
+        this.$refs.tree && this.$refs.tree.filter(value);
+      },
+      // 搜索过滤:部门名称包含关键字即保留
+      filterNode(value, data) {
+        if (!value) {
+          return true;
+        }
+        return (data.name || '').includes(value);
+      },
+      // 点击树节点选中
+      handleNodeClick(data) {
+        this.currentNode = data;
+      },
+      // 确定:返回所选部门
+      handleConfirm() {
+        if (!this.currentNode) {
+          return this.$message.warning('请至少选择一个部门');
+        }
+        this.$emit('changeParent', this.currentNode);
+        this.handleClose();
+      },
+      // 关闭弹框
+      handleClose() {
+        this.visible = false;
+        this.currentNode = null;
+        this.keyword = '';
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .dept-search {
+    margin-bottom: 10px;
+  }
+  .dept-tree {
+    max-height: 50vh;
+    overflow: auto;
+  }
+</style>

+ 3 - 108
src/views/DangerousWorkList/add.vue

@@ -130,25 +130,6 @@
         </el-col>
       </el-row>
 
-      <!-- 作业位置标记(每张底图可分别打点,由后端 map_point 字段单独保存) -->
-      <el-row :gutter="20">
-        <el-col :span="24">
-          <el-form-item label="作业位置标记:">
-            <div class="map-marker-field">
-              <map-marker-preview :markers="mapMarkers" />
-              <el-button
-                v-if="!isView"
-                size="mini"
-                icon="el-icon-edit"
-                @click="openMapMarker"
-              >
-                {{ mapMarkers.length ? '重新标记' : '标记位置' }}
-              </el-button>
-            </div>
-          </el-form-item>
-        </el-col>
-      </el-row>
-
       <el-form-item label="作业票模板:">
         <el-input
           v-model="templateName"
@@ -239,7 +220,6 @@
       <el-button @click="cancel">{{ isView ? '关闭' : '取消' }}</el-button>
     </template>
     <releaseRules ref="releaseRulesRef" @success="onReleaseRulesSuccess" />
-    <map-marker-dialog ref="mapMarkerRef" @confirm="onMapMarkerConfirm" />
   </ele-modal>
 </template>
 
@@ -250,8 +230,6 @@
   import personSelect from '@/components/CommomSelect/person-select.vue';
   import experimentationProcessNew from '@/components/releaseRules/experimentationProcessNew.vue';
   import workTicketForm from './components/workTicketForm.vue';
-  import mapMarkerDialog from './components/mapMarkerDialog.vue';
-  import mapMarkerPreview from './components/mapMarkerPreview.vue';
   import areaMixin from '@/mixins/areaMixin';
   import dictMixins from '@/mixins/dictMixins';
   import dictEnum from '@/enum/dict';
@@ -268,15 +246,14 @@
     getProcessInstanceId
   } from './constants';
   import { buildWorkTypeOptions, resolveWorkTypeLabel } from './workTypes';
-  import { parseExtension, parseMapMarkers, buildMapPoint } from './mapMarker';
+  import { parseExtension } from './mapMarker';
   import bpmDetail from '@/views/bpm/processInstance/detail.vue';
   import {
     ticketSave,
     ticketUpdate,
     ticketGetById,
     ticketSubmit,
-    ticketListByWorkId,
-    ticketMapPoint
+    ticketListByWorkId
   } from '@/api/dangerousWork';
   import { listOrganizations, getUserPage } from '@/api/system/organization';
 
@@ -310,8 +287,6 @@
       releaseRules,
       experimentationProcessNew,
       workTicketForm,
-      mapMarkerDialog,
-      mapMarkerPreview,
       bpmDetail
     },
     mixins: [areaMixin, dictMixins],
@@ -336,10 +311,6 @@
         frontendTemplate: null,
         // 未匹配到前端模板时的原始作业票内容(降级展示)
         rawExtension: null,
-        // 作业位置标记(由后端 map_point 字段单独保存)
-        mapMarkers: [],
-        // 作业票未保存时暂存的地图位置标记,保存后补存
-        pendingMarkers: null,
         // 选中的作业票模板(发布规则)
         templateData: {},
         templateName: '',
@@ -648,8 +619,6 @@
         this.templateName = '';
         this.frontendTemplate = null;
         this.rawExtension = null;
-        this.mapMarkers = [];
-        this.pendingMarkers = null;
         this.$refs.experimentationProcessRef?.init('[]');
       },
       // 用作业票详情回填表单
@@ -680,10 +649,6 @@
         // 解析已保存的作业票内容
         const extension = parseExtension(detail.extensionJson);
         this.form.remark = extension.remark || '';
-        // 回显作业位置标记(每张底图一个标记点)
-        this.mapMarkers = parseMapMarkers(
-          detail?.mapPoint ?? detail?.map_point
-        );
         // 按作业类型匹配前端内置模板,匹配到则结构化回显
         const option = this.workTypeList.find(
           (item) => String(item.value) === String(detail.workType)
@@ -718,54 +683,6 @@
           this.$refs.form && this.$refs.form.clearValidate();
         });
       },
-      // 打开地图位置标记弹框
-      openMapMarker() {
-        this.$refs.mapMarkerRef.open(this.mapMarkers);
-      },
-      // 地图位置标记确认:调用后端专用接口保存(整段覆盖)
-      async onMapMarkerConfirm(markers) {
-        const list = markers || [];
-        this.mapMarkers = list;
-        // 作业票还没保存时没有 id,先暂存,等作业票保存后补存
-        if (!this.form.id) {
-          this.pendingMarkers = list;
-          this.$message.success(
-            list.length
-              ? '地图位置已标记,保存作业票后生效'
-              : '已清除地图位置标记'
-          );
-          return;
-        }
-        try {
-          await ticketMapPoint({
-            ticketId: this.form.id,
-            // 没有标记时传 null,后端按空值清除标记
-            mapPoint: buildMapPoint(list)
-          });
-          this.$message.success(
-            list.length ? '地图位置标记成功' : '已清除地图位置标记'
-          );
-        } catch (e) {
-          // 失败提示由响应拦截器统一处理
-        }
-      },
-      // 保存暂存的地图位置标记:新增作业票保存后补存(编辑态标记已即时保存)
-      // 标记保存失败不影响作业票本身的保存/提交,失败提示由响应拦截器统一处理
-      // @param ticketId 作业票 id
-      async flushPendingMarkers(ticketId) {
-        if (!ticketId || !this.pendingMarkers) {
-          return;
-        }
-        try {
-          await ticketMapPoint({
-            ticketId,
-            mapPoint: buildMapPoint(this.pendingMarkers)
-          });
-          this.pendingMarkers = null;
-        } catch (e) {
-          // 忽略:作业票已保存,标记可稍后重新标记
-        }
-      },
       cancel() {
         this.form = { ...defForm };
         this.personList = [];
@@ -773,8 +690,6 @@
         this.templateName = '';
         this.frontendTemplate = null;
         this.rawExtension = null;
-        this.mapMarkers = [];
-        this.pendingMarkers = null;
         this.detailLoading = false;
         this.submitLoading = false;
         this.newTicketId = '';
@@ -875,18 +790,7 @@
             ? ticketUpdate(params)
             : ticketSave(params);
           request
-            .then(async (res) => {
-              // 新增作业票:保存后补存暂存的地图位置标记
-              if (this.pendingMarkers && !this.form.id) {
-                const ticketId = await this.resolveSavedTicketId(res, params);
-                if (ticketId) {
-                  await this.flushPendingMarkers(ticketId);
-                } else {
-                  this.$message.warning(
-                    '作业票已保存,但未获取到作业票ID,地图位置标记未保存,请在作业票列表中重新标记'
-                  );
-                }
-              }
+            .then(() => {
               this.$message.success(this.isEdit ? '保存成功' : '申请成功');
               this.cancel();
               this.$emit('reload');
@@ -962,8 +866,6 @@
               return;
             }
             this.newTicketId = ticketId;
-            // 新增作业票:补存暂存的地图位置标记
-            await this.flushPendingMarkers(ticketId);
           }
           await ticketSubmit({ id: ticketId, processDefinitionKey });
           this.$message.success('提交成功,已发起审批');
@@ -1036,13 +938,6 @@
     line-height: 20px;
   }
 
-  /* 作业位置标记:缩略图与按钮并排 */
-  .map-marker-field {
-    display: flex;
-    align-items: flex-start;
-    gap: 12px;
-  }
-
   /* 未匹配到前端模板时的原始作业票内容 */
   .raw-extension {
     width: 100%;

+ 0 - 150
src/views/DangerousWorkList/components/mapMarkerPreview.vue

@@ -1,150 +0,0 @@
-<!--
-  作业票「地图位置标记」展示组件
-  - 按底图逐个展示缩略图,并在各自的标记位置绘制标记点
-  - 每张底图的缩略图尺寸按原始宽高比换算,保证标记点与底图严格对齐
--->
-<template>
-  <div class="map-marker-preview">
-    <div v-if="!markers.length" class="map-preview-empty">未标记地图位置</div>
-    <div
-      v-for="item in markers"
-      :key="item.mapImageId"
-      class="map-preview-item"
-    >
-      <div class="map-preview-box" :style="getBoxStyle(item)">
-        <img
-          :src="resolveUrl(item.imageUrl)"
-          class="map-preview-image"
-          alt="地图底图"
-          @load="onImageLoad(item, $event)"
-          @error="onImageError(item)"
-        />
-        <div
-          v-if="!errors[item.mapImageId]"
-          class="map-pin"
-          :style="getPinStyle(item)"
-        >
-          <i class="el-icon-location"></i>
-        </div>
-      </div>
-      <div class="map-preview-name">
-        {{ item.mapImageName || `底图${item.mapImageId}` }}
-        <span v-if="errors[item.mapImageId]" class="map-preview-error">
-          (图片加载失败)
-        </span>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-  import { resolveMapImageUrl } from '../mapMarker';
-
-  export default {
-    name: 'MapMarkerPreview',
-    props: {
-      // 标记列表 [{ mapImageId, xRate, yRate, mapImageName, imageUrl, ... }]
-      markers: {
-        type: Array,
-        default: () => []
-      },
-      // 缩略图展示宽度(像素)
-      width: {
-        type: Number,
-        default: 240
-      }
-    },
-    data() {
-      return {
-        // 各底图原始尺寸 { [mapImageId]: { width, height } }
-        naturalSizes: {},
-        // 图片加载失败的底图 id
-        errors: {}
-      };
-    },
-    watch: {
-      markers() {
-        this.naturalSizes = {};
-        this.errors = {};
-      }
-    },
-    methods: {
-      // 解析底图图片地址
-      resolveUrl(path) {
-        return resolveMapImageUrl(path);
-      },
-      // 展示容器尺寸:高度按底图宽高比换算,图片铺满容器,标记点位置才不会偏移
-      getBoxStyle(item) {
-        const style = { width: this.width + 'px' };
-        const size = this.naturalSizes[item.mapImageId];
-        if (size?.width && size?.height) {
-          style.height =
-            Math.round((this.width * size.height) / size.width) + 'px';
-        }
-        return style;
-      },
-      // 标记点位置(相对容器的百分比)
-      getPinStyle(item) {
-        return {
-          left: item.xRate * 100 + '%',
-          top: item.yRate * 100 + '%'
-        };
-      },
-      // 图片加载完成后读取原始尺寸
-      onImageLoad(item, event) {
-        this.$set(this.errors, item.mapImageId, false);
-        this.$set(this.naturalSizes, item.mapImageId, {
-          width: event.target.naturalWidth || 0,
-          height: event.target.naturalHeight || 0
-        });
-      },
-      // 图片加载失败
-      onImageError(item) {
-        this.$set(this.errors, item.mapImageId, true);
-      }
-    }
-  };
-</script>
-
-<style lang="scss" scoped>
-  .map-marker-preview {
-    display: flex;
-    gap: 16px;
-    flex-wrap: wrap;
-  }
-  .map-preview-empty {
-    font-size: 13px;
-    color: #c0c4cc;
-    line-height: 20px;
-  }
-  .map-preview-box {
-    position: relative;
-    overflow: hidden;
-    border: 1px solid #ebeef5;
-    border-radius: 4px;
-    background: #f6f7f9;
-  }
-  .map-preview-image {
-    display: block;
-    width: 100%;
-    height: 100%;
-  }
-  /* 标记点:锚点在标记位置的正下方 */
-  .map-pin {
-    position: absolute;
-    transform: translate(-50%, -100%);
-    color: #f56c6c;
-    font-size: 20px;
-    line-height: 1;
-    pointer-events: none;
-    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
-  }
-  .map-preview-name {
-    margin-top: 6px;
-    font-size: 12px;
-    color: #909399;
-  }
-  .map-preview-error {
-    color: #f56c6c;
-  }
-</style>

+ 93 - 14
src/views/DangerousWorkList/components/workTicketForm.vue

@@ -5,7 +5,8 @@
       scope: 'all'   人员范围为全部人员,与申请单位/作业单位无关;改用人员选择弹框
                     (弹框内可按姓名/工号/账号搜索并分页),点击输入框即打开弹框
       multiple: true 多选,提交时名称与 id 用逗号连接
-  - 部门字段 dept:提交部门名称并附带部门 id
+  - 部门字段 dept:点击输入框打开部门选择弹框(弹框内可按部门名称搜索),提交部门名称并附带部门 id
+  - 作业类型字段 work_type:锁定不可调整,由申请时选择的作业类型自动带出
   - 日期字段 date:el-date-picker 日期时间选择
   - 子表:可新增/删除行
 -->
@@ -48,6 +49,7 @@
                 <el-input
                   v-if="field.type === 'text'"
                   v-model="formData[field.id]"
+                  :disabled="isFieldLocked(field)"
                   :placeholder="field.placeholder || getPlaceholder(field)"
                 />
                 <el-input
@@ -61,6 +63,7 @@
                   v-else-if="field.type === 'select'"
                   v-model="formData[field.id]"
                   :placeholder="field.placeholder || '请选择'"
+                  :disabled="isFieldLocked(field)"
                   clearable
                   style="width: 100%"
                 >
@@ -77,17 +80,25 @@
                   type="datetime"
                   placeholder="请选择时间"
                   value-format="yyyy-MM-dd HH:mm:ss"
+                  :disabled="isFieldLocked(field)"
                   style="width: 100%"
                 />
-                <dept-select
+                <!-- 部门字段:点击输入框打开部门选择弹框 -->
+                <el-input
                   v-else-if="field.type === 'dept'"
-                  v-model="formData[field.id]"
-                  :init="false"
-                  :selectList="deptList"
+                  class="dept-picker"
+                  :value="getDeptDisplay(field)"
+                  readonly
                   :placeholder="field.placeholder || '请选择部门'"
                   style="width: 100%"
-                  @changeGroup="onDeptChange(field, $event)"
-                />
+                  @click.native="onDeptInputClick(field, $event)"
+                >
+                  <i
+                    v-if="formData[field.id]"
+                    class="el-icon-circle-close dept-clear"
+                    @click.stop="clearDept(field)"
+                  ></i>
+                </el-input>
                 <!-- 全量人员:点击输入框打开人员选择弹框(单选/多选同一样式) -->
                 <el-input
                   v-else-if="field.type === 'person' && field.scope === 'all'"
@@ -205,12 +216,15 @@
       :selected-list="dialogSelectedList"
       @changeParent="onUserSelected"
     />
+
+    <!-- 部门选择弹框(部门名称搜索 + 部门树) -->
+    <dept-select-dialog ref="deptSelectRef" @changeParent="onDeptSelected" />
   </div>
 </template>
 
 <script>
   import PersonSelect from '@/components/CommomSelect/person-select.vue';
-  import DeptSelect from '@/components/CommomSelect/dept-select.vue';
+  import DeptSelectDialog from '@/components/CommomSelect/dept-select-dialog.vue';
   import UserSelect from '@/components/user-select/user-select.vue';
   import FileMain from '@/components/addDoc/index.vue';
   import SignCanvas from '@/components/upload/signCanvas.vue';
@@ -218,7 +232,13 @@
   import { getTemplateFields } from '../templates';
 
   export default {
-    components: { PersonSelect, DeptSelect, UserSelect, FileMain, SignCanvas },
+    components: {
+      PersonSelect,
+      DeptSelectDialog,
+      UserSelect,
+      FileMain,
+      SignCanvas
+    },
     props: {
       // 模板 schema
       template: {
@@ -261,7 +281,9 @@
         // 签名文件 id { 字段id: 文件id }
         signFileIds: {},
         // 当前正在选人的字段
-        currentSelectField: null
+        currentSelectField: null,
+        // 当前正在选部门的字段
+        currentDeptField: null
       };
     },
     computed: {
@@ -392,6 +414,11 @@
         }
         return `请输入${field.label}`;
       },
+      // 锁定的字段不允许人工调整:
+      // 作业类型(work_type)由申请时选择的作业类型决定,票面只做展示
+      isFieldLocked(field) {
+        return field.id === 'work_type';
+      },
       // 上传、签名字段占满整行,避免控件被压缩
       isFullField(field) {
         return !!field.full || field.type === 'file' || field.type === 'sign';
@@ -558,10 +585,46 @@
         });
         this.personList = linked;
       },
-      // 部门变更
-      onDeptChange(field, deptId) {
-        const dept = this.deptList.find((item) => item.id === deptId);
-        this.setFieldName(field.id, deptId, dept?.name || '');
+      // 部门字段回显文本
+      getDeptDisplay(field) {
+        const value = this.formData[field.id];
+        if (!value) {
+          return '';
+        }
+        return this.getFieldName(field.id, value) || '';
+      },
+      // 点击部门输入框打开部门选择弹框
+      // element-ui 的 el-input 不会向外派发 click 事件,这里用 .native 监听根元素
+      onDeptInputClick(field) {
+        this.openDeptSelect(field);
+      },
+      // 打开部门选择弹框
+      openDeptSelect(field) {
+        this.currentDeptField = field;
+        const value = this.formData[field.id];
+        const current = value
+          ? { id: value, name: this.getFieldName(field.id, value) || '' }
+          : null;
+        // 等当前字段的已选部门同步到弹框后再打开,保证回显正确
+        this.$nextTick(() => {
+          this.$refs.deptSelectRef && this.$refs.deptSelectRef.open(current);
+        });
+      },
+      // 弹框选择结果
+      onDeptSelected(data) {
+        const field = this.currentDeptField;
+        if (!field || !data) {
+          return;
+        }
+        this.$set(this.formData, field.id, data.id);
+        this.setFieldName(field.id, data.id, data.name || '');
+        // 部门变更后刷新部门联动的人员候选列表
+        this.loadPersonList(this.getPersonDeptId());
+      },
+      // 清空已选部门
+      clearDept(field) {
+        this.$set(this.formData, field.id, '');
+        this.$set(this.fieldNames, field.id, {});
         this.loadPersonList(this.getPersonDeptId());
       },
       // 人员变更(部门联动字段)
@@ -849,6 +912,22 @@
         cursor: pointer;
       }
     }
+    /* 部门选择输入框:点击打开部门选择弹框 */
+    .dept-picker {
+      ::v-deep .el-input__inner {
+        cursor: pointer;
+        /* 给右侧的清除图标留出空间,避免部门名称被遮挡 */
+        padding-right: 30px;
+      }
+    }
+    .dept-clear {
+      cursor: pointer;
+      color: #c0c4cc;
+
+      &:hover {
+        color: #909399;
+      }
+    }
     /* 附件上传、手写签名单独占一行 */
     .field-upload,
     .field-sign {

+ 2 - 0
src/views/DangerousWorkList/templates.js

@@ -20,6 +20,8 @@
  *                   改用人员选择弹框(弹框内可按姓名/工号/账号搜索并分页)
  *                   不设置时人员范围取「作业单位」部门,未选作业单位时取「申请单位」部门
  *     multiple: true  多选,提交时名称与 id 用逗号连接
+ *   字段 id 为 work_type(作业类型)的字段由 workTicketForm 锁定,
+ *   值取自申请时选择的作业类型,不允许人工调整。
  */
 
 import {