yyb
13 小时以前 0a58164ce2ea3f1a2b46781757d78b94b212883b
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
@@ -38,7 +38,7 @@
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openFormDialog('add')">新增调岗申请</el-button>
        <el-button type="primary" @click="openAddWithTemplate">新增调岗申请</el-button>
      </div>
    </div>
    <div class="table_list">
@@ -56,103 +56,49 @@
    <!-- 新增 / 编辑 -->
    <el-dialog
      v-if="formDialog.visible"
      v-model="formDialog.visible"
      :title="formDialog.title"
      width="720px"
      width="960px"
      append-to-body
      destroy-on-close
      class="transfer-apply-form-dialog"
      @closed="onFormClosed"
    >
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="120px" class="transfer-apply-form">
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="transfer-apply-form">
        <el-form-item v-if="form.templateSnapshot" label="审批模板">
          <span class="template-name">{{ form.templateSnapshot.label || form.templateName }}</span>
          <el-button
            v-if="formDialog.mode === 'add'"
            type="primary"
            link
            class="ml12"
            @click="reopenTemplateBind"
          >
            更换模板
          </el-button>
        </el-form-item>
        <FormPayloadFields :fields="templateDisplayFields" :form-payload="form.formPayload" />
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="申请人" prop="applicantId">
              <el-select
                v-model="form.applicantId"
                filterable
                remote
                clearable
                reserve-keyword
                placeholder="请选择或搜索申请人"
                style="width: 100%"
                :remote-method="remoteSearchApplicantForm"
                :loading="applicantFormSearchLoading"
                @change="onApplicantChange"
              >
                <el-option
                  v-for="u in applicantFormOptions"
                  :key="u.userId"
                  :label="userSelectLabel(u)"
                  :value="u.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="转岗日期" prop="transferDate">
              <el-date-picker
                v-model="form.transferDate"
                type="date"
                placeholder="请选择转岗日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="原岗位" prop="originalPostName">
              <el-input v-model="form.originalPostName" placeholder="选择申请人后自动带出" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="转入岗位" prop="targetPostId">
              <el-select v-model="form.targetPostId" placeholder="请选择转入岗位" clearable filterable style="width: 100%">
                <el-option
                  v-for="p in targetPostOptions"
                  :key="p.postId"
                  :label="p.postName"
                  :value="p.postId"
                  :disabled="p.status === '1' || p.status === 1"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="审批方式" prop="approvalMode">
              <el-radio-group v-model="form.approvalMode">
                <el-radio value="parallel">与签</el-radio>
                <el-radio value="countersign">会签</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="审批人" prop="approverIds">
              <el-tree-select
                v-model="form.approverIds"
                :data="approverTreeData"
                multiple
                collapse-tags
                collapse-tags-tooltip
                :max-collapse-tags="2"
                :render-after-expand="false"
                placeholder="请选择审批人(可多选)"
                style="width: 100%"
                :props="{ value: 'id', label: 'label', children: 'children', disabled: 'disabled' }"
                check-strictly
              />
            </el-form-item>
          </el-col>
        </el-row>
        <ApprovalTemplateFormSection
            :active-template="form.templateSnapshot"
            :fields="form.formFieldDefs"
            :form-payload="form.formPayload"
            v-model:flow-nodes="form.flowNodes"
            v-model:attachments="form.storageBlobDTOs"
            :template-attachments="form.templateAttachments"
            :user-options="flowUserOptions"
            flow-attachments-only
            hide-template-name
            :allow-change-template="false"
          />
      </el-form>
      <template #footer>
        <div class="dialog-footer">
@@ -161,6 +107,14 @@
        </div>
      </template>
    </el-dialog>
    <ApprovalTemplateBindDialog
      v-model:visible="templateBindVisible"
      :module-key="APPROVAL_MODULE_KEYS.TRANSFER"
      skip-form-confirm
      @confirm="onTemplateBound"
      @closed="onTemplateBindClosed"
    />
    <!-- 详情 -->
    <el-dialog v-model="detailDialog.visible" title="调岗申请详情" width="560px" append-to-body>
