yyb
12 小时以前 352f7bbb74f1b6c57b3d3e576849d0565932fbd4
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
@@ -34,7 +34,7 @@
          style="width: 260px"
          clearable
        />
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">搜索</el-button>
        <el-button type="primary" style="margin-left: 10px" @click="onSearch">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
@@ -49,64 +49,32 @@
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        @pagination="pagination"
        @pagination="onPagination"
        :total="page.total"
      />
    </div>
    <!-- 新增 / 编辑 -->
    <el-dialog
      v-if="formDialog.visible"
      v-model="formDialog.visible"
      :title="formDialog.title"
      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="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="12">
            <el-form-item label="原岗位" prop="originalPostName">
              <el-input v-model="form.originalPostName" placeholder="选择申请人后自动带出" disabled />
            </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"
    <ApprovalInstanceSubmitDialog
      v-model="submitDialog.visible"
      :title="submitDialogTitle"
      :form="submitForm"
      :rules="submitFormRules"
      :fields="submitFormFields"
      :active-template="activeTemplate"
            :user-options="flowUserOptions"
      :is-edit="isSubmitEdit"
      :saving="submitSaving"
      :form-ref="submitFormRef"
            flow-attachments-only
            hide-template-name
            :allow-change-template="false"
          />
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="formDialog.visible = false">取 消</el-button>
        </div>
      @submit="onSubmit"
    >
      <template #before="{ form, fields }">
        <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" />
        <el-form-item label="原岗位">
          <el-input :model-value="originalPostName" placeholder="选择申请人后自动带出" disabled />
        </el-form-item>
      </template>
    </el-dialog>
    </ApprovalInstanceSubmitDialog>
    <ApprovalTemplateBindDialog
      v-model:visible="templateBindVisible"
@@ -116,64 +84,29 @@
      @closed="onTemplateBindClosed"
    />
    <!-- 详情 -->
    <el-dialog v-model="detailDialog.visible" title="调岗申请详情" width="560px" append-to-body>
      <el-descriptions :column="1" border>
        <el-descriptions-item label="申请人">{{ detailRow.applicantName }}</el-descriptions-item>
        <el-descriptions-item label="转岗日期">{{ detailRow.transferDate }}</el-descriptions-item>
        <el-descriptions-item label="原岗位">{{ detailRow.originalPostName }}</el-descriptions-item>
        <el-descriptions-item label="转入岗位">{{ detailRow.targetPostName }}</el-descriptions-item>
        <el-descriptions-item label="审批结果">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
        <el-descriptions-item label="审批方式">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
        <el-descriptions-item label="审批人">{{ detailRow.approverNames || "—" }}</el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="detailDialog.visible = false">关 闭</el-button>
        </div>
      </template>
    </el-dialog>
    <ApprovalInstanceDetailDialog
      v-model="detailDialog.visible"
      title="调岗申请详情"
      :row="detailRow"
      @edit="openEditFromDetail"
    />
  </div>
