index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. <template>
  2. <div class="app-container">
  3. <!-- 搜索区域 -->
  4. <el-card class="search-card" shadow="never">
  5. <el-form :model="queryParams" ref="queryForm" @submit.native.prevent :inline="true" label-width="90px">
  6. <el-form-item label="派送员" prop="appUserName">
  7. <el-input
  8. v-model="queryParams.appUserName"
  9. placeholder="派送员姓名"
  10. clearable
  11. prefix-icon="el-icon-user"
  12. style="width: 200px"
  13. @keyup.enter.native="handleQuery"
  14. />
  15. </el-form-item>
  16. <el-form-item label="手机号" prop="phoneNumber">
  17. <el-input
  18. v-model="queryParams.phoneNumber"
  19. placeholder="手机号码"
  20. clearable
  21. prefix-icon="el-icon-mobile-phone"
  22. style="width: 200px"
  23. maxlength="11"
  24. @keyup.enter.native="handleQuery"
  25. />
  26. </el-form-item>
  27. <el-form-item>
  28. <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
  29. <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
  30. </el-form-item>
  31. </el-form>
  32. </el-card>
  33. <!-- 操作按钮区域 -->
  34. <el-row :gutter="10" class="mb8">
  35. <el-col :span="1.5">
  36. <el-button
  37. type="primary"
  38. plain
  39. icon="el-icon-plus"
  40. @click="handleAdd"
  41. v-hasPermi="['app:delivery:add']"
  42. >新增派送员</el-button>
  43. </el-col>
  44. <el-col :span="1.5">
  45. <el-button
  46. type="success"
  47. plain
  48. icon="el-icon-edit"
  49. :disabled="single"
  50. @click="handleUpdate"
  51. v-hasPermi="['app:delivery:edit']"
  52. >编辑</el-button>
  53. </el-col>
  54. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  55. </el-row>
  56. <!-- 派送员列表 -->
  57. <Page
  58. uri="/mapi/app/delivery/page"
  59. :request-params="queryParams"
  60. ref="pagination"
  61. >
  62. <el-table-column label="ID" align="center" prop="id" width="100" />
  63. <el-table-column label="派送员姓名" align="left" prop="realName" width="130" />
  64. <el-table-column label="派送员电话" align="left" prop="phoneNumber" width="130" />
  65. <el-table-column label="绑定门店" align="center" width="120">
  66. <template slot-scope="scope">
  67. <el-button type="text" @click="showStoreList(scope.row)">
  68. <span class="store-count">{{ scope.row.relationStoreNum || 0 }}</span>
  69. <span class="store-text">家门店</span>
  70. </el-button>
  71. </template>
  72. </el-table-column>
  73. <el-table-column label="注册时间" align="left" prop="createTime" width="150" />
  74. <el-table-column label="简介" align="left" min-width="250">
  75. <template slot-scope="scope">
  76. <div class="level-desc">
  77. <div class="description text-gray">{{ scope.row.description || '暂无简介' }}</div>
  78. </div>
  79. </template>
  80. </el-table-column>
  81. <el-table-column label="状态" align="center" width="100">
  82. <template slot-scope="scope">
  83. <el-switch
  84. v-model="scope.row.status"
  85. active-value="0"
  86. inactive-value="1"
  87. @change="handleStatusChange(scope.row)"
  88. >
  89. </el-switch>
  90. </template>
  91. </el-table-column>
  92. <el-table-column label="操作" align="center" width="200" fixed="right">
  93. <template slot-scope="scope">
  94. <el-button
  95. type="text"
  96. icon="el-icon-edit"
  97. @click="handleUpdate(scope.row)"
  98. v-hasPermi="['app:delivery:edit']"
  99. >编辑</el-button>
  100. <el-button
  101. type="text"
  102. icon="el-icon-tickets"
  103. @click="recordDetail(scope.row)"
  104. >接单记录</el-button>
  105. </template>
  106. </el-table-column>
  107. </Page>
  108. <!-- 添加或修改派送员对话框 -->
  109. <el-dialog
  110. :title="title"
  111. :visible.sync="open"
  112. width="700px"
  113. :close-on-click-modal="false"
  114. :before-close="handleDialogClose"
  115. append-to-body
  116. >
  117. <!-- 选择用户弹窗 -->
  118. <el-dialog
  119. width="500px"
  120. title="选择用户"
  121. :visible.sync="userSelectVisible"
  122. append-to-body
  123. :close-on-click-modal="false"
  124. >
  125. <div class="user-search">
  126. <el-form :inline="true">
  127. <el-form-item label="手机号">
  128. <el-input
  129. v-model="searchPhone"
  130. placeholder="请输入手机号"
  131. clearable
  132. style="width: 200px"
  133. >
  134. <el-button slot="append" icon="el-icon-search" @click="searchUser"></el-button>
  135. </el-input>
  136. </el-form-item>
  137. </el-form>
  138. <div v-if="searchUserInfo" class="user-result">
  139. <el-card shadow="never" class="user-card">
  140. <div class="user-info">
  141. <el-avatar :size="60" icon="el-icon-user"></el-avatar>
  142. <div class="info-content">
  143. <div class="name">{{ searchUserInfo.realName }}</div>
  144. <div class="phone">{{ searchUserInfo.phoneNumber }}</div>
  145. <div class="tags">
  146. <el-tag size="small" type="warning">Level {{ searchUserInfo.level }}</el-tag>
  147. <el-tag size="small" type="info">积分: {{ searchUserInfo.pointBalance }}</el-tag>
  148. </div>
  149. </div>
  150. </div>
  151. <div class="select-btn">
  152. <el-button type="primary" size="small" @click="confirmSelectUser">选择此用户</el-button>
  153. </div>
  154. </el-card>
  155. </div>
  156. <div v-else-if="searchPhone && !searchUserInfo" class="no-result">
  157. <i class="el-icon-warning-outline"></i>
  158. <span>未找到该用户</span>
  159. </div>
  160. </div>
  161. </el-dialog>
  162. <div v-if="!appUserInfo && !form.id" class="select-user-tip">
  163. <el-empty description="请先选择用户">
  164. <el-button type="primary" @click="userSelectVisible = true">选择用户</el-button>
  165. </el-empty>
  166. </div>
  167. <template v-else>
  168. <el-tabs v-model="activeTab">
  169. <el-tab-pane label="基本信息" name="basic">
  170. <el-form ref="form" :model="form" :rules="rules" label-width="100px">
  171. <!-- 用户信息展示区 -->
  172. <div class="user-info-card">
  173. <el-descriptions :column="2" border>
  174. <el-descriptions-item label="用户姓名">
  175. <span class="primary-text">{{ appUserInfo.realName }}</span>
  176. </el-descriptions-item>
  177. <el-descriptions-item label="手机号码">
  178. {{ appUserInfo.phoneNumber }}
  179. </el-descriptions-item>
  180. <el-descriptions-item label="用户等级">
  181. <el-tag size="small" type="warning">Level {{ appUserInfo.level }}</el-tag>
  182. </el-descriptions-item>
  183. <el-descriptions-item label="积分">
  184. {{ appUserInfo.pointBalance }}
  185. </el-descriptions-item>
  186. </el-descriptions>
  187. </div>
  188. <el-form-item label="派送员简介" prop="description">
  189. <el-input
  190. v-model="description"
  191. type="textarea"
  192. :rows="4"
  193. placeholder="请输入派送员简介信息"
  194. maxlength="500"
  195. show-word-limit
  196. style="width: 100%"
  197. />
  198. </el-form-item>
  199. </el-form>
  200. </el-tab-pane>
  201. <el-tab-pane label="绑定门店" name="store">
  202. <div class="store-transfer">
  203. <el-transfer
  204. v-model="orgValue"
  205. :data="orgData"
  206. :titles="['可选门店', '已选门店']"
  207. filterable
  208. :filter-method="filterStore"
  209. filter-placeholder="请输入门店名称"
  210. >
  211. <template slot="left-footer">
  212. <div class="transfer-footer">可选门店数: {{ orgData.length - orgValue.length }}</div>
  213. </template>
  214. <template slot="right-footer">
  215. <div class="transfer-footer">已选门店数: {{ orgValue.length }}</div>
  216. </template>
  217. </el-transfer>
  218. </div>
  219. </el-tab-pane>
  220. </el-tabs>
  221. <div slot="footer" class="dialog-footer">
  222. <el-button type="primary" :loading="submitLoading" @click="submitForm">确 定</el-button>
  223. <el-button @click="cancel">取 消</el-button>
  224. </div>
  225. </template>
  226. </el-dialog>
  227. <!-- 门店列表弹窗 -->
  228. <data-list
  229. ref="storeList"
  230. title="绑定门店列表"
  231. :columns="storeColumns"
  232. :fetch-data="fetchStoreList"
  233. />
  234. <!-- 接单记录弹窗 -->
  235. <el-dialog
  236. title="接单记录"
  237. :visible.sync="openDetail"
  238. width="900px"
  239. :before-close="handleDetailClose" append-to-body
  240. >
  241. <div class="record-list" >
  242. <el-table
  243. :data="recordDetailList"
  244. border
  245. stripe
  246. style="width: 100%"
  247. >
  248. <el-table-column label="订单信息" align="left" min-width="200">
  249. <template slot-scope="scope">
  250. <div class="order-info">
  251. <div class="order-no">{{ scope.row.orderNo }}</div>
  252. <div class="store-name text-gray">{{ scope.row.storeName }}</div>
  253. </div>
  254. </template>
  255. </el-table-column>
  256. <el-table-column label="派送方式" align="center" width="100">
  257. <template slot-scope="scope">
  258. <el-tag :type="scope.row.sendClothWay === '0' ? 'success' : 'primary'" size="small">
  259. {{ scope.row.sendClothWay === '0' ? '到店' : '上门' }}
  260. </el-tag>
  261. </template>
  262. </el-table-column>
  263. <el-table-column label="派送时间" align="center" width="180">
  264. <template slot-scope="scope">
  265. {{ scope.row.sendClothWay === '0' ? scope.row.sendToStoreTime : scope.row.sendToUserTime }}
  266. </template>
  267. </el-table-column>
  268. <el-table-column label="取件时间" align="center" width="180" prop="pickUpTime" />
  269. <el-table-column label="送达时间" align="center" width="180" prop="finishTime" />
  270. </el-table>
  271. <pagination
  272. v-show="recordTotal > 0"
  273. :total="recordTotal"
  274. :page.sync="queryDetailParams.pageNum"
  275. :limit.sync="queryDetailParams.pageSize"
  276. @pagination="recordDetail"
  277. />
  278. </div>
  279. </el-dialog>
  280. </div>
  281. </template>
  282. <style lang="scss" scoped>
  283. .app-container {
  284. .search-card {
  285. margin-bottom: 16px;
  286. }
  287. .delivery-info {
  288. display: flex;
  289. align-items: center;
  290. padding: 8px 0;
  291. .avatar {
  292. margin-right: 12px;
  293. background: #f0f2f5;
  294. }
  295. .info {
  296. .name {
  297. font-size: 15px;
  298. font-weight: 500;
  299. margin-bottom: 4px;
  300. }
  301. .phone {
  302. font-size: 13px;
  303. }
  304. }
  305. }
  306. .level-desc {
  307. .description {
  308. margin-top: 8px;
  309. font-size: 13px;
  310. line-height: 1.5;
  311. }
  312. }
  313. .store-count {
  314. font-size: 16px;
  315. font-weight: 500;
  316. color: #409EFF;
  317. margin-right: 4px;
  318. }
  319. .store-text {
  320. color: #606266;
  321. font-size: 13px;
  322. }
  323. .user-info-card {
  324. margin: 16px 0;
  325. border-radius: 4px;
  326. .primary-text {
  327. font-weight: 500;
  328. color: #303133;
  329. }
  330. }
  331. .store-transfer {
  332. padding: 16px 0;
  333. .transfer-footer {
  334. padding: 8px 0;
  335. text-align: center;
  336. color: #909399;
  337. font-size: 13px;
  338. }
  339. }
  340. .record-list {
  341. .order-info {
  342. .order-no {
  343. font-weight: 500;
  344. margin-bottom: 4px;
  345. }
  346. .store-name {
  347. font-size: 13px;
  348. }
  349. }
  350. }
  351. .user-search {
  352. .user-result {
  353. margin-top: 20px;
  354. .user-card {
  355. .user-info {
  356. display: flex;
  357. align-items: flex-start;
  358. .info-content {
  359. margin-left: 16px;
  360. flex: 1;
  361. .name {
  362. font-size: 16px;
  363. font-weight: 500;
  364. margin-bottom: 8px;
  365. }
  366. .phone {
  367. color: #606266;
  368. margin-bottom: 8px;
  369. }
  370. .tags {
  371. .el-tag + .el-tag {
  372. margin-left: 8px;
  373. }
  374. }
  375. }
  376. }
  377. .select-btn {
  378. margin-top: 16px;
  379. text-align: right;
  380. }
  381. }
  382. }
  383. .no-result {
  384. text-align: center;
  385. color: #909399;
  386. padding: 32px 0;
  387. i {
  388. font-size: 32px;
  389. margin-bottom: 8px;
  390. }
  391. span {
  392. display: block;
  393. }
  394. }
  395. }
  396. .select-user-tip {
  397. padding: 32px 0;
  398. }
  399. }
  400. .text-gray {
  401. color: #909399;
  402. }
  403. </style>
  404. <script>
  405. import { listDelivery, getDelivery, delDelivery, addDelivery, updateDelivery, updateDeliveryStatus, getDeliveryOrderRecordList, getDeliveryStoreList } from '@/api/app/delivery'
  406. import { findUserByPhoneNumber } from '@/api/app/user'
  407. import { allOrg } from '@/api/system/store'
  408. import DataList from '@/components/DataList'
  409. export default {
  410. name: 'Delivery',
  411. components: {
  412. DataList
  413. },
  414. data() {
  415. return {
  416. // 门店列表列配置
  417. storeColumns: [
  418. { prop: 'code', label: '门店编号' },
  419. { prop: 'name', label: '门店名称' },
  420. { prop: 'address', label: '门店地址' },
  421. { prop: 'contactName', label: '联系人' },
  422. { prop: 'contactPhone', label: '联系电话' }
  423. ],
  424. // 当前选中的配送员ID
  425. currentDeliveryId: null,
  426. // 遮罩层
  427. loading: true,
  428. // 选中数组
  429. ids: [],
  430. phoneNumber: [],
  431. appUserId: null,
  432. // 非单个禁用
  433. single: true,
  434. // 非多个禁用
  435. multiple: true,
  436. // 显示搜索条件
  437. showSearch: true,
  438. // 总条数
  439. total: 0,
  440. recordTotal: 0,
  441. // 配送员表格数据
  442. deliveryList: [],
  443. // 弹出层标题
  444. title: '',
  445. recordDetailTitle: '',
  446. // 是否显示弹出层
  447. open: false,
  448. openDetail: false,
  449. // 查询参数
  450. queryParams: {
  451. appUserName: null,
  452. phoneNumber: null
  453. },
  454. // 查询参数
  455. queryDetailParams: {
  456. pageNum: 1,
  457. pageSize: 10,
  458. appUserId: null
  459. },
  460. // 表单参数
  461. form: {},
  462. // 表单校验
  463. rules: {
  464. description: [
  465. { required: true, message: '请输入派送员简介', trigger: 'blur' }
  466. ]
  467. },
  468. appUserInfo: null,
  469. // 简介
  470. description: null,
  471. isUpdate: true,
  472. orgData: [],
  473. orgValue: [],
  474. recordDetailList: [],
  475. activeTab: 'basic',
  476. submitLoading: false,
  477. userSelectVisible: false, // 用户选择弹窗显示状态
  478. searchPhone: '', // 搜索手机号
  479. searchUserInfo: null, // 搜索到的用户信息
  480. }
  481. },
  482. created() {
  483. allOrg().then((res) => {
  484. this.orgData = []
  485. res.data.forEach((org) => {
  486. if (org.sourceType == '02') {
  487. this.orgData.push({
  488. key: org.sourceType + ',' + org.id,
  489. label: org.name
  490. })
  491. }
  492. })
  493. })
  494. this.getList()
  495. },
  496. methods: {
  497. /**
  498. * 显示门店列表
  499. * @param {Object} row 当前行数据
  500. */
  501. showStoreList(row) {
  502. this.currentDeliveryId = row.appUserId
  503. this.$refs.storeList.show()
  504. },
  505. // 获取部门数据
  506. fetchStoreList(params) {
  507. return getDeliveryStoreList({
  508. pageNum: params.pageNum,
  509. pageSize: params.pageSize,
  510. deliveryId: this.currentDeliveryId
  511. })
  512. },
  513. //搜索用户
  514. searchUser() {
  515. if (!this.searchPhone) {
  516. this.$message.warning('请输入手机号')
  517. return
  518. }
  519. findUserByPhoneNumber({ phoneNumber: this.searchPhone }).then(res => {
  520. this.searchUserInfo = res.data
  521. })
  522. },
  523. /** 查询配送员列表 */
  524. getList() {
  525. this.$nextTick(() => {
  526. this.$refs.pagination.handleSearch(true)
  527. })
  528. },
  529. // 取消按钮
  530. cancel() {
  531. this.open = false
  532. this.reset()
  533. },
  534. /** 搜索按钮操作 */
  535. handleQuery() {
  536. this.getList()
  537. },
  538. /** 重置按钮操作 */
  539. resetQuery() {
  540. this.resetForm('queryForm')
  541. this.handleQuery()
  542. },
  543. // 多选框选中数据
  544. handleSelectionChange(selection) {
  545. this.ids = selection.map((item) => item.id)
  546. this.phoneNumber = selection.map((item) => item.phoneNumber)
  547. this.single = selection.length !== 1
  548. this.multiple = !selection.length
  549. },
  550. /** 新增按钮操作 */
  551. handleAdd() {
  552. this.reset()
  553. this.open = true
  554. this.title = '添加配送员'
  555. },
  556. /** 修改按钮操作 */
  557. handleUpdate(row) {
  558. this.reset()
  559. this.open = true
  560. this.title = '修改配送员'
  561. // 先获取用户信息
  562. findUserByPhoneNumber({ phoneNumber: row.phoneNumber }).then((res) => {
  563. this.appUserInfo = res.data
  564. // 获取配送员信息
  565. getDelivery(row.id).then((response) => {
  566. this.form = response.data
  567. this.description = response.data.description
  568. // 设置已绑定门店
  569. if (this.form.relationList && this.form.relationList.length > 0) {
  570. this.orgValue = this.form.relationList.map(vo => vo.sourceType + ',' + vo.orgId)
  571. }
  572. })
  573. })
  574. },
  575. /** 提交按钮 */
  576. submitForm() {
  577. if (!this.appUserInfo) {
  578. this.$modal.msgError('请先选择用户')
  579. return
  580. }
  581. if (!this.description) {
  582. this.$modal.msgError('请输入派送员简介')
  583. return
  584. }
  585. if (this.orgValue.length <= 0) {
  586. this.$modal.msgError('请选择关联门店')
  587. return
  588. }
  589. this.submitLoading = true
  590. try {
  591. const relationList = this.orgValue.map(vo => ({
  592. sourceType: vo.split(',')[0],
  593. orgId: vo.split(',')[1]
  594. }))
  595. const params = {
  596. ...this.form,
  597. appUserId: this.appUserInfo.id,
  598. description: this.description,
  599. relationList
  600. }
  601. const request = this.form.id ? updateDelivery(params) : addDelivery(params)
  602. request
  603. .then(() => {
  604. this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功')
  605. this.open = false
  606. this.getList()
  607. })
  608. .finally(() => {
  609. this.submitLoading = false
  610. })
  611. } catch (error) {
  612. this.submitLoading = false
  613. console.error('提交失败:', error)
  614. }
  615. },
  616. recordDetail(row) {
  617. if (row.appUserId != null && row.appUserId != '') {
  618. this.appUserId = row.appUserId
  619. this.queryDetailParams.appUserId = row.appUserId
  620. } else {
  621. this.queryDetailParams.appUserId = this.appUserId
  622. }
  623. getDeliveryOrderRecordList(this.queryDetailParams).then((response) => {
  624. this.recordDetailList = response.rows
  625. this.recordTotal = response.total
  626. this.openDetail = true
  627. this.recordDetailTitle = '接单记录'
  628. })
  629. },
  630. checkClose(done) {
  631. this.$confirm('是否关闭表单,关闭后数据将丢失?')
  632. .then(function () {
  633. done()
  634. })
  635. .then(() => { })
  636. .catch(() => { })
  637. },
  638. checkCloseDetail(done) {
  639. this.$confirm('是否关闭接单记录明细?')
  640. .then(function () {
  641. done()
  642. })
  643. .then(() => {
  644. this.appUserId = null
  645. this.queryDetailParams = {
  646. pageNum: 1,
  647. pageSize: 10,
  648. appUserId: null
  649. }
  650. })
  651. .catch(() => { })
  652. },
  653. handleStatusChange(row) {
  654. let text = row.status === '0' ? '启用' : '停用'
  655. this.$confirm('确认要' + text + ' ' + row.name + ' 吗?')
  656. .then(function () {
  657. return updateDeliveryStatus(row.id, row.status)
  658. })
  659. .then(() => {
  660. this.$modal.msgSuccess(text + '成功')
  661. })
  662. .catch(function () {
  663. row.status = row.status === '0' ? '1' : '0'
  664. })
  665. },
  666. // 过滤门店
  667. filterStore(query, item) {
  668. return item.label.toLowerCase().includes(query.toLowerCase())
  669. },
  670. // 处理对话框关闭
  671. handleDialogClose(done) {
  672. if (this.submitLoading) {
  673. this.$message.warning('正在提交数据,请稍候...')
  674. return
  675. }
  676. this.$confirm('确认关闭?未保存的数据将会丢失')
  677. .then(_ => {
  678. done()
  679. this.reset()
  680. })
  681. .catch(_ => {})
  682. },
  683. // 处理详情对话框关闭
  684. handleDetailClose(done) {
  685. done()
  686. this.queryDetailParams = {
  687. pageNum: 1,
  688. pageSize: 10,
  689. appUserId: null
  690. }
  691. },
  692. // 确认选择用户
  693. confirmSelectUser() {
  694. this.appUserInfo = this.searchUserInfo
  695. this.userSelectVisible = false
  696. this.searchPhone = ''
  697. this.searchUserInfo = null
  698. },
  699. // 重置表单
  700. reset() {
  701. this.form = {}
  702. this.appUserInfo = null
  703. this.description = null
  704. this.searchPhone = ''
  705. this.searchUserInfo = null
  706. this.orgValue = []
  707. this.activeTab = 'basic'
  708. }
  709. }
  710. }
  711. </script>