You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

517 lines
20 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="p-2">
<el-card shadow="never" style="margin-top: 0">
<!-- 审批按钮组件 -->
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.tripId"
:status="form.flowStatus"
:pageType="routeParams.type"
:mode="false"
/>
</el-card>
<el-card shadow="never" style="margin-top: 0">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 24px">出差申请{{ form.tripId ? ' - 修改' : ' - 新增' }}</div>
</template>
<el-form ref="businessTripApplyFormRef" :model="form" :loading="buttonLoading" :rules="rules" label-width="120px">
<!-- 通用字段区域 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="出差类型" prop="tripType">
<el-select v-model="form.tripType" placeholder="请选择出差类型" :disabled="isFormDisabled || !!form.tripId">
<el-option v-for="dict in trip_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请单号" prop="applyCode">
<el-input v-model="form.applyCode" placeholder="自动生成" disabled />
</el-form-item>
</el-col>
<!-- 动态字段:安装调试 (Type 1) -->
<template v-if="form.tripType === '1'">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectId">
<el-input v-model="form.projectName" placeholder="请选择项目" readonly :disabled="isFormDisabled" @click="openProjectSelect">
<template #suffix>
<el-icon style="cursor: pointer" @click.stop="openProjectSelect"><Search /></el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<!-- 隐藏字段用于存储ID -->
<el-col :span="0">
<el-form-item v-show="false" prop="projectId">
<el-input v-model="form.projectId" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目号" prop="projectCode">
<el-input v-model="form.projectCode" placeholder="选择项目后自动显示" disabled />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="出差事由" prop="tripReason">
<el-input
v-model="form.tripReason"
type="textarea"
placeholder="请输入出差事由"
:disabled="isFormDisabled"
maxlength="800"
show-word-limit
:rows="3"
/>
</el-form-item>
</el-col>
</template>
<!-- 动态字段:市场交流 (Type 2) -->
<template v-if="form.tripType === '2'">
<el-col :span="12">
<el-form-item label="出差地点" prop="tripLocation">
<el-input v-model="form.tripLocation" placeholder="如:北京、上海、杭州" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交流对象" prop="exchangeObject">
<el-input v-model="form.exchangeObject" placeholder="拜访客户名称" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="业务方向" prop="businessDirection">
<el-select v-model="form.businessDirection" placeholder="选择业务方向" :disabled="isFormDisabled">
<el-option v-for="dict in business_direction" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="交流目的" prop="exchangePurpose">
<el-input v-model="form.exchangePurpose" placeholder="请输入交流目的" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
</template>
<!-- 动态字段:展会/会议 (Type 3) -->
<template v-if="form.tripType === '3'">
<el-col :span="12">
<el-form-item label="出差地点" prop="tripLocation">
<el-input v-model="form.tripLocation" placeholder="如:北京、上海、杭州" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="会议/展会名称" prop="meetingName">
<el-input v-model="form.meetingName" placeholder="请输入" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
</template>
<!-- 动态字段:其他 (Type 4)以及Type 1的部分复用字段 -->
<template v-if="form.tripType === '4'">
<el-col :span="24">
<el-form-item label="出差地点" prop="tripLocation">
<el-input v-model="form.tripLocation" placeholder="如:北京、上海、杭州" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="出差事由" prop="tripReason">
<el-input
v-model="form.tripReason"
type="textarea"
placeholder="请输入出差事由"
:disabled="isFormDisabled"
maxlength="800"
show-word-limit
:rows="3"
/>
</el-form-item>
</el-col>
</template>
<!-- 安装调试的出差地点统一放这里 -->
<template v-if="form.tripType === '1'">
<el-col :span="24">
<el-form-item label="出差地点" prop="tripLocation">
<el-input v-model="form.tripLocation" placeholder="如:北京、上海、杭州" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
</template>
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
:disabled="isFormDisabled"
@change="calculateDuration"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="form.endTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
:disabled="isFormDisabled"
@change="calculateDuration"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="时长(天)" prop="durationDays">
<el-input-number v-model="form.durationDays" :precision="1" :step="0.5" :min="0" disabled style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="!isFormDisabled">
<el-form-item label="下一步审批人" prop="variables.approverId">
<el-select
v-model="form.variables.approverId"
placeholder="请选择或搜索审批人"
filterable
clearable
style="width: 100%"
@change="handleApproverSelectChange"
>
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="!isFormDisabled">
<el-form-item label="抄送人员" prop="copyUserIds">
<el-select v-model="form.copyUserIds" placeholder="请选择抄送人员" filterable clearable multiple style="width: 100%">
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="String(user.userId)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 提交审批组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 审批记录 -->
<approvalRecord ref="approvalRecordRef" />
<!-- 项目选择组件 -->
<ProjectSelect ref="projectSelectRef" :multiple="false" @confirm-call-back="projectInfoSelectCallBack"></ProjectSelect>
</div>
</template>
<script setup name="BusinessTripApplyEdit" lang="ts">
import {
addBusinessTripApply,
updateBusinessTripApply,
getBusinessTripApply,
submitBusinessTripApplyAndFlowStart
} from '@/api/oa/crm/businessTripApply';
import { BusinessTripApplyForm } from '@/api/oa/crm/businessTripApply/types';
import { getRuleGenerateCode } from '@/api/system/codeRule';
import { startWorkFlow } from '@/api/workflow/task';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ProjectSelect from '@/components/ProjectSelect/index.vue';
import { CodeRuleEnum, FlowCodeEnum } from '@/enums/OAEnum';
import { getInfo } from '@/api/login';
import { listUser } from '@/api/system/user'; // API
import { allListDept } from '@/api/system/dept'; // API
import { ProjectInfoVO } from '@/api/oa/erp/projectInfo/types';
import dayjs from 'dayjs';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
//
const { trip_type, business_direction } = toRefs<any>(proxy?.useDict('trip_type', 'business_direction'));
// 路由参数
const routeParams = ref<Record<string, any>>({});
const buttonLoading = ref(false);
const businessTripApplyFormRef = ref<ElFormInstance>();
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const projectSelectRef = ref<InstanceType<typeof ProjectSelect>>();
// const userSelectRef = ref<InstanceType<typeof UserSelect>>(); // 审批人选择 ref
const userList = ref<any[]>([]); // 用户列表
const deptInfoList = ref<any[]>([]); // 部门列表
// 默认抄送人员的昵称列表
const defaultCopyUserNames = ['米兰', '于洋', '张兰艳', '张东辉', '冯俊杰'];
// 流程相关数据
const submitFormData = ref<StartProcessBo>({
businessId: '',
flowCode: FlowCodeEnum.BUSINESS_TRIP_CODE,
variables: {},
bizExt: {}
});
const taskVariables = ref<Record<string, any>>({});
const initFormData: BusinessTripApplyForm = {
tripId: undefined,
applyCode: undefined,
tripType: undefined,
applicantId: undefined,
applicantName: undefined,
deptId: undefined,
deptName: undefined,
tripLocation: undefined,
startTime: undefined,
endTime: undefined,
durationDays: undefined,
tripReason: undefined,
projectId: undefined,
customerId: undefined,
exchangeObject: undefined,
businessDirection: undefined,
exchangePurpose: undefined,
exchangeProcess: undefined,
meetingName: undefined,
feedback: undefined,
tripStatus: '1', // 默认暂存
flowStatus: 'draft',
remark: undefined,
ossId: undefined,
variables: {}, // 初始化 variables
copyUserIds: [] as string[] // 抄送人员ID列表
};
const data = reactive({
form: { ...initFormData },
rules: {
tripType: [{ required: true, message: '出差类型不能为空', trigger: 'change' }],
// applyCode: [{ required: true, message: '申请单号不能为空', trigger: 'blur' }],
tripLocation: [{ required: true, message: '出差地点不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
durationDays: [{ required: true, message: '时长不能为空', trigger: 'blur' }],
// 动态校验
projectId: [{ required: true, message: '项目名称不能为空', trigger: 'change' }],
exchangeObject: [{ required: true, message: '交流对象不能为空', trigger: 'blur' }],
businessDirection: [{ required: true, message: '业务方向不能为空', trigger: 'change' }],
exchangePurpose: [{ required: true, message: '交流目的不能为空', trigger: 'blur' }],
exchangeProcess: [{ required: true, message: '交流过程简述不能为空', trigger: 'blur' }],
feedback: [{ required: true, message: '结果反馈不能为空', trigger: 'blur' }],
meetingName: [{ required: true, message: '会议/展会名称不能为空', trigger: 'blur' }],
'variables.approverId': [{ required: true, message: '请选择下一步审批人', trigger: 'change' }]
}
});
const { form, rules } = toRefs(data);
const isFormDisabled = computed(() => {
return routeParams.value.type === 'view' || routeParams.value.type === 'approval';
});
// 计算动态校验规则
const computedRules = computed(() => {
const baseRules = { ...rules.value };
// 根据 tripType 过滤不需要的规则 (简单处理:校验时 el-form-item 不渲染则不校验,或者在这里动态调整)
// Element Plus Form 组件通常只校验渲染出来的 Item所以只要 v-if 控制好了rules 可以放全集。
// 但是为了保险,可以在提交时做更严格的检查。
return baseRules;
});
onMounted(async () => {
nextTick(async () => {
routeParams.value = route.query;
const id = routeParams.value.id;
// 加载用户列表 (用于审批人选择和抄送人员选择)
const userRes = await listUser({ pageNum: 1, pageSize: 1000 });
userList.value = userRes.rows;
// 加载部门列表 (用于获取部门负责人和分管副总)
const deptRes = await allListDept({ deptCategory: '03' } as any);
deptInfoList.value = deptRes.data || [];
// 自动填充申请人信息
if (!id) {
try {
const infoRes = await getInfo();
if (infoRes.data?.user) {
form.value.applicantId = infoRes.data.user.userId;
form.value.applicantName = infoRes.data.user.nickName;
form.value.deptId = infoRes.data.user.deptId;
form.value.deptName = infoRes.data.user.deptName;
// 根据部门ID获取部门负责人和分管副总
const deptInfo = deptInfoList.value.find((d: any) => d.deptId === infoRes.data.user.deptId);
if (deptInfo) {
const deptLeaderId = deptInfo.leader;
const vicePresidentId = deptInfo.vicePresident;
// 将部门负责人和分管副总加入默认抄送人员
const deptCopyIds: string[] = [];
if (deptLeaderId) deptCopyIds.push(String(deptLeaderId));
if (vicePresidentId) deptCopyIds.push(String(vicePresidentId));
// 根据昵称匹配默认抄送人员
const defaultCopyIds = userList.value.filter((u: any) => defaultCopyUserNames.includes(u.nickName)).map((u: any) => String(u.userId));
// 合并默认抄送人员和部门负责人/分管副总(去重)
form.value.copyUserIds = [...new Set([...defaultCopyIds, ...deptCopyIds])];
}
}
} catch (e) {
console.error('获取用户信息失败', e);
}
// 从 index 页面传来 tripType
if (routeParams.value.tripType) {
form.value.tripType = routeParams.value.tripType;
}
}
if (id && (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval')) {
const res = await getBusinessTripApply(id);
Object.assign(form.value, res.data);
// 将抄送人员字符串转换为数组
if (form.value.copyUserIds && typeof form.value.copyUserIds === 'string') {
form.value.copyUserIds = (form.value.copyUserIds as string).split(',').filter((id) => id);
}
}
});
});
// 计算时长
const calculateDuration = () => {
if (form.value.startTime && form.value.endTime) {
const start = dayjs(form.value.startTime);
const end = dayjs(form.value.endTime);
const diff = end.diff(start, 'day');
// 简单的天数计算,实际可能需要包含半天,这里先按自然日 + 1 (如果是同一天算1天) 或者 diff
// 需求是 decimal(10,1),通常是 0.5 天为单位。
// 假设同一天算 1 天
if (diff >= 0) {
form.value.durationDays = diff + 1;
} else {
form.value.durationDays = 0;
}
}
};
// 打开项目选择
const openProjectSelect = () => {
if (isFormDisabled.value) return;
projectSelectRef.value?.open();
};
// 项目选择回调
const projectInfoSelectCallBack = (data: ProjectInfoVO[]) => {
if (data && data.length > 0) {
form.value.projectId = data[0].projectId;
form.value.projectName = data[0].projectName;
form.value.projectCode = data[0].projectCode;
}
};
// 审批人选择变更
const handleApproverSelectChange = (val: any) => {
const user = userList.value.find((u) => u.userId === val);
if (user) {
form.value.variables = {
...form.value.variables,
approverName: user.nickName
};
} else {
form.value.variables = {
...form.value.variables,
approverName: undefined
};
}
};
// 提交表单 (包含暂存和提交审批)
const submitForm = (status: string, mode: boolean) => {
if (status === 'draft') {
executeSubmit(status, mode);
return;
}
businessTripApplyFormRef.value?.validate(async (valid) => {
if (valid) {
executeSubmit(status, mode);
}
});
};
const executeSubmit = async (status: string, mode: boolean) => {
buttonLoading.value = true;
try {
if (status !== 'draft') {
// 提交审批
form.value.tripStatus = '2'; // 审批中
form.value.flowStatus = 'waiting';
// 将抄送人员ID数组转换为逗号分隔字符串
const copyUserIdsStr = Array.isArray(form.value.copyUserIds) ? form.value.copyUserIds.join(',') : form.value.copyUserIds || '';
form.value.variables = {
...form.value.variables,
tripType: form.value.tripType,
businessTripCopyUsers: copyUserIdsStr
};
form.value.bizExt = {
businessTitle: '出差申请',
businessCode: form.value.applyCode
};
// 确保流程编码存在
form.value.flowCode = FlowCodeEnum.BUSINESS_TRIP_CODE;
const res = await submitBusinessTripApplyAndFlowStart(form.value);
// 成功后处理
proxy?.$modal.msgSuccess('提交成功');
submitCallback();
} else {
// 暂存
form.value.tripStatus = '1';
form.value.flowStatus = 'draft';
if (form.value.tripId) {
await updateBusinessTripApply(form.value);
} else {
const res = await addBusinessTripApply(form.value);
}
proxy?.$modal.msgSuccess('暂存成功');
submitCallback();
}
} catch (e) {
console.error(e);
} finally {
buttonLoading.value = false;
}
};
// 审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value?.init(form.value.tripId);
};
// 打开审批详情
const approvalVerifyOpen = async () => {
await submitVerifyRef.value?.openDialog(routeParams.value.taskId);
};
// 提交成功回调
const submitCallback = () => {
proxy?.$tab.closePage(route);
router.go(-1);
};
</script>