gaoluyang
2026-06-16 c4d25912d11ab9059f8165c25a161634bb9b5e97
src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -17,7 +17,7 @@
      </view>
      <template v-else-if="detail">
        <up-form :model="form"
                 label-width="88"
                 label-width="100"
                 input-align="right">
          <u-cell-group title="基本信息"
                        class="form-section">
@@ -53,13 +53,14 @@
          </view>
          <up-form v-if="formConfigData.fields.length"
                   :model="formValues"
                   label-width="88"
                   label-width="100"
                   input-align="right"
                   class="dynamic-form">
            <up-form-item v-for="field in formConfigData.fields"
            <up-form-item v-for="field in displayTemplateFields"
                          :key="field.key"
                          :label="field.label"
                          :required="!!field.required"
                          :label-position="formItemLabelPosition(field)"
                          :class="formItemClass(field)">
              <up-textarea v-if="isTextareaField(field)"
                           v-model="formValues[field.key]"
@@ -120,6 +121,66 @@
          </up-form>
          <view v-else
                class="empty-hint">该模板暂无填报项</view>
          <!-- 请假:假期余额 + 时长自动计算 -->
          <view v-if="isLeaveModule"
                class="module-extra-block">
            <up-form :model="extraForm"
                     label-width="100"
                     input-align="right"
                     class="dynamic-form">
              <up-form-item label="假期余额"
                            required
                            class="form-item-inline">
                <up-input v-model="extraForm.leaveBalanceDays"
                          type="digit"
                          placeholder="请输入天数"
                          clearable />
              </up-form-item>
              <up-form-item label="请假时长"
                            class="form-item-inline">
                <view class="readonly-with-unit">
                  <up-input :model-value="leaveDurationText"
                            readonly
                            placeholder="根据请假时间自动计算" />
                  <text class="unit-text">天</text>
                </view>
              </up-form-item>
            </up-form>
          </view>
          <!-- 加班:时长自动计算 -->
          <view v-if="isOvertimeModule"
                class="module-extra-block">
            <up-form label-width="100"
                     input-align="right"
                     class="dynamic-form">
              <up-form-item label="加班时长"
                            class="form-item-inline">
                <view class="readonly-with-unit">
                  <up-input :model-value="overtimeHoursText"
                            readonly
                            placeholder="根据加班时间自动计算" />
                  <text class="unit-text">小时</text>
                </view>
              </up-form-item>
            </up-form>
          </view>
          <!-- 调岗:原岗位自动带出 -->
          <view v-if="isTransferModule"
                class="module-extra-block">
            <up-form label-width="100"
                     input-align="right"
                     class="dynamic-form">
              <up-form-item label="原岗位"
                            class="form-item-readonly">
                <up-input :model-value="extraForm.originalPostName"
                          readonly
                          placeholder="选择申请人后自动带出" />
              </up-form-item>
            </up-form>
          </view>
        </view>
        <view class="section-card">
@@ -200,7 +261,7 @@
</template>
<script setup>
  import { computed, reactive, ref } from "vue";
  import { computed, reactive, ref, watch } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import FooterButtons from "@/components/FooterButtons.vue";
@@ -212,7 +273,25 @@
  import useUserStore from "@/store/modules/user";
  import { parseTime } from "@/utils/ruoyi";
  import { getDept } from "@/api/collaborativeApproval/approvalProcess.js";
  import { findPostOptions } from "@/api/system/post.js";
  import { userListNoPageByTenantId } from "@/api/system/user";
  import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
  import {
    computeLeaveDurationDisplay,
    computeOvertimeHoursDisplay,
    displayTemplateFieldsByModule,
    findApplicantTemplateField,
    findLeaveTimeTemplateField,
    findOvertimeTimeTemplateField,
    inferModuleKeyFromRow,
    loadModuleExtrasFromRow,
    resolveOriginalPostName,
    syncModuleExtrasToFormValues,
    unwrapUserArray,
    userById,
    validateModuleExtras,
    buildPostIdToNameMap,
  } from "../../_utils/approvalModuleApplyExtras.js";
  import {
    formatDatetimerangeDisplay,
    formatFieldDateValue,
@@ -233,10 +312,12 @@
    parseFieldDateToTs,
  } from "../../_utils/approvalFormField.js";
  const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
  import { EDIT_STORAGE_KEY } from "../../_utils/approveListUtils.js";
  const LEVEL_TEXT = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
  const userStore = useUserStore();
  const moduleKey = ref("");
  const templateId = ref("");
  const instanceId = ref("");
  const instanceRow = ref(null);
