feat(oa/crm): 重构出差申请表单为主子表结构

- 将原有静态字段改为动态行程明细表格模式
- 添加行程明细的增删改查功能
- 实现明细数据自动汇总到主表功能
- 优化出差类型相关的动态字段显示逻辑
- 调整审批流程中的抄送人员处理机制
- 更新表单验证规则以适配新的数据结构
- 添加业务方向负责人的权限控制逻辑
dev
Yangk 5 days ago
parent e7578436ee
commit fe9a7d4596

@ -0,0 +1,76 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { BusinessTripDetailsVO, BusinessTripDetailsForm, BusinessTripDetailsQuery } from '@/api/oa/crm/businessTripDetails/types';
/**
*
* @param query
* @returns {*}
*/
export const listBusinessTripDetails = (query?: BusinessTripDetailsQuery): AxiosPromise<BusinessTripDetailsVO[]> => {
return request({
url: '/oa/crm/businessTripDetails/list',
method: 'get',
params: query
});
};
/**
*
* @param tripDetailsId
*/
export const getBusinessTripDetails = (tripDetailsId: string | number): AxiosPromise<BusinessTripDetailsVO> => {
return request({
url: '/oa/crm/businessTripDetails/' + tripDetailsId,
method: 'get'
});
};
/**
*
* @param data
*/
export const addBusinessTripDetails = (data: BusinessTripDetailsForm) => {
return request({
url: '/oa/crm/businessTripDetails',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateBusinessTripDetails = (data: BusinessTripDetailsForm) => {
return request({
url: '/oa/crm/businessTripDetails',
method: 'put',
data: data
});
};
/**
*
* @param tripDetailsId
*/
export const delBusinessTripDetails = (tripDetailsId: string | number | Array<string | number>) => {
return request({
url: '/oa/crm/businessTripDetails/' + tripDetailsId,
method: 'delete'
});
};
/**
*
* @param query
* @returns {*}
*/
export function getCrmBusinessTripDetailsList (query) {
return request({
url: '/oa/crm/businessTripDetails/getCrmBusinessTripDetailsList',
method: 'get',
params: query
});
};

@ -0,0 +1,231 @@
export interface BusinessTripDetailsVO {
/**
* ID
*/
tripDetailsId: string | number;
/**
* ID
*/
tripId: string | number;
/**
* (1)
*/
itineraryNumber: number;
/**
*
*/
tripLocation: string;
/**
*
*/
startTime: string;
/**
*
*/
endTime: string;
/**
* ()
*/
durationDays: number;
/**
* ID
*/
projectId: string | number;
/**
* ID
*/
customerId: string | number;
/**
* /
*/
meetingName: string;
/**
*
*/
exchangePurpose: string;
/**
*
*/
exchangeFeedback: string;
/**
*
*/
tripReason: string;
/**
*
*/
remark: string;
/**
* ID
*/
ossId: string | number;
}
export interface BusinessTripDetailsForm extends BaseEntity {
/**
* ID
*/
tripDetailsId?: string | number;
/**
* ID
*/
tripId?: string | number;
/**
* (1)
*/
itineraryNumber?: number;
/**
*
*/
tripLocation?: string;
/**
*
*/
startTime?: string;
/**
*
*/
endTime?: string;
/**
* ()
*/
durationDays?: number;
/**
* ID
*/
projectId?: string | number;
/**
* ID
*/
customerId?: string | number;
/**
* /
*/
meetingName?: string;
/**
*
*/
exchangePurpose?: string;
/**
*
*/
exchangeFeedback?: string;
/**
*
*/
tripReason?: string;
/**
*
*/
remark?: string;
/**
* ID
*/
ossId?: string | number;
}
export interface BusinessTripDetailsQuery extends PageQuery {
/**
* ID
*/
tripId?: string | number;
/**
* (1)
*/
itineraryNumber?: number;
/**
*
*/
tripLocation?: string;
/**
*
*/
startTime?: string;
/**
*
*/
endTime?: string;
/**
* ()
*/
durationDays?: number;
/**
* ID
*/
projectId?: string | number;
/**
* ID
*/
customerId?: string | number;
/**
* /
*/
meetingName?: string;
/**
*
*/
exchangePurpose?: string;
/**
*
*/
exchangeFeedback?: string;
/**
*
*/
tripReason?: string;
/**
* ID
*/
ossId?: string | number;
/**
*
*/
params?: any;
}

@ -46,167 +46,49 @@
</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" v-if="form.tripType === '2' || form.tripType === '3'">
<el-form-item label="业务方向" prop="businessDirection">
<el-select v-model="form.businessDirection" placeholder="请选择业务方向" :disabled="isFormDisabled" style="width: 100%">
<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="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="customerId">
<el-select v-model="form.customerId" placeholder="请选择客户" :disabled="isFormDisabled" filterable clearable style="width: 100%">
<el-option
v-for="customer in customerList"
:key="customer.customerId"
:label="customer.customerName"
:value="customer.customerId"
/>
</el-select>
</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="12">
<el-form-item label="交流目的" prop="exchangePurpose">
<el-input v-model="form.exchangePurpose" placeholder="请输入交流目的" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="24" v-if="routeParams.type === 'view'">
<el-form-item label="交流过程简述" prop="exchangeProcess">
<el-input
v-model="form.exchangeProcess"
type="textarea"
placeholder="如与某某客户某某负责人交谈参观XX等"
:disabled="isFormDisabled"
:rows="3"
/>
</el-form-item>
</el-col>
<el-col :span="24" v-if="routeParams.type === 'view'">
<el-form-item label="结果反馈" prop="feedback">
<el-input v-model="form.feedback" type="textarea" placeholder="达成某种共识或初步合作意向" :disabled="isFormDisabled" :rows="3" />
</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>
<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>
</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="tripReason">
<el-input
v-model="form.tripReason"
type="textarea"
placeholder="请输入出差事由"
:disabled="isFormDisabled"
maxlength="800"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<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"
placeholder="由明细自动计算"
:disabled="true"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<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"
placeholder="由明细自动计算"
:disabled="true"
style="width: 100%"
/>
</el-form-item>
@ -261,6 +143,232 @@
</el-form>
</el-card>
<!-- 行程明细 -->
<el-card shadow="never" style="margin-top: 10px">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span>行程明细</span>
<el-button type="primary" plain icon="Plus" size="small" @click="handleAddDetail" v-if="!isFormDisabled"></el-button>
</div>
</template>
<el-table
:data="form.crmBusinessTripDetailsList"
border
style="width: 100%"
:row-key="(row) => row.tripDetailsId || row.tempId"
:expand-row-keys="expandedRowKeys"
>
<el-table-column type="expand">
<template #default="scope">
<div style="padding: 10px 20px">
<el-row :gutter="20">
<!-- 交流对象 / 客户 (Type 2 场景) -->
<el-col :span="12" v-if="form.tripType === '2'">
<el-form-item label="交流对象">
<el-select
v-model="scope.row.customerId"
placeholder="请选择客户"
filterable
clearable
:disabled="isFormDisabled"
style="width: 100%"
>
<el-option
v-for="customer in customerList"
:key="customer.customerId"
:label="customer.customerName"
:value="customer.customerId"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 对应项目 (Type 1) -->
<el-col :span="12" v-if="form.tripType === '1'">
<el-form-item label="对应项目">
<el-input
v-model="scope.row.projectName"
placeholder="请选择项目"
readonly
:disabled="isFormDisabled"
@click="openDetailProjectSelect(scope.$index)"
>
<template #suffix>
<el-icon style="cursor: pointer" @click.stop="openDetailProjectSelect(scope.$index)"><Search /></el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<!-- 项目号 (Type 1) -->
<el-col :span="12" v-if="form.tripType === '1'">
<el-form-item label="项目号">
<el-input v-model="scope.row.projectCode" placeholder="选择项目后自动显示" disabled />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.tripType === '3'">
<el-form-item label="会议/展会名称">
<el-input v-model="scope.row.meetingName" placeholder="会议/展会名称" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.tripType === '2'">
<el-form-item label="交流目的">
<el-input v-model="scope.row.exchangePurpose" placeholder="交流目的" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="24" v-if="(form.tripType === '2' || form.tripType === '3') && canViewFeedback">
<el-form-item label="交流过程与反馈">
<el-input
v-model="scope.row.exchangeFeedback"
type="textarea"
placeholder="达成共识或反馈"
:rows="3"
:disabled="isFormDisabled"
/>
</el-form-item>
</el-col>
<!-- <el-col :span="24">
<el-form-item label="具体事由">
<el-input
v-model="scope.row.tripReason"
type="textarea"
placeholder="填写本次行程具体事由"
:rows="3"
:disabled="isFormDisabled"
/>
</el-form-item>
</el-col> -->
</el-row>
</div>
</template>
</el-table-column>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column prop="tripLocation">
<template #header> <span style="color: #f56c6c; margin-right: 4px">*</span>出差地点 </template>
<template #default="scope">
<el-form-item
:prop="'crmBusinessTripDetailsList.' + scope.$index + '.tripLocation'"
:rules="[
{
required: true,
validator: (rule, value, callback) => {
const item = form.crmBusinessTripDetailsList?.[scope.$index];
if (!item || !item.tripLocation || !item.tripLocation.trim()) {
callback(new Error('出差地点不能为空'));
} else {
callback();
}
},
trigger: ['blur', 'change']
}
]"
>
<el-input
v-model="scope.row.tripLocation"
placeholder="出差地点"
:disabled="isFormDisabled"
@input="refreshValidate(scope.$index, 'tripLocation')"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="startTime" width="170">
<template #header> <span style="color: #f56c6c; margin-right: 4px">*</span>开始日期 </template>
<template #default="scope">
<el-form-item
:prop="'crmBusinessTripDetailsList.' + scope.$index + '.startTime'"
:rules="[
{
required: true,
validator: (rule, value, callback) => {
const item = form.crmBusinessTripDetailsList?.[scope.$index];
if (!item || !item.startTime) {
callback(new Error('开始日期不能为空'));
} else {
callback();
}
},
trigger: ['blur', 'change']
}
]"
>
<el-date-picker
v-model="scope.row.startTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="开始"
:disabled="isFormDisabled"
@change="
() => {
calculateDetailDuration(scope.row);
refreshValidate(scope.$index, 'startTime');
}
"
style="width: 100%"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="endTime" width="170">
<template #header> <span style="color: #f56c6c; margin-right: 4px">*</span>结束日期 </template>
<template #default="scope">
<el-form-item
:prop="'crmBusinessTripDetailsList.' + scope.$index + '.endTime'"
:rules="[
{
required: true,
validator: (rule, value, callback) => {
const item = form.crmBusinessTripDetailsList?.[scope.$index];
if (!item || !item.endTime) {
callback(new Error('结束日期不能为空'));
} else {
callback();
}
},
trigger: ['blur', 'change']
}
]"
>
<el-date-picker
v-model="scope.row.endTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="结束"
:disabled="isFormDisabled"
@change="
() => {
calculateDetailDuration(scope.row);
refreshValidate(scope.$index, 'endTime');
}
"
style="width: 100%"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="时长(天)" prop="durationDays" width="90">
<template #default="scope">
<el-input-number v-model="scope.row.durationDays" :precision="1" :step="0.5" :min="0" disabled style="width: 100%" :controls="false" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right" v-if="!isFormDisabled">
<template #default="scope">
<el-button link type="danger" icon="Delete" @click="handleDeleteDetail(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 提交审批组件 -->
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 审批记录 -->
@ -287,6 +395,7 @@ 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 { useUserStore } from '@/store/modules/user';
import { listUser } from '@/api/system/user'; // API
import { allListDept } from '@/api/system/dept'; // API
import { getCrmCustomerInfoList } from '@/api/oa/crm/customerInfo'; // API
@ -323,6 +432,33 @@ const submitFormData = ref<StartProcessBo>({
});
const taskVariables = ref<Record<string, any>>({});
// ID
const BUSINESS_DIRECTION_LEADER_MAP: Record<number, string> = {
1: '1985254821554475009',
2: '1985258519835889666',
3: '1985251968270127105',
4: '1985257496048226305',
5: '1985254145713688578'
};
//
const canViewFeedback = computed(() => {
const currentUserId = String(useUserStore().userId);
// 1.
if (form.value.applicantId && currentUserId === String(form.value.applicantId)) return true;
// 2. userList
const fixedNames = ['陈海军', '张东辉'];
const fixedIds = fixedNames
.map((name) => userList.value.find((u: any) => u.nickName === name))
.filter(Boolean)
.map((u: any) => String(u.userId));
if (fixedIds.includes(currentUserId)) return true;
// 3.
const bd = Number(form.value.businessDirection);
if (bd && BUSINESS_DIRECTION_LEADER_MAP[bd] && currentUserId === BUSINESS_DIRECTION_LEADER_MAP[bd]) return true;
return false;
});
const initFormData: BusinessTripApplyForm = {
tripId: undefined,
applyCode: undefined,
@ -341,15 +477,14 @@ const initFormData: BusinessTripApplyForm = {
exchangeObject: undefined,
businessDirection: undefined,
exchangePurpose: undefined,
exchangeProcess: undefined,
meetingName: undefined,
feedback: undefined,
exchangeFeedback: undefined,
tripStatus: '1', //
flowStatus: 'draft',
remark: undefined,
// ossId: undefined,
variables: {}, // variables
copyUserIds: [] as string[] // ID
copyUserIds: [] as string[], // ID
crmBusinessTripDetailsList: [] //
};
const data = reactive({
@ -357,18 +492,14 @@ const data = reactive({
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' }],
customerId: [{ required: true, message: '请选择交流对象(客户)', trigger: 'change' }],
businessDirection: [{ required: true, message: '业务方向不能为空', trigger: 'change' }],
exchangePurpose: [{ required: true, message: '交流目的不能为空', trigger: 'blur' }],
exchangeProcess: [{ required: true, message: '交流过程简述不能为空', trigger: 'blur' }],
feedback: [{ required: true, message: '结果反馈不能为空', trigger: 'blur' }],
exchangeFeedback: [{ required: true, message: '交流过程与反馈不能为空', trigger: 'blur' }],
meetingName: [{ required: true, message: '会议/展会名称不能为空', trigger: 'blur' }],
'variables.approverId': [
{
@ -389,7 +520,17 @@ const data = reactive({
const { form, rules } = toRefs(data);
const isFormDisabled = computed(() => {
return routeParams.value.type === 'view' || routeParams.value.type === 'approval';
if (routeParams.value.type === 'view') {
return true;
}
if (routeParams.value.type === 'approval') {
//
if (String(useUserStore().userId) === String(form.value.applicantId)) {
return false; //
}
return true; //
}
return false;
});
//
@ -484,6 +625,36 @@ onMounted(async () => {
setDefaultCopyUsers();
}
//
// @@businessTripCopyUsers@@
if (routeParams.value.type === 'approval' && String(useUserStore().userId) === String(form.value.applicantId)) {
const confirmCopyIds: string[] = [];
['张东辉', '陈海军'].forEach((name) => {
const matchedUser = userList.value.find((u: any) => u.nickName === name);
if (matchedUser) {
confirmCopyIds.push(String(matchedUser.userId));
}
});
// ID SpelRuleComponent.java
const BUSINESS_DIRECTION_TO_USERNAME: Record<number, string> = {
1: '1985254821554475009',
2: '1985258519835889666',
3: '1985251968270127105',
4: '1985257496048226305',
5: '1985254145713688578'
};
const bd = Number(form.value.businessDirection);
if (bd && BUSINESS_DIRECTION_TO_USERNAME[bd]) {
confirmCopyIds.push(BUSINESS_DIRECTION_TO_USERNAME[bd]);
}
//
form.value.copyUserIds = Array.from(new Set([...(form.value.copyUserIds || []), ...confirmCopyIds]));
//
taskVariables.value.businessTripCopyUsers = form.value.copyUserIds.join(',');
}
// variables
if (!form.value.variables) {
form.value.variables = {};
@ -506,12 +677,30 @@ const calculateDuration = () => {
} else {
form.value.durationDays = 0;
}
} else {
form.value.durationDays = undefined;
}
};
//
const refreshValidate = (index: number, field: string) => {
setTimeout(() => {
businessTripApplyFormRef.value?.clearValidate(`crmBusinessTripDetailsList.${index}.${field}`);
}, 0);
};
//
const currentEditRowIndex = ref<number>(-1);
const openProjectSelect = () => {
if (isFormDisabled.value) return;
currentEditRowIndex.value = -1; // -1
projectSelectRef.value?.open();
};
const openDetailProjectSelect = (index: number) => {
if (isFormDisabled.value) return;
currentEditRowIndex.value = index; //
projectSelectRef.value?.open();
};
@ -519,22 +708,124 @@ const openProjectSelect = () => {
const projectInfoSelectCallBack = (data: ProjectInfoVO[]) => {
if (data && data.length > 0) {
const selectedProject = data[0];
form.value.projectId = selectedProject.projectId;
form.value.projectName = selectedProject.projectName;
form.value.projectCode = selectedProject.projectCode;
if (currentEditRowIndex.value === -1) {
//
form.value.projectId = selectedProject.projectId;
form.value.projectName = selectedProject.projectName;
form.value.projectCode = selectedProject.projectCode;
} else {
//
const index = currentEditRowIndex.value;
if (form.value.crmBusinessTripDetailsList[index]) {
form.value.crmBusinessTripDetailsList[index].projectId = selectedProject.projectId;
form.value.crmBusinessTripDetailsList[index].projectName = selectedProject.projectName;
form.value.crmBusinessTripDetailsList[index].projectCode = selectedProject.projectCode; //
//
if (form.value.tripType === '1' && selectedProject.managerId) {
if (!form.value.variables) {
form.value.variables = {};
//
if (form.value.tripType === '1' && selectedProject.managerId) {
if (!form.value.variables) {
form.value.variables = {};
}
form.value.variables.approverId = String(selectedProject.managerId);
handleApproverSelectChange(String(selectedProject.managerId));
proxy?.$modal.msgSuccess('已关联项目经理作为下一步审批人');
}
}
form.value.variables.approverId = String(selectedProject.managerId);
handleApproverSelectChange(String(selectedProject.managerId));
proxy?.$modal.msgSuccess('已关联项目经理作为下一步审批人');
}
}
};
// ================== ==================
const handleAddDetail = () => {
form.value.crmBusinessTripDetailsList.push({
tempId: Date.now() + Math.random(),
tripLocation: undefined,
startTime: undefined,
endTime: undefined,
durationDays: undefined,
projectId: undefined,
projectName: undefined,
customerId: undefined,
meetingName: undefined,
exchangePurpose: undefined,
exchangeFeedback: undefined,
tripReason: undefined,
remark: undefined
});
};
//
const expandedRowKeys = computed(() => {
return (form.value.crmBusinessTripDetailsList || []).map((item: any) => item.tripDetailsId || item.tempId);
});
const handleDeleteDetail = (index: number) => {
form.value.crmBusinessTripDetailsList.splice(index, 1);
};
const calculateDetailDuration = (row: any) => {
if (row.startTime && row.endTime) {
const start = dayjs(row.startTime);
const end = dayjs(row.endTime);
const diff = end.diff(start, 'day');
if (diff >= 0) {
row.durationDays = diff + 1;
} else {
row.durationDays = 0;
}
}
};
//
watch(
() => form.value.crmBusinessTripDetailsList,
(newList) => {
if (newList && newList.length > 0) {
let totalDays = 0;
let minStart = newList[0].startTime;
let maxEnd = newList[0].endTime;
newList.forEach((item) => {
// 1.
if (item.startTime && item.endTime) {
const start = dayjs(item.startTime);
const end = dayjs(item.endTime);
const diff = end.diff(start, 'day');
item.durationDays = diff >= 0 ? diff + 1 : 0;
} else {
item.durationDays = 0;
}
// 2.
if (item.durationDays) {
totalDays += Number(item.durationDays);
}
if (item.startTime && (!minStart || dayjs(item.startTime).isBefore(dayjs(minStart)))) {
minStart = item.startTime;
}
if (item.endTime && (!maxEnd || dayjs(item.endTime).isAfter(dayjs(maxEnd)))) {
maxEnd = item.endTime;
}
});
form.value.durationDays = totalDays;
form.value.startTime = minStart;
form.value.endTime = maxEnd;
//
form.value.tripLocation = newList[0].tripLocation;
form.value.customerId = newList[0].customerId;
form.value.exchangePurpose = newList[0].exchangePurpose;
form.value.exchangeFeedback = newList[0].exchangeFeedback;
form.value.meetingName = newList[0].meetingName;
form.value.projectId = newList[0].projectId;
} else {
form.value.durationDays = 0;
form.value.startTime = undefined;
form.value.endTime = undefined;
}
},
{ deep: true }
);
//
const handleApproverSelectChange = (val: any) => {
const user = userList.value.find((u) => u.userId === val);
@ -564,9 +855,43 @@ const submitForm = (status: string, mode: boolean) => {
});
};
const aggregateDetailsToForm = (): boolean => {
const details = form.value.crmBusinessTripDetailsList;
if (!details || details.length === 0) {
proxy?.$modal.msgError('请添加至少一条行程明细!');
return false;
}
let totalDays = 0;
let minStart = details[0].startTime;
let maxEnd = details[0].endTime;
form.value.tripLocation = details[0].tripLocation; //
details.forEach((item) => {
if (item.durationDays) {
totalDays += Number(item.durationDays);
}
if (item.startTime && (!minStart || dayjs(item.startTime).isBefore(dayjs(minStart)))) {
minStart = item.startTime;
}
if (item.endTime && (!maxEnd || dayjs(item.endTime).isAfter(dayjs(maxEnd)))) {
maxEnd = item.endTime;
}
});
form.value.durationDays = totalDays;
form.value.startTime = minStart;
form.value.endTime = maxEnd;
return true;
};
const executeSubmit = async (status: string, mode: boolean) => {
buttonLoading.value = true;
try {
if (!aggregateDetailsToForm()) {
buttonLoading.value = false;
return;
}
if (status !== 'draft') {
//
form.value.tripStatus = '2'; //
@ -615,6 +940,24 @@ const handleApprovalRecord = () => {
//
const approvalVerifyOpen = async () => {
if (!isFormDisabled.value) {
let isValid = false;
await businessTripApplyFormRef.value?.validate((valid) => {
isValid = valid;
});
if (!isValid) return;
if (!aggregateDetailsToForm()) return;
buttonLoading.value = true;
try {
if (form.value.tripId) {
await updateBusinessTripApply(form.value);
}
} finally {
buttonLoading.value = false;
}
}
await submitVerifyRef.value?.openDialog(routeParams.value.taskId);
};

@ -119,9 +119,13 @@
</template>
</el-table-column>
<el-table-column label="交流目的" align="center" prop="exchangePurpose" width="150" show-overflow-tooltip v-if="columns[17].visible" />
<el-table-column label="交流过程简述" align="center" prop="exchangeProcess" width="200" show-overflow-tooltip v-if="columns[18].visible" />
<el-table-column label="会议/展会名称" align="center" prop="meetingName" width="150" show-overflow-tooltip v-if="columns[19].visible" />
<el-table-column label="结果反馈" align="center" prop="feedback" width="200" show-overflow-tooltip v-if="columns[20].visible" />
<el-table-column label="交流过程与反馈" align="center" prop="exchangeFeedback" width="200" show-overflow-tooltip v-if="columns[20].visible">
<template #default="scope">
<span v-if="canViewRowFeedback(scope.row)">{{ scope.row.exchangeFeedback }}</span>
<span v-else style="color: #c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="申请状态" align="center" prop="tripStatus" width="100" v-if="columns[21].visible">
<template #default="scope">
<dict-tag :options="trip_status" :value="scope.row.tripStatus" />
@ -145,7 +149,7 @@
<el-tooltip
content="填写反馈"
placement="top"
v-if="scope.row.tripStatus === '3' && (scope.row.tripType === '2' || scope.row.tripType === '3')"
v-if="scope.row.tripStatus === '3' && (scope.row.tripType === '2' || scope.row.tripType === '3') && canViewRowFeedback(scope.row)"
>
<el-button
link
@ -180,11 +184,8 @@
<el-form-item label="出差类型">
<dict-tag :options="trip_type" :value="feedbackForm.tripType" />
</el-form-item>
<el-form-item label="交流过程简述" prop="exchangeProcess" v-if="feedbackForm.tripType === '2'">
<el-input v-model="feedbackForm.exchangeProcess" type="textarea" placeholder="如与某某客户某某负责人交谈参观XX等" :rows="4" />
</el-form-item>
<el-form-item label="结果反馈" prop="feedback">
<el-input v-model="feedbackForm.feedback" type="textarea" placeholder="达成某种共识或初步合作意向或会议成果" :rows="4" />
<el-form-item label="交流过程与反馈" prop="exchangeFeedback">
<el-input v-model="feedbackForm.exchangeFeedback" type="textarea" placeholder="达成某种共识或初步合作意向或会议成果" :rows="4" />
</el-form-item>
</el-form>
<template #footer>
@ -202,6 +203,7 @@ import { getUserList, listUser } from '@/api/system/user';
import { UserQuery, UserVO } from '@/api/system/user/types';
import { allListDept, listDept } from '@/api/system/dept';
import { DeptVO } from '@/api/system/dept/types';
import { useUserStore } from '@/store/modules/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { trip_status, trip_type, business_direction } = toRefs<any>(proxy?.useDict('trip_status', 'trip_type', 'business_direction'));
@ -217,6 +219,32 @@ const router = useRouter();
const userList = ref<UserVO[]>([]);
const deptList = ref<DeptVO[]>([]);
// ID
const BUSINESS_DIRECTION_LEADER_MAP: Record<number, string> = {
1: '1985254821554475009',
2: '1985258519835889666',
3: '1985251968270127105',
4: '1985257496048226305',
5: '1985254145713688578'
};
//
const canViewRowFeedback = (row: BusinessTripApplyVO): boolean => {
const currentUserId = String(useUserStore().userId);
// 1.
if (row.applicantId && currentUserId === String(row.applicantId)) return true;
// 2.
const fixedIds = ['陈海军', '张东辉']
.map(name => userList.value.find((u: any) => u.nickName === name))
.filter(Boolean)
.map((u: any) => String(u.userId));
if (fixedIds.includes(currentUserId)) return true;
// 3.
const bd = Number(row.businessDirection);
if (bd && BUSINESS_DIRECTION_LEADER_MAP[bd] && currentUserId === BUSINESS_DIRECTION_LEADER_MAP[bd]) return true;
return false;
};
const queryFormRef = ref<ElFormInstance>();
//
@ -239,9 +267,9 @@ const columns = ref<FieldOption[]>([
{ key: 15, label: `交流对象`, visible: true },
{ key: 16, label: `业务方向`, visible: true },
{ key: 17, label: `业务方向`, visible: true },
{ key: 18, label: `交流过程简述`, visible: true },
{ key: 18, label: `交流过程简述`, visible: false },
{ key: 19, label: `会议/展会名称`, visible: true },
{ key: 20, label: `结果反馈`, visible: true },
{ key: 20, label: `交流过程与反馈`, visible: true },
{ key: 21, label: `申请状态`, visible: true },
{ key: 22, label: `流程状态`, visible: false },
{ key: 23, label: `备注`, visible: true },
@ -380,11 +408,10 @@ const feedbackForm = ref<{
startTime?: string;
endTime?: string;
durationDays?: number;
exchangeProcess?: string;
feedback?: string;
exchangeFeedback?: string;
}>({});
const feedbackRules = {
feedback: [{ required: true, message: '请填写结果反馈', trigger: 'blur' }]
exchangeFeedback: [{ required: true, message: '请填写交流过程与反馈', trigger: 'blur' }]
};
/** 填写反馈按钮操作 */
@ -398,8 +425,7 @@ const handleFeedback = (row: BusinessTripApplyVO) => {
startTime: row.startTime,
endTime: row.endTime,
durationDays: row.durationDays,
exchangeProcess: row.exchangeProcess || '',
feedback: row.feedback || ''
exchangeFeedback: row.exchangeFeedback || ''
};
feedbackDialogVisible.value = true;
};
@ -417,8 +443,7 @@ const submitFeedback = async () => {
startTime: feedbackForm.value.startTime,
endTime: feedbackForm.value.endTime,
durationDays: feedbackForm.value.durationDays,
exchangeProcess: feedbackForm.value.exchangeProcess,
feedback: feedbackForm.value.feedback
exchangeFeedback: feedbackForm.value.exchangeFeedback
});
proxy?.$modal.msgSuccess('反馈提交成功');
feedbackDialogVisible.value = false;

Loading…
Cancel
Save