</template>
<script setup>
import { findPostOptions } from "@/api/system/post.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 { ElMessage } from "element-plus";
import { computed, onMounted, reactive, ref, watch } from "vue";
import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue";
import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import {
  applyBindingToForm,
  buildFormPayloadRules,
  validateTemplateBinding,
} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js";
const { proxy } = getCurrentInstance();
/** 与后端约定字段(本地占位,后期接口对齐) */
const createEmptyForm = () => ({
  id: undefined,
  applicantId: "",
  applicantName: "",
  transferDate: "",
  originalPostId: "",
  originalPostName: "",
  targetPostId: "",
  targetPostName: "",
  hasTemplateBinding: false,
  templateId: "",
  templateName: "",
  templateSnapshot: null,
  formFieldDefs: [],
  formPayload: {},
  flowNodes: [],
  templateAttachments: [],
  storageBlobDTOs: [],
});
function isOriginalPostField(field) {
  const label = String(field?.label || "");
@@ -185,6 +118,10 @@
  );
}
function displayTemplateFields(fields = []) {
  return (fields || []).filter((f) => !isOriginalPostField(f));
}
function findApplicantTemplateField(fields = []) {
  return (
    fields.find((f) => String(f?.label || "").includes("申请人")) ||
@@ -193,45 +130,73 @@
  );
}
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 = "";
  }
}
const searchForm = reactive({
  applicantId: "",
  transferDateRange: null,
});
/** 系统用户缓存(/system/user/userListNoPageByTenantId,与转正申请等一致) */
const mod = useApprovalInstanceModule({
  moduleKey: APPROVAL_MODULE_KEYS.TRANSFER,
  buildExtraListParams(sf) {
    const range = sf?.transferDateRange;
    if (Array.isArray(range) && range[0]) {
      return { createTime: range[0], createTimeEnd: range[1] };
    }
    return {};
  },
});
const {
  tableData,
  tableLoading,
  page,
  detailDialog,
  detailRow,
  submitDialog,
  submitForm,
  submitFormRef,
  submitSaving,
  isSubmitEdit,
  activeTemplate,
  submitFormFields,
  submitFormRules,
  submitDialogTitle,
  templateBindVisible,
  handleQuery,
  initModuleList,
  pagination,
  openAddWithTemplate,
  onTemplateBound,
  onTemplateBindClosed,
  openEditFromDetail,
  submitInstanceForm,
  buildTableActions,
} = mod;
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
const allUsersCache = ref([]);
/** 岗位字典 postId -> postName(/system/post/optionselect,与员工档案入职表单一致) */
const postIdToName = ref({});
const targetPostOptions = ref([]);
const applicantSearchLoading = ref(false);
const applicantSearchOptions = ref([]);
const originalPostName = ref("");
function rebuildPostIdMap() {
  const m = {};
  for (const p of targetPostOptions.value || []) {
    const id = p.postId ?? p.value ?? p.id;
    if (id != null && id !== "") m[String(id)] = p.postName ?? p.label ?? "";
  }
  postIdToName.value = m;
}
function targetPostNameById(postId) {
  if (postId == null || postId === "") return "";
  const k = String(postId);
  return (
    postIdToName.value[k] ||
    targetPostOptions.value.find((x) => String(x.postId ?? x.id ?? x.value) === k)?.postName ||
    ""
const applicantTemplateField = computed(() =>
  findApplicantTemplateField(submitForm.formFieldDefs)
  );
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload && Array.isArray(payload.data)) return payload.data;
  if (payload && Array.isArray(payload.rows)) return payload.rows;
  return [];
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function userSelectLabel(u) {
@@ -248,30 +213,19 @@
  return undefined;
}
/** 从用户对象解析「原岗位」(兼容 postName / postIds / posts 等常见返回) */
function resolveOriginalPost(user) {
  if (!user) return { originalPostId: "", originalPostName: "" };
  if (!user) return { originalPostName: "" };
  const nameStr = (user.postName ?? user.postname ?? "").toString().trim();
  if (nameStr) {
    const pid = firstPostId(user);
    return { originalPostId: pid != null && pid !== "" ? String(pid) : "", originalPostName: nameStr };
  }
  if (nameStr) return { originalPostName: nameStr };
  if (Array.isArray(user.posts) && user.posts.length) {
    const p0 = user.posts[0];
    return {
      originalPostId: p0.postId != null ? String(p0.postId) : "",
      originalPostName: (p0.postName ?? "").toString() || "未命名岗位",
    };
    return { originalPostName: (user.posts[0].postName ?? "").toString() || "未命名岗位" };
  }
  const pid = firstPostId(user);
  if (pid != null && pid !== "") {
    const n = postIdToName.value[String(pid)] || "";
    return {
      originalPostId: String(pid),
      originalPostName: n || "当前岗位(未在岗位字典中)",
    };
    return { originalPostName: n || "当前岗位(未在岗位字典中)" };
  }
  return { originalPostId: "", originalPostName: "未分配岗位" };
  return { originalPostName: "未分配岗位" };
}
function userById(id) {
@@ -308,353 +262,79 @@
  } catch {
    targetPostOptions.value = [];
  }
  rebuildPostIdMap();
  const m = {};
  for (const p of targetPostOptions.value) {
    const id = p.postId ?? p.value ?? p.id;
    if (id != null && id !== "") m[String(id)] = p.postName ?? p.label ?? "";
}
/** 查询区:下拉远程模糊(数据来自 userListNoPageByTenantId,前端过滤) */
const applicantSearchLoading = ref(false);
const applicantSearchOptions = ref([]);
  postIdToName.value = m;
}
async function remoteSearchApplicant(query) {
  applicantSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    if (!allUsersCache.value.length) await loadUserPool();
    applicantSearchOptions.value = filterUsersByQuery(query);
  } finally {
    applicantSearchLoading.value = false;
  }
}
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload && Array.isArray(payload.data)) return payload.data;
  if (payload && Array.isArray(payload.rows)) return payload.rows;
  return [];
