src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -56,7 +56,7 @@
                   label-width="88"
                   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"
@@ -120,6 +120,66 @@
          </up-form>
          <view v-else
                class="empty-hint">该模板暂无填报项</view>
          <!-- 请假:假期余额 + 时长自动计算 -->
          <view v-if="isLeaveModule"
                class="module-extra-block">
            <up-form :model="extraForm"
                     label-width="88"
                     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="88"
                     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="88"
                     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 +260,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 +272,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 +311,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 +325,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());
@@ -288,6 +384,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);
@@ -414,7 +535,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 +574,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 +701,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 +719,9 @@
      return;
    }
    instanceRow.value = row;
    if (!moduleKey.value) {
      moduleKey.value = inferModuleKeyFromRow(row);
    }
    templateId.value = row.templateId;
    form.title = row.title || "";
@@ -587,11 +730,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 +811,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();
@@ -1015,4 +1222,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>