index.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981
  1. <template>
  2. <div class="tenant">
  3. <div class="content">
  4. <byTable
  5. :source="sourceList.data"
  6. :pagination="sourceList.pagination"
  7. :config="config"
  8. :loading="loading"
  9. :selectConfig="selectConfig"
  10. highlight-current-row
  11. :action-list="[
  12. {
  13. text: '添加客户',
  14. action: () => openModal(),
  15. },
  16. ]"
  17. @get-list="getList">
  18. <template #isTop="{ item }">
  19. <div>
  20. <img style="cursor: pointer; width: 20px; transform: translateY(5px)" :src="'/img/isTop.png'" @click="deleteTop(item)" v-if="item.isTop === 1" />
  21. <img style="cursor: pointer; width: 20px; transform: translateY(5px)" :src="'/img/noTop.png'" @click="addTop(item)" v-else />
  22. </div>
  23. </template>
  24. <template #address="{ item }">
  25. <span>{{ item.countryName }}</span>
  26. <span v-if="item.provinceName"> ,{{ item.provinceName }}</span>
  27. <span v-if="item.cityName"> ,{{ item.cityName }}</span>
  28. </template>
  29. <template #name="{ item }">
  30. <div style="cursor: pointer; color: #409eff" @click="handleClickName(item)">
  31. {{ item.name }}
  32. </div>
  33. </template>
  34. <template #tags="{ item }">
  35. <div style="width: 100%">
  36. <el-tag style="margin-right: 8px" type="success" v-for="(tag, index) in item.tag" closable :key="index" @close="tagClose(tag, item)">
  37. {{ dictValueLabel(tag, customerTag) }}
  38. </el-tag>
  39. <el-select
  40. v-if="item.addTagShow"
  41. v-model="addTag"
  42. style="width: 100%"
  43. @change="
  44. (val) => {
  45. return changeTag(val, item);
  46. }
  47. ">
  48. <el-option v-for="tag in customerTag" :key="tag.value" :label="tag.label" :value="tag.value" :disabled="judgeTagSelect(item.tag, tag.value)" />
  49. </el-select>
  50. <el-tag style="cursor: pointer" type="success" @click="showSelect(item)" v-else> + </el-tag>
  51. </div>
  52. </template>
  53. <template #follow="{ item }">
  54. <div :class="'getWidth' + item.id" style="width: 100%">
  55. <div style="width: 100%; display: flex">
  56. <template v-if="item.customerFollowRecordsList && item.customerFollowRecordsList.length > 0">
  57. <div
  58. :style="
  59. index > 2
  60. ? 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer; display: none'
  61. : 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer'
  62. "
  63. v-for="(record, index) in item.customerFollowRecordsList"
  64. :key="record.id">
  65. <el-popover placement="bottom" :width="300" trigger="hover" @show="recordShow(record)">
  66. <template #reference>
  67. <div>
  68. <span>{{ item.createTime.substr(0, 10) }}</span>
  69. <el-icon style="margin-left: 8px; transform: translateY(2px)" @click="deleteFollow(record)"><DeleteFilled /></el-icon>
  70. </div>
  71. </template>
  72. <template #default>
  73. <div style="width: 100%">
  74. <div style="color: #909399; margin: 8px 0">跟进时间: {{ record.createTime }}</div>
  75. <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
  76. <div v-else>跟进记录:</div>
  77. <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
  78. <div style="width: 36px">附件:</div>
  79. <div style="width: calc(100% - 36px)">
  80. <div v-for="(file, index) in record.fileList" :key="index">
  81. <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. </template>
  87. </el-popover>
  88. </div>
  89. <div
  90. style="line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer"
  91. @click="clickMore(item)"
  92. v-if="item.customerFollowRecordsList.length >= 3">
  93. 更多
  94. </div>
  95. </template>
  96. </div>
  97. </div>
  98. </template>
  99. </byTable>
  100. </div>
  101. <el-dialog :title="modalType == 'add' ? '新增' : '编辑'" v-if="dialogVisible" v-model="dialogVisible" width="800" v-loading="loadingOperation">
  102. <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="submit">
  103. <template #address>
  104. <el-row style="width: 100%">
  105. <el-col :span="8">
  106. <el-form-item prop="countryId">
  107. <el-select v-model="formData.data.countryId" placeholder="国家" @change="(val) => getCityData(val, '20', true)">
  108. <el-option v-for="item in countryData" :label="item.chineseName" :value="item.id"> </el-option>
  109. </el-select>
  110. </el-form-item>
  111. </el-col>
  112. <el-col :span="8">
  113. <el-form-item prop="provinceId">
  114. <el-select v-model="formData.data.provinceId" placeholder="省/洲" @change="(val) => getCityData(val, '30', true)">
  115. <el-option v-for="item in provinceData" :label="item.name" :value="item.id"> </el-option>
  116. </el-select>
  117. </el-form-item>
  118. </el-col>
  119. <el-col :span="8">
  120. <el-form-item prop="cityId">
  121. <el-select v-model="formData.data.cityId" placeholder="城市">
  122. <el-option v-for="item in cityData" :label="item.name" :value="item.id"> </el-option>
  123. </el-select>
  124. </el-form-item>
  125. </el-col>
  126. </el-row>
  127. <el-row style="margin-top: 20px; width: 100%">
  128. <el-col :span="24">
  129. <el-form-item prop="address">
  130. <el-input v-model="formData.data.address" type="textarea"> </el-input>
  131. </el-form-item>
  132. </el-col>
  133. </el-row>
  134. </template>
  135. <template #person>
  136. <div style="width: 100%">
  137. <el-button type="primary" @click="clickAddPerson">添 加</el-button>
  138. <el-table :data="formData.data.customerUserList" style="width: 100%; margin-top: 16px">
  139. <el-table-column label="联系人" width="160">
  140. <template #default="{ row, $index }">
  141. <div style="width: 100%">
  142. <el-form-item :prop="'customerUserList.' + $index + '.name'" :rules="rules.name2" :inline-message="true">
  143. <el-input v-model="row.name" placeholder="请输入联系人" />
  144. </el-form-item>
  145. </div>
  146. </template>
  147. </el-table-column>
  148. <el-table-column label="电子邮箱">
  149. <template #default="{ row, $index }">
  150. <div style="width: 100%">
  151. <el-form-item :prop="'customerUserList.' + $index + '.email'" :rules="rules.email" :inline-message="true">
  152. <el-input v-model="row.email" placeholder="请输入电子邮箱" />
  153. </el-form-item>
  154. </div>
  155. </template>
  156. </el-table-column>
  157. <el-table-column align="center" label="操作" width="120" fixed="right">
  158. <template #default="{ row, $index }">
  159. <el-button type="primary" link @click="clickInformationMore(row, $index)">更多</el-button>
  160. <el-button type="primary" link @click="clickDelete($index)">删除</el-button>
  161. </template>
  162. </el-table-column>
  163. </el-table>
  164. </div>
  165. </template>
  166. </byForm>
  167. <template #footer>
  168. <el-button @click="dialogVisible = false" size="large">取 消</el-button>
  169. <el-button type="primary" @click="submitForm()" size="large" :loading="submitLoading">确 定</el-button>
  170. </template>
  171. </el-dialog>
  172. <el-dialog title="更多联系方式" v-model="openPerson" width="700">
  173. <el-form :label-position="'top'" :model="formPerson.data" :rules="rulesPerson" ref="person">
  174. <el-form-item label="联系人" prop="name">
  175. <el-input v-model="formPerson.data.name" />
  176. </el-form-item>
  177. <el-form-item label="电子邮箱" prop="email">
  178. <el-input v-model="formPerson.data.email" />
  179. </el-form-item>
  180. <el-form-item label="更多联系方式">
  181. <div style="width: 100%">
  182. <el-button type="primary" @click="clickAddMoreInformation">添 加</el-button>
  183. <el-table :data="formPerson.data.contact" style="width: 100%; margin-top: 16px">
  184. <el-table-column label="类型" width="180">
  185. <template #default="{ row, $index }">
  186. <div style="width: 100%">
  187. <el-form-item :prop="'contact.' + $index + '.type'" :rules="rulesPerson.type" :inline-message="true">
  188. <el-select v-model="row.type" placeholder="请选择类型" style="width: 100%">
  189. <el-option v-for="item in contactType" :key="item.value" :label="item.label" :value="item.value" />
  190. </el-select>
  191. </el-form-item>
  192. </div>
  193. </template>
  194. </el-table-column>
  195. <el-table-column label="联系号码">
  196. <template #default="{ row, $index }">
  197. <div style="width: 100%">
  198. <el-form-item :prop="'contact.' + $index + '.contactNo'" :rules="rulesPerson.contactNo" :inline-message="true">
  199. <el-input v-model="row.contactNo" placeholder="请输入联系号码" />
  200. </el-form-item>
  201. </div>
  202. </template>
  203. </el-table-column>
  204. <el-table-column align="center" label="操作" width="120" fixed="right">
  205. <template #default="{ $index }">
  206. <el-button type="primary" link @click="clickInformationDelete($index)">删除</el-button>
  207. </template>
  208. </el-table-column>
  209. </el-table>
  210. </div>
  211. </el-form-item>
  212. </el-form>
  213. <template #footer>
  214. <el-button @click="openPerson = false" size="large">取 消</el-button>
  215. <el-button type="primary" @click="submitPerson()" size="large">确 定</el-button>
  216. </template>
  217. </el-dialog>
  218. <el-dialog title="添加跟进记录" v-if="openFollow" v-model="openFollow" width="500" destroy-on-close>
  219. <byForm :formConfig="formConfigAFollow" :formOption="formOption" v-model="formFollow.data" :rules="rulesFollow" ref="follow">
  220. <template #fileSlot>
  221. <div style="width: 100%">
  222. <el-upload
  223. v-model:fileList="fileList"
  224. action="https://winfaster.obs.cn-south-1.myhuaweicloud.com"
  225. :data="uploadData"
  226. multiple
  227. :before-upload="uploadFile"
  228. :on-preview="onPreviewFile">
  229. <el-button type="primary">文件上传</el-button>
  230. </el-upload>
  231. </div>
  232. </template>
  233. </byForm>
  234. <template #footer>
  235. <el-button @click="openFollow = false" size="large">取 消</el-button>
  236. <el-button type="primary" @click="submitFollow()" size="large">确 定</el-button>
  237. </template>
  238. </el-dialog>
  239. <el-dialog title="跟进记录" v-if="openRecordMore" v-model="openRecordMore" width="800" destroy-on-close>
  240. <div>
  241. <div style="padding: 8px 0">
  242. <el-button type="primary" @click="clickFollowUp(rowData)" plain>添加跟进记录</el-button>
  243. </div>
  244. <div style="padding-top: 16px">
  245. <div v-infinite-scroll="infiniteScroll" class="infinite-scroll" :infinite-scroll-disabled="judgeTotal()">
  246. <el-timeline>
  247. <el-timeline-item v-for="(record, index) in recordList" :key="index" :timestamp="record.createTime" hide-timestamp>
  248. <div>
  249. <div style="padding: 0 0 8px 0; display: flex; justify-content: space-between">
  250. <span>{{ dictValueLabel(record.createUser, userList) }}</span>
  251. <span>{{ record.createTime }}</span>
  252. </div>
  253. <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
  254. <div style="margin: 8px 0" v-else>跟进记录:</div>
  255. <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
  256. <div style="width: 36px">附件:</div>
  257. <div style="width: calc(100% - 36px)">
  258. <div v-for="(file, index) in record.fileList" :key="index">
  259. <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
  260. </div>
  261. </div>
  262. </div>
  263. </div>
  264. </el-timeline-item>
  265. </el-timeline>
  266. </div>
  267. </div>
  268. </div>
  269. <template #footer>
  270. <el-button @click="openRecordMore = false" size="large">关 闭</el-button>
  271. </template>
  272. </el-dialog>
  273. </div>
  274. </template>
  275. <script setup>
  276. import { ElMessage, ElMessageBox } from "element-plus";
  277. import byTable from "@/components/byTable/index";
  278. import byForm from "@/components/byForm/index";
  279. import { computed, ref } from "vue";
  280. import useUserStore from "@/store/modules/user";
  281. const { proxy } = getCurrentInstance();
  282. const loading = ref(false);
  283. const loadingOperation = ref(false);
  284. const submitLoading = ref(false);
  285. const openPerson = ref(false);
  286. const customerTag = ref([]);
  287. const customerSource = ref([]);
  288. const customerStatus = ref([]);
  289. const contactType = ref([]);
  290. const userList = ref([]);
  291. const fileList = ref([]);
  292. const uploadData = ref({});
  293. const sourceList = ref({
  294. data: [],
  295. pagination: {
  296. total: 0,
  297. pageNum: 1,
  298. pageSize: 10,
  299. status: "",
  300. source: "",
  301. type: "",
  302. },
  303. });
  304. const selectConfig = computed(() => {
  305. return [
  306. {
  307. label: "客户状态",
  308. prop: "type",
  309. data: [
  310. {
  311. label: "公海",
  312. value: "0",
  313. },
  314. {
  315. label: "私海",
  316. value: "1",
  317. },
  318. ],
  319. },
  320. {
  321. label: "客户来源",
  322. prop: "source",
  323. data: customerSource.value,
  324. },
  325. {
  326. label: "客户类型",
  327. prop: "status",
  328. data: customerStatus.value,
  329. },
  330. ];
  331. });
  332. const config = computed(() => {
  333. return [
  334. {
  335. attrs: {
  336. label: "",
  337. slot: "isTop",
  338. fixed: "left",
  339. width: 60,
  340. align: "center",
  341. },
  342. },
  343. {
  344. attrs: {
  345. label: "客户名称",
  346. prop: "name",
  347. slot: "name",
  348. fixed: "left",
  349. width: 160,
  350. },
  351. },
  352. {
  353. attrs: {
  354. label: "所在城市",
  355. slot: "address",
  356. width: 160,
  357. },
  358. },
  359. {
  360. attrs: {
  361. label: "客户代码",
  362. prop: "customerCode",
  363. width: 120,
  364. },
  365. },
  366. {
  367. attrs: {
  368. label: "客户来源",
  369. prop: "source",
  370. width: 120,
  371. },
  372. render(type) {
  373. return proxy.dictValueLabel(type, customerSource.value);
  374. },
  375. },
  376. {
  377. attrs: {
  378. label: "客户类型",
  379. prop: "status",
  380. width: 120,
  381. },
  382. render(type) {
  383. return proxy.dictValueLabel(type, customerStatus.value);
  384. },
  385. },
  386. {
  387. attrs: {
  388. label: "客户标签",
  389. slot: "tags",
  390. width: 180,
  391. },
  392. },
  393. {
  394. attrs: {
  395. label: "业务员",
  396. prop: "userId",
  397. width: 140,
  398. },
  399. render(type) {
  400. let data = userList.value.filter((item) => item.value == type);
  401. if (data && data.length > 0) {
  402. return data[0].label;
  403. } else {
  404. return "";
  405. }
  406. },
  407. },
  408. {
  409. attrs: {
  410. label: "跟进",
  411. slot: "follow",
  412. "min-width": 440,
  413. },
  414. },
  415. {
  416. attrs: {
  417. label: "操作",
  418. width: 140,
  419. align: "center",
  420. fixed: "right",
  421. },
  422. renderHTML(row) {
  423. return [
  424. {
  425. attrs: {
  426. label: "跟进",
  427. type: "primary",
  428. text: true,
  429. },
  430. el: "button",
  431. click() {
  432. clickFollowUp(row);
  433. },
  434. },
  435. {
  436. attrs: {
  437. label: "退回公海",
  438. type: "primary",
  439. text: true,
  440. },
  441. el: "button",
  442. click() {
  443. ElMessageBox.confirm("是否确定将该客户退回公海?", "提示", {
  444. confirmButtonText: "确定",
  445. cancelButtonText: "取消",
  446. type: "warning",
  447. }).then(() => {
  448. proxy
  449. .post("/customer/CustomerAllocation", {
  450. id: row.id,
  451. userId: "",
  452. })
  453. .then(() => {
  454. ElMessage({
  455. message: "退回成功",
  456. type: "success",
  457. });
  458. getList();
  459. });
  460. });
  461. },
  462. },
  463. ];
  464. },
  465. },
  466. ];
  467. });
  468. let modalType = ref("add");
  469. let dialogVisible = ref(false);
  470. let openFollow = ref(false);
  471. let formData = reactive({
  472. data: {
  473. countryId: "China",
  474. },
  475. });
  476. let formPerson = reactive({
  477. data: {},
  478. });
  479. let formAllocation = reactive({
  480. data: {},
  481. });
  482. let formFollow = reactive({
  483. data: {},
  484. });
  485. const formOption = reactive({
  486. inline: true,
  487. labelWidth: 100,
  488. itemWidth: 100,
  489. rules: [],
  490. });
  491. const formConfig = computed(() => {
  492. return [
  493. {
  494. type: "input",
  495. prop: "name",
  496. label: "客户名称",
  497. required: true,
  498. itemWidth: 100,
  499. itemType: "text",
  500. },
  501. {
  502. type: "slot",
  503. slotName: "address",
  504. prop: "countryId",
  505. label: "详细地址",
  506. },
  507. {
  508. type: "input",
  509. prop: "customerCode",
  510. label: "客户代码",
  511. required: true,
  512. itemWidth: 100,
  513. itemType: "text",
  514. },
  515. {
  516. type: "select",
  517. label: "客户来源",
  518. prop: "source",
  519. itemWidth: 50,
  520. data: customerSource.value,
  521. },
  522. {
  523. type: "select",
  524. label: "客户类型",
  525. prop: "status",
  526. itemWidth: 50,
  527. data: customerStatus.value,
  528. },
  529. {
  530. type: "select",
  531. label: "业务员",
  532. prop: "userId",
  533. itemWidth: 100,
  534. data: userList.value,
  535. disabled: true,
  536. },
  537. {
  538. type: "select",
  539. label: "客户标签",
  540. prop: "tags",
  541. itemWidth: 100,
  542. multiple: true,
  543. data: customerTag.value,
  544. style: {
  545. width: "100%",
  546. },
  547. },
  548. {
  549. type: "slot",
  550. slotName: "person",
  551. label: "客户联系人",
  552. },
  553. ];
  554. });
  555. let rules = ref({
  556. name: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
  557. name2: [{ required: true, message: "请输入联系人", trigger: "blur" }],
  558. email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
  559. countryId: [{ required: true, message: "请选择国家", trigger: "change" }],
  560. provinceId: [{ required: true, message: "请选择省/州", trigger: "change" }],
  561. cityId: [{ required: true, message: "请选择城市", trigger: "change" }],
  562. source: [{ required: true, message: "请选择客户来源", trigger: "change" }],
  563. status: [{ required: true, message: "请选择类型", trigger: "change" }],
  564. });
  565. const formConfigAFollow = computed(() => {
  566. return [
  567. {
  568. type: "date",
  569. itemType: "datetime",
  570. label: "跟进时间",
  571. prop: "date",
  572. itemWidth: 100,
  573. },
  574. {
  575. type: "input",
  576. itemType: "textarea",
  577. label: "跟进内容",
  578. prop: "content",
  579. itemWidth: 100,
  580. },
  581. {
  582. type: "slot",
  583. label: "上传附件",
  584. prop: "fileList",
  585. slotName: "fileSlot",
  586. },
  587. ];
  588. });
  589. let rulesPerson = ref({
  590. name: [{ required: true, message: "请输入联系人", trigger: "blur" }],
  591. email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
  592. type: [{ required: true, message: "请选择类型", trigger: "change" }],
  593. contactNo: [{ required: true, message: "请输入联系号码", trigger: "blur" }],
  594. });
  595. let rulesFollow = ref({
  596. date: [{ required: true, message: "请选择跟进时间", trigger: "change" }],
  597. content: [{ required: true, message: "请输入跟进内容", trigger: "blur" }],
  598. });
  599. const submit = ref(null);
  600. const person = ref(null);
  601. const allocation = ref(null);
  602. const follow = ref(null);
  603. const getList = async (req) => {
  604. sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
  605. loading.value = true;
  606. proxy.post("customer/privateSeaPage", sourceList.value.pagination).then((res) => {
  607. res.rows.forEach((x) => {
  608. x.addTagShow = false;
  609. if (x.tag) {
  610. x.tag = x.tag.split(",");
  611. } else {
  612. x.tag = [];
  613. }
  614. });
  615. sourceList.value.data = res.rows;
  616. sourceList.value.pagination.total = res.total;
  617. setTimeout(() => {
  618. loading.value = false;
  619. }, 200);
  620. });
  621. };
  622. const openModal = () => {
  623. modalType.value = "add";
  624. formData.data = {
  625. countryId: "China",
  626. userId: useUserStore().user.userId,
  627. tags: [],
  628. };
  629. getCityData(formData.data.countryId, "20");
  630. loadingOperation.value = false;
  631. dialogVisible.value = true;
  632. };
  633. const countryData = ref([]);
  634. const provinceData = ref([]);
  635. const cityData = ref([]);
  636. const getCityData = (id, type, isChange) => {
  637. proxy.post("/areaInfo/list", { parentId: id }).then((res) => {
  638. if (type === "20") {
  639. provinceData.value = res;
  640. if (isChange) {
  641. formData.data.provinceId = "";
  642. formData.data.cityId = "";
  643. }
  644. } else if (type === "30") {
  645. cityData.value = res;
  646. if (isChange) {
  647. formData.data.cityId = "";
  648. }
  649. } else {
  650. countryData.value = res;
  651. }
  652. });
  653. };
  654. getCityData("0");
  655. const clickAddPerson = () => {
  656. if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
  657. formData.data.customerUserList.push({
  658. name: "",
  659. email: "",
  660. });
  661. } else {
  662. formData.data.customerUserList = [
  663. {
  664. name: "",
  665. email: "",
  666. },
  667. ];
  668. }
  669. };
  670. const submitFollow = () => {
  671. follow.value.handleSubmit(() => {
  672. if (fileList.value && fileList.value.length > 0) {
  673. formFollow.data.fileList = fileList.value.map((item) => {
  674. return {
  675. id: item.raw.id,
  676. fileName: item.raw.fileName,
  677. fileUrl: item.raw.fileUrl,
  678. };
  679. });
  680. } else {
  681. formFollow.data.fileList = [];
  682. }
  683. proxy.post("/customerFollowRecords/" + modalType.value, formFollow.data).then(
  684. () => {
  685. ElMessage({
  686. message: modalType.value == "add" ? "添加成功" : "编辑成功",
  687. type: "success",
  688. });
  689. openFollow.value = false;
  690. getList();
  691. },
  692. (err) => {
  693. console.log(err);
  694. }
  695. );
  696. });
  697. };
  698. const submitForm = () => {
  699. submit.value.handleSubmit(() => {
  700. if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
  701. formData.data.tag = formData.data.tags.join(",");
  702. submitLoading.value = true;
  703. proxy.post("/customer/" + modalType.value, formData.data).then(
  704. () => {
  705. ElMessage({
  706. message: modalType.value == "add" ? "添加成功" : "编辑成功",
  707. type: "success",
  708. });
  709. dialogVisible.value = false;
  710. submitLoading.value = false;
  711. getList();
  712. },
  713. (err) => {
  714. console.log(err);
  715. submitLoading.value = false;
  716. }
  717. );
  718. } else {
  719. ElMessage("请添加客户联系人");
  720. }
  721. });
  722. };
  723. const getDict = () => {
  724. proxy.getDictOne(["customer_tag", "customer_source", "customer_status", "contact_type"]).then((res) => {
  725. customerTag.value = res["customer_tag"].map((x) => ({
  726. label: x.dictValue,
  727. value: x.dictKey,
  728. }));
  729. customerSource.value = res["customer_source"].map((x) => ({
  730. label: x.dictValue,
  731. value: x.dictKey,
  732. }));
  733. customerStatus.value = res["customer_status"].map((x) => ({
  734. label: x.dictValue,
  735. value: x.dictKey,
  736. }));
  737. contactType.value = res["contact_type"].map((x) => ({
  738. label: x.dictValue,
  739. value: x.dictKey,
  740. }));
  741. });
  742. proxy
  743. .get("/tenantUser/list", {
  744. pageNum: 1,
  745. pageSize: 10000,
  746. tenantId: useUserStore().user.tenantId,
  747. })
  748. .then((res) => {
  749. userList.value = res.rows.map((item) => {
  750. return {
  751. label: item.nickName,
  752. value: item.userId,
  753. };
  754. });
  755. });
  756. };
  757. getDict();
  758. getList();
  759. const handleClickName = (row) => {
  760. proxy.$router.push({
  761. path: "/customer/customer/portrait",
  762. query: {
  763. id: row.id,
  764. },
  765. });
  766. };
  767. const deleteFollow = (data) => {
  768. ElMessageBox.confirm("是否确认删除该跟进?", "提示", {
  769. confirmButtonText: "确定",
  770. cancelButtonText: "取消",
  771. type: "warning",
  772. }).then(() => {
  773. proxy
  774. .post("/customerFollowRecords/delete", {
  775. id: data.id,
  776. })
  777. .then(() => {
  778. ElMessage({
  779. message: "删除成功",
  780. type: "success",
  781. });
  782. getList();
  783. });
  784. });
  785. };
  786. const addTag = ref("");
  787. const judgeTagSelect = (data, val) => {
  788. if (data && data.length > 0) {
  789. if (data.includes(val)) {
  790. return true;
  791. }
  792. }
  793. return false;
  794. };
  795. const changeTag = (val, item) => {
  796. let data = {
  797. id: item.id,
  798. tag: JSON.parse(JSON.stringify(item.tag)),
  799. };
  800. data.tag.push(val);
  801. data.tag = data.tag.join(",");
  802. proxy.post("/customer/editTag", data).then(() => {
  803. ElMessage({
  804. message: "添加成功",
  805. type: "success",
  806. });
  807. item.addTagShow = false;
  808. addTag.value = "";
  809. getList();
  810. });
  811. };
  812. const tagClose = (val, item) => {
  813. let data = {
  814. id: item.id,
  815. tag: JSON.parse(JSON.stringify(item.tag)),
  816. };
  817. data.tag = data.tag.filter((row) => row !== val);
  818. if (data.tag && data.tag.length > 0) {
  819. data.tag = data.tag.join(",");
  820. } else {
  821. data.tag = "";
  822. }
  823. proxy.post("/customer/editTag", data).then(() => {
  824. ElMessage({
  825. message: "添加成功",
  826. type: "success",
  827. });
  828. item.addTagShow = false;
  829. addTag.value = "";
  830. getList();
  831. });
  832. };
  833. const showSelect = (item) => {
  834. item.addTagShow = true;
  835. };
  836. const clickFollowUp = (item) => {
  837. formFollow.data = {
  838. customerId: item.id,
  839. fileList: [],
  840. };
  841. fileList.value = [];
  842. modalType.value = "add";
  843. openFollow.value = true;
  844. openRecordMore.value = false;
  845. };
  846. const uploadFile = async (file) => {
  847. const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
  848. uploadData.value = res.uploadBody;
  849. file.id = res.id;
  850. file.fileName = res.fileName;
  851. file.fileUrl = res.fileUrl;
  852. return true;
  853. };
  854. const onPreviewFile = (file) => {
  855. window.open(file.raw.fileUrl, "_blank");
  856. };
  857. const getStyle = (val) => {
  858. if (val) {
  859. return "跟进记录: " + val.replace(/\n|\r\n/g, "<br>");
  860. } else {
  861. return "";
  862. }
  863. };
  864. const recordShow = (item) => {
  865. if (!(item.fileList && item.fileList.length > 0)) {
  866. proxy.post("/fileInfo/getList", { businessIdList: [item.id] }).then((fileObj) => {
  867. item.fileList = fileObj[item.id] || [];
  868. });
  869. }
  870. };
  871. const openFile = (path) => {
  872. window.open(path, "_blank");
  873. };
  874. const recordList = ref([]);
  875. const rowData = ref({});
  876. const openRecordMore = ref(false);
  877. const queryParams = ref({
  878. total: 0,
  879. pageNum: 1,
  880. pageSize: 10,
  881. customerId: "",
  882. });
  883. const clickMore = (item) => {
  884. if (openRecordMore.value === false) {
  885. recordList.value = [];
  886. rowData.value = item;
  887. queryParams.value.customerId = item.id;
  888. }
  889. proxy.post("/customerFollowRecords/page", queryParams.value).then((res) => {
  890. recordList.value = recordList.value.concat(res.rows);
  891. queryParams.value.total = res.total;
  892. proxy.post("/fileInfo/getList", { businessIdList: res.rows.map((rows) => rows.id) }).then((fileObj) => {
  893. for (let i = 0; i < res.rows.length; i++) {
  894. recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].fileList =
  895. fileObj[recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].id] || [];
  896. }
  897. });
  898. });
  899. openRecordMore.value = true;
  900. };
  901. const infiniteScroll = () => {
  902. queryParams.value.pageNum++;
  903. clickMore();
  904. };
  905. const judgeTotal = () => {
  906. if (queryParams.value.pageNum * queryParams.value.pageSize >= queryParams.value.total) {
  907. return true;
  908. }
  909. return false;
  910. };
  911. const moreIndex = ref(0);
  912. const clickInformationMore = (item, index) => {
  913. moreIndex.value = index;
  914. if (item.contactJson) {
  915. item.contact = JSON.parse(item.contactJson);
  916. } else {
  917. item.contact = [];
  918. }
  919. formPerson.data = JSON.parse(JSON.stringify(item));
  920. openPerson.value = true;
  921. };
  922. const clickDelete = (index) => {
  923. formData.data.customerUserList.splice(index, 1);
  924. };
  925. const clickAddMoreInformation = () => {
  926. if (formPerson.data.contact && formPerson.data.contact.length > 0) {
  927. formPerson.data.contact.push({
  928. type: "",
  929. contactNo: "",
  930. });
  931. } else {
  932. formPerson.data.contact = [
  933. {
  934. type: "",
  935. contactNo: "",
  936. },
  937. ];
  938. }
  939. };
  940. const clickInformationDelete = (index) => {
  941. formPerson.data.contact.splice(index, 1);
  942. };
  943. const submitPerson = () => {
  944. person.value.validate((valid) => {
  945. if (valid) {
  946. formPerson.data.contactJson = JSON.stringify(formPerson.data.contact);
  947. formData.data.customerUserList[moreIndex.value] = formPerson.data;
  948. openPerson.value = false;
  949. }
  950. });
  951. };
  952. const deleteTop = (item) => {
  953. proxy.post("/customerTop/delete", { customerId: item.id }).then(() => {
  954. item.isTop = 0;
  955. });
  956. };
  957. const addTop = (item) => {
  958. proxy.post("/customerTop/add", { customerId: item.id }).then(() => {
  959. item.isTop = 1;
  960. });
  961. };
  962. </script>
  963. <style lang="scss" scoped>
  964. .tenant {
  965. padding: 20px;
  966. }
  967. .infinite-scroll {
  968. max-height: calc(89vh - 94px - 70px - 58px - 16px);
  969. overflow-y: auto;
  970. &::-webkit-scrollbar {
  971. width: 0px;
  972. }
  973. }
  974. </style>