index.vue 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261
  1. <template>
  2. <div class="tenant">
  3. <div style="padding: 20px; background: #fff; margin-bottom: 20px">
  4. <el-button type="primary" style="margin-left: 10px" @click="openModal()">添加客户</el-button>
  5. </div>
  6. <div style="padding: 20px 20px 0 20px; background: #fff; margin-bottom: 20px">
  7. <div style="display: flex">
  8. <div style="font-size: 14px; cursor: pointer" class="by-dropdown">
  9. <div class="by-dropdown-title">
  10. <span>{{ dictValueLabel(sourceList.paginationTwo.statisticsType, statisticsType) }}</span>
  11. <el-icon style="margin-left: 5px; font-size: 16px"><CaretBottom /></el-icon>
  12. </div>
  13. <ul class="by-dropdown-lists">
  14. <li
  15. v-for="item in statisticsType"
  16. :key="item.value"
  17. @click="searchItemSelect(item.value)"
  18. style="display: flex; align-items: center; justify-content: center">
  19. {{ item.label }}
  20. </li>
  21. </ul>
  22. </div>
  23. </div>
  24. <div style="display: flex; width: 100%; margin: 10px 0 0 10px; flex-wrap: wrap">
  25. <div style="padding: 20px; border-radius: 10px; width: 200px; background-color: #d1caff59; margin: 0 20px 20px 0">
  26. <div style="margin-bottom: 10px; display: flex">
  27. <div style="width: 8px; height: 8px; background-color: #5bacff; border-radius: 50px; margin-top: 6px"></div>
  28. <span style="padding-left: 8px">合计</span>
  29. </div>
  30. <div style="color: black; font-size: 20px; font-weight: 700">{{ statisticalData.countAmount }}</div>
  31. </div>
  32. <template v-if="sourceList.paginationTwo.statisticsType === 1">
  33. <div
  34. style="padding: 20px; border-radius: 10px; width: 200px; background-color: #a2d8ff70; margin: 0 20px 20px 0"
  35. v-for="(item, index) in customerSource"
  36. :key="index">
  37. <div style="margin-bottom: 10px; display: flex">
  38. <div style="width: 8px; height: 8px; background-color: #5bacff; border-radius: 50px; margin-top: 6px"></div>
  39. <span style="padding-left: 8px">{{ item.label }}</span>
  40. </div>
  41. <div style="color: black; font-size: 20px; font-weight: 700">{{ getNum(item.value) }}</div>
  42. </div>
  43. </template>
  44. <template v-else-if="sourceList.paginationTwo.statisticsType === 2">
  45. <div
  46. style="padding: 20px; border-radius: 10px; width: 200px; background-color: #a2d8ff70; margin: 0 20px 20px 0"
  47. v-for="(item, index) in customerStatus"
  48. :key="index">
  49. <div style="margin-bottom: 10px; display: flex">
  50. <div style="width: 8px; height: 8px; background-color: #5bacff; border-radius: 50px; margin-top: 6px"></div>
  51. <span style="padding-left: 8px">{{ item.label }}</span>
  52. </div>
  53. <div style="color: black; font-size: 20px; font-weight: 700">{{ getNum(item.value) }}</div>
  54. </div>
  55. </template>
  56. <template v-else-if="sourceList.paginationTwo.statisticsType === 3">
  57. <div
  58. style="padding: 20px; border-radius: 10px; width: 200px; background-color: #a2d8ff70; margin: 0 20px 20px 0"
  59. v-for="(item, index) in userList"
  60. :key="index">
  61. <div style="margin-bottom: 10px; display: flex">
  62. <div style="width: 8px; height: 8px; background-color: #5bacff; border-radius: 50px; margin-top: 6px"></div>
  63. <span style="padding-left: 8px">{{ item.label }}</span>
  64. </div>
  65. <div style="color: black; font-size: 20px; font-weight: 700">{{ getNum(item.value) }}</div>
  66. </div>
  67. </template>
  68. </div>
  69. </div>
  70. <byTable
  71. :source="sourceList.data"
  72. :pagination="sourceList.pagination"
  73. :config="config"
  74. :loading="loading"
  75. :selectConfig="selectConfig"
  76. highlight-current-row
  77. @get-list="getList">
  78. <template #isTop="{ item }">
  79. <div>
  80. <img style="cursor: pointer; width: 20px; transform: translateY(5px)" :src="'/img/isTop.png'" @click="deleteTop(item)" v-if="item.isTop === 1" />
  81. <img style="cursor: pointer; width: 20px; transform: translateY(5px)" :src="'/img/noTop.png'" @click="addTop(item)" v-else />
  82. </div>
  83. </template>
  84. <template #address="{ item }">
  85. <span>{{ item.countryName }}</span>
  86. <span v-if="item.provinceName"> ,{{ item.provinceName }}</span>
  87. <span v-if="item.cityName"> ,{{ item.cityName }}</span>
  88. </template>
  89. <template #name="{ item }">
  90. <div style="cursor: pointer; color: #409eff" @click="handleClickName(item)">
  91. {{ item.name }}
  92. </div>
  93. </template>
  94. <template #tags="{ item }">
  95. <div style="width: 100%">
  96. <el-tag style="margin-right: 8px" type="success" v-for="(tag, index) in item.tag" closable :key="index" @close="tagClose(tag, item)">
  97. {{ dictValueLabel(tag, customerTag) }}
  98. </el-tag>
  99. <template v-if="item.tag.length !== customerTag.length">
  100. <el-select
  101. v-if="item.addTagShow"
  102. v-model="addTag"
  103. style="width: 100%"
  104. @change="
  105. (val) => {
  106. return changeTag(val, item);
  107. }
  108. ">
  109. <el-option v-for="tag in customerTag" :key="tag.value" :label="tag.label" :value="tag.value" :disabled="judgeTagSelect(item.tag, tag.value)" />
  110. </el-select>
  111. <el-tag style="cursor: pointer" type="success" @click="showSelect(item)" v-else> + </el-tag>
  112. </template>
  113. </div>
  114. </template>
  115. <template #follow="{ item }">
  116. <div :class="'getWidth' + item.id" style="width: 100%">
  117. <div style="width: 100%; display: flex">
  118. <template v-if="item.customerFollowRecordsList && item.customerFollowRecordsList.length > 0">
  119. <div
  120. :style="
  121. index > 2
  122. ? 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer; display: none'
  123. : 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer'
  124. "
  125. v-for="(record, index) in item.customerFollowRecordsList"
  126. :key="record.id">
  127. <el-popover placement="bottom" :width="300" trigger="hover" @show="recordShow(record)">
  128. <template #reference>
  129. <div>
  130. <span v-if="record.date">{{ record.date.substr(0, 10) }}</span>
  131. <el-icon style="margin-left: 8px; transform: translateY(2px)" @click="deleteFollow(record)"><DeleteFilled /></el-icon>
  132. </div>
  133. </template>
  134. <template #default>
  135. <div style="width: 100%">
  136. <div style="color: #909399; margin: 8px 0">跟进时间: {{ record.date }}</div>
  137. <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
  138. <div v-else>跟进记录:</div>
  139. <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
  140. <div style="width: 36px">附件:</div>
  141. <div style="width: calc(100% - 36px)">
  142. <div v-for="(file, index) in record.fileList" :key="index">
  143. <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. </template>
  149. </el-popover>
  150. </div>
  151. <div
  152. style="line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer"
  153. @click="clickMore(item)"
  154. v-if="item.customerFollowRecordsList.length >= 3">
  155. 更多
  156. </div>
  157. </template>
  158. </div>
  159. </div>
  160. </template>
  161. </byTable>
  162. <el-dialog :title="modalType == 'add' ? '新增' : '编辑'" v-if="dialogVisible" v-model="dialogVisible" width="800" v-loading="loadingOperation">
  163. <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="submit">
  164. <template #address>
  165. <el-row style="width: 100%">
  166. <el-col :span="8">
  167. <el-form-item prop="countryId">
  168. <el-select v-model="formData.data.countryId" placeholder="国家" @change="(val) => getCityData(val, '20', true)">
  169. <el-option v-for="item in countryData" :label="item.chineseName" :value="item.id"> </el-option>
  170. </el-select>
  171. </el-form-item>
  172. </el-col>
  173. <el-col :span="8">
  174. <el-form-item prop="provinceId">
  175. <el-select v-model="formData.data.provinceId" placeholder="省/洲" filterable allow-create @change="(val) => getCityData(val, '30', true)">
  176. <el-option v-for="item in provinceData" :label="item.name" :value="item.id"> </el-option>
  177. </el-select>
  178. </el-form-item>
  179. </el-col>
  180. <el-col :span="8">
  181. <el-form-item prop="cityId">
  182. <el-select v-model="formData.data.cityId" filterable allow-create placeholder="城市">
  183. <el-option v-for="item in cityData" :label="item.name" :value="item.id"> </el-option>
  184. </el-select>
  185. </el-form-item>
  186. </el-col>
  187. </el-row>
  188. <el-row style="margin-top: 20px; width: 100%">
  189. <el-col :span="24">
  190. <el-form-item prop="address">
  191. <el-input v-model="formData.data.address" type="textarea"> </el-input>
  192. </el-form-item>
  193. </el-col>
  194. </el-row>
  195. </template>
  196. <template #person>
  197. <div style="width: 100%">
  198. <el-button type="primary" @click="clickAddPerson">添 加</el-button>
  199. <el-table :data="formData.data.customerUserList" style="width: 100%; margin-top: 16px">
  200. <el-table-column label="联系人" width="160">
  201. <template #default="{ row, $index }">
  202. <div style="width: 100%">
  203. <el-form-item :prop="'customerUserList.' + $index + '.name'" :rules="rules.name2" :inline-message="true">
  204. <el-input v-model="row.name" placeholder="请输入联系人" />
  205. </el-form-item>
  206. </div>
  207. </template>
  208. </el-table-column>
  209. <el-table-column label="电子邮箱">
  210. <template #default="{ row, $index }">
  211. <div style="width: 100%">
  212. <el-form-item :prop="'customerUserList.' + $index + '.email'" :rules="rules.email" :inline-message="true">
  213. <el-input v-model="row.email" placeholder="请输入电子邮箱" />
  214. </el-form-item>
  215. </div>
  216. </template>
  217. </el-table-column>
  218. <el-table-column align="center" label="操作" width="120" fixed="right">
  219. <template #default="{ row, $index }">
  220. <el-button type="primary" link @click="clickInformationMore(row, $index)">更多</el-button>
  221. <el-button type="primary" link @click="clickDelete($index)">删除</el-button>
  222. </template>
  223. </el-table-column>
  224. </el-table>
  225. </div>
  226. </template>
  227. </byForm>
  228. <template #footer>
  229. <el-button @click="dialogVisible = false" size="large">取 消</el-button>
  230. <el-button type="primary" @click="submitForm()" size="large" :loading="submitLoading">确 定</el-button>
  231. </template>
  232. </el-dialog>
  233. <el-dialog title="更多联系方式" v-if="openPerson" v-model="openPerson" width="700">
  234. <el-form :label-position="'top'" :model="formPerson.data" :rules="rulesPerson" ref="person">
  235. <el-form-item label="联系人" prop="name">
  236. <el-input v-model="formPerson.data.name" />
  237. </el-form-item>
  238. <el-form-item label="电子邮箱" prop="email">
  239. <el-input v-model="formPerson.data.email" />
  240. </el-form-item>
  241. <el-form-item label="更多联系方式">
  242. <div style="width: 100%">
  243. <el-button type="primary" @click="clickAddMoreInformation">添 加</el-button>
  244. <el-table :data="formPerson.data.contact" style="width: 100%; margin-top: 16px">
  245. <el-table-column label="类型" width="180">
  246. <template #default="{ row, $index }">
  247. <div style="width: 100%">
  248. <el-form-item :prop="'contact.' + $index + '.type'" :rules="rulesPerson.type" :inline-message="true">
  249. <el-select v-model="row.type" placeholder="请选择类型" style="width: 100%">
  250. <el-option v-for="item in contactType" :key="item.value" :label="item.label" :value="item.value" />
  251. </el-select>
  252. </el-form-item>
  253. </div>
  254. </template>
  255. </el-table-column>
  256. <el-table-column label="联系号码">
  257. <template #default="{ row, $index }">
  258. <div style="width: 100%">
  259. <el-form-item :prop="'contact.' + $index + '.contactNo'" :rules="rulesPerson.contactNo" :inline-message="true">
  260. <el-input v-model="row.contactNo" placeholder="请输入联系号码" />
  261. </el-form-item>
  262. </div>
  263. </template>
  264. </el-table-column>
  265. <el-table-column align="center" label="操作" width="120" fixed="right">
  266. <template #default="{ $index }">
  267. <el-button type="primary" link @click="clickInformationDelete($index)">删除</el-button>
  268. </template>
  269. </el-table-column>
  270. </el-table>
  271. </div>
  272. </el-form-item>
  273. </el-form>
  274. <template #footer>
  275. <el-button @click="openPerson = false" size="large">取 消</el-button>
  276. <el-button type="primary" @click="submitPerson()" size="large">确 定</el-button>
  277. </template>
  278. </el-dialog>
  279. <el-dialog title="分配" v-if="openAllocation" v-model="openAllocation" width="300">
  280. <byForm :formConfig="formConfigAllocation" :formOption="formOption" v-model="formAllocation.data" :rules="rulesAllocation" ref="allocation"> </byForm>
  281. <template #footer>
  282. <el-button @click="openAllocation = false" size="large">取 消</el-button>
  283. <el-button type="primary" @click="submitAllocation()" size="large">确 定</el-button>
  284. </template>
  285. </el-dialog>
  286. <el-dialog title="添加跟进记录" v-if="openFollow" v-model="openFollow" width="500" destroy-on-close>
  287. <byForm :formConfig="formConfigAFollow" :formOption="formOption" v-model="formFollow.data" :rules="rulesFollow" ref="follow">
  288. <template #fileSlot>
  289. <div style="width: 100%">
  290. <el-upload
  291. v-model:fileList="fileList"
  292. action="https://winfaster.obs.cn-south-1.myhuaweicloud.com"
  293. :data="uploadData"
  294. multiple
  295. :before-upload="uploadFile"
  296. :on-preview="onPreviewFile">
  297. <el-button type="primary">文件上传</el-button>
  298. </el-upload>
  299. </div>
  300. </template>
  301. </byForm>
  302. <template #footer>
  303. <el-button @click="openFollow = false" size="large">取 消</el-button>
  304. <el-button type="primary" @click="submitFollow()" size="large">确 定</el-button>
  305. </template>
  306. </el-dialog>
  307. <el-dialog title="跟进记录" v-if="openRecordMore" v-model="openRecordMore" width="800" destroy-on-close>
  308. <div>
  309. <div style="padding: 8px 0">
  310. <el-button type="primary" @click="clickFollowUp(rowData)" plain>添加跟进记录</el-button>
  311. </div>
  312. <div style="padding-top: 16px">
  313. <div v-infinite-scroll="infiniteScroll" class="infinite-scroll" :infinite-scroll-disabled="judgeTotal()">
  314. <el-timeline>
  315. <el-timeline-item v-for="(record, index) in recordList" :key="index" :timestamp="record.date" hide-timestamp>
  316. <div>
  317. <div style="padding: 0 0 8px 0; display: flex; justify-content: space-between">
  318. <span>{{ dictValueLabel(record.createUser, userList) }}</span>
  319. <span>{{ record.date }}</span>
  320. </div>
  321. <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
  322. <div style="margin: 8px 0" v-else>跟进记录:</div>
  323. <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
  324. <div style="width: 36px">附件:</div>
  325. <div style="width: calc(100% - 36px)">
  326. <div v-for="(file, index) in record.fileList" :key="index">
  327. <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
  328. </div>
  329. </div>
  330. </div>
  331. </div>
  332. </el-timeline-item>
  333. </el-timeline>
  334. </div>
  335. </div>
  336. </div>
  337. <template #footer>
  338. <el-button @click="openRecordMore = false" size="large">关 闭</el-button>
  339. </template>
  340. </el-dialog>
  341. </div>
  342. </template>
  343. <script setup>
  344. import { ElMessage, ElMessageBox } from "element-plus";
  345. import byTable from "@/components/byTable/index";
  346. import byForm from "@/components/byForm/index";
  347. import { computed, ref } from "vue";
  348. import useUserStore from "@/store/modules/user";
  349. const { proxy } = getCurrentInstance();
  350. const loading = ref(false);
  351. const loadingOperation = ref(false);
  352. const submitLoading = ref(false);
  353. const openPerson = ref(false);
  354. const openAllocation = ref(false);
  355. const customerTag = ref([]);
  356. const customerSource = ref([]);
  357. const customerStatus = ref([]);
  358. const contactType = ref([]);
  359. const userList = ref([]);
  360. const fileList = ref([]);
  361. const uploadData = ref({});
  362. const statisticsType = ref([
  363. {
  364. label: "客户来源统计",
  365. value: 1,
  366. },
  367. {
  368. label: "客户类型统计",
  369. value: 2,
  370. },
  371. {
  372. label: "业务员统计",
  373. value: 3,
  374. },
  375. ]);
  376. const sourceList = ref({
  377. data: [],
  378. pagination: {
  379. total: 0,
  380. pageNum: 1,
  381. pageSize: 10,
  382. keyword: "",
  383. status: "",
  384. source: "",
  385. type: "",
  386. },
  387. paginationTwo: {
  388. statisticsType: 1,
  389. type: null,
  390. },
  391. });
  392. const selectConfig = computed(() => {
  393. return [
  394. {
  395. label: "客户状态",
  396. prop: "type",
  397. data: [
  398. {
  399. label: "公海",
  400. value: "0",
  401. },
  402. {
  403. label: "私海",
  404. value: "1",
  405. },
  406. ],
  407. },
  408. {
  409. label: "客户来源",
  410. prop: "source",
  411. data: customerSource.value,
  412. },
  413. {
  414. label: "客户类型",
  415. prop: "status",
  416. data: customerStatus.value,
  417. },
  418. ];
  419. });
  420. const config = computed(() => {
  421. return [
  422. {
  423. attrs: {
  424. label: "",
  425. slot: "isTop",
  426. fixed: "left",
  427. width: 60,
  428. align: "center",
  429. },
  430. },
  431. {
  432. attrs: {
  433. label: "客户名称",
  434. prop: "name",
  435. slot: "name",
  436. fixed: "left",
  437. width: 160,
  438. },
  439. },
  440. {
  441. attrs: {
  442. label: "所在城市",
  443. slot: "address",
  444. width: 160,
  445. },
  446. },
  447. {
  448. attrs: {
  449. label: "客户代码",
  450. prop: "customerCode",
  451. width: 120,
  452. },
  453. },
  454. {
  455. attrs: {
  456. label: "客户来源",
  457. prop: "source",
  458. width: 120,
  459. },
  460. render(type) {
  461. return proxy.dictValueLabel(type, customerSource.value);
  462. },
  463. },
  464. {
  465. attrs: {
  466. label: "客户类型",
  467. prop: "status",
  468. width: 120,
  469. },
  470. render(type) {
  471. return proxy.dictValueLabel(type, customerStatus.value);
  472. },
  473. },
  474. {
  475. attrs: {
  476. label: "客户标签",
  477. slot: "tags",
  478. width: 180,
  479. },
  480. },
  481. {
  482. attrs: {
  483. label: "业务员",
  484. prop: "userId",
  485. width: 140,
  486. },
  487. render(type) {
  488. let data = userList.value.filter((item) => item.value == type);
  489. if (data && data.length > 0) {
  490. return data[0].label;
  491. } else {
  492. return "";
  493. }
  494. },
  495. },
  496. {
  497. attrs: {
  498. label: "跟进",
  499. slot: "follow",
  500. "min-width": 440,
  501. },
  502. },
  503. {
  504. attrs: {
  505. label: "操作",
  506. width: 190,
  507. align: "center",
  508. fixed: "right",
  509. },
  510. renderHTML(row) {
  511. return [
  512. {
  513. attrs: {
  514. label: "分配",
  515. type: "primary",
  516. text: true,
  517. },
  518. el: "button",
  519. click() {
  520. formAllocation.data = {
  521. id: row.id,
  522. userId: row.userId,
  523. };
  524. openAllocation.value = true;
  525. },
  526. },
  527. {
  528. attrs: {
  529. label: "跟进",
  530. type: "primary",
  531. text: true,
  532. },
  533. el: "button",
  534. click() {
  535. clickFollowUp(row);
  536. },
  537. },
  538. {
  539. attrs: {
  540. label: "修改",
  541. type: "primary",
  542. text: true,
  543. },
  544. el: "button",
  545. click() {
  546. update(row);
  547. },
  548. },
  549. {
  550. attrs: {
  551. label: "删除",
  552. type: "primary",
  553. text: true,
  554. },
  555. el: "button",
  556. click() {
  557. ElMessageBox.confirm("此操作将永久删除该数据, 是否继续?", "提示", {
  558. confirmButtonText: "确定",
  559. cancelButtonText: "取消",
  560. type: "warning",
  561. }).then(() => {
  562. proxy
  563. .post("/customer/delete", {
  564. id: row.id,
  565. })
  566. .then(() => {
  567. ElMessage({
  568. message: "删除成功",
  569. type: "success",
  570. });
  571. getList();
  572. obtainStatisticalData();
  573. });
  574. });
  575. },
  576. },
  577. ];
  578. },
  579. },
  580. ];
  581. });
  582. let modalType = ref("add");
  583. let dialogVisible = ref(false);
  584. let openFollow = ref(false);
  585. let formData = reactive({
  586. data: {
  587. countryId: "China",
  588. },
  589. });
  590. let formPerson = reactive({
  591. data: {},
  592. });
  593. let formAllocation = reactive({
  594. data: {},
  595. });
  596. let formFollow = reactive({
  597. data: {},
  598. });
  599. const formOption = reactive({
  600. inline: true,
  601. labelWidth: 100,
  602. itemWidth: 100,
  603. rules: [],
  604. });
  605. const formConfig = computed(() => {
  606. return [
  607. {
  608. type: "input",
  609. prop: "name",
  610. label: "客户名称",
  611. required: true,
  612. itemWidth: 100,
  613. itemType: "text",
  614. },
  615. {
  616. type: "slot",
  617. slotName: "address",
  618. prop: "countryId",
  619. label: "详细地址",
  620. },
  621. {
  622. type: "input",
  623. prop: "customerCode",
  624. label: "客户代码",
  625. required: true,
  626. itemWidth: 100,
  627. itemType: "text",
  628. },
  629. {
  630. type: "select",
  631. label: "客户来源",
  632. prop: "source",
  633. itemWidth: 50,
  634. data: customerSource.value,
  635. },
  636. {
  637. type: "select",
  638. label: "客户类型",
  639. prop: "status",
  640. itemWidth: 50,
  641. data: customerStatus.value,
  642. },
  643. {
  644. type: "select",
  645. label: "业务员",
  646. prop: "userId",
  647. itemWidth: 100,
  648. data: userList.value,
  649. clearable: true,
  650. },
  651. {
  652. type: "select",
  653. label: "客户标签",
  654. prop: "tags",
  655. itemWidth: 100,
  656. multiple: true,
  657. data: customerTag.value,
  658. style: {
  659. width: "100%",
  660. },
  661. },
  662. {
  663. type: "slot",
  664. slotName: "person",
  665. label: "客户联系人",
  666. },
  667. ];
  668. });
  669. let rules = ref({
  670. name: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
  671. name2: [{ required: true, message: "请输入联系人", trigger: "blur" }],
  672. email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
  673. countryId: [{ required: true, message: "请选择国家", trigger: "change" }],
  674. provinceId: [{ required: true, message: "请选择省/州", trigger: "change" }],
  675. cityId: [{ required: true, message: "请选择城市", trigger: "change" }],
  676. source: [{ required: true, message: "请选择客户来源", trigger: "change" }],
  677. status: [{ required: true, message: "请选择类型", trigger: "change" }],
  678. });
  679. const formConfigAllocation = computed(() => {
  680. return [
  681. {
  682. type: "select",
  683. label: "业务员",
  684. prop: "userId",
  685. itemWidth: 100,
  686. data: userList.value,
  687. clearable: true,
  688. },
  689. ];
  690. });
  691. let rulesAllocation = ref({
  692. userId: [{ required: true, message: "请选择业务员", trigger: "change" }],
  693. });
  694. const formConfigAFollow = computed(() => {
  695. return [
  696. {
  697. type: "date",
  698. itemType: "datetime",
  699. label: "跟进时间",
  700. prop: "date",
  701. itemWidth: 100,
  702. },
  703. {
  704. type: "input",
  705. itemType: "textarea",
  706. label: "跟进内容",
  707. prop: "content",
  708. itemWidth: 100,
  709. },
  710. {
  711. type: "slot",
  712. label: "上传附件",
  713. prop: "fileList",
  714. slotName: "fileSlot",
  715. },
  716. ];
  717. });
  718. let rulesPerson = ref({
  719. name: [{ required: true, message: "请输入联系人", trigger: "blur" }],
  720. email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
  721. type: [{ required: true, message: "请选择类型", trigger: "change" }],
  722. contactNo: [{ required: true, message: "请输入联系号码", trigger: "blur" }],
  723. });
  724. let rulesFollow = ref({
  725. date: [{ required: true, message: "请选择跟进时间", trigger: "change" }],
  726. content: [{ required: true, message: "请输入跟进内容", trigger: "blur" }],
  727. });
  728. const submit = ref(null);
  729. const person = ref(null);
  730. const allocation = ref(null);
  731. const follow = ref(null);
  732. const getList = async (req) => {
  733. sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
  734. loading.value = true;
  735. proxy.post("/customer/page", sourceList.value.pagination).then((res) => {
  736. res.rows.forEach((x) => {
  737. x.addTagShow = false;
  738. if (x.tag) {
  739. x.tag = x.tag.split(",");
  740. } else {
  741. x.tag = [];
  742. }
  743. });
  744. sourceList.value.data = res.rows;
  745. sourceList.value.pagination.total = res.total;
  746. setTimeout(() => {
  747. loading.value = false;
  748. }, 200);
  749. });
  750. };
  751. const openModal = () => {
  752. modalType.value = "add";
  753. formData.data = {
  754. countryId: "China",
  755. tags: [],
  756. };
  757. getCityData(formData.data.countryId, "20");
  758. loadingOperation.value = false;
  759. dialogVisible.value = true;
  760. };
  761. const update = (row) => {
  762. modalType.value = "edit";
  763. loadingOperation.value = true;
  764. proxy.post("/customer/detail", { id: row.id }).then((res) => {
  765. if (res.tag) {
  766. res.tags = res.tag.split(",");
  767. } else {
  768. res.tags = [];
  769. }
  770. formData.data = res;
  771. getCityData(formData.data.countryId, "20");
  772. getCityData(formData.data.provinceId, "30");
  773. loadingOperation.value = false;
  774. });
  775. dialogVisible.value = true;
  776. };
  777. const countryData = ref([]);
  778. const provinceData = ref([]);
  779. const cityData = ref([]);
  780. const getCityData = (id, type, isChange) => {
  781. proxy.post("/areaInfo/list", { parentId: id }).then((res) => {
  782. if (type === "20") {
  783. provinceData.value = res;
  784. if (isChange) {
  785. formData.data.provinceId = "";
  786. formData.data.cityId = "";
  787. }
  788. } else if (type === "30") {
  789. cityData.value = res;
  790. if (isChange) {
  791. formData.data.cityId = "";
  792. }
  793. } else {
  794. countryData.value = res;
  795. }
  796. });
  797. };
  798. getCityData("0");
  799. const clickAddPerson = () => {
  800. if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
  801. formData.data.customerUserList.push({
  802. name: "",
  803. email: "",
  804. });
  805. } else {
  806. formData.data.customerUserList = [
  807. {
  808. name: "",
  809. email: "",
  810. },
  811. ];
  812. }
  813. };
  814. const submitAllocation = () => {
  815. allocation.value.handleSubmit(() => {
  816. proxy.post("/customer/CustomerAllocation", formAllocation.data).then(() => {
  817. ElMessage({
  818. message: "分配成功",
  819. type: "success",
  820. });
  821. openAllocation.value = false;
  822. getList();
  823. obtainStatisticalData();
  824. });
  825. });
  826. };
  827. const submitFollow = () => {
  828. follow.value.handleSubmit(() => {
  829. if (fileList.value && fileList.value.length > 0) {
  830. formFollow.data.fileList = fileList.value.map((item) => {
  831. return {
  832. id: item.raw.id,
  833. fileName: item.raw.fileName,
  834. fileUrl: item.raw.fileUrl,
  835. };
  836. });
  837. } else {
  838. formFollow.data.fileList = [];
  839. }
  840. proxy.post("/customerFollowRecords/" + modalType.value, formFollow.data).then(
  841. () => {
  842. ElMessage({
  843. message: modalType.value == "add" ? "添加成功" : "编辑成功",
  844. type: "success",
  845. });
  846. openFollow.value = false;
  847. getList();
  848. },
  849. (err) => {
  850. console.log(err);
  851. }
  852. );
  853. });
  854. };
  855. const submitForm = () => {
  856. submit.value.handleSubmit(() => {
  857. if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
  858. formData.data.tag = formData.data.tags.join(",");
  859. submitLoading.value = true;
  860. proxy.post("/customer/" + modalType.value, formData.data).then(
  861. () => {
  862. ElMessage({
  863. message: modalType.value == "add" ? "添加成功" : "编辑成功",
  864. type: "success",
  865. });
  866. dialogVisible.value = false;
  867. submitLoading.value = false;
  868. getList();
  869. obtainStatisticalData();
  870. },
  871. (err) => {
  872. console.log(err);
  873. submitLoading.value = false;
  874. }
  875. );
  876. } else {
  877. ElMessage("请添加客户联系人");
  878. }
  879. });
  880. };
  881. const getDict = () => {
  882. proxy.getDictOne(["customer_tag", "customer_source", "customer_status", "contact_type"]).then((res) => {
  883. customerTag.value = res["customer_tag"].map((x) => ({
  884. label: x.dictValue,
  885. value: x.dictKey,
  886. }));
  887. customerSource.value = res["customer_source"].map((x) => ({
  888. label: x.dictValue,
  889. value: x.dictKey,
  890. }));
  891. customerStatus.value = res["customer_status"].map((x) => ({
  892. label: x.dictValue,
  893. value: x.dictKey,
  894. }));
  895. contactType.value = res["contact_type"].map((x) => ({
  896. label: x.dictValue,
  897. value: x.dictKey,
  898. }));
  899. });
  900. proxy
  901. .get("/tenantUser/list", {
  902. pageNum: 1,
  903. pageSize: 10000,
  904. tenantId: useUserStore().user.tenantId,
  905. })
  906. .then((res) => {
  907. userList.value = res.rows.map((item) => {
  908. return {
  909. label: item.nickName,
  910. value: item.userId,
  911. };
  912. });
  913. });
  914. };
  915. getDict();
  916. getList();
  917. const handleClickName = (row) => {
  918. proxy.$router.push({
  919. path: "/ERP/customer/portrait",
  920. query: {
  921. id: row.id,
  922. },
  923. });
  924. };
  925. const deleteFollow = (data) => {
  926. ElMessageBox.confirm("是否确认删除该跟进?", "提示", {
  927. confirmButtonText: "确定",
  928. cancelButtonText: "取消",
  929. type: "warning",
  930. }).then(() => {
  931. proxy
  932. .post("/customerFollowRecords/delete", {
  933. id: data.id,
  934. })
  935. .then(() => {
  936. ElMessage({
  937. message: "删除成功",
  938. type: "success",
  939. });
  940. getList();
  941. });
  942. });
  943. };
  944. const addTag = ref("");
  945. const judgeTagSelect = (data, val) => {
  946. if (data && data.length > 0) {
  947. if (data.includes(val)) {
  948. return true;
  949. }
  950. }
  951. return false;
  952. };
  953. const changeTag = (val, item) => {
  954. let data = {
  955. id: item.id,
  956. tag: JSON.parse(JSON.stringify(item.tag)),
  957. };
  958. data.tag.push(val);
  959. data.tag = data.tag.join(",");
  960. proxy.post("/customer/editTag", data).then(() => {
  961. ElMessage({
  962. message: "添加成功",
  963. type: "success",
  964. });
  965. item.addTagShow = false;
  966. addTag.value = "";
  967. getList();
  968. });
  969. };
  970. const tagClose = (val, item) => {
  971. let data = {
  972. id: item.id,
  973. tag: JSON.parse(JSON.stringify(item.tag)),
  974. };
  975. data.tag = data.tag.filter((row) => row !== val);
  976. if (data.tag && data.tag.length > 0) {
  977. data.tag = data.tag.join(",");
  978. } else {
  979. data.tag = "";
  980. }
  981. proxy.post("/customer/editTag", data).then(() => {
  982. ElMessage({
  983. message: "添加成功",
  984. type: "success",
  985. });
  986. item.addTagShow = false;
  987. addTag.value = "";
  988. getList();
  989. });
  990. };
  991. const showSelect = (item) => {
  992. item.addTagShow = true;
  993. };
  994. const clickFollowUp = (item) => {
  995. formFollow.data = {
  996. customerId: item.id,
  997. fileList: [],
  998. };
  999. fileList.value = [];
  1000. modalType.value = "add";
  1001. openFollow.value = true;
  1002. openRecordMore.value = false;
  1003. };
  1004. const uploadFile = async (file) => {
  1005. const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
  1006. uploadData.value = res.uploadBody;
  1007. file.id = res.id;
  1008. file.fileName = res.fileName;
  1009. file.fileUrl = res.fileUrl;
  1010. return true;
  1011. };
  1012. const onPreviewFile = (file) => {
  1013. window.open(file.raw.fileUrl, "_blank");
  1014. };
  1015. const getStyle = (val) => {
  1016. if (val) {
  1017. return "跟进记录: " + val.replace(/\n|\r\n/g, "<br>");
  1018. } else {
  1019. return "";
  1020. }
  1021. };
  1022. const recordShow = (item) => {
  1023. if (!(item.fileList && item.fileList.length > 0)) {
  1024. proxy.post("/fileInfo/getList", { businessIdList: [item.id] }).then((fileObj) => {
  1025. item.fileList = fileObj[item.id] || [];
  1026. });
  1027. }
  1028. };
  1029. const openFile = (path) => {
  1030. window.open(path, "_blank");
  1031. };
  1032. const judgeWidth = (item, index) => {
  1033. if (item && item.id) {
  1034. let dom = document.querySelector(".getWidth" + item.id);
  1035. if (dom) {
  1036. let width = getComputedStyle(dom).width.replace("px", "");
  1037. let num = parseInt(width / 118);
  1038. let num2 = parseInt((width - 52) / 118);
  1039. if (num2 === num) {
  1040. if (index + 1 > num) {
  1041. return "display: none";
  1042. }
  1043. } else {
  1044. if (index + 1 >= num) {
  1045. return "display: none";
  1046. }
  1047. }
  1048. }
  1049. }
  1050. return "";
  1051. };
  1052. const recordList = ref([]);
  1053. const rowData = ref({});
  1054. const openRecordMore = ref(false);
  1055. const queryParams = ref({
  1056. total: 0,
  1057. pageNum: 1,
  1058. pageSize: 10,
  1059. customerId: "",
  1060. });
  1061. const clickMore = (item) => {
  1062. if (openRecordMore.value === false) {
  1063. queryParams.value.pageNum = 1;
  1064. recordList.value = [];
  1065. rowData.value = item;
  1066. queryParams.value.customerId = item.id;
  1067. }
  1068. proxy.post("/customerFollowRecords/page", queryParams.value).then((res) => {
  1069. recordList.value = recordList.value.concat(res.rows);
  1070. queryParams.value.total = res.total;
  1071. proxy.post("/fileInfo/getList", { businessIdList: res.rows.map((rows) => rows.id) }).then((fileObj) => {
  1072. for (let i = 0; i < res.rows.length; i++) {
  1073. recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].fileList =
  1074. fileObj[recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].id] || [];
  1075. }
  1076. });
  1077. });
  1078. openRecordMore.value = true;
  1079. };
  1080. const infiniteScroll = () => {
  1081. queryParams.value.pageNum++;
  1082. clickMore();
  1083. };
  1084. const judgeTotal = () => {
  1085. if (queryParams.value.pageNum * queryParams.value.pageSize >= queryParams.value.total) {
  1086. return true;
  1087. }
  1088. return false;
  1089. };
  1090. const moreIndex = ref(0);
  1091. const clickInformationMore = (item, index) => {
  1092. moreIndex.value = index;
  1093. if (item.contactJson) {
  1094. item.contact = JSON.parse(item.contactJson);
  1095. } else {
  1096. item.contact = [];
  1097. }
  1098. formPerson.data = proxy.deepClone(item);
  1099. openPerson.value = true;
  1100. };
  1101. const clickDelete = (index) => {
  1102. formData.data.customerUserList.splice(index, 1);
  1103. };
  1104. const clickAddMoreInformation = () => {
  1105. if (formPerson.data.contact && formPerson.data.contact.length > 0) {
  1106. formPerson.data.contact.push({
  1107. type: "",
  1108. contactNo: "",
  1109. });
  1110. } else {
  1111. formPerson.data.contact = [
  1112. {
  1113. type: "",
  1114. contactNo: "",
  1115. },
  1116. ];
  1117. }
  1118. };
  1119. const clickInformationDelete = (index) => {
  1120. formPerson.data.contact.splice(index, 1);
  1121. };
  1122. const submitPerson = () => {
  1123. person.value.validate((valid) => {
  1124. if (valid) {
  1125. formPerson.data.contactJson = JSON.stringify(formPerson.data.contact);
  1126. formData.data.customerUserList[moreIndex.value] = formPerson.data;
  1127. openPerson.value = false;
  1128. }
  1129. });
  1130. };
  1131. const deleteTop = (item) => {
  1132. proxy.post("/customerTop/delete", { customerId: item.id }).then(() => {
  1133. item.isTop = 0;
  1134. });
  1135. };
  1136. const addTop = (item) => {
  1137. proxy.post("/customerTop/add", { customerId: item.id }).then(() => {
  1138. item.isTop = 1;
  1139. });
  1140. };
  1141. const searchItemSelect = (val) => {
  1142. sourceList.value.paginationTwo.statisticsType = val;
  1143. obtainStatisticalData();
  1144. };
  1145. const statisticalData = ref({
  1146. countAmount: 0,
  1147. customerList: [],
  1148. });
  1149. const obtainStatisticalData = () => {
  1150. proxy.post("/customer/sourceStatistics", sourceList.value.paginationTwo).then((res) => {
  1151. statisticalData.value = res;
  1152. });
  1153. };
  1154. obtainStatisticalData();
  1155. const getNum = (val) => {
  1156. let num = 0;
  1157. if (statisticalData.value.customerList && statisticalData.value.customerList.length > 0) {
  1158. statisticalData.value.customerList.map((item) => {
  1159. if (sourceList.value.paginationTwo.statisticsType === 1) {
  1160. if (item.source === val) {
  1161. num = item.count;
  1162. }
  1163. } else if (sourceList.value.paginationTwo.statisticsType === 2) {
  1164. if (item.status === val) {
  1165. num = item.count;
  1166. }
  1167. } else if (sourceList.value.paginationTwo.statisticsType === 3) {
  1168. if (item.userId === val) {
  1169. num = item.count;
  1170. }
  1171. }
  1172. });
  1173. }
  1174. return num;
  1175. };
  1176. </script>
  1177. <style lang="scss" scoped>
  1178. .tenant {
  1179. padding: 20px;
  1180. }
  1181. .infinite-scroll {
  1182. max-height: calc(89vh - 94px - 70px - 58px - 16px);
  1183. overflow-y: auto;
  1184. &::-webkit-scrollbar {
  1185. width: 0px;
  1186. }
  1187. }
  1188. .by-dropdown {
  1189. position: relative;
  1190. text-align: left;
  1191. height: 32px;
  1192. z-index: 1010;
  1193. padding: 0 10px;
  1194. transition: all 0.5s ease;
  1195. cursor: pointer;
  1196. line-height: 32px;
  1197. .by-dropdown-title {
  1198. font-size: 14px;
  1199. background-color: #fff;
  1200. }
  1201. ul {
  1202. position: absolute;
  1203. left: 0;
  1204. top: 32px;
  1205. padding: 0;
  1206. margin: 0;
  1207. z-index: 1200;
  1208. display: none;
  1209. white-space: nowrap;
  1210. background-color: #fff;
  1211. li {
  1212. list-style: none;
  1213. z-index: 1200;
  1214. font-size: 12px;
  1215. height: 30px;
  1216. padding: 0 10px;
  1217. }
  1218. li:hover {
  1219. background-color: #eff6ff;
  1220. color: #0084ff;
  1221. }
  1222. }
  1223. }
  1224. .by-dropdown::before {
  1225. display: block;
  1226. width: 1px;
  1227. content: " ";
  1228. position: absolute;
  1229. height: 14px;
  1230. top: 8px;
  1231. background-color: #ddd;
  1232. right: 0;
  1233. z-index: 1011;
  1234. }
  1235. .by-dropdown:hover {
  1236. background: #ffffff;
  1237. border-radius: 2px 2px 2px 2px;
  1238. opacity: 1;
  1239. ul {
  1240. background: #ffffff;
  1241. box-shadow: 0px 2px 16px 1px rgba(0, 0, 0, 0.06);
  1242. border-radius: 2px 2px 2px 2px;
  1243. opacity: 1;
  1244. display: block;
  1245. text-align: left;
  1246. }
  1247. }
  1248. .by-dropdown-lists {
  1249. max-height: 50vh;
  1250. overflow-y: auto;
  1251. line-height: 1;
  1252. }
  1253. </style>