Przeglądaj źródła

feat(oa): allow picking initiator-select users inside flow preview

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 miesięcy temu
rodzic
commit
eafd3c7d9e
1 zmienionych plików z 231 dodań i 3 usunięć
  1. 231 3
      web-admin/src/components/oa-approval/OaFlowPreview.vue

+ 231 - 3
web-admin/src/components/oa-approval/OaFlowPreview.vue

@@ -28,7 +28,33 @@
             <span v-if="s.resolveError" class="oa-flow-card__err" :title="s.resolveError">
               {{ s.resolveError }}
             </span>
-            <span v-if="(s.people || []).length" class="oa-flow-card__people">
+            <template v-if="isInitiatorSelect(s)">
+              <span class="oa-flow-card__select">
+                <span class="oa-flow-card__people">
+                  <template v-if="localPeople(s).length">
+                    <span v-for="p in localPeople(s)" :key="p.id" class="oa-person-chip">
+                      <span class="oa-person-chip__dot">{{ avatarChar(p.nickName) }}</span>
+                      {{ p.nickName }}
+                      <button
+                        type="button"
+                        class="oa-person-chip__x"
+                        title="移除"
+                        @click="removeUser(s.id, p.id)"
+                      >
+                        ×
+                      </button>
+                    </span>
+                  </template>
+                  <span v-else class="oa-flow-card__placeholder">
+                    {{ s.type === 'cc' ? '请选择抄送人' : '请选择审批人' }}
+                  </span>
+                </span>
+                <button type="button" class="oa-flow-add-btn" title="添加" @click="openPicker(s)">
+                  <el-icon><Plus /></el-icon>
+                </button>
+              </span>
+            </template>
+            <span v-else-if="(s.people || []).length" class="oa-flow-card__people">
               <span v-for="p in s.people" :key="p.id" class="oa-person-chip">
                 <span class="oa-person-chip__dot">{{ avatarChar(p.nickName) }}</span>
                 {{ p.nickName || p.id }}
@@ -38,15 +64,60 @@
         </div>
       </div>
     </div>
+
+    <el-dialog
+      v-model="pickerVisible"
+      :title="pickerTitle"
+      width="520px"
+      align-center
+      append-to-body
+      destroy-on-close
+      class="dialog-form-layout"
+      modal-class="protocol-dialog-mask"
+      @closed="onPickerClosed"
+    >
+      <el-select
+        v-model="pickerIds"
+        multiple
+        filterable
+        clearable
+        collapse-tags
+        collapse-tags-tooltip
+        placeholder="请选择人员"
+        style="width: 100%"
+      >
+        <el-option
+          v-for="u in userOptions"
+          :key="u.id"
+          :label="userOptionLabel(u)"
+          :value="u.id"
+        />
+      </el-select>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="pickerVisible = false">
+            <el-icon><Close /></el-icon>
+            <span>取消</span>
+          </el-button>
+          <el-button type="primary" @click="confirmPicker">
+            <el-icon><Check /></el-icon>
+            <span>确定</span>
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { computed } from 'vue'
+  import { computed, ref } from 'vue'
+  import { Check, Close, Plus } from '@element-plus/icons-vue'
   import type { OaFlowPreviewStep } from '@/api/oaApproval'
 
   defineOptions({ name: 'OaFlowPreview' })
 
+  type UserOption = { id: string; nickName: string; userNo?: string }
+
   const props = withDefaults(
     defineProps<{
       steps?: OaFlowPreviewStep[]
@@ -54,18 +125,100 @@
       hint?: string
       resolved?: boolean
       pendingMessage?: string
+      selectByNode?: Record<string, string[]>
+      userOptions?: UserOption[]
     }>(),
     {
       steps: () => [],
       loading: false,
       hint: '按当前发起人解析审批人',
       resolved: true,
-      pendingMessage: '必填信息填写后,流程将自动显示'
+      pendingMessage: '必填信息填写后,流程将自动显示',
+      selectByNode: () => ({}),
+      userOptions: () => []
     }
   )
 
+  const emit = defineEmits<{
+    'update:selectByNode': [Record<string, string[]>]
+    'select-change': []
+  }>()
+
   const steps = computed(() => props.steps || [])
   const resolved = computed(() => props.resolved !== false)
