edit.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. <template>
  2. <el-drawer
  3. v-model="visible"
  4. :title="form.id ? '编辑联系人' : '新建联系人'"
  5. size="80%"
  6. :modal="false"
  7. @close="handleClose"
  8. destroy-on-close
  9. class="contact-form-drawer"
  10. :with-header="false"
  11. >
  12. <div class="form-container" v-loading="loading">
  13. <div class="form-toolbar">
  14. <div class="toolbar-left">
  15. <span class="breadcrumb" v-if="form.contactNo">个人资料 / <span class="highlight">人员编号: {{ form.contactNo }}</span></span>
  16. <span class="breadcrumb" v-else>新建联系人</span>
  17. </div>
  18. <div class="toolbar-right">
  19. <el-button type="primary" icon="Check" @click="submitForm">保存</el-button>
  20. <el-button @click="handleClose">取消</el-button>
  21. </div>
  22. </div>
  23. <el-form ref="contactPersonRef" :model="form" :rules="rules" label-width="120px" label-position="right" label-suffix=":">
  24. <!-- 个人信息 -->
  25. <div class="form-section">
  26. <div class="section-title">个人信息</div>
  27. <el-row :gutter="20">
  28. <el-col :span="8">
  29. <el-form-item label="人员编号">
  30. <el-input :model-value="form.contactNo || ''" disabled />
  31. </el-form-item>
  32. </el-col>
  33. <el-col :span="8">
  34. <el-form-item label="姓名" prop="contactName">
  35. <el-input v-model="form.contactName" placeholder="请输入姓名" />
  36. </el-form-item>
  37. </el-col>
  38. <el-col :span="8">
  39. <el-form-item label="联系人类型" prop="type">
  40. <el-select v-model="form.type" placeholder="请选择" class="w100">
  41. <el-option label="公司职员" value="1" />
  42. <el-option label="关系资源人" value="2" />
  43. </el-select>
  44. </el-form-item>
  45. </el-col>
  46. </el-row>
  47. <el-row :gutter="20">
  48. <el-col :span="8">
  49. <el-form-item label="性别" prop="gender">
  50. <el-select v-model="form.gender" placeholder="请选择" class="w100">
  51. <el-option label="男" value="0" />
  52. <el-option label="女" value="1" />
  53. </el-select>
  54. </el-form-item>
  55. </el-col>
  56. <el-col :span="8">
  57. <el-form-item label="年龄" prop="age">
  58. <el-input v-model="form.age" placeholder="请输入" />
  59. </el-form-item>
  60. </el-col>
  61. <el-col :span="8">
  62. <el-form-item label="籍贯" prop="nativePlace">
  63. <el-input v-model="form.nativePlace" placeholder="请输入籍贯" />
  64. </el-form-item>
  65. </el-col>
  66. </el-row>
  67. <el-row :gutter="20">
  68. <el-col :span="8">
  69. <el-form-item label="生日" prop="birthday">
  70. <el-date-picker clearable
  71. v-model="form.birthday"
  72. type="date"
  73. value-format="YYYY-MM-DD"
  74. placeholder="请选择生日"
  75. style="width: 100%">
  76. </el-date-picker>
  77. </el-form-item>
  78. </el-col>
  79. </el-row>
  80. <el-row :gutter="20">
  81. <el-col :span="24">
  82. <el-form-item label="描述" prop="remark">
  83. <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" :rows="3" maxlength="200" show-word-limit />
  84. </el-form-item>
  85. </el-col>
  86. </el-row>
  87. <div class="section-title" style="margin-top: 10px;">办公信息</div>
  88. <el-row :gutter="20">
  89. <el-col :span="8">
  90. <el-form-item label="归属公司">
  91. <el-input :model-value="form.companyName || '优易达科技集团有限公司'" disabled />
  92. </el-form-item>
  93. </el-col>
  94. <el-col :span="8">
  95. <el-form-item label="客户名称">
  96. <el-input :model-value="form.customerName || ''" disabled />
  97. </el-form-item>
  98. </el-col>
  99. </el-row>
  100. <el-row :gutter="20">
  101. <el-col :span="8">
  102. <el-form-item label="在职状态" prop="jobStatus">
  103. <el-select v-model="form.jobStatus" placeholder="请选择" class="w100">
  104. <el-option label="在职" value="1" />
  105. <el-option label="离职" value="2" />
  106. </el-select>
  107. </el-form-item>
  108. </el-col>
  109. <el-col :span="8">
  110. <el-form-item label="手机号码" prop="phone">
  111. <el-input v-model="form.phone" placeholder="请输入手机号码" />
  112. </el-form-item>
  113. </el-col>
  114. <el-col :span="8">
  115. <el-form-item label="部门" prop="deptName">
  116. <el-input v-model="form.deptName" placeholder="请输入部门" />
  117. </el-form-item>
  118. </el-col>
  119. </el-row>
  120. <el-row :gutter="20">
  121. <el-col :span="8">
  122. <el-form-item label="职位" prop="position">
  123. <el-input v-model="form.position" placeholder="请输入职位" />
  124. </el-form-item>
  125. </el-col>
  126. <el-col :span="8">
  127. <el-form-item label="办公座机" prop="officePhone">
  128. <el-input v-model="form.officePhone" placeholder="请输入办公座机" />
  129. </el-form-item>
  130. </el-col>
  131. <el-col :span="8">
  132. <el-form-item label="办公地址" prop="addressDetail">
  133. <div style="display: flex; gap: 10px; width: 100%">
  134. <el-cascader
  135. v-model="form.region"
  136. ref="regionCascader"
  137. :options="areaOptions"
  138. :props="{ label: 'areaName', value: 'id', children: 'children' }"
  139. placeholder="请选择"
  140. style="width: 150px"
  141. clearable
  142. />
  143. <el-input v-model="form.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
  144. </div>
  145. </el-form-item>
  146. </el-col>
  147. </el-row>
  148. <el-row :gutter="20">
  149. <el-col :span="24">
  150. <el-form-item label="工作内容" prop="jobContent">
  151. <el-input v-model="form.jobContent" type="textarea" placeholder="请输入内容" :rows="2" maxlength="200" show-word-limit />
  152. </el-form-item>
  153. </el-col>
  154. </el-row>
  155. <el-row :gutter="20">
  156. <el-col :span="12">
  157. <el-form-item label="项目角色" prop="projectRole">
  158. <el-select v-model="form.projectRole" placeholder="请选择项目角色" class="w100" clearable>
  159. <el-option v-for="dict in projectRoleOptions" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
  160. </el-select>
  161. </el-form-item>
  162. </el-col>
  163. <el-col :span="12">
  164. <el-form-item label="是否关键人" prop="isKeyPerson">
  165. <el-select v-model="form.isKeyPerson" placeholder="请选择" class="w100" clearable>
  166. <el-option label="是" :value="1" />
  167. <el-option label="否" :value="0" />
  168. </el-select>
  169. </el-form-item>
  170. </el-col>
  171. </el-row>
  172. <el-row :gutter="20">
  173. <el-col :span="24">
  174. <el-form-item label="公关情况" prop="prStatus">
  175. <el-input v-model="form.prStatus" type="textarea" placeholder="请输入内容" :rows="2" maxlength="200" show-word-limit />
  176. </el-form-item>
  177. </el-col>
  178. </el-row>
  179. <div class="section-title" style="margin-top: 10px;">家庭信息</div>
  180. <el-row :gutter="20">
  181. <el-col :span="16">
  182. <el-form-item label="家庭住址" prop="homeAddressDetail">
  183. <div style="display: flex; gap: 10px; width: 100%">
  184. <el-cascader
  185. v-model="form.homeRegion"
  186. :options="areaOptions"
  187. :props="{ label: 'areaName', value: 'id', children: 'children' }"
  188. placeholder="请选择"
  189. style="width: 200px"
  190. clearable
  191. />
  192. <el-input v-model="form.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
  193. </div>
  194. </el-form-item>
  195. </el-col>
  196. </el-row>
  197. <el-row :gutter="20">
  198. <el-col :span="8">
  199. <el-form-item label="家庭情况" prop="familyStatus">
  200. <el-input v-model="form.familyStatus" placeholder="请输入家庭情况" />
  201. </el-form-item>
  202. </el-col>
  203. <el-col :span="8">
  204. <el-form-item label="爱好" prop="hobby">
  205. <el-input v-model="form.hobby" placeholder="请输入爱好" />
  206. </el-form-item>
  207. </el-col>
  208. <el-col :span="8">
  209. <el-form-item label="性格特征" prop="characterTrait">
  210. <el-input v-model="form.characterTrait" placeholder="请输入性格特征" />
  211. </el-form-item>
  212. </el-col>
  213. </el-row>
  214. <el-row :gutter="20">
  215. <el-col :span="8">
  216. <el-form-item label="是否抽烟" prop="isSmoke">
  217. <el-radio-group v-model="form.isSmoke">
  218. <el-radio value="1">是</el-radio>
  219. <el-radio value="0">否</el-radio>
  220. </el-radio-group>
  221. </el-form-item>
  222. </el-col>
  223. <el-col :span="8">
  224. <el-form-item label="是否喝酒" prop="isDrink">
  225. <el-radio-group v-model="form.isDrink">
  226. <el-radio value="1">是</el-radio>
  227. <el-radio value="0">否</el-radio>
  228. </el-radio-group>
  229. </el-form-item>
  230. </el-col>
  231. </el-row>
  232. </div>
  233. </el-form>
  234. </div>
  235. </el-drawer>
  236. </template>
  237. <script setup name="ContactPersonEdit">
  238. import { ref, reactive, toRefs, getCurrentInstance, onMounted } from 'vue';
  239. import { useRouter, useRoute } from 'vue-router';
  240. import { getContactPerson } from "@/api/customer/contactPerson";
  241. import { updateContact } from "@/api/customer/crmContact";
  242. import { listProvinceWithCities } from "@/api/customer/addressArea";
  243. const { proxy } = getCurrentInstance();
  244. const { LXRJE0001: projectRoleOptions } = proxy.useDict("LXRJE0001");
  245. const router = useRouter();
  246. const route = useRoute();
  247. const visible = ref(false);
  248. const loading = ref(false);
  249. const activeTab = ref("basic");
  250. const areaOptions = ref([]);
  251. const regionCascader = ref(null);
  252. const data = reactive({
  253. form: {
  254. id: null,
  255. customerId: null,
  256. contactNo: null,
  257. contactName: null,
  258. type: '1',
  259. gender: '0',
  260. age: null,
  261. nativePlace: null,
  262. birthday: null,
  263. jobStatus: '1',
  264. deptName: null,
  265. position: null,
  266. phone: null,
  267. officePhone: null,
  268. region: [],
  269. addressDetail: null,
  270. jobContent: null,
  271. projectRole: null,
  272. isKeyPerson: 0,
  273. prStatus: null,
  274. homeRegion: [],
  275. homeAddressDetail: null,
  276. familyStatus: null,
  277. hobby: null,
  278. characterTrait: null,
  279. isSmoke: '0',
  280. isDrink: '0',
  281. remark: null
  282. },
  283. rules: {
  284. contactName: [
  285. { required: true, message: "姓名不能为空", trigger: "blur" }
  286. ],
  287. type: [
  288. { required: true, message: "联系人类型不能为空", trigger: "change" }
  289. ],
  290. jobStatus: [
  291. { required: true, message: "在职状态不能为空", trigger: "change" }
  292. ],
  293. phone: [
  294. { required: true, message: "手机号码不能为空", trigger: "blur" }
  295. ],
  296. deptName: [
  297. { required: true, message: "部门不能为空", trigger: "blur" }
  298. ],
  299. position: [
  300. { required: true, message: "职位不能为空", trigger: "blur" }
  301. ]
  302. }
  303. });
  304. const { form, rules } = toRefs(data);
  305. const open = (id, customerId) => {
  306. reset();
  307. visible.value = true;
  308. if (id) {
  309. loadData(id);
  310. } else if (customerId) {
  311. form.value.customerId = customerId;
  312. }
  313. };
  314. /** 重置表单 */
  315. function reset() {
  316. form.value = {
  317. id: null,
  318. customerId: null,
  319. contactNo: null,
  320. contactName: null,
  321. type: '1',
  322. gender: '0',
  323. age: null,
  324. nativePlace: null,
  325. birthday: null,
  326. jobStatus: '1',
  327. deptName: null,
  328. position: null,
  329. phone: null,
  330. officePhone: null,
  331. region: [],
  332. addressDetail: null,
  333. jobContent: null,
  334. projectRole: null,
  335. isKeyPerson: 0,
  336. prStatus: null,
  337. homeRegion: [],
  338. homeAddressDetail: null,
  339. familyStatus: null,
  340. hobby: null,
  341. characterTrait: null,
  342. isSmoke: '0',
  343. isDrink: '0',
  344. remark: null
  345. };
  346. if (proxy.$refs["contactPersonRef"]) {
  347. proxy.$refs["contactPersonRef"].resetFields();
  348. }
  349. }
  350. const emit = defineEmits(['success']);
  351. /** 获取详情 */
  352. function loadData(id) {
  353. loading.value = true;
  354. getContactPerson(id).then(response => {
  355. const resData = response.data;
  356. // 区域反查逻辑 (适配代码转换)
  357. const findIdByCode = (code) => {
  358. if (!code) return null;
  359. const findInTree = (nodes, targetCode) => {
  360. for (const node of nodes) {
  361. if (String(node.areaCode) === String(targetCode)) return node.id;
  362. if (node.children) {
  363. const found = findInTree(node.children, targetCode);
  364. if (found) return found;
  365. }
  366. }
  367. return null;
  368. };
  369. return findInTree(areaOptions.value, code);
  370. };
  371. // 初始化办公地址 region (使用 addressProvince/City/County)
  372. resData.region = [];
  373. if (resData.addressProvince) {
  374. const pid = /^\d+$/.test(resData.addressProvince) && resData.addressProvince.length < 5 ? resData.addressProvince : findIdByCode(resData.addressProvince);
  375. if (pid) resData.region.push(Number(pid));
  376. }
  377. if (resData.addressCity) {
  378. const cid = /^\d+$/.test(resData.addressCity) && resData.addressCity.length < 7 ? resData.addressCity : findIdByCode(resData.addressCity);
  379. if (cid) resData.region.push(Number(cid));
  380. }
  381. if (resData.addressCounty) {
  382. const aid = /^\d+$/.test(resData.addressCounty) && resData.addressCounty.length < 9 ? resData.addressCounty : findIdByCode(resData.addressCounty);
  383. if (aid) resData.region.push(Number(aid));
  384. }
  385. // 初始化家庭地址 homeRegion
  386. resData.homeRegion = [];
  387. if (resData.homeProvinceId) resData.homeRegion.push(Number(resData.homeProvinceId));
  388. if (resData.homeCityId) resData.homeRegion.push(Number(resData.homeCityId));
  389. if (resData.homeAreaId) resData.homeRegion.push(Number(resData.homeAreaId));
  390. form.value = resData;
  391. loading.value = false;
  392. });
  393. }
  394. onMounted(async () => {
  395. const resArea = await listProvinceWithCities();
  396. const list = resArea.rows || [];
  397. if (list.length > 0) {
  398. areaOptions.value = handleTree(list, "id", "parentId");
  399. }
  400. });
  401. /** 构造树型结构数据 */
  402. function handleTree(data, id, parentId, children) {
  403. let config = {
  404. id: id || 'id',
  405. parentId: parentId || 'parentId',
  406. childrenList: children || 'children'
  407. };
  408. var childrenListMap = {};
  409. var nodeIds = {};
  410. var tree = [];
  411. for (let d of data) {
  412. let pId = d[config.parentId];
  413. if (childrenListMap[pId] == null) {
  414. childrenListMap[pId] = [];
  415. }
  416. nodeIds[d[config.id]] = d;
  417. childrenListMap[pId].push(d);
  418. }
  419. for (let d of data) {
  420. let pId = d[config.parentId];
  421. if (nodeIds[pId] == null) {
  422. tree.push(d);
  423. }
  424. }
  425. for (let t of tree) {
  426. adaptToChildrenList(t);
  427. }
  428. function adaptToChildrenList(o) {
  429. if (childrenListMap[o[config.id]] !== null) {
  430. o[config.childrenList] = childrenListMap[o[config.id]];
  431. }
  432. if (o[config.childrenList]) {
  433. for (let c of o[config.childrenList]) {
  434. adaptToChildrenList(c);
  435. }
  436. }
  437. }
  438. return tree;
  439. }
  440. /** 提交按钮 */
  441. function submitForm() {
  442. proxy.$refs["contactPersonRef"].validate(valid => {
  443. if (valid) {
  444. // 自动映射 roleId
  445. form.value.roleId = form.value.type === '1' ? 1 : 2;
  446. // 获取地址名称
  447. const checkedNodes = regionCascader.value?.getCheckedNodes();
  448. let provincialCityCounty = form.value.provincialCityCounty;
  449. if (checkedNodes && checkedNodes.length > 0) {
  450. provincialCityCounty = checkedNodes[0].pathLabels.join("");
  451. }
  452. // 获取选中的 AreaCode 列表 (用于办公地址转换)
  453. const findCodeById = (id) => {
  454. const findInTree = (nodes, targetId) => {
  455. for (const node of nodes) {
  456. if (Number(node.id) === Number(targetId)) return node.areaCode;
  457. if (node.children) {
  458. const found = findInTree(node.children, targetId);
  459. if (found) return found;
  460. }
  461. }
  462. return null;
  463. };
  464. return findInTree(areaOptions.value, id);
  465. };
  466. const payload = {
  467. ...form.value,
  468. // 办公地址 ID (Long)
  469. provinceId: form.value.region?.[0] || null,
  470. cityId: form.value.region?.[1] || null,
  471. districtId: form.value.region?.[2] || null,
  472. // 办公地址编码 (String) - 转换为后端需要的 AreaCode
  473. addressProvince: form.value.region?.[0] ? findCodeById(form.value.region[0]) : null,
  474. addressCity: form.value.region?.[1] ? findCodeById(form.value.region[1]) : null,
  475. addressCounty: form.value.region?.[2] ? findCodeById(form.value.region[2]) : null,
  476. provincialCityCounty: provincialCityCounty,
  477. // 家庭地址 ID
  478. homeProvinceId: form.value.homeRegion?.[0] || null,
  479. homeCityId: form.value.homeRegion?.[1] || null,
  480. homeAreaId: form.value.homeRegion?.[2] || null
  481. };
  482. updateContact(payload).then(response => {
  483. proxy.$modal.msgSuccess("修改成功");
  484. visible.value = false;
  485. emit('success');
  486. });
  487. }
  488. });
  489. }
  490. /** 关闭抽屉 */
  491. function handleClose() {
  492. visible.value = false;
  493. }
  494. defineExpose({ open });
  495. </script>
  496. <style lang="scss" scoped>
  497. .contact-form-drawer {
  498. :deep(.el-drawer__body) {
  499. padding: 0;
  500. }
  501. }
  502. .form-container {
  503. display: flex;
  504. flex-direction: column;
  505. height: 100%;
  506. background-color: #fff;
  507. }
  508. .form-toolbar {
  509. height: 56px;
  510. padding: 0 24px;
  511. background-color: #fff;
  512. border-bottom: 1px solid #ebeef5;
  513. display: flex;
  514. justify-content: space-between;
  515. align-items: center;
  516. flex-shrink: 0;
  517. .toolbar-left {
  518. .breadcrumb {
  519. font-size: 14px;
  520. color: #606266;
  521. .highlight {
  522. color: #409eff;
  523. font-weight: 500;
  524. }
  525. }
  526. }
  527. }
  528. .el-form {
  529. flex: 1;
  530. overflow-y: auto;
  531. padding: 24px;
  532. /* 隐藏滚动条 */
  533. &::-webkit-scrollbar {
  534. width: 0 !important;
  535. height: 0 !important;
  536. display: none !important;
  537. }
  538. -ms-overflow-style: none !important;
  539. scrollbar-width: none !important;
  540. }
  541. .form-section {
  542. padding: 0;
  543. margin-bottom: 0;
  544. .section-title {
  545. font-size: 16px;
  546. font-weight: 600;
  547. color: #303133;
  548. margin-bottom: 24px;
  549. display: flex;
  550. align-items: center;
  551. &::before {
  552. content: '';
  553. width: 4px;
  554. height: 16px;
  555. background-color: #409eff;
  556. margin-right: 8px;
  557. border-radius: 2px;
  558. }
  559. }
  560. }
  561. .w100 {
  562. width: 100%;
  563. }
  564. </style>