<!--
|
OA / 审批管理 / 发起审批
|
路由:/pages/oa/ApproveManage/approve-list/apply
|
-->
|
<template>
|
<view class="approve-apply-page">
|
<PageHeader :title="pageTitle"
|
@back="goBack" />
|
|
<scroll-view class="form-scroll"
|
scroll-y
|
:show-scrollbar="false">
|
<view v-if="loading"
|
class="loading-wrap">
|
<up-loading-icon mode="circle" />
|
<text class="loading-text">加载中...</text>
|
</view>
|
<template v-else-if="detail">
|
<up-form :model="form"
|
label-width="100"
|
input-align="right">
|
<u-cell-group title="基本信息"
|
class="form-section">
|
<up-form-item label="审批标题"
|
required
|
class="form-item-name">
|
<up-input v-model="form.title"
|
class="name-input-inline"
|
placeholder="请输入审批标题"
|
maxlength="100"
|
clearable />
|
</up-form-item>
|
<up-form-item label="审批模板"
|
class="form-item-readonly">
|
<up-input :model-value="templateName"
|
readonly />
|
</up-form-item>
|
<up-form-item label="申请人"
|
class="form-item-readonly">
|
<up-input :model-value="displayApplicantName"
|
readonly />
|
</up-form-item>
|
</u-cell-group>
|
</up-form>
|
|
<view class="section-card">
|
<view class="section-head">
|
<text class="section-title">填报内容</text>
|
</view>
|
<view v-if="formConfigData.prompt"
|
class="form-prompt">
|
{{ formConfigData.prompt }}
|
</view>
|
<up-form v-if="formConfigData.fields.length"
|
:model="formValues"
|
label-width="100"
|
input-align="right"
|
class="dynamic-form">
|
<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]"
|
:placeholder="`请输入${field.label}`"
|
maxlength="500"
|
border="surround"
|
height="80" />
|
<view v-else-if="isDatetimerangeField(field)"
|
class="daterange-fill">
|
<view class="range-fill-row"
|
@click="openRangePicker(field, 'start')">
|
<text class="range-fill-label">开始</text>
|
<up-input :model-value="getRangePartDisplay(field, 'start')"
|
placeholder="开始时间"
|
readonly />
|
<up-icon name="calendar"
|
size="16"
|
color="#909399" />
|
</view>
|
<text class="range-fill-sep">至</text>
|
<view class="range-fill-row"
|
@click="openRangePicker(field, 'end')">
|
<text class="range-fill-label">结束</text>
|
<up-input :model-value="getRangePartDisplay(field, 'end')"
|
placeholder="结束时间"
|
readonly />
|
<up-icon name="calendar"
|
size="16"
|
color="#909399" />
|
</view>
|
</view>
|
<view v-else-if="isDateLikeField(field)"
|
class="field-trigger"
|
@click="openDatePicker(field)">
|
<up-input :model-value="formatFieldDisplayValue(field, formValues[field.key])"
|
:placeholder="`请选择${field.label}`"
|
readonly />
|
<up-icon :name="getDatePickerMode(field) === 'time' ? 'clock' : 'calendar'"
|
size="18"
|
color="#909399" />
|
</view>
|
<view v-else-if="isSelectField(field)"
|
class="field-trigger"
|
@click="openSelectPicker(field)">
|
<up-input :model-value="getSelectDisplayText(field)"
|
:placeholder="`请选择${field.label}`"
|
readonly />
|
<up-icon name="arrow-right"
|
size="16"
|
color="#c0c4cc" />
|
</view>
|
<up-input v-else
|
v-model="formValues[field.key]"
|
:type="isNumberField(field) ? 'digit' : 'text'"
|
:placeholder="`请输入${field.label}`"
|
clearable />
|
</up-form-item>
|
</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">
|
<view class="section-head">
|
<text class="section-title">审批流程</text>
|
</view>
|
<view v-if="detail.nodes?.length"
|
class="flow-wrap">
|
<view v-for="(node, nodeIndex) in detail.nodes"
|
:key="node.id || nodeIndex"
|
class="flow-node-block">
|
<view class="flow-node-card">
|
<view class="node-header">
|
<view class="node-level-badge">{{ node.levelNo || nodeIndex + 1 }}</view>
|
<text class="node-level-text">第{{ levelLabel(node.levelNo || nodeIndex + 1) }}级</text>
|
</view>
|
<view class="approve-type-row approve-type-row--readonly">
|
<view class="type-btn"
|
:class="{ active: node.approveType !== 'OR' }">
|
会签
|
</view>
|
<view class="type-btn"
|
:class="{ active: node.approveType === 'OR' }">
|
或签
|
</view>
|
</view>
|
<view class="approver-list">
|
<view v-for="(approver, aIdx) in node.approvers || []"
|
:key="approver.id || aIdx"
|
class="approver-chip">
|
<view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view>
|
<text class="approver-name">{{ approver.approverName || "-" }}</text>
|
</view>
|
<text v-if="!(node.approvers || []).length"
|
class="empty-hint inline">暂无审批人</text>
|
</view>
|
</view>
|
<view v-if="nodeIndex < detail.nodes.length - 1"
|
class="flow-connector">
|
<view class="flow-connector-line" />
|
</view>
|
</view>
|
</view>
|
<view v-else
|
class="empty-hint">暂无审批节点</view>
|
</view>
|
</template>
|
<view v-else
|
class="empty-wrap">
|
<up-empty mode="data"
|
text="未获取到模板详情" />
|
</view>
|
</scroll-view>
|
|
<FooterButtons v-if="!loading && detail"
|
cancel-text="取消"
|
:confirm-text="confirmText"
|
:loading="submitting"
|
@cancel="goBack"
|
@confirm="handleSubmit" />
|
|
<up-popup :show="showDatePicker"
|
mode="bottom"
|
@close="showDatePicker = false">
|
<up-datetime-picker :show="true"
|
v-model="datePickerTs"
|
:mode="datePickerMode"
|
@confirm="onDateConfirm"
|
@cancel="onDatePickerCancel" />
|
</up-popup>
|
|
<up-action-sheet :show="showSelectSheet"
|
:title="selectSheetTitle"
|
:actions="selectSheetActions"
|
@select="onSelectOption"
|
@close="showSelectSheet = false" />
|
</view>
|
</template>
|
|
<script setup>
|
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 { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js";
|
import {
|
saveApprovalInstance,
|
updateApprovalInstance,
|
} from "@/api/oa/approvalInstance.js";
|
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,
|
formatFieldDisplayValue,
|
getDatePickerMode,
|
getFieldInitialValue,
|
getFieldOptionLabel,
|
isDatetimerangeField,
|
isDateLikeField,
|
isNumberField,
|
isSelectField,
|
isTextareaField,
|
joinDatetimerangeValue,
|
mergeFormConfigForEdit,
|
parseDatetimerangeValue,
|
resolveFieldOptions,
|
parseApprovalFormConfig,
|
parseFieldDateToTs,
|
} from "../../_utils/approvalFormField.js";
|
|
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 detail = ref(null);
|
const loading = ref(false);
|
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());
|
const activeDateField = ref(null);
|
const activeRangePart = ref("start");
|
|
const datePickerMode = computed(() => {
|
const field = activeDateField.value;
|
if (!field) return "date";
|
if (isDatetimerangeField(field)) return "datetime";
|
return getDatePickerMode(field);
|
});
|
|
const showSelectSheet = ref(false);
|
const activeSelectField = ref(null);
|
const pickerUserList = ref([]);
|
const pickerDeptList = ref([]);
|
|
const isEditMode = computed(() => !!instanceId.value);
|
|
const pageTitle = computed(() => (isEditMode.value ? "修改审批" : "发起审批"));
|
const confirmText = computed(() => (isEditMode.value ? "保存修改" : "提交审批"));
|
|
const applicantName = computed(
|
() => userStore.nickName || userStore.name || "-"
|
);
|
|
const displayApplicantName = computed(
|
() => instanceRow.value?.applicantName || applicantName.value
|
);
|
|
const templateName = computed(
|
() => detail.value?.templateName || instanceRow.value?.templateName || "-"
|
);
|
|
const formConfigData = computed(() => {
|
if (isEditMode.value) {
|
return mergeFormConfigForEdit(
|
detail.value?.formConfig,
|
instanceRow.value?.formConfig
|
);
|
}
|
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(
|
() => (activeSelectField.value?.label ? `选择${activeSelectField.value.label}` : "请选择")
|
);
|
|
const selectSheetActions = computed(() => {
|
const field = activeSelectField.value;
|
if (!field) return [];
|
return resolveFieldOptions(field, {
|
users: pickerUserList.value,
|
depts: pickerDeptList.value,
|
}).map(opt => ({
|
name: opt.label,
|
value: opt.value,
|
}));
|
});
|
|
const formItemClass = field => {
|
if (isTextareaField(field)) return "form-item-textarea";
|
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) => {
|
const parts = parseDatetimerangeValue(formValues[field.key]);
|
const val = part === "start" ? parts.start : parts.end;
|
return val ? formatFieldDisplayValue({ type: "datetime" }, val) : "";
|
};
|
|
const openRangePicker = (field, part) => {
|
activeDateField.value = field;
|
activeRangePart.value = part;
|
const parts = parseDatetimerangeValue(formValues[field.key]);
|
const val = part === "start" ? parts.start : parts.end;
|
datePickerTs.value = parseFieldDateToTs(val) ?? Date.now();
|
showDatePicker.value = true;
|
};
|
|
const getSelectDisplayText = field => {
|
const stored = formValues[field.key];
|
const options = resolveFieldOptions(field, {
|
users: pickerUserList.value,
|
depts: pickerDeptList.value,
|
});
|
const matched = options.find(
|
opt =>
|
String(opt.value) === String(stored) || String(opt.label) === String(stored)
|
);
|
return (
|
matched?.label ||
|
getFieldOptionLabel(field, stored) ||
|
(stored !== undefined && stored !== null ? String(stored) : "")
|
);
|
};
|
|
const initFormValues = fields => {
|
Object.keys(formValues).forEach(key => {
|
delete formValues[key];
|
});
|
fields.forEach(field => {
|
if (!field?.key) return;
|
formValues[field.key] = getFieldInitialValue(field);
|
});
|
};
|
|
const openSelectPicker = field => {
|
const options = resolveFieldOptions(field, {
|
users: pickerUserList.value,
|
depts: pickerDeptList.value,
|
});
|
if (!options.length) {
|
uni.showToast({ title: "该字段未配置下拉选项", icon: "none" });
|
return;
|
}
|
activeSelectField.value = field;
|
showSelectSheet.value = true;
|
};
|
|
const onSelectOption = action => {
|
const key = activeSelectField.value?.key;
|
if (key) {
|
formValues[key] = action.value;
|
}
|
showSelectSheet.value = false;
|
activeSelectField.value = null;
|
};
|
|
const openDatePicker = field => {
|
activeDateField.value = field;
|
const current = formValues[field.key];
|
datePickerTs.value = parseFieldDateToTs(current) ?? Date.now();
|
showDatePicker.value = true;
|
};
|
|
const onDatePickerCancel = () => {
|
showDatePicker.value = false;
|
activeDateField.value = null;
|
};
|
|
const onDateConfirm = e => {
|
const ts = e?.value ?? datePickerTs.value;
|
const field = activeDateField.value;
|
if (field?.key) {
|
if (isDatetimerangeField(field)) {
|
const parts = parseDatetimerangeValue(formValues[field.key]);
|
const formatted = formatFieldDateValue({ type: "datetime" }, ts);
|
formValues[field.key] = joinDatetimerangeValue(
|
activeRangePart.value === "start" ? formatted : parts.start,
|
activeRangePart.value === "end" ? formatted : parts.end
|
);
|
} else {
|
formValues[field.key] = formatFieldDateValue(field, ts);
|
}
|
}
|
onDatePickerCancel();
|
};
|
|
const validateForm = () => {
|
if (!form.title?.trim()) {
|
uni.showToast({ title: "请输入审批标题", icon: "none" });
|
return false;
|
}
|
for (const field of displayTemplateFields.value) {
|
if (!field.required) continue;
|
const val = formValues[field.key];
|
if (val === undefined || val === null || String(val).trim() === "") {
|
const action =
|
isSelectField(field) || isDateLikeField(field) || isDatetimerangeField(field)
|
? "请选择"
|
: "请填写";
|
uni.showToast({ title: `${action}${field.label}`, icon: "none" });
|
return false;
|
}
|
if (isDatetimerangeField(field)) {
|
const { start, end } = parseDatetimerangeValue(val);
|
if (!start || !end) {
|
uni.showToast({ title: `请完整选择${field.label}`, icon: "none" });
|
return false;
|
}
|
}
|
if (isSelectField(field)) {
|
const options = resolveFieldOptions(field, {
|
users: pickerUserList.value,
|
depts: pickerDeptList.value,
|
});
|
if (
|
options.length &&
|
!options.some(
|
opt =>
|
String(opt.value) === String(val) || String(opt.label) === String(val)
|
)
|
) {
|
uni.showToast({ title: `${field.label}选项无效`, icon: "none" });
|
return false;
|
}
|
}
|
}
|
if (!detail.value?.nodes?.length) {
|
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 = () => {
|
syncModuleExtrasToFormValues(
|
moduleKey.value,
|
formValues,
|
extraForm,
|
formConfigData.value.fields
|
);
|
const allFields = formConfigData.value.fields || [];
|
return JSON.stringify({
|
prompt: formConfigData.value.prompt,
|
fields: allFields.map(field => ({
|
...field,
|
value: formValues[field.key] ?? "",
|
})),
|
});
|
};
|
|
const buildSavePayload = () => ({
|
templateId: detail.value.id,
|
templateName: detail.value.templateName,
|
businessType: detail.value.businessType,
|
title: form.title.trim(),
|
status: "PENDING",
|
currentLevel: 1,
|
applicantId: userStore.id,
|
applicantName: applicantName.value,
|
applyTime: parseTime(new Date()),
|
deptId: userStore.currentDeptId || undefined,
|
formConfig: buildFormConfigPayload(),
|
});
|
|
const buildUpdatePayload = () => {
|
const row = instanceRow.value || {};
|
return {
|
id: instanceId.value,
|
instanceNo: row.instanceNo,
|
templateId: row.templateId ?? detail.value?.id,
|
templateName: row.templateName ?? detail.value?.templateName,
|
businessId: row.businessId,
|
businessType: row.businessType ?? detail.value?.businessType,
|
title: form.title.trim(),
|
status: row.status || "PENDING",
|
currentLevel: row.currentLevel,
|
applicantId: row.applicantId,
|
applicantName: row.applicantName,
|
applyTime: row.applyTime,
|
finishTime: row.finishTime,
|
createUser: row.createUser,
|
createTime: row.createTime,
|
updateUser: row.updateUser,
|
updateTime: row.updateTime,
|
deptId: row.deptId,
|
deleted: row.deleted,
|
formConfig: buildFormConfigPayload(),
|
approveAction: row.approveAction,
|
approveComment: row.approveComment,
|
};
|
};
|
|
const handleSubmit = () => {
|
if (!validateForm() || submitting.value) return;
|
|
submitting.value = true;
|
const submitApi = isEditMode.value
|
? updateApprovalInstance
|
: saveApprovalInstance;
|
const payload = isEditMode.value ? buildUpdatePayload() : buildSavePayload();
|
|
submitApi(payload)
|
.then(() => {
|
uni.showToast({
|
title: isEditMode.value ? "修改成功" : "提交成功",
|
icon: "success",
|
});
|
if (isEditMode.value) {
|
uni.removeStorageSync(EDIT_STORAGE_KEY);
|
}
|
setTimeout(() => {
|
uni.navigateBack({ delta: isEditMode.value ? 1 : 2 });
|
}, 300);
|
})
|
.catch(() => {
|
uni.showToast({
|
title: isEditMode.value ? "修改失败" : "提交失败",
|
icon: "none",
|
});
|
})
|
.finally(() => {
|
submitting.value = false;
|
});
|
};
|
|
const loadTemplateDetail = () => {
|
if (!templateId.value) return Promise.resolve();
|
return getApprovalTemplateDetail(templateId.value)
|
.then(res => {
|
detail.value = res?.data || null;
|
if (!detail.value) {
|
uni.showToast({ title: "未获取到模板详情", icon: "none" });
|
}
|
return detail.value;
|
})
|
.catch(() => {
|
uni.showToast({ title: "获取模板详情失败", icon: "none" });
|
return null;
|
});
|
};
|
|
const loadForCreate = async () => {
|
loading.value = true;
|
detail.value = null;
|
try {
|
await loadTemplateDetail();
|
if (!detail.value) return;
|
initFormValues(displayTemplateFields.value);
|
resetModuleExtras();
|
if (!form.title && detail.value.templateName) {
|
form.title = `${detail.value.templateName}申请`;
|
}
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
const loadForEdit = async () => {
|
const row = uni.getStorageSync(EDIT_STORAGE_KEY);
|
if (!row || String(row.id) !== String(instanceId.value)) {
|
uni.showToast({ title: "未获取到审批数据", icon: "none" });
|
setTimeout(() => uni.navigateBack(), 500);
|
return;
|
}
|
instanceRow.value = row;
|
if (!moduleKey.value) {
|
moduleKey.value = inferModuleKeyFromRow(row);
|
}
|
templateId.value = row.templateId;
|
form.title = row.title || "";
|
|
loading.value = true;
|
detail.value = null;
|
try {
|
await loadTemplateDetail();
|
if (!detail.value) return;
|
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();
|
};
|
|
const loadPickerSourceData = () => {
|
userListNoPageByTenantId()
|
.then(res => {
|
pickerUserList.value = res?.data || [];
|
})
|
.catch(() => {
|
pickerUserList.value = [];
|
});
|
getDept()
|
.then(res => {
|
pickerDeptList.value = res?.data || [];
|
})
|
.catch(() => {
|
pickerDeptList.value = [];
|
});
|
};
|
|
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();
|
return;
|
}
|
if (options?.templateId) {
|
templateId.value = options.templateId;
|
loadForCreate();
|
return;
|
}
|
uni.showToast({ title: "缺少页面参数", icon: "none" });
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/static/scss/form-common.scss";
|
|
$primary: #2979ff;
|
$text: #1f2d3d;
|
$text-secondary: #606266;
|
$text-muted: #909399;
|
$bg-page: #f0f3f8;
|
$radius-lg: 12px;
|
$radius-md: 10px;
|
$shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05);
|
|
.approve-apply-page {
|
display: flex;
|
flex-direction: column;
|
min-height: 100vh;
|
background: $bg-page;
|
}
|
|
.form-scroll {
|
flex: 1;
|
height: 0;
|
padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
|
}
|
|
.loading-wrap {
|
padding: 48px 0;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 12px;
|
}
|
|
.loading-text {
|
font-size: 14px;
|
color: $text-muted;
|
}
|
|
.form-section {
|
margin-bottom: 10px;
|
border-radius: $radius-lg;
|
overflow: hidden;
|
box-shadow: $shadow-card;
|
}
|
|
:deep(.form-section .u-cell-group__title) {
|
padding: 12px 16px 8px !important;
|
font-size: 15px !important;
|
font-weight: 600 !important;
|
color: $text !important;
|
background: #fff !important;
|
}
|
|
:deep(.form-section .u-form-item) {
|
padding: 0 16px !important;
|
}
|
|
:deep(.form-section .u-form-item__body) {
|
padding: 10px 0 !important;
|
min-height: auto !important;
|
}
|
|
:deep(.form-item-name .u-form-item__body) {
|
flex-direction: row !important;
|
align-items: center !important;
|
}
|
|
:deep(.form-item-name .u-form-item__content) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
justify-content: flex-end !important;
|
}
|
|
:deep(.name-input-inline),
|
:deep(.name-input-inline .u-input__content) {
|
width: 100% !important;
|
flex: 1 !important;
|
}
|
|
:deep(.name-input-inline input),
|
:deep(.name-input-inline .u-input__content__field-wrapper__field) {
|
width: 100% !important;
|
text-align: right !important;
|
font-size: 15px !important;
|
}
|
|
:deep(.form-item-readonly .u-form-item__body) {
|
align-items: center !important;
|
}
|
|
:deep(.form-item-readonly .u-form-item__content) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
justify-content: flex-end !important;
|
}
|
|
:deep(.form-item-readonly .u-input__content__field-wrapper__field) {
|
text-align: right !important;
|
color: #303133 !important;
|
}
|
|
.dynamic-form {
|
padding: 0 0 4px;
|
}
|
|
:deep(.dynamic-form .u-form-item) {
|
padding: 0 16px !important;
|
}
|
|
:deep(.dynamic-form .u-form-item__body) {
|
padding: 10px 0 !important;
|
min-height: auto !important;
|
}
|
|
:deep(.form-item-inline .u-form-item__body) {
|
flex-direction: row !important;
|
align-items: center !important;
|
}
|
|
:deep(.form-item-inline .u-form-item__content) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
justify-content: flex-end !important;
|
}
|
|
:deep(.form-item-inline input),
|
:deep(.form-item-inline .u-input__content__field-wrapper__field) {
|
text-align: right !important;
|
font-size: 15px !important;
|
}
|
|
:deep(.form-item-select .u-form-item__body) {
|
align-items: center !important;
|
}
|
|
:deep(.form-item-select .u-form-item__content) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
justify-content: flex-end !important;
|
}
|
|
: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__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 {
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
gap: 6px;
|
width: 100%;
|
min-width: 0;
|
}
|
|
:deep(.field-trigger .u-input) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
}
|
|
:deep(.field-trigger .u-input__content__field-wrapper__field) {
|
text-align: right !important;
|
font-size: 15px !important;
|
}
|
|
.daterange-fill {
|
width: 100%;
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
}
|
|
.range-fill-row {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 8px 10px;
|
background: #f7f9fc;
|
border: 1px solid #eef1f6;
|
border-radius: 8px;
|
}
|
|
.range-fill-label {
|
flex-shrink: 0;
|
width: 36px;
|
font-size: 13px;
|
color: #909399;
|
}
|
|
.range-fill-sep {
|
font-size: 12px;
|
color: #c0c4cc;
|
text-align: center;
|
}
|
|
:deep(.range-fill-row .u-input) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
}
|
|
.section-card {
|
margin-bottom: 10px;
|
background: #fff;
|
border-radius: $radius-lg;
|
overflow: hidden;
|
box-shadow: $shadow-card;
|
}
|
|
.section-head {
|
padding: 12px 16px;
|
border-bottom: 1px solid #f2f4f7;
|
}
|
|
.section-title {
|
font-size: 15px;
|
font-weight: 600;
|
color: $text;
|
padding-left: 10px;
|
border-left: 3px solid $primary;
|
line-height: 1.2;
|
}
|
|
.form-prompt {
|
margin: 12px 16px 0;
|
padding: 10px 12px;
|
font-size: 13px;
|
color: $text-secondary;
|
background: #f8fafc;
|
border-radius: 8px;
|
line-height: 1.5;
|
}
|
|
.flow-wrap {
|
padding: 10px 16px 14px;
|
}
|
|
.flow-node-block {
|
display: flex;
|
flex-direction: column;
|
align-items: stretch;
|
}
|
|
.flow-node-card {
|
background: #fafbfd;
|
border: 1px solid #e8eef5;
|
border-radius: $radius-md;
|
padding: 12px;
|
}
|
|
.node-header {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
margin-bottom: 10px;
|
}
|
|
.node-level-badge {
|
width: 26px;
|
height: 26px;
|
border-radius: 8px;
|
background: $primary;
|
color: #fff;
|
font-size: 14px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
}
|
|
.node-level-text {
|
flex: 1;
|
font-size: 15px;
|
font-weight: 600;
|
color: $text;
|
}
|
|
.approve-type-row {
|
display: flex;
|
background: #f0f3f8;
|
border-radius: 8px;
|
padding: 3px;
|
margin-bottom: 10px;
|
|
&--readonly {
|
pointer-events: none;
|
}
|
}
|
|
.type-btn {
|
flex: 1;
|
text-align: center;
|
padding: 8px 0;
|
font-size: 14px;
|
color: $text-secondary;
|
border-radius: 6px;
|
|
&.active {
|
background: #fff;
|
color: $primary;
|
font-weight: 500;
|
}
|
}
|
|
.approver-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
align-items: center;
|
}
|
|
.approver-chip {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 6px 12px 6px 6px;
|
background: #fff;
|
border: 1px solid #dce8f8;
|
border-radius: 24px;
|
box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06);
|
}
|
|
.approver-avatar {
|
width: 26px;
|
height: 26px;
|
border-radius: 50%;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: #fff;
|
font-size: 12px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.approver-name {
|
font-size: 13px;
|
color: $text;
|
max-width: 120px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.flow-connector {
|
display: flex;
|
justify-content: center;
|
padding: 4px 0;
|
}
|
|
.flow-connector-line {
|
width: 2px;
|
height: 14px;
|
background: #d0dff0;
|
}
|
|
.empty-hint {
|
padding: 12px 16px 16px;
|
font-size: 13px;
|
color: $text-muted;
|
|
&.inline {
|
padding: 0;
|
}
|
}
|
|
.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>
|