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

<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>