| | |
| | | </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"> |
| | |
| | | </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]" |
| | |
| | | </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"> |
| | |
| | | </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"; |
| | |
| | | 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, |
| | |
| | | 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); |
| | |
| | | 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()); |
| | |
| | | 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( |
| | |
| | | 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) => { |
| | |
| | | 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() === "") { |
| | |
| | | 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, |
| | |
| | | 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}申请`; |
| | | } |
| | |
| | | return; |
| | | } |
| | | instanceRow.value = row; |
| | | if (!moduleKey.value) { |
| | | moduleKey.value = inferModuleKeyFromRow(row); |
| | | } |
| | | templateId.value = row.templateId; |
| | | form.title = row.title || ""; |
| | | |
| | |
| | | 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(); |
| | |
| | | }); |
| | | }; |
| | | |
| | | 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(); |
| | |
| | | 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 { |
| | |
| | | :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 { |
| | |
| | | .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> |