add.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <template>
  2. <!-- 新增拜访计划抽屉 -->
  3. <el-drawer title="新增拜访计划" :model-value="modelValue" @update:model-value="val => $emit('update:modelValue', val)" size="80%" direction="rtl" destroy-on-close class="plan-drawer">
  4. <div class="dialog-content-area">
  5. <!-- 基本信息 -->
  6. <div class="section-container">
  7. <div class="section-title">基本信息</div>
  8. <div class="section-content">
  9. <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="basic-info-form">
  10. <el-row :gutter="20">
  11. <el-col :span="8">
  12. <el-form-item label="拜访人:" prop="visitorId">
  13. <el-select v-model="form.visitorId" placeholder="请选择" style="width: 100%" clearable filterable @change="handleStaffChange">
  14. <el-option v-for="item in staffOptions" :key="item.staffId" :label="item.staffName" :value="item.staffId" />
  15. </el-select>
  16. </el-form-item>
  17. </el-col>
  18. <el-col :span="8">
  19. <el-form-item label="开始时间:" prop="startTime">
  20. <el-date-picker v-model="form.startTime" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
  21. </el-form-item>
  22. </el-col>
  23. <el-col :span="8">
  24. <el-form-item label="结束时间:" prop="endTime">
  25. <el-date-picker v-model="form.endTime" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
  26. </el-form-item>
  27. </el-col>
  28. </el-row>
  29. </el-form>
  30. </div>
  31. </div>
  32. <!-- 日程信息 -->
  33. <div class="section-container">
  34. <div class="section-header">
  35. <div class="section-title">日程信息</div>
  36. <el-button link type="primary" icon="Plus" @click="handleAddSchedule">新建</el-button>
  37. </div>
  38. <el-table :data="form.detailList" border class="schedule-table" :header-cell-style="{ background: '#f8fafc', color: '#333', fontWeight: 'normal' }">
  39. <el-table-column label="拜访日期" align="center" prop="callDate" width="150" sortable>
  40. <template #default="scope">
  41. <span>{{ parseTime(scope.row.callDate, '{y}-{m}-{d}') }}</span>
  42. </template>
  43. </el-table-column>
  44. <el-table-column label="客户名称" align="center" prop="customerName" min-width="150" />
  45. <el-table-column label="关联对象" align="center" prop="objectName" min-width="150" />
  46. <el-table-column label="关联类型" align="center" prop="relevanceType" width="120">
  47. <template #default="scope">
  48. {{ scope.row.relevanceType === 0 ? '客户' : (scope.row.relevanceType === 1 ? '项目商机' : '年度入围') }}
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="紧要程度" align="center" prop="importantLevel" width="100">
  52. <template #default="scope">
  53. {{ getUrgencyLabel(scope.row.importantLevel) }}
  54. </template>
  55. </el-table-column>
  56. <el-table-column label="拜访目的" align="center" prop="purposeVisit" min-width="150" show-overflow-tooltip />
  57. <el-table-column label="操作" align="center" width="120">
  58. <template #default="scope">
  59. <el-button link type="primary" @click="handleEditSchedule(scope.$index, scope.row)">编辑</el-button>
  60. <el-button link type="danger" @click="handleDeleteSchedule(scope.$index)">删除</el-button>
  61. </template>
  62. </el-table-column>
  63. <template #empty>
  64. <div style="padding: 30px 0; color: #909399;">暂无数据</div>
  65. </template>
  66. </el-table>
  67. </div>
  68. </div>
  69. <template #footer>
  70. <div class="drawer-footer">
  71. <el-button type="primary" @click="submitForm" :loading="submitting">确认新增</el-button>
  72. <el-button @click="cancel">取 消</el-button>
  73. </div>
  74. </template>
  75. <!-- 新建日程对话框 -->
  76. <el-dialog :title="scheduleTitle" v-model="scheduleOpen" width="600px" append-to-body destroy-on-close class="custom-dialog">
  77. <el-form ref="scheduleFormRef" :model="scheduleForm" :rules="scheduleRules" label-width="100px" label-position="right">
  78. <el-form-item label="关联类型" prop="relevanceType">
  79. <el-radio-group v-model="scheduleForm.relevanceType">
  80. <el-radio :label="0">客户</el-radio>
  81. <el-radio :label="1">项目商机</el-radio>
  82. <el-radio :label="2">年度入围</el-radio>
  83. </el-radio-group>
  84. </el-form-item>
  85. <el-form-item :label="scheduleTypeLabel" prop="customerName">
  86. <el-select v-model="scheduleForm.customerName" placeholder="请选择" style="width: 100%" filterable>
  87. <el-option v-for="item in currentOptions" :key="item.id || item.customerNo" :label="item.projectName || item.customerName" :value="item.projectName || item.customerName" />
  88. </el-select>
  89. </el-form-item>
  90. <el-row :gutter="20">
  91. <el-col :span="12">
  92. <el-form-item label="拜访日期" prop="callDate">
  93. <el-date-picker v-model="scheduleForm.callDate" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
  94. </el-form-item>
  95. </el-col>
  96. <el-col :span="12">
  97. <el-form-item label="重要级别" prop="importantLevel">
  98. <el-select v-model="scheduleForm.importantLevel" placeholder="请选择" style="width: 100%">
  99. <el-option v-for="dict in urgencyOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
  100. </el-select>
  101. </el-form-item>
  102. </el-col>
  103. </el-row>
  104. <el-form-item label="随访人" prop="followPeopleName">
  105. <el-select v-model="scheduleForm.followPeopleName" placeholder="请选择" style="width: 100%" multiple clearable filterable collapse-tags>
  106. <el-option v-for="item in staffOptions" :key="item.staffId" :label="item.staffName" :value="item.staffName" />
  107. </el-select>
  108. </el-form-item>
  109. <el-form-item label="拜访目的" prop="purposeVisit">
  110. <el-input v-model="scheduleForm.purposeVisit" type="textarea" :rows="4" placeholder="请输入内容" />
  111. </el-form-item>
  112. </el-form>
  113. <template #footer>
  114. <div class="dialog-footer">
  115. <el-button type="primary" @click="submitSchedule" :loading="scheduleLoading">确 认</el-button>
  116. <el-button @click="scheduleOpen = false">取 消</el-button>
  117. </div>
  118. </template>
  119. </el-dialog>
  120. </el-drawer>
  121. </template>
  122. <script setup>
  123. import { ref, reactive, computed, watch, getCurrentInstance, onMounted, toRefs } from 'vue';
  124. import { addPlan } from "@/api/visit/plan";
  125. import { listComStaff } from "@/api/system/comStaff/index";
  126. import { listCustomerInfo } from "@/api/customer/customerInfo/index";
  127. import { listOpportunity } from "@/api/saleManage/opportunity";
  128. import { listProjectSelection } from "@/api/saleManage/projectSelection";
  129. const props = defineProps({
  130. modelValue: Boolean
  131. });
  132. const emit = defineEmits(['update:modelValue', 'success']);
  133. const proxy = getCurrentInstance().proxy;
  134. const { importance_level: urgencyOptions } = toRefs(reactive(proxy.useDict("importance_level")));
  135. const formRef = ref();
  136. const scheduleFormRef = ref();
  137. const staffOptions = ref([]);
  138. const customerOptions = ref([]);
  139. const opportunityOptions = ref([]);
  140. const annualOptions = ref([]);
  141. const scheduleOpen = ref(false);
  142. const scheduleTitle = ref("新建日程");
  143. const currentEditIndex = ref(-1);
  144. const scheduleForm = ref({
  145. relevanceType: 0,
  146. customerName: '',
  147. objectName: '',
  148. callDate: '',
  149. importantLevel: undefined,
  150. followPeopleName: [],
  151. purposeVisit: '',
  152. customerNo: ''
  153. });
  154. const currentOptions = computed(() => {
  155. if (scheduleForm.value.relevanceType === 1) return opportunityOptions.value;
  156. if (scheduleForm.value.relevanceType === 2) return annualOptions.value;
  157. return customerOptions.value;
  158. });
  159. const scheduleTypeLabel = computed(() => {
  160. if (scheduleForm.value.relevanceType === 1) return '项目商机';
  161. if (scheduleForm.value.relevanceType === 2) return '年度入围';
  162. return '客户';
  163. });
  164. watch(() => scheduleForm.value.relevanceType, () => {
  165. scheduleForm.value.customerName = '';
  166. scheduleForm.value.objectName = '';
  167. });
  168. watch(() => scheduleForm.value.customerName, (val) => {
  169. scheduleForm.value.objectName = val;
  170. if (val) {
  171. const item = currentOptions.value.find(i => (i.projectName || i.customerName) === val);
  172. if (item) {
  173. scheduleForm.value.customerNo = item.customerNo || item.projectNo || item.id;
  174. }
  175. }
  176. });
  177. const form = ref({
  178. visitorId: undefined,
  179. visitorName: '',
  180. startTime: '',
  181. endTime: '',
  182. detailList: []
  183. });
  184. const rules = {
  185. visitorId: [{ required: true, message: "请选择拜访人", trigger: "change" }],
  186. startTime: [{ required: true, message: "请选择开始时间", trigger: "change" }],
  187. endTime: [{ required: true, message: "请选择结束时间", trigger: "change" }]
  188. };
  189. const getUrgencyLabel = (val) => {
  190. const found = urgencyOptions.value.find(item => String(item.value) === String(val));
  191. return found ? found.label : (val || '--');
  192. };
  193. const scheduleRules = {
  194. customerName: [{ required: true, message: "请选择关联对象", trigger: "change" }],
  195. callDate: [{ required: true, message: "请选择拜访日期", trigger: "change" }],
  196. importantLevel: [{ required: true, message: "请选择紧要程度", trigger: "change" }],
  197. purposeVisit: [{ required: true, message: "请输入拜访目的", trigger: "blur" }]
  198. };
  199. /** 选择人员处理 */
  200. const handleStaffChange = (val) => {
  201. if (val) {
  202. const staff = staffOptions.value.find(item => item.staffId === val);
  203. if (staff) {
  204. form.value.visitorName = staff.staffName;
  205. }
  206. } else {
  207. form.value.visitorName = '';
  208. }
  209. };
  210. const handleAddSchedule = () => {
  211. scheduleForm.value = {
  212. relevanceType: 0,
  213. customerName: '',
  214. objectName: '',
  215. callDate: '',
  216. importantLevel: undefined,
  217. followPeopleName: [],
  218. purposeVisit: '',
  219. customerNo: ''
  220. };
  221. currentEditIndex.value = -1;
  222. scheduleTitle.value = "新建日程";
  223. scheduleOpen.value = true;
  224. };
  225. const handleEditSchedule = (index, row) => {
  226. const data = JSON.parse(JSON.stringify(row));
  227. if (data.followPeopleName && typeof data.followPeopleName === 'string') {
  228. data.followPeopleName = data.followPeopleName.split(',');
  229. } else if (!Array.isArray(data.followPeopleName)) {
  230. data.followPeopleName = [];
  231. }
  232. if (data.importantLevel !== undefined && data.importantLevel !== null) {
  233. data.importantLevel = String(data.importantLevel);
  234. }
  235. scheduleForm.value = data;
  236. currentEditIndex.value = index;
  237. scheduleTitle.value = "编辑日程";
  238. scheduleOpen.value = true;
  239. };
  240. const handleDeleteSchedule = (index) => {
  241. form.value.detailList.splice(index, 1);
  242. };
  243. const submitSchedule = () => {
  244. scheduleFormRef.value.validate((valid) => {
  245. if (valid) {
  246. scheduleLoading.value = true;
  247. const detail = {
  248. ...scheduleForm.value,
  249. followPeopleName: (scheduleForm.value.followPeopleName && scheduleForm.value.followPeopleName.length > 0) ? scheduleForm.value.followPeopleName.join(',') : ''
  250. };
  251. if (currentEditIndex.value === -1) {
  252. form.value.detailList.push(detail);
  253. } else {
  254. form.value.detailList[currentEditIndex.value] = detail;
  255. }
  256. scheduleOpen.value = false;
  257. scheduleLoading.value = false;
  258. }
  259. });
  260. };
  261. const submitForm = () => {
  262. formRef.value.validate((valid) => {
  263. if (valid) {
  264. submitting.value = true;
  265. addPlan(form.value).then(response => {
  266. proxy.$modal.msgSuccess("新增成功");
  267. emit('update:modelValue', false);
  268. emit('success');
  269. resetForm();
  270. }).finally(() => {
  271. submitting.value = false;
  272. });
  273. }
  274. });
  275. };
  276. const cancel = () => {
  277. emit('update:modelValue', false);
  278. resetForm();
  279. };
  280. const submitting = ref(false);
  281. const scheduleLoading = ref(false);
  282. const resetForm = () => {
  283. form.value = {
  284. visitorId: undefined,
  285. visitorName: '',
  286. startTime: '',
  287. endTime: '',
  288. detailList: []
  289. };
  290. };
  291. onMounted(() => {
  292. listComStaff({ pageSize: 1000 }).then(response => {
  293. staffOptions.value = response.rows || response.data || [];
  294. });
  295. listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(response => {
  296. customerOptions.value = response.rows || [];
  297. });
  298. listOpportunity({ pageSize: 500 }).then(response => {
  299. opportunityOptions.value = response.rows || [];
  300. });
  301. listProjectSelection({ pageSize: 500 }).then(response => {
  302. annualOptions.value = response.rows || [];
  303. });
  304. });
  305. </script>
  306. <style scoped lang="scss">
  307. .plan-drawer {
  308. :deep(.el-drawer__header) {
  309. border-bottom: 1px solid #f0f0f0;
  310. margin-bottom: 0;
  311. padding: 15px 20px;
  312. .el-drawer__title { font-size: 15px; color: #333; font-weight: normal; }
  313. }
  314. }
  315. .dialog-content-area { padding: 0 10px; }
  316. .section-container { margin-bottom: 20px; }
  317. .section-header {
  318. display: flex;
  319. justify-content: space-between;
  320. align-items: center;
  321. background-color: #f4f6fc;
  322. padding: 8px 15px;
  323. margin-bottom: 5px;
  324. }
  325. .section-title {
  326. font-size: 14px;
  327. color: #409eff;
  328. font-weight: normal;
  329. background-color: #f4f6fc;
  330. padding: 8px 15px;
  331. display: inline-block;
  332. width: 100%;
  333. }
  334. .basic-info-form :deep(.el-form-item__label),
  335. .schedule-dialog :deep(.el-form-item__label),
  336. .schedule-dialog :deep(.el-dialog__title),
  337. .schedule-dialog :deep(.el-button),
  338. .plan-drawer :deep(.el-form-item__label),
  339. .plan-drawer :deep(.el-drawer__title) {
  340. font-weight: normal !important;
  341. }
  342. .section-header .section-title { padding: 0; width: auto; }
  343. .section-content {
  344. padding: 15px;
  345. :deep(.el-form-item) {
  346. margin-bottom: 0;
  347. display: flex;
  348. align-items: center;
  349. }
  350. }
  351. .dialog-footer { text-align: right; padding: 20px 20px; }
  352. </style>
  353. <!-- 非作用域样式,用于覆盖 append-to-body 的弹窗 -->
  354. <style lang="scss">
  355. .schedule-dialog, .plan-drawer {
  356. .el-form-item__label,
  357. .el-dialog__title,
  358. .el-drawer__title,
  359. .el-button {
  360. font-weight: normal !important;
  361. }
  362. }
  363. </style>