@@ -245,6 +326,22 @@
  const submitting = ref(false);
  const formValues = reactive({});
  const form = reactive({ title: "" });
  const extraForm = reactive({
    leaveBalanceDays: undefined,
    originalPostName: "",
  });
  const postIdToName = ref({});
  const transferUserPool = ref([]);
  const isLeaveModule = computed(
    () => moduleKey.value === APPROVAL_MODULE_KEYS.LEAVE
  );
  const isOvertimeModule = computed(
    () => moduleKey.value === APPROVAL_MODULE_KEYS.OVERTIME
  );
  const isTransferModule = computed(
    () => moduleKey.value === APPROVAL_MODULE_KEYS.TRANSFER
  );
  const showDatePicker = ref(false);
  const datePickerTs = ref(Date.now());
@@ -290,6 +387,31 @@
    return parseApprovalFormConfig(detail.value?.formConfig);
  });
  const displayTemplateFields = computed(() =>
    displayTemplateFieldsByModule(moduleKey.value, formConfigData.value.fields)
  );
  const leaveDurationText = computed(() => {
    if (!isLeaveModule.value) return "";
    const fields = formConfigData.value.fields;
    const timeField = findLeaveTimeTemplateField(fields);
    if (timeField?.key) void formValues[timeField.key];
    return computeLeaveDurationDisplay(fields, formValues);
  });
  const overtimeHoursText = computed(() => {
    if (!isOvertimeModule.value) return "";
    const fields = formConfigData.value.fields;
    const timeField = findOvertimeTimeTemplateField(fields);
    if (timeField?.key) void formValues[timeField.key];
    return computeOvertimeHoursDisplay(fields, formValues);
  });
  const applicantPickerValue = computed(() => {
    const f = findApplicantTemplateField(formConfigData.value.fields);
    return f?.key ? formValues[f.key] : undefined;
  });
  const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
  const selectSheetTitle = computed(
@@ -313,6 +435,12 @@
    if (isDatetimerangeField(field)) return "form-item-daterange";
    if (isSelectField(field) || isDateLikeField(field)) return "form-item-select";
    return "form-item-inline";
  };
  /** 多行文本、日期范围:标签置顶,避免长文案在窄列内断行 */
  const formItemLabelPosition = field => {
    if (isTextareaField(field) || isDatetimerangeField(field)) return "top";
    return "left";
  };
  const getRangePartDisplay = (field, part) => {
@@ -414,7 +542,7 @@
      uni.showToast({ title: "请输入审批标题", icon: "none" });
      return false;
    }
    for (const field of formConfigData.value.fields) {
    for (const field of displayTemplateFields.value) {
      if (!field.required) continue;
      const val = formValues[field.key];
      if (val === undefined || val === null || String(val).trim() === "") {
@@ -453,17 +581,35 @@
      uni.showToast({ title: "模板未配置审批流程", icon: "none" });
      return false;
    }
    const moduleMsg = validateModuleExtras(
      moduleKey.value,
      formConfigData.value.fields,
      formValues,
      extraForm
    );
    if (moduleMsg) {
      uni.showToast({ title: moduleMsg, icon: "none" });
      return false;
    }
    return true;
  };
  const buildFormConfigPayload = () =>
    JSON.stringify({
  const buildFormConfigPayload = () => {
    syncModuleExtrasToFormValues(
      moduleKey.value,
      formValues,
      extraForm,
      formConfigData.value.fields
    );
    const allFields = formConfigData.value.fields || [];
    return JSON.stringify({
      prompt: formConfigData.value.prompt,
      fields: formConfigData.value.fields.map(field => ({
      fields: allFields.map(field => ({
        ...field,
        value: formValues[field.key] ?? "",
      })),
    });
  };
  const buildSavePayload = () => ({
    templateId: detail.value.id,
@@ -562,7 +708,8 @@
    try {
      await loadTemplateDetail();
      if (!detail.value) return;
      initFormValues(formConfigData.value.fields);
      initFormValues(displayTemplateFields.value);
      resetModuleExtras();
      if (!form.title && detail.value.templateName) {
        form.title = `${detail.value.templateName}申请`;
      }
@@ -579,6 +726,9 @@
      return;
    }
    instanceRow.value = row;
    if (!moduleKey.value) {
      moduleKey.value = inferModuleKeyFromRow(row);
    }
    templateId.value = row.templateId;
    form.title = row.title || "";
@@ -587,11 +737,65 @@
    try {
      await loadTemplateDetail();
      if (!detail.value) return;
      initFormValues(formConfigData.value.fields);
      initFormValues(displayTemplateFields.value);
      applyModuleExtrasFromRow();
      if (isTransferModule.value) {
        await ensureTransferLookupData();
        syncOriginalPostFromApplicant(applicantPickerValue.value);
      }
    } finally {
      loading.value = false;
    }
  };
  function resetModuleExtras() {
    extraForm.leaveBalanceDays = undefined;
    extraForm.originalPostName = "";
  }
  function applyModuleExtrasFromRow() {
    const loaded = loadModuleExtrasFromRow(
      moduleKey.value,
      instanceRow.value,
      formValues
    );
    if (loaded.leaveBalanceDays != null) {
      extraForm.leaveBalanceDays = loaded.leaveBalanceDays;
    }
    if (loaded.originalPostName) {
      extraForm.originalPostName = loaded.originalPostName;
    }
  }
  async function ensureTransferLookupData() {
    if (!transferUserPool.value.length) {
      try {
        const res = await userListNoPageByTenantId();
        transferUserPool.value = unwrapUserArray(res);
      } catch {
        transferUserPool.value = [];
      }
    }
    if (!Object.keys(postIdToName.value).length) {
      try {
        const res = await findPostOptions();
        const rows = res?.data ?? res?.rows ?? [];
        postIdToName.value = buildPostIdToNameMap(Array.isArray(rows) ? rows : []);
      } catch {
        postIdToName.value = {};
      }
    }
  }
  function syncOriginalPostFromApplicant(uid) {
    if (!isTransferModule.value) return;
    if (!uid) {
      extraForm.originalPostName = "";
      return;
    }
    const user = userById(transferUserPool.value, uid);
    extraForm.originalPostName = resolveOriginalPostName(user, postIdToName.value);
  }
  const goBack = () => {
    uni.navigateBack();
@@ -614,8 +818,18 @@
      });
  };
  watch(applicantPickerValue, async uid => {
    if (!isTransferModule.value) return;
    await ensureTransferLookupData();
    syncOriginalPostFromApplicant(uid);
  });
  onLoad(options => {
    moduleKey.value = options?.moduleKey || "";
    loadPickerSourceData();
    if (isTransferModule.value) {
      ensureTransferLookupData();
    }
    if (options?.id) {
      instanceId.value = options.id;
      loadForEdit();
@@ -771,15 +985,42 @@
    justify-content: flex-end !important;
  }
  :deep(.form-item-textarea .u-form-item__body) {
  :deep(.form-item-textarea .u-form-item__body),
  :deep(.form-item-daterange .u-form-item__body) {
    flex-direction: column !important;
    align-items: stretch !important;
    padding: 10px 0 12px !important;
  }
  :deep(.form-item-textarea .u-form-item__content) {
  :deep(.form-item-textarea .u-form-item__body__left),
  :deep(.form-item-daterange .u-form-item__body__left) {
    width: 100% !important;
    max-width: 100% !important;
    margin-bottom: 8px !important;
    padding-right: 0 !important;
  }
  :deep(.form-item-textarea .u-form-item__body__left__content__label),
  :deep(.form-item-daterange .u-form-item__body__left__content__label) {
    white-space: normal !important;
    line-height: 1.45 !important;
    font-size: 14px !important;
  }
  :deep(.form-item-textarea .u-form-item__body__right),
  :deep(.form-item-daterange .u-form-item__body__right) {
    width: 100% !important;
    flex: none !important;
  }
  :deep(.form-item-textarea .u-form-item__content),
  :deep(.form-item-daterange .u-form-item__content) {
    width: 100% !important;
    justify-content: stretch !important;
  }
  :deep(.dynamic-form .u-form-item__body__left__content__label) {
    white-space: nowrap;
  }
  .field-trigger {
@@ -799,16 +1040,6 @@
  :deep(.field-trigger .u-input__content__field-wrapper__field) {
    text-align: right !important;
    font-size: 15px !important;
  }
  :deep(.form-item-daterange .u-form-item__body) {
    flex-direction: column !important;
    align-items: stretch !important;
  }
  :deep(.form-item-daterange .u-form-item__content) {
    width: 100% !important;
    justify-content: stretch !important;
  }
  .daterange-fill {
@@ -1015,4 +1246,29 @@
  .empty-wrap {
    padding: 48px 20px;
  }
  .module-extra-block {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dashed #e8ecf0;
  }
  .readonly-with-unit {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    justify-content: flex-end;
  }
  .readonly-with-unit :deep(.u-input) {
    flex: 1;
    min-width: 0;
  }
  .unit-text {
    flex-shrink: 0;
    font-size: 14px;
    color: $text-muted;
  }
</style>