+  const userOptions = computed(() => props.userOptions || [])
+
+  const pickerVisible = ref(false)
+  const pickerNodeId = ref('')
+  const pickerNodeType = ref('')
+  const pickerIds = ref<string[]>([])
+
+  const pickerTitle = computed(() =>
+    pickerNodeType.value === 'cc' ? '选择抄送人' : '选择审批人'
+  )
+
+  function isInitiatorSelect(s: OaFlowPreviewStep) {
+    return (
+      s.assigneeType === 'initiator_select' && (s.type === 'approval' || s.type === 'cc')
+    )
+  }
+
+  function localPeople(s: OaFlowPreviewStep) {
+    const ids = (props.selectByNode || {})[s.id] || []
+    const byId = new Map(userOptions.value.map((u) => [u.id, u]))
+    return ids.map((id) => {
+      const u = byId.get(id)
+      return { id, nickName: u?.nickName || id }
+    })
+  }
+
+  function userOptionLabel(u: UserOption) {
+    const name = u.nickName || u.id
+    return u.userNo ? `${name}(${u.userNo})` : name
+  }
+
+  function updateSelect(nodeId: string, ids: string[]) {
+    const next: Record<string, string[]> = { ...(props.selectByNode || {}) }
+    const uniq: string[] = []
+    const seen = new Set<string>()
+    for (const id of ids) {
+      if (!id || seen.has(id)) continue
+      seen.add(id)
+      uniq.push(id)
+    }
+    if (uniq.length) next[nodeId] = uniq
+    else delete next[nodeId]
+    emit('update:selectByNode', next)
+    emit('select-change')
+  }
+
+  function removeUser(nodeId: string, userId: string) {
+    const cur = (props.selectByNode || {})[nodeId] || []
+    updateSelect(
+      nodeId,
+      cur.filter((id) => id !== userId)
+    )
+  }
+
+  function openPicker(s: OaFlowPreviewStep) {
+    pickerNodeId.value = s.id
+    pickerNodeType.value = s.type || ''
+    pickerIds.value = [...((props.selectByNode || {})[s.id] || [])]
+    pickerVisible.value = true
+  }
+
+  function confirmPicker() {
+    if (pickerNodeId.value) {
+      updateSelect(pickerNodeId.value, [...pickerIds.value])
+    }
+    pickerVisible.value = false
+  }
+
+  function onPickerClosed() {
+    pickerNodeId.value = ''
+    pickerNodeType.value = ''
+    pickerIds.value = []
+  }
 
   function signBadge(s: OaFlowPreviewStep) {
     if (s.type === 'cc' || s.signMode === 'cc') return 'cc'
@@ -222,6 +375,17 @@
     white-space: nowrap;
   }
 
+  .oa-flow-card__select {
+    display: inline-flex;
+    flex: 1 1 140px;
+    flex-wrap: wrap;
+    gap: 6px;
+    align-items: center;
+    justify-content: flex-end;
+    min-width: 0;
+    margin-left: auto;
+  }
+
   .oa-flow-card__people {
     display: inline-flex;
     flex: 1 1 140px;
@@ -233,6 +397,45 @@
     margin-left: auto;
   }
 
+  .oa-flow-card__select .oa-flow-card__people {
+    flex: 0 1 auto;
+    margin-left: 0;
+  }
+
+  .oa-flow-card__placeholder {
+    color: var(--mint-text-muted, #94a3b8);
+    font-size: 12px;
+    line-height: 1.2;
+  }
+
+  .oa-flow-add-btn {
+    display: inline-flex;
+    flex-shrink: 0;
+    align-items: center;
+    justify-content: center;
+    width: 28px;
+    height: 28px;
+    margin-left: 0;
+    padding: 0;
+    color: var(--mint-text, #334155);
+    font-size: 14px;
+    line-height: 1;
+    cursor: pointer;
+    background: #fff;
+    border: 1px solid #e2e8f0;
+    border-radius: 6px;
+
+    &:hover {
+      color: var(--primary, #10b981);
+      border-color: #a7f3d0;
+      background: var(--primary-bg, #ecfdf5);
+    }
+  }
+
+  .oa-flow-card__select .oa-flow-add-btn {
+    margin-left: 0;
+  }
+
   .oa-person-chip {
     display: inline-flex;
     gap: 3px;
@@ -258,6 +461,21 @@
     border-radius: 50%;
   }
 
+  .oa-person-chip__x {
+    margin: 0;
+    padding: 0 2px;
+    color: var(--mint-text-muted, #94a3b8);
+    font-size: 12px;
+    line-height: 1;
+    cursor: pointer;
+    background: transparent;
+    border: none;
+
+    &:hover {
+      color: #dc2626;
+    }
+  }
+
   .oa-flow-badge {
     flex-shrink: 0;
     padding: 0 5px;
@@ -282,4 +500,14 @@
       background: #ecfeff;
     }
   }
+
+  .dialog-footer {
+    display: flex;
+    gap: 8px;
+    justify-content: flex-end;
+
+    .el-button .el-icon {
+      margin-right: 6px;
+    }
+  }
 </style>