<!--
|
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">
|
<view class="section">
|
<view class="section-title">基本信息</view>
|
<view class="form-body">
|
<view class="form-row">
|
<text class="form-label required">审批标题</text>
|
<up-input v-model="form.title"
|
placeholder="请输入审批标题"
|
maxlength="100"
|
clearable />
|
</view>
|
<view class="form-row">
|
<text class="form-label">审批模板</text>
|
<text class="form-readonly">{{ templateName }}</text>
|
</view>
|
<view class="form-row">
|
<text class="form-label">申请人</text>
|
<text class="form-readonly">{{ displayApplicantName }}</text>
|
</view>
|
</view>
|
</view>
|
|
<view class="section">
|
<view class="section-title">填报内容</view>
|
<view v-if="formConfigData.prompt"
|
class="form-prompt">
|
{{ formConfigData.prompt }}
|
</view>
|
<view v-if="formConfigData.fields.length"
|
class="form-body">
|
<view v-for="field in formConfigData.fields"
|
:key="field.key"
|
class="form-row form-row--field">
|
<text class="form-label"
|
:class="{ required: field.required }">{{ field.label }}</text>
|
<up-textarea v-if="field.type === 'textarea'"
|
v-model="formValues[field.key]"
|
:placeholder="`请输入${field.label}`"
|
maxlength="500"
|
border="surround"
|
height="80" />
|
<view v-else-if="field.type === 'date'"
|
class="date-trigger"
|
@click="openDatePicker(field.key)">
|
<up-input :model-value="formValues[field.key]"
|
:placeholder="`请选择${field.label}`"
|
readonly />
|
</view>
|
<up-input v-else
|
v-model="formValues[field.key]"
|
:type="field.type === 'number' ? 'digit' : 'text'"
|
:placeholder="`请输入${field.label}`"
|
clearable />
|
</view>
|
</view>
|
<view v-else
|
class="empty-hint">该模板暂无填报项</view>
|
</view>
|
|
<view class="section">
|
<view class="section-title">审批流程</view>
|
<view v-if="detail.nodes?.length"
|
class="flow-list">
|
<view v-for="(node, index) in detail.nodes"
|
:key="node.id || index"
|
class="flow-card">
|
<view class="flow-card-head">
|
<text class="flow-level">第{{ levelLabel(node.levelNo || index + 1) }}级</text>
|
<text class="flow-type">{{ approveTypeText(node.approveType) }}</text>
|
</view>
|
<view class="approver-tags">
|
<text v-for="(approver, aIdx) in node.approvers || []"
|
:key="approver.id || aIdx"
|
class="approver-tag">
|
{{ approver.approverName || "-" }}
|
</text>
|
<text v-if="!(node.approvers || []).length"
|
class="empty-hint inline">暂无审批人</text>
|
</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="date"
|
@confirm="onDateConfirm"
|
@cancel="showDatePicker = false" />
|
</up-popup>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, reactive, ref } 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 { formatDateToYMD, parseTime } from "@/utils/ruoyi";
|
|
const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
|
const LEVEL_TEXT = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
|
|
const userStore = useUserStore();
|
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 showDatePicker = ref(false);
|
const datePickerTs = ref(Date.now());
|
const activeDateFieldKey = 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 parseFormConfig = raw => {
|
if (!raw) return { prompt: "", fields: [] };
|
try {
|
const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
|
return {
|
prompt: obj?.prompt || "",
|
fields: Array.isArray(obj?.fields) ? obj.fields : [],
|
};
|
} catch {
|
return { prompt: "", fields: [] };
|
}
|
};
|
|
const formConfigData = computed(() => {
|
const raw = isEditMode.value
|
? instanceRow.value?.formConfig
|
: detail.value?.formConfig;
|
return parseFormConfig(raw);
|
});
|
|
const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
|
const approveTypeText = type => (type === "OR" ? "或签" : "会签");
|
|
const initFormValues = fields => {
|
Object.keys(formValues).forEach(key => {
|
delete formValues[key];
|
});
|
fields.forEach(field => {
|
if (!field?.key) return;
|
formValues[field.key] = field.value ?? field.defaultValue ?? "";
|
});
|
};
|
|
const openDatePicker = fieldKey => {
|
activeDateFieldKey.value = fieldKey;
|
const current = formValues[fieldKey];
|
datePickerTs.value = current ? new Date(current).getTime() : Date.now();
|
showDatePicker.value = true;
|
};
|
|
const onDateConfirm = e => {
|
const ts = e?.value ?? datePickerTs.value;
|
if (activeDateFieldKey.value) {
|
formValues[activeDateFieldKey.value] = formatDateToYMD(ts);
|
}
|
showDatePicker.value = false;
|
};
|
|
const validateForm = () => {
|
if (!form.title?.trim()) {
|
uni.showToast({ title: "请输入审批标题", icon: "none" });
|
return false;
|
}
|
for (const field of formConfigData.value.fields) {
|
if (!field.required) continue;
|
const val = formValues[field.key];
|
if (val === undefined || val === null || String(val).trim() === "") {
|
uni.showToast({ title: `请填写${field.label}`, icon: "none" });
|
return false;
|
}
|
}
|
if (!detail.value?.nodes?.length) {
|
uni.showToast({ title: "模板未配置审批流程", icon: "none" });
|
return false;
|
}
|
return true;
|
};
|
|
const buildFormConfigPayload = () =>
|
JSON.stringify({
|
prompt: formConfigData.value.prompt,
|
fields: formConfigData.value.fields.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,
|
title: form.title.trim(),
|
status: row.status || "PENDING",
|
currentLevel: row.currentLevel,
|
applicantId: row.applicantId,
|
applicantName: row.applicantName,
|
applyTime: row.applyTime,
|
deptId: row.deptId,
|
formConfig: buildFormConfigPayload(),
|
};
|
};
|
|
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(formConfigData.value.fields);
|
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" });
|
return;
|
}
|
uni.removeStorageSync(EDIT_STORAGE_KEY);
|
instanceRow.value = row;
|
templateId.value = row.templateId;
|
form.title = row.title || "";
|
|
loading.value = true;
|
detail.value = null;
|
try {
|
await loadTemplateDetail();
|
initFormValues(formConfigData.value.fields);
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
onLoad(options => {
|
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">
|
.approve-apply-page {
|
display: flex;
|
flex-direction: column;
|
min-height: 100vh;
|
background: #f0f3f8;
|
}
|
|
.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: #909399;
|
}
|
|
.section {
|
background: #fff;
|
border-radius: 12px;
|
margin-bottom: 10px;
|
overflow: hidden;
|
box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
|
}
|
|
.section-title {
|
padding: 12px 16px;
|
font-size: 15px;
|
font-weight: 600;
|
color: #1f2d3d;
|
border-bottom: 1px solid #f2f4f7;
|
border-left: 3px solid #2979ff;
|
padding-left: 13px;
|
}
|
|
.form-body {
|
padding: 8px 16px 16px;
|
}
|
|
.form-row {
|
padding: 10px 0;
|
border-bottom: 1px solid #f5f7fa;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
|
&--field {
|
flex-direction: column;
|
align-items: stretch;
|
}
|
}
|
|
.form-label {
|
display: block;
|
margin-bottom: 8px;
|
font-size: 14px;
|
color: #606266;
|
|
&.required::before {
|
content: "*";
|
color: #f56c6c;
|
margin-right: 4px;
|
}
|
}
|
|
.form-readonly {
|
font-size: 14px;
|
color: #303133;
|
}
|
|
.form-prompt {
|
margin: 12px 16px 0;
|
padding: 10px 12px;
|
font-size: 13px;
|
color: #606266;
|
background: #f8fafc;
|
border-radius: 8px;
|
line-height: 1.5;
|
}
|
|
.date-trigger {
|
width: 100%;
|
}
|
|
.flow-list {
|
padding: 12px;
|
}
|
|
.flow-card {
|
padding: 12px;
|
margin-bottom: 8px;
|
background: #f8fafc;
|
border-radius: 8px;
|
border: 1px solid #eef2f6;
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
}
|
|
.flow-card-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 8px;
|
}
|
|
.flow-level {
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.flow-type {
|
font-size: 13px;
|
color: #2979ff;
|
}
|
|
.approver-tags {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.approver-tag {
|
padding: 4px 10px;
|
font-size: 13px;
|
color: #303133;
|
background: #fff;
|
border: 1px solid #dce8f8;
|
border-radius: 16px;
|
}
|
|
.empty-hint {
|
padding: 12px 16px 16px;
|
font-size: 13px;
|
color: #909399;
|
|
&.inline {
|
padding: 0;
|
}
|
}
|
|
.empty-wrap {
|
padding: 48px 20px;
|
}
|
</style>
|