index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <template>
  2. <basic-container>
  3. <avue-crud :option="option" :data="data" ref="crud" v-model="form" :page.sync="page"
  4. :permission="permissionList" :before-open="beforeOpen" :table-loading="loading" @row-del="rowDel"
  5. @row-update="rowUpdate" @row-save="rowSave" @search-change="searchChange" @search-reset="searchReset"
  6. @selection-change="selectionChange" @current-change="currentChange" @size-change="sizeChange"
  7. @refresh-change="refreshChange" @on-load="onLoad">
  8. <template slot="menuLeft">
  9. <el-button type="danger" size="small" plain icon="el-icon-delete" v-if="permission.order_lead_delete"
  10. @click="handleDelete">
  11. 删除
  12. </el-button>
  13. </template>
  14. <template slot-scope="{row}" slot="status">
  15. <el-tag :type="getStatusType(row.status)">
  16. {{ getStatusText(row.status) }}
  17. </el-tag>
  18. </template>
  19. <template slot-scope="{row}" slot="priority">
  20. <el-tag :type="getPriorityType(row.priority)">
  21. {{ getPriorityText(row.priority) }}
  22. </el-tag>
  23. </template>
  24. <template slot-scope="{row}" slot="endTime">
  25. <span :class="{ 'text-danger': isOverdue(row.endTime, row.status) }">
  26. {{ row.endTime }}
  27. </span>
  28. </template>
  29. </avue-crud>
  30. </basic-container>
  31. </template>
  32. <script>
  33. import { getList, add, update, remove, getDetail } from '@/api/order/lead'
  34. import { getCustomerList } from '@/api/common/index'
  35. import { mapGetters } from 'vuex'
  36. /**
  37. * 销售线索记录类型定义
  38. * @typedef {Object} LeadRecord
  39. * @property {string} id - 线索ID
  40. * @property {string} createUser - 创建用户ID
  41. * @property {string} createDept - 创建部门ID
  42. * @property {string} createTime - 创建时间
  43. * @property {string} updateUser - 更新用户ID
  44. * @property {string} updateTime - 更新时间
  45. * @property {number} status - 状态 1-新建 2-跟进中 3-已关闭
  46. * @property {number} isDeleted - 是否删除
  47. * @property {string} leadCode - 线索编码
  48. * @property {number} customerId - 客户ID
  49. * @property {string} customerCode - 客户编码
  50. * @property {string} customerName - 客户名称
  51. * @property {string} contactName - 联系人姓名
  52. * @property {string} contactPhone - 联系人电话
  53. * @property {string} title - 线索标题
  54. * @property {string} endTime - 截止时间
  55. * @property {number} priority - 优先级
  56. * @property {string} source - 线索来源
  57. * @property {string} groupName - 分组名称
  58. * @property {string|null} closeReason - 关闭原因
  59. */
  60. /**
  61. * 销售线索查询参数类型定义
  62. * @typedef {Object} LeadQueryParams
  63. * @property {string} [leadCode] - 线索编码
  64. * @property {string} [customerCode] - 客户编码
  65. * @property {string} [customerName] - 客户名称
  66. * @property {string} [contactName] - 联系人姓名
  67. * @property {string} [contactPhone] - 联系人电话
  68. * @property {string} [title] - 线索标题
  69. * @property {number} [status] - 状态
  70. * @property {number} [priority] - 优先级
  71. * @property {string} [source] - 线索来源
  72. * @property {string} [groupName] - 分组名称
  73. * @property {string[]} [endTime] - 截止时间范围
  74. */
  75. export default {
  76. name: 'Lead',
  77. data() {
  78. return {
  79. /**
  80. * 表格数据
  81. * @type {LeadRecord[]}
  82. */
  83. data: [],
  84. /**
  85. * 查询参数
  86. * @type {LeadQueryParams}
  87. */
  88. query: {},
  89. /**
  90. * 加载状态
  91. * @type {boolean}
  92. */
  93. loading: true,
  94. /**
  95. * 表单数据
  96. * @type {LeadRecord}
  97. */
  98. form: {},
  99. /**
  100. * 选中的数据列表
  101. * @type {LeadRecord[]}
  102. */
  103. selectionList: [],
  104. /**
  105. * 分页信息
  106. * @type {{total: number, currentPage: number, pageSize: number}}
  107. */
  108. page: {
  109. total: 0,
  110. currentPage: 1,
  111. pageSize: 10
  112. },
  113. /**
  114. * 客户选项列表
  115. * @type {{label: string, value: string}[]}
  116. */
  117. customerOptions: [],
  118. /**
  119. * 表格配置
  120. */
  121. option: {
  122. height: 'auto',
  123. calcHeight: 30,
  124. tip: false,
  125. searchShow: true,
  126. searchMenuSpan: 6,
  127. border: true,
  128. index: true,
  129. viewBtn: true,
  130. selection: true,
  131. dialogClickModal: false,
  132. column: [
  133. {
  134. label: '线索编码',
  135. prop: 'leadCode',
  136. rules: [{
  137. required: true,
  138. message: '请输入线索编码',
  139. trigger: 'blur'
  140. }],
  141. search: true
  142. },
  143. {
  144. label: '客户编码',
  145. prop: 'customerCode',
  146. rules: [{
  147. required: true,
  148. message: '请输入客户编码',
  149. trigger: 'blur'
  150. }],
  151. search: true
  152. },
  153. {
  154. label: '客户名称',
  155. prop: 'customerName',
  156. rules: [{
  157. required: true,
  158. message: '请输入客户名称',
  159. trigger: 'blur'
  160. }],
  161. search: true
  162. },
  163. {
  164. label: '联系人',
  165. prop: 'contactName',
  166. rules: [{
  167. required: true,
  168. message: '请输入联系人姓名',
  169. trigger: 'blur'
  170. }],
  171. search: true
  172. },
  173. {
  174. label: '联系电话',
  175. prop: 'contactPhone',
  176. rules: [{
  177. required: true,
  178. message: '请输入联系电话',
  179. trigger: 'blur'
  180. }, {
  181. pattern: /^1[3-9]\d{9}$/,
  182. message: '请输入正确的手机号码',
  183. trigger: 'blur'
  184. }],
  185. search: true
  186. },
  187. {
  188. label: '线索标题',
  189. prop: 'title',
  190. rules: [{
  191. required: true,
  192. message: '请输入线索标题',
  193. trigger: 'blur'
  194. }],
  195. search: true,
  196. overHidden: true
  197. },
  198. {
  199. label: '截止时间',
  200. prop: 'endTime',
  201. type: 'datetime',
  202. format: 'yyyy-MM-dd HH:mm:ss',
  203. valueFormat: 'yyyy-MM-dd HH:mm:ss',
  204. rules: [{
  205. required: true,
  206. message: '请选择截止时间',
  207. trigger: 'blur'
  208. }],
  209. search: true,
  210. searchRange: true,
  211. slot: true
  212. },
  213. {
  214. label: '优先级',
  215. prop: 'priority',
  216. type: 'select',
  217. dicData: [
  218. { label: '高', value: 1 },
  219. { label: '中', value: 2 },
  220. { label: '低', value: 3 }
  221. ],
  222. rules: [{
  223. required: true,
  224. message: '请选择优先级',
  225. trigger: 'blur'
  226. }],
  227. search: true,
  228. slot: true
  229. },
  230. {
  231. label: '线索来源',
  232. prop: 'source',
  233. rules: [{
  234. required: true,
  235. message: '请输入线索来源',
  236. trigger: 'blur'
  237. }],
  238. search: true
  239. },
  240. {
  241. label: '分组名称',
  242. prop: 'groupName',
  243. rules: [{
  244. required: true,
  245. message: '请输入分组名称',
  246. trigger: 'blur'
  247. }],
  248. search: true
  249. },
  250. {
  251. label: '状态',
  252. prop: 'status',
  253. type: 'select',
  254. dicData: [
  255. { label: '新建', value: 1 },
  256. { label: '跟进中', value: 2 },
  257. { label: '已关闭', value: 3 }
  258. ],
  259. rules: [{
  260. required: true,
  261. message: '请选择状态',
  262. trigger: 'blur'
  263. }],
  264. search: true,
  265. slot: true
  266. },
  267. {
  268. label: '关闭原因',
  269. prop: 'closeReason',
  270. type: 'textarea',
  271. span: 24,
  272. hide: true,
  273. viewDisplay: true
  274. },
  275. {
  276. label: '创建时间',
  277. prop: 'createTime',
  278. type: 'datetime',
  279. format: 'yyyy-MM-dd HH:mm:ss',
  280. valueFormat: 'yyyy-MM-dd HH:mm:ss',
  281. addDisplay: false,
  282. editDisplay: false,
  283. viewDisplay: true
  284. },
  285. {
  286. label: '更新时间',
  287. prop: 'updateTime',
  288. type: 'datetime',
  289. format: 'yyyy-MM-dd HH:mm:ss',
  290. valueFormat: 'yyyy-MM-dd HH:mm:ss',
  291. addDisplay: false,
  292. editDisplay: false,
  293. viewDisplay: true
  294. }
  295. ]
  296. },
  297. /**
  298. * 权限列表
  299. */
  300. permissionList: {
  301. // addBtn: this.vaildData(this.permission.order_lead_add, false),
  302. // viewBtn: this.vaildData(this.permission.order_lead_view, false),
  303. // delBtn: this.vaildData(this.permission.order_lead_delete, false),
  304. // editBtn: this.vaildData(this.permission.order_lead_edit, false)
  305. addBtn: true,
  306. viewBtn: true,
  307. delBtn: false,
  308. editBtn: true,
  309. }
  310. }
  311. },
  312. computed: {
  313. ...mapGetters(['permission']),
  314. ids() {
  315. const ids = []
  316. this.selectionList.forEach(ele => {
  317. ids.push(ele.id)
  318. })
  319. return ids.join(',')
  320. }
  321. },
  322. methods: {
  323. /**
  324. * 获取状态文本
  325. * @param {number} status - 状态值
  326. * @returns {string} 状态文本
  327. */
  328. getStatusText(status) {
  329. const statusMap = {
  330. 1: '新建',
  331. 2: '跟进中',
  332. 3: '已关闭'
  333. }
  334. return statusMap[status] || '未知状态'
  335. },
  336. /**
  337. * 获取状态标签类型
  338. * @param {number} status - 状态值
  339. * @returns {string} 标签类型
  340. */
  341. getStatusType(status) {
  342. const typeMap = {
  343. 1: 'info',
  344. 2: 'warning',
  345. 3: 'success'
  346. }
  347. return typeMap[status] || 'info'
  348. },
  349. /**
  350. * 获取优先级文本
  351. * @param {number} priority - 优先级值
  352. * @returns {string} 优先级文本
  353. */
  354. getPriorityText(priority) {
  355. const priorityMap = {
  356. 1: '高',
  357. 2: '中',
  358. 3: '低'
  359. }
  360. return priorityMap[priority] || '未知优先级'
  361. },
  362. /**
  363. * 获取优先级标签类型
  364. * @param {number} priority - 优先级值
  365. * @returns {string} 标签类型
  366. */
  367. getPriorityType(priority) {
  368. const typeMap = {
  369. 1: 'danger',
  370. 2: 'warning',
  371. 3: 'success'
  372. }
  373. return typeMap[priority] || 'info'
  374. },
  375. /**
  376. * 判断是否逾期
  377. * @param {string} endTime - 截止时间
  378. * @param {number} status - 状态
  379. * @returns {boolean} 是否逾期
  380. */
  381. isOverdue(endTime, status) {
  382. if (status === 3) return false // 已关闭的不显示逾期
  383. const now = new Date()
  384. const end = new Date(endTime)
  385. return end < now
  386. },
  387. /**
  388. * 删除操作
  389. * @returns {Promise<void>}
  390. */
  391. async rowDel(row) {
  392. try {
  393. await this.$confirm('确定将选择数据删除?', {
  394. confirmButtonText: '确定',
  395. cancelButtonText: '取消',
  396. type: 'warning'
  397. })
  398. await remove(row.id)
  399. await this.onLoad(this.page)
  400. this.$message({
  401. type: 'success',
  402. message: '操作成功!'
  403. })
  404. } catch (error) {
  405. console.error('删除失败:', error)
  406. }
  407. },
  408. /**
  409. * 批量删除
  410. * @returns {Promise<void>}
  411. */
  412. async handleDelete() {
  413. if (this.selectionList.length === 0) {
  414. this.$message.warning('请选择至少一条数据')
  415. return
  416. }
  417. try {
  418. await this.$confirm('确定将选择数据删除?', {
  419. confirmButtonText: '确定',
  420. cancelButtonText: '取消',
  421. type: 'warning'
  422. })
  423. await remove(this.ids)
  424. await this.onLoad(this.page)
  425. this.$message({
  426. type: 'success',
  427. message: '操作成功!'
  428. })
  429. this.$refs.crud.toggleSelection()
  430. } catch (error) {
  431. console.error('批量删除失败:', error)
  432. }
  433. },
  434. /**
  435. * 更新操作
  436. * @param {LeadRecord} row - 行数据
  437. * @param {number} index - 行索引
  438. * @param {Function} done - 完成回调
  439. * @param {Function} loading - 加载回调
  440. * @returns {Promise<void>}
  441. */
  442. async rowUpdate(row, index, done, loading) {
  443. try {
  444. await update(row)
  445. await this.onLoad(this.page)
  446. this.$message({
  447. type: 'success',
  448. message: '操作成功!'
  449. })
  450. done()
  451. } catch (error) {
  452. console.error('更新失败:', error)
  453. loading()
  454. }
  455. },
  456. /**
  457. * 新增操作
  458. * @param {LeadRecord} row - 行数据
  459. * @param {Function} done - 完成回调
  460. * @param {Function} loading - 加载回调
  461. * @returns {Promise<void>}
  462. */
  463. async rowSave(row, done, loading) {
  464. try {
  465. await add(row)
  466. await this.onLoad(this.page)
  467. this.$message({
  468. type: 'success',
  469. message: '操作成功!'
  470. })
  471. done()
  472. } catch (error) {
  473. console.error('新增失败:', error)
  474. loading()
  475. }
  476. },
  477. /**
  478. * 新增前的回调
  479. * @param {Function} done - 完成回调
  480. * @param {string} type - 操作类型
  481. */
  482. beforeOpen(done, type) {
  483. if (['edit', 'view'].includes(type)) {
  484. // 编辑和查看时获取详情
  485. getDetail(this.form.id).then(res => {
  486. this.form = res.data.data
  487. })
  488. }
  489. done()
  490. },
  491. /**
  492. * 获取数据
  493. * @param {Object} page - 分页信息
  494. * @param {LeadQueryParams} [params] - 查询参数
  495. * @returns {Promise<void>}
  496. */
  497. async onLoad(page, params = {}) {
  498. this.loading = true
  499. try {
  500. // 处理时间范围查询
  501. const queryParams = { ...params }
  502. if (queryParams.endTime && Array.isArray(queryParams.endTime)) {
  503. queryParams.endTimeStart = queryParams.endTime[0]
  504. queryParams.endTimeEnd = queryParams.endTime[1]
  505. delete queryParams.endTime
  506. }
  507. const res = await getList(page.currentPage, page.pageSize, queryParams)
  508. const data = res.data.data
  509. this.data = data.records || []
  510. this.page.total = data.total || 0
  511. } catch (error) {
  512. console.error('获取数据失败:', error)
  513. this.$message.error('获取数据失败')
  514. } finally {
  515. this.loading = false
  516. }
  517. },
  518. /**
  519. * 搜索变更
  520. * @param {LeadQueryParams} params - 查询参数
  521. * @param {Function} done - 完成回调
  522. */
  523. searchChange(params, done) {
  524. this.query = params
  525. this.onLoad(this.page, params)
  526. done()
  527. },
  528. /**
  529. * 搜索重置
  530. * @param {Function} done - 完成回调
  531. */
  532. searchReset(done) {
  533. this.query = {}
  534. this.onLoad(this.page)
  535. done()
  536. },
  537. /**
  538. * 选择变更
  539. * @param {LeadRecord[]} list - 选中的数据
  540. */
  541. selectionChange(list) {
  542. this.selectionList = list
  543. },
  544. /**
  545. * 清空选择
  546. */
  547. selectionClear() {
  548. this.selectionList = []
  549. },
  550. /**
  551. * 当前页变更
  552. * @param {number} currentPage - 当前页
  553. */
  554. currentChange(currentPage) {
  555. this.page.currentPage = currentPage
  556. },
  557. /**
  558. * 页大小变更
  559. * @param {number} pageSize - 页大小
  560. */
  561. sizeChange(pageSize) {
  562. this.page.pageSize = pageSize
  563. },
  564. /**
  565. * 刷新变更
  566. */
  567. refreshChange() {
  568. this.onLoad(this.page, this.query)
  569. }
  570. }
  571. }
  572. </script>
  573. <style lang="scss" scoped>
  574. .text-danger {
  575. color: #f56c6c;
  576. font-weight: bold;
  577. }
  578. // 表格中的优先级和状态样式
  579. ::v-deep .el-table {
  580. .el-tag {
  581. margin: 0;
  582. }
  583. }
  584. </style>