|
|
|
|
@ -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);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|