@@ -184,8 +138,19 @@
<script setup>
import { findPostOptions } from "@/api/system/post.js";
import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
import ApprovalTemplateFormSection from "../../ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue";
import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import {
  applyBindingToForm,
  buildFormPayloadRules,
  validateTemplateBinding,
} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js";
const { proxy } = getCurrentInstance();
@@ -199,10 +164,50 @@
  originalPostName: "",
  targetPostId: "",
  targetPostName: "",
  approvalMode: "parallel",
  approverIds: [],
  approverNames: "",
  hasTemplateBinding: false,
  templateId: "",
  templateName: "",
  templateSnapshot: null,
  formFieldDefs: [],
  formPayload: {},
  flowNodes: [],
  templateAttachments: [],
  storageBlobDTOs: [],
});
function isOriginalPostField(field) {
  const label = String(field?.label || "");
  return (
    label.includes("原岗位") ||
    field?.key === "originalPost" ||
    field?.key === "originalPostName" ||
    field?.key === "originalPostId"
  );
}
function findApplicantTemplateField(fields = []) {
  return (
    fields.find((f) => String(f?.label || "").includes("申请人")) ||
    fields.find((f) => f?.type === "select" && f?.optionSource === SELECT_OPTION_SOURCE.USER) ||
    null
  );
}
function syncApplicantFromUser(uid) {
  const u = userById(uid);
  if (u) {
    form.applicantId = uid != null && uid !== "" ? uid : "";
    form.applicantName = u.nickName || u.userName || "";
    const { originalPostId, originalPostName } = resolveOriginalPost(u);
    form.originalPostId = originalPostId;
    form.originalPostName = originalPostName;
  } else {
    form.applicantId = "";
    form.applicantName = "";
    form.originalPostId = "";
    form.originalPostName = "";
  }
}
/** 系统用户缓存(/system/user/userListNoPageByTenantId,与转正申请等一致) */
const allUsersCache = ref([]);
@@ -322,39 +327,6 @@
  }
}
/** 表单内申请人下拉 */
const applicantFormSearchLoading = ref(false);
const applicantFormOptions = ref([]);
async function remoteSearchApplicantForm(query) {
  applicantFormSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    applicantFormOptions.value = filterUsersByQuery(query);
  } finally {
    applicantFormSearchLoading.value = false;
  }
}
function onApplicantChange(uid) {
  const u = userById(uid);
  if (u) {
    form.applicantName = u.nickName || u.userName || "";
    const { originalPostId, originalPostName } = resolveOriginalPost(u);
    form.originalPostId = originalPostId;
    form.originalPostName = originalPostName;
  } else {
    form.applicantName = "";
    form.originalPostId = "";
    form.originalPostName = "";
  }
}
/** 审批人树 */
const approverTreeData = ref([]);
const approverLabelMap = ref({});
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
@@ -363,146 +335,10 @@
  return [];
}
function filterDisabledDept(deptList) {
  if (!Array.isArray(deptList)) return [];
  return deptList.filter((dept) => {
    if (dept.disabled) return false;
    if (dept.children?.length) {
      dept.children = filterDisabledDept(dept.children);
    }
    return true;
  });
}
function getUserDeptId(u) {
  return u.deptId ?? u.sysDeptId ?? u.dept?.deptId ?? u.dept?.id ?? u.dept_id;
}
function getDeptNodeKey(node) {
  const k = node?.id ?? node?.value ?? node?.deptId;
  if (k == null || k === "") return null;
  return k;
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function userToTreeLeaf(u) {
  return {
    id: String(u.userId ?? u.id),
    label: u.nickName || u.userName || `用户${u.userId ?? u.id}`,
  };
}
function buildUsersByDeptId(users) {
  const map = new Map();
  const unassigned = [];
  for (const u of users) {
    if (!isActiveUser(u)) continue;
    const did = getUserDeptId(u);
    if (did == null || did === "" || did === 0 || did === "0") {
      unassigned.push(u);
      continue;
    }
    const k = String(did);
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(u);
  }
  return { map, unassigned };
}
function collectUserLabels(nodes, map) {
  (nodes || []).forEach((n) => {
    if (n.children?.length) {
      collectUserLabels(n.children, map);
    } else if (n.id != null && !String(n.id).startsWith("dept_")) {
      map[String(n.id)] = n.label;
    }
  });
}
function mergeDeptTreeWithUsers(nodes, usersByDept) {
  if (!Array.isArray(nodes)) return [];
  const out = [];
  for (const node of nodes) {
    const deptIdRaw = getDeptNodeKey(node);
    if (deptIdRaw == null) continue;
    const sub = mergeDeptTreeWithUsers(node.children || [], usersByDept);
    const usersHere = usersByDept.get(String(deptIdRaw)) || [];
    const userChildren = usersHere.map(userToTreeLeaf);
    const children = [...sub, ...userChildren];
    if (!children.length) continue;
    out.push({
      id: `dept_${deptIdRaw}`,
      label: node.label ?? node.deptName ?? "部门",
      disabled: true,
      children,
    });
  }
  return out;
}
function buildFlatApproverTree(users) {
  const list = users.filter(isActiveUser).map(userToTreeLeaf);
  if (!list.length) return [];
  return [
    {
      id: "dept_all_users",
      label: "系统用户",
      disabled: true,
      children: list,
    },
  ];
}
async function loadApproverTree() {
  try {
    const needFetchUsers = !allUsersCache.value.length;
    const [deptRes, userRes] = await Promise.all([
      deptTreeSelect(),
      needFetchUsers ? userListNoPageByTenantId() : Promise.resolve(null),
    ]);
    let rawTree = unwrapArray(deptRes);
    rawTree = rawTree.length ? JSON.parse(JSON.stringify(rawTree)) : [];
    let deptTree = filterDisabledDept(JSON.parse(JSON.stringify(rawTree)));
    if (!deptTree.length && rawTree.length) {
      deptTree = JSON.parse(JSON.stringify(rawTree));
    }
    let users = needFetchUsers ? unwrapArray(userRes) : [...allUsersCache.value];
    if (needFetchUsers && users.length) {
      allUsersCache.value = users;
    }
    const { map: usersByDept, unassigned } = buildUsersByDeptId(users);
    let merged = mergeDeptTreeWithUsers(deptTree, usersByDept);
    if (unassigned.length) {
      merged.push({
        id: "dept_unassigned",
        label: "未分配部门",
        disabled: true,
        children: unassigned.map(userToTreeLeaf),
      });
    }
    if (!merged.length && users.length) {
      merged = buildFlatApproverTree(users);
    }
    approverTreeData.value = merged;
    const map = {};
    collectUserLabels(merged, map);
    approverLabelMap.value = map;
  } catch {
    approverTreeData.value = [];
    approverLabelMap.value = {};
    proxy?.$modal?.msgWarning?.("审批人数据加载失败,请检查网络或稍后重试");
  }
}
function resolveApproverNames(ids) {
  if (!ids?.length) return "";
  const map = approverLabelMap.value;
  return ids.map((id) => map[String(id)] || id).join("、");
}
function approvalModeLabel(mode) {
@@ -600,15 +436,33 @@
});
const formRef = ref();
const form = reactive(createEmptyForm());
const templateBindVisible = ref(false);
const pendingTemplateBinding = ref(null);
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
const formRules = {
  applicantId: [{ required: true, message: "请选择申请人", trigger: "change" }],
  transferDate: [{ required: true, message: "请选择转岗日期", trigger: "change" }],
  originalPostName: [{ required: true, message: "原岗位不能为空", trigger: "change" }],
  targetPostId: [{ required: true, message: "请选择转入岗位", trigger: "change" }],
  approvalMode: [{ required: true, message: "请选择审批方式", trigger: "change" }],
  approverIds: [{ type: "array", required: true, message: "请选择审批人", trigger: "change" }],
};
const templateDisplayFields = computed(() =>
  (form.formFieldDefs || []).filter((f) => !isOriginalPostField(f))
);
const applicantTemplateField = computed(() => findApplicantTemplateField(form.formFieldDefs));
const formRules = computed(() => ({
  ...buildFormPayloadRules(templateDisplayFields.value),
  originalPostName: [{ required: true, message: "请选择申请人以带出原岗位", trigger: "change" }],
}));
watch(
  () => {
    const key = applicantTemplateField.value?.key;
    return key ? form.formPayload[key] : undefined;
  },
  async (uid) => {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    syncApplicantFromUser(uid);
  }
);
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
@@ -638,29 +492,53 @@
  detailDialog.visible = true;
}
function ensureApplicantInFormOptions(row) {
  if (!row?.applicantId) return;
  const id = String(row.applicantId);
  if (!applicantFormOptions.value.some((u) => String(u.userId ?? u.id) === id)) {
    applicantFormOptions.value = [
      {
        userId: row.applicantId,
        nickName: row.applicantName,
        userName: row.applicantUserName,
      },
      ...applicantFormOptions.value,
    ];
function openAddWithTemplate() {
  formDialog.visible = false;
  pendingTemplateBinding.value = null;
  templateBindVisible.value = true;
}
function onTemplateBound(binding) {
  pendingTemplateBinding.value = binding;
}
async function onTemplateBindClosed() {
  const binding = pendingTemplateBinding.value;
  if (!binding) return;
  pendingTemplateBinding.value = null;
  await openFormWithBinding(binding);
}
async function openFormWithBinding(binding) {
  Object.assign(form, createEmptyForm());
  applyBindingToForm(form, binding);
  form.hasTemplateBinding = true;
  formDialog.mode = "add";
  formDialog.title = "新增调岗申请";
  await Promise.all([loadUserPool(), loadPostOptions(), loadFlowUsers()]);
  const applicantKey = applicantTemplateField.value?.key;
  if (applicantKey && form.formPayload[applicantKey]) {
    syncApplicantFromUser(form.formPayload[applicantKey]);
  }
  formDialog.visible = true;
  nextTick(() => formRef.value?.clearValidate?.());
}
function reopenTemplateBind() {
  formDialog.visible = false;
  pendingTemplateBinding.value = null;
  templateBindVisible.value = true;
}
async function openFormDialog(mode, row) {
  if (mode === "edit" && row && !row.hasTemplateBinding) {
    proxy?.$modal?.msgWarning?.("该记录为旧版数据,请重新通过模板发起申请");
    return;
  }
  formDialog.mode = mode;
  formDialog.title = mode === "add" ? "新增调岗申请" : "编辑调岗申请";
  loadApproverTree();
  formDialog.title = "编辑调岗申请";
  Object.assign(form, createEmptyForm());
  await remoteSearchApplicantForm("");
  if (mode === "edit" && row) {
    ensureApplicantInFormOptions(row);
    Object.assign(form, {
      id: row.id,
      applicantId: row.applicantId,
@@ -670,10 +548,22 @@
      originalPostName: row.originalPostName,
      targetPostId: row.targetPostId,
      targetPostName: row.targetPostName,
      approvalMode: row.approvalMode,
      approverIds: (row.approverIds || []).map((id) => String(id)),
      approverNames: row.approverNames,
      hasTemplateBinding: true,
      templateId: row.templateId,
      templateName: row.templateName,
      templateSnapshot: row.templateSnapshot,
      formFieldDefs: row.formFieldDefs || [],
      formPayload: JSON.parse(JSON.stringify(row.formPayload || {})),
      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [])),
      templateAttachments: JSON.parse(JSON.stringify(row.templateAttachments || [])),
      storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
    });
    await loadUserPool();
    const applicantKey = applicantTemplateField.value?.key;
    if (applicantKey) {
      syncApplicantFromUser(form.formPayload[applicantKey]);
    }
    loadFlowUsers();
  }
  formDialog.visible = true;
  nextTick(() => formRef.value?.clearValidate?.());
@@ -689,7 +579,17 @@
  } catch {
    return;
  }
  form.approverNames = resolveApproverNames(form.approverIds);
  const flowCheck = validateTemplateBinding({ flowNodes: form.flowNodes });
  if (!flowCheck.ok) {
    proxy?.$modal?.msgWarning?.(flowCheck.message || "请完善审批流程");
    return;
  }
  form.flowNodes = flowCheck.nodes;
  const applicantKey = applicantTemplateField.value?.key;
  if (applicantKey) {
    syncApplicantFromUser(form.formPayload[applicantKey]);
  }
  syncTransferFieldsFromPayload();
  form.targetPostName = targetPostNameById(form.targetPostId);
  const payload = {
    applicantId: form.applicantId,
@@ -699,9 +599,15 @@
    originalPostName: form.originalPostName,
    targetPostId: form.targetPostId,
    targetPostName: form.targetPostName,
    approvalMode: form.approvalMode,
    approverIds: [...form.approverIds],
    approverNames: form.approverNames,
    hasTemplateBinding: true,
    templateId: form.templateId,
    templateName: form.templateName,
    templateSnapshot: form.templateSnapshot,
    formFieldDefs: form.formFieldDefs,
    formPayload: JSON.parse(JSON.stringify(form.formPayload || {})),
    flowNodes: JSON.parse(JSON.stringify(form.flowNodes || [])),
    templateAttachments: JSON.parse(JSON.stringify(form.templateAttachments || [])),
    storageBlobDTOs: JSON.parse(JSON.stringify(form.storageBlobDTOs || [])),
  };
  if (formDialog.mode === "add") {
    const id = `local_${Date.now()}`;
@@ -727,10 +633,27 @@
  handleQuery();
}
/** 从模板填报项同步转岗日期、转入岗位到列表字段 */
function syncTransferFieldsFromPayload() {
  const defs = form.formFieldDefs || [];
  const payload = form.formPayload || {};
  for (const f of defs) {
    const label = String(f.label || "");
    const val = payload[f.key];
    if (label.includes("转岗") && (label.includes("日期") || label.includes("时间")) && f.type === "date") {
      form.transferDate = val || "";
    }
    if (label.includes("转入岗位") && f.type === "select") {
      form.targetPostId = val != null && val !== "" ? val : "";
      form.targetPostName = targetPostNameById(form.targetPostId);
    }
  }
}
onMounted(async () => {
  await Promise.all([loadUserPool(), loadPostOptions()]);
  rebuildPostIdMap();
  loadApproverTree();
  loadFlowUsers();
  await remoteSearchApplicant("");
});
</script>
@@ -759,4 +682,11 @@
.transfer-apply-form-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
.template-name {
  font-weight: 600;
  color: var(--el-text-color-primary);
}
.ml12 {
  margin-left: 12px;
}
</style>