function syncOriginalPostFromApplicant(uid) {
  const u = userById(uid);
  originalPostName.value = resolveOriginalPost(u).originalPostName;
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function approvalModeLabel(mode) {
  if (mode === "countersign") return "会签";
  return "与签";
}
function approvalResultLabel(v) {
  if (v === "approved") return "已通过";
  if (v === "rejected") return "已驳回";
  if (v === "cancelled") return "已撤销";
  return "待审批";
}
const allRows = ref([]);
const searchForm = reactive({
  applicantId: "",
  transferDateRange: null,
});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const filteredList = computed(() => {
  let list = [...allRows.value];
  if (searchForm.applicantId) {
    list = list.filter((r) => String(r.applicantId) === String(searchForm.applicantId));
  }
  const range = searchForm.transferDateRange;
  if (range && range.length === 2) {
    const [start, end] = range;
    list = list.filter((r) => r.transferDate >= start && r.transferDate <= end);
  }
  return list.sort((a, b) => (a.transferDate < b.transferDate ? 1 : -1));
});
watch(
  filteredList,
  (list) => {
    page.total = list.length;
    const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
    if (page.current > maxPage) {
      page.current = maxPage;
    }
  },
  { immediate: true }
);
const tableData = computed(() => {
  const list = filteredList.value;
  const start = (page.current - 1) * page.size;
  return list.slice(start, start + page.size);
});
const tableColumn = ref([
  { label: "申请人", prop: "applicantName", minWidth: 100 },
  { label: "转岗日期", prop: "transferDate", width: 120 },
  { label: "原岗位", prop: "originalPostName", minWidth: 140 },
  { label: "转入岗位", prop: "targetPostName", minWidth: 160 },
  {
    label: "审批结果",
    prop: "approvalResult",
    width: 110,
    dataType: "tag",
    formatData: (v) => approvalResultLabel(v),
    formatType: (v) => {
      if (v === "approved") return "success";
      if (v === "rejected") return "danger";
      if (v === "cancelled") return "info";
      return "warning";
    },
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 180,
    operation: [
      { name: "编辑", type: "text", clickFun: (row) => openFormDialog("edit", row) },
      { name: "查看详情", type: "text", clickFun: (row) => openDetail(row) },
    ],
  },
]);
const formDialog = reactive({
  visible: false,
  title: "",
  mode: "add",
});
const formRef = ref();
const form = reactive(createEmptyForm());
const templateBindVisible = ref(false);
const pendingTemplateBinding = ref(null);
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
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;
    return key ? submitForm.formPayload[key] : undefined;
  },
  async (uid) => {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    syncApplicantFromUser(uid);
    if (!applicantTemplateField.value) return;
    if (!allUsersCache.value.length) await loadUserPool();
    syncOriginalPostFromApplicant(uid);
  }
);
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
watch(
  () => submitDialog.visible,
  async (v) => {
    if (!v) return;
    const key = applicantTemplateField.value?.key;
    if (key && submitForm.formPayload[key]) {
      syncOriginalPostFromApplicant(submitForm.formPayload[key]);
    }
  }
);
function handleQuery() {
  page.current = 1;
  tableLoading.value = true;
  setTimeout(() => {
    tableLoading.value = false;
  }, 150);
const tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
function onSearch() {
  handleQuery(searchForm);
}
async function resetSearch() {
  searchForm.applicantId = "";
  searchForm.transferDateRange = null;
  handleQuery();
  onSearch();
  await remoteSearchApplicant("");
}
function pagination(obj) {
  page.current = obj.page;
  page.size = obj.limit;
function onPagination(obj) {
  pagination(obj, searchForm);
}
function openDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
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 = "编辑调岗申请";
  Object.assign(form, createEmptyForm());
  if (mode === "edit" && row) {
    Object.assign(form, {
      id: row.id,
      applicantId: row.applicantId,
      applicantName: row.applicantName,
      transferDate: row.transferDate,
      originalPostId: row.originalPostId,
      originalPostName: row.originalPostName,
      targetPostId: row.targetPostId,
      targetPostName: row.targetPostName,
      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?.());
}
function onFormClosed() {
  formRef.value?.resetFields?.();
}
async function submitForm() {
  try {
    await formRef.value?.validate?.();
  } catch {
    return;
  }
  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,
    applicantName: form.applicantName,
    transferDate: form.transferDate,
    originalPostId: form.originalPostId,
    originalPostName: form.originalPostName,
    targetPostId: form.targetPostId,
    targetPostName: form.targetPostName,
    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()}`;
    allRows.value.unshift({
      id,
      ...payload,
      approvalResult: "pending",
    });
    proxy?.$modal?.msgSuccess?.("新增成功");
  } else {
    const idx = allRows.value.findIndex((r) => r.id === form.id);
    const prev = idx !== -1 ? allRows.value[idx] : {};
    if (idx !== -1) {
      allRows.value[idx] = {
        ...prev,
        id: form.id,
        ...payload,
      };
    }
    proxy?.$modal?.msgSuccess?.("保存成功");
  }
  formDialog.visible = false;
  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);
    }
  }
async function onSubmit() {
  const ok = await submitInstanceForm({ skipValidate: true });
  if (ok) ElMessage.success(isSubmitEdit.value ? "修改成功" : "提交成功");
}
onMounted(async () => {
  await Promise.all([loadUserPool(), loadPostOptions()]);
  rebuildPostIdMap();
  loadFlowUsers();
  await remoteSearchApplicant("");
  await initModuleList(searchForm);
});
</script>
@@ -672,21 +352,5 @@
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
.transfer-apply-form :deep(.el-row) {
  margin-bottom: 0;
}
.transfer-apply-form :deep(.el-form-item) {
  margin-bottom: 18px;
}
.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>