index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. <template>
  2. <view class="page">
  3. <view class="page-content">
  4. <u-navbar title="门店管理" :autoBack="true" :placeholder="true" :border-bottom="false"></u-navbar>
  5. <!-- 下单类型 -->
  6. <view class="mode-card">
  7. <text class="mode-label">下单类型</text>
  8. <u-radio-group v-model="orderMode" active-color="#5B9EFF" :disabled="modeSaving" @change="onModeChange">
  9. <u-radio name="orderOnly" active-color="#3B4DF8" class="u-m-r-52">仅下单</u-radio>
  10. <u-radio name="orderPay" active-color="#3B4DF8" class="radio-item">下单和支付</u-radio>
  11. </u-radio-group>
  12. </view>
  13. <view class="table-card">
  14. <view class="table-head">
  15. <text class="table-title">桌号·共{{ total }}桌</text>
  16. <view v-if="tableList.length" class="add-link" @click="openAdd">
  17. <u-icon name="plus" color="#165DFF" size="26"></u-icon>
  18. <text>添加桌号</text>
  19. </view>
  20. </view>
  21. <!-- 空态 -->
  22. <view v-if="!tableList.length" class="empty-box">
  23. <view class="empty-illust">
  24. <u-image width="350rpx" height="350rpx" :src="`${$config.staticUrl}/static/common/empty.png`"></u-image>
  25. </view>
  26. <text class="empty-text">您的门店暂无桌号哦~</text>
  27. <view class="empty-btn" @click="openAdd">去添加桌号</view>
  28. </view>
  29. <!-- 列表:仅此区域滚动 -->
  30. <scroll-view
  31. v-else
  32. scroll-y
  33. class="table-scroll"
  34. :lower-threshold="80"
  35. @scrolltolower="loadMore"
  36. >
  37. <view class="table-list">
  38. <view class="list-row" v-for="item in tableList" :key="item.id || item.no">
  39. <text class="no-text">{{ item.no }}-桌号</text>
  40. <view class="qr-entry" @click="openQr(item)">
  41. <image class="qr-ico" :src="`${$config.staticUrl}/static/order-food/icon-qrcode.png`" mode="aspectFit"></image>
  42. <text>二维码</text>
  43. </view>
  44. </view>
  45. <u-loadmore :status="loadStatus" margin-top="24" margin-bottom="24" />
  46. </view>
  47. </scroll-view>
  48. </view>
  49. </view>
  50. <view class="footer" v-if="tableList.length">
  51. <view class="btn soft" @click="goDownload">下载链接</view>
  52. </view>
  53. <!-- 添加桌号:底部弹层 -->
  54. <u-popup v-model="addVisible" mode="bottom" border-radius="24" :mask-close-able="true">
  55. <view class="add-sheet">
  56. <view class="sheet-title">门店桌号</view>
  57. <view class="sheet-card">
  58. <view class="sheet-sub">桌号·共{{ total }}桌</view>
  59. <view class="range-row">
  60. <input
  61. class="range-input"
  62. type="number"
  63. v-model="startNo"
  64. placeholder="请输入开始桌号"
  65. placeholder-class="ph"
  66. />
  67. <text class="range-dash"></text>
  68. <input
  69. class="range-input"
  70. type="number"
  71. v-model="endNo"
  72. placeholder="请输入结束桌号"
  73. placeholder-class="ph"
  74. />
  75. </view>
  76. </view>
  77. <view class="btn primary sheet-btn" @click="confirmAdd">确认添加</view>
  78. </view>
  79. </u-popup>
  80. <!-- 校验提示弹窗 -->
  81. <u-popup v-model="tipVisible" mode="center" border-radius="24" :mask-close-able="false">
  82. <view class="tip-dialog">
  83. <view class="tip-title">提示</view>
  84. <view class="tip-content">{{ tipMessage }}</view>
  85. <view class="tip-ok" @click="tipVisible = false">确定</view>
  86. </view>
  87. </u-popup>
  88. <!-- 查看二维码:底部弹层 -->
  89. <QrCodePopup
  90. v-model="qrVisible"
  91. :table-no="currentTable.no"
  92. :mini-qr-url="miniQrUrl"
  93. :official-qr-url="officialQrUrl"
  94. ></QrCodePopup>
  95. </view>
  96. </template>
  97. <script>
  98. import QrCodePopup from './components/QrCodePopup.vue'
  99. import {
  100. batchCreateTables,
  101. fetchDiningConfig,
  102. fetchTableList,
  103. getMerchantId,
  104. mapDiningModeToOrderMode,
  105. mapOrderModeToDiningMode,
  106. normalizeTableList,
  107. parseDiningConfigResponse,
  108. parseTableListResponse,
  109. saveDiningConfig
  110. } from '@/api/diningTable.js'
  111. const STORAGE_MODE = 'ordering_order_mode'
  112. function buildQrContent(merchantId, tableNo) {
  113. return `${merchantId || ''}桌号${tableNo}`
  114. }
  115. function buildQrImageUrl(content) {
  116. return `https://api.qrserver.com/v1/create-qr-code/?size=280x280&data=${encodeURIComponent(content)}`
  117. }
  118. export default {
  119. data() {
  120. return {
  121. orderMode: 'orderOnly',
  122. tableList: [],
  123. pageNo: 1,
  124. pageSize: 10,
  125. total: 0,
  126. loadStatus: 'loadmore',
  127. listLoading: false,
  128. addVisible: false,
  129. startNo: '',
  130. endNo: '',
  131. qrVisible: false,
  132. currentTable: { no: '' },
  133. miniQrUrl: '',
  134. officialQrUrl: '',
  135. merchantId: '',
  136. modeSaving: false,
  137. tipVisible: false,
  138. tipMessage: '',
  139. submitting: false
  140. }
  141. },
  142. components: {
  143. QrCodePopup
  144. },
  145. onShow() {
  146. this.merchantId = getMerchantId()
  147. this.loadDiningConfig()
  148. this.loadData(true)
  149. },
  150. methods: {
  151. storageKey(base) {
  152. return `${base}_${this.merchantId || 'default'}`
  153. },
  154. loadMore() {
  155. this.loadData(false)
  156. },
  157. loadDiningConfig() {
  158. if (!this.merchantId) return
  159. fetchDiningConfig(this.merchantId).then(res => {
  160. if (res.data.code !== 200 || !res.data.result) return
  161. const cfg = parseDiningConfigResponse(res.data.result)
  162. if (cfg.diningMode) {
  163. this.orderMode = mapDiningModeToOrderMode(cfg.diningMode)
  164. uni.setStorageSync(this.storageKey(STORAGE_MODE), this.orderMode)
  165. }
  166. }).catch(() => {})
  167. },
  168. loadData(refresh = true) {
  169. if (!this.merchantId) {
  170. this.tableList = []
  171. this.total = 0
  172. return
  173. }
  174. if (this.listLoading) return
  175. if (!refresh && this.loadStatus === 'nomore') return
  176. if (refresh) {
  177. this.pageNo = 1
  178. this.loadStatus = 'loadmore'
  179. }
  180. const requestPage = refresh ? 1 : this.pageNo
  181. this.listLoading = true
  182. if (!refresh) this.loadStatus = 'loading'
  183. fetchTableList(this.merchantId, {
  184. pageNo: requestPage,
  185. pageSize: this.pageSize
  186. }).then(res => {
  187. if (res.data.code !== 200) {
  188. if (refresh) {
  189. this.tableList = []
  190. this.total = 0
  191. }
  192. this.showTip(res.data.message || '获取桌号列表失败')
  193. return
  194. }
  195. const parsed = parseTableListResponse(res.data.result)
  196. const newList = normalizeTableList(parsed.list)
  197. this.tableList = refresh
  198. ? newList
  199. : normalizeTableList(this.tableList.concat(newList))
  200. this.total = parsed.total
  201. this.pageNo = parsed.current + 1
  202. this.loadStatus = parsed.hasMore ? 'loadmore' : 'nomore'
  203. }).catch(() => {
  204. if (refresh) {
  205. this.tableList = []
  206. this.total = 0
  207. }
  208. this.loadStatus = 'loadmore'
  209. this.showTip('获取桌号列表失败,请稍后重试')
  210. }).finally(() => {
  211. this.listLoading = false
  212. })
  213. },
  214. showTip(msg) {
  215. this.tipMessage = msg
  216. this.tipVisible = true
  217. },
  218. onModeChange(val) {
  219. if (!this.merchantId) {
  220. this.showTip('未获取到门店信息')
  221. this.$nextTick(() => {
  222. this.orderMode = val === 'orderPay' ? 'orderOnly' : 'orderPay'
  223. })
  224. return
  225. }
  226. if (this.modeSaving) return
  227. const prevMode = val === 'orderPay' ? 'orderOnly' : 'orderPay'
  228. this.modeSaving = true
  229. uni.showLoading({ title: '保存中', mask: true })
  230. saveDiningConfig({
  231. merchantId: this.merchantId,
  232. diningMode: mapOrderModeToDiningMode(val),
  233. enabled: true, // 是否启用线上扫码点餐
  234. version: 0 // 版本号
  235. }).then(res => {
  236. if (res.data.code === 200) {
  237. const cfg = parseDiningConfigResponse(res.data.result || {})
  238. if (cfg.diningMode) this.orderMode = mapDiningModeToOrderMode(cfg.diningMode)
  239. uni.setStorageSync(this.storageKey(STORAGE_MODE), this.orderMode)
  240. uni.showToast({ title: '设置成功', icon: 'success' })
  241. return
  242. }
  243. this.orderMode = prevMode
  244. this.showTip(res.data.message || '保存失败')
  245. }).catch((err) => {
  246. this.orderMode = prevMode
  247. const msg = (err && err.data && err.data.message) || '保存失败,请稍后重试'
  248. this.showTip(msg)
  249. }).finally(() => {
  250. this.modeSaving = false
  251. uni.hideLoading()
  252. })
  253. },
  254. openAdd() {
  255. this.startNo = ''
  256. this.endNo = ''
  257. this.addVisible = true
  258. },
  259. confirmAdd() {
  260. const startRaw = String(this.startNo).trim()
  261. const endRaw = String(this.endNo).trim()
  262. if (!startRaw || !endRaw) {
  263. this.showTip('桌号不能为空')
  264. return
  265. }
  266. if (!/^[0-9]+$/.test(startRaw) || !/^[0-9]+$/.test(endRaw)) {
  267. this.showTip('桌号仅支持正整数')
  268. return
  269. }
  270. const start = parseInt(startRaw, 10)
  271. const end = parseInt(endRaw, 10)
  272. if (start < 1 || end < 1) {
  273. this.showTip('桌号不能为空')
  274. return
  275. }
  276. if (start > end) {
  277. this.showTip('开始桌号不能大于结束桌号')
  278. return
  279. }
  280. if (end - start + 1 > 200) {
  281. this.showTip('单次最多添加200个桌号')
  282. return
  283. }
  284. if (!this.merchantId) {
  285. this.showTip('未获取到门店信息')
  286. return
  287. }
  288. if (this.submitting) return
  289. const payload = {
  290. merchantId: this.merchantId,
  291. startTableCode: startRaw,
  292. endTableCode: endRaw,
  293. diningMode: mapOrderModeToDiningMode(this.orderMode)
  294. }
  295. this.submitting = true
  296. uni.showLoading({ title: '提交中', mask: true })
  297. batchCreateTables(payload).then(res => {
  298. if (res.data.code === 200) {
  299. this.addVisible = false
  300. this.startNo = ''
  301. this.endNo = ''
  302. this.loadData(true)
  303. uni.showToast({ title: '添加成功', icon: 'success' })
  304. } else {
  305. this.showTip(res.data.message || '添加失败')
  306. }
  307. }).catch((err) => {
  308. const msg = (err && err.data && err.data.message) || '网络异常,请稍后重试'
  309. this.showTip(msg)
  310. }).finally(() => {
  311. this.submitting = false
  312. uni.hideLoading()
  313. })
  314. },
  315. goDownload() {
  316. uni.navigateTo({ url: '/pages/orderingSet/download' })
  317. },
  318. openQr(item) {
  319. this.currentTable = item
  320. if (item.miniQrUrl) {
  321. this.miniQrUrl = item.miniQrUrl
  322. this.officialQrUrl = item.officialQrUrl || ''
  323. } else {
  324. const content = buildQrContent(this.merchantId, item.no)
  325. this.miniQrUrl = buildQrImageUrl(content)
  326. this.officialQrUrl = ''
  327. }
  328. this.qrVisible = true
  329. }
  330. }
  331. }
  332. </script>
  333. <style lang="scss" scoped>
  334. $theme: #5b9eff;
  335. $theme-soft: #E8F3FF;
  336. $page-bg: #F7F7F7;
  337. .page {
  338. height: 100vh;
  339. display: flex;
  340. flex-direction: column;
  341. overflow: hidden;
  342. background: $page-bg;
  343. box-sizing: border-box;
  344. font-family: PingFang SC, PingFang SC;
  345. font-weight: 400;
  346. line-height: 44rpx;
  347. }
  348. .page-content {
  349. flex: 1;
  350. min-height: 0;
  351. display: flex;
  352. flex-direction: column;
  353. overflow: hidden;
  354. padding: 0 32rpx 5rpx;
  355. box-sizing: border-box;
  356. }
  357. .mode-card {
  358. flex-shrink: 0;
  359. margin: 24rpx 0 32rpx;
  360. padding: 32rpx;
  361. background: #fff;
  362. border-radius: 24rpx;
  363. display: flex;
  364. align-items: center;
  365. justify-content: space-between;
  366. font-size: 28rpx;
  367. color: #1D2129;
  368. .mode-label {
  369. font-weight: 500;
  370. font-size: 32rpx;
  371. color: #1D2129;
  372. }
  373. }
  374. .table-card {
  375. flex: 1;
  376. min-height: 0;
  377. display: flex;
  378. flex-direction: column;
  379. overflow: hidden;
  380. background: #fff;
  381. border-radius: 24rpx;
  382. background: linear-gradient( -216deg, #DEEEFF 0%, #FFFFFF 28%);
  383. }
  384. .table-head {
  385. flex-shrink: 0;
  386. display: flex;
  387. align-items: center;
  388. justify-content: space-between;
  389. padding: 32rpx;
  390. border-bottom: 1rpx solid #EEEEEE;
  391. .table-title {
  392. font-size: 30rpx;
  393. color: #1d2129;
  394. font-weight: 600;
  395. }
  396. .add-link {
  397. display: flex;
  398. align-items: center;
  399. gap: 8rpx;
  400. font-size: 28rpx;
  401. font-weight: 500;
  402. font-size: 28rpx;
  403. color: #165DFF;
  404. }
  405. }
  406. .empty-box {
  407. flex: 1;
  408. min-height: 0;
  409. display: flex;
  410. flex-direction: column;
  411. align-items: center;
  412. // justify-content: center;
  413. padding-bottom: 72rpx;
  414. .empty-text {
  415. font-size: 28rpx;
  416. color: #86909C;
  417. margin: 8rpx 0 48rpx;
  418. }
  419. .empty-btn {
  420. width: 264rpx;
  421. height: 72rpx;
  422. background: #E8F3FF;
  423. border-radius: 140rpx;
  424. border: 1rpx solid #1C7CF9;
  425. display: flex;
  426. align-items: center;
  427. justify-content: center;
  428. font-weight: 500;
  429. font-size: 28rpx;
  430. color: #165DFF;
  431. }
  432. }
  433. .table-scroll {
  434. flex: 1;
  435. height: 0;
  436. min-height: 0;
  437. }
  438. .table-list {
  439. padding: 0 20rpx 10rpx 32rpx;
  440. }
  441. .list-row {
  442. display: flex;
  443. align-items: center;
  444. justify-content: space-between;
  445. padding: 32rpx 8rpx;
  446. margin: 0 12rpx;
  447. border-bottom: 1rpx solid #f0f2f5;
  448. &:last-child {
  449. border-bottom: none;
  450. }
  451. .row-left {
  452. display: flex;
  453. align-items: center;
  454. gap: 20rpx;
  455. }
  456. .no-text {
  457. font-size: 32rpx;
  458. color: #4E5969;
  459. }
  460. .qr-entry {
  461. display: flex;
  462. align-items: center;
  463. gap: 8rpx;
  464. font-weight: 500;
  465. font-size: 26rpx;
  466. color: #86909c;
  467. .qr-ico {
  468. width: 32rpx;
  469. height: 32rpx;
  470. }
  471. }
  472. }
  473. .footer {
  474. flex-shrink: 0;
  475. padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
  476. background: #fff;
  477. z-index: 20;
  478. }
  479. .btn {
  480. height: 88rpx;
  481. border-radius: 140rpx;
  482. display: flex;
  483. align-items: center;
  484. justify-content: center;
  485. font-weight: 500;
  486. font-size: 32rpx;
  487. color: #FFFFFF;
  488. &.soft {
  489. background: #E8F3FF;
  490. border-radius: 140rpx;
  491. border: 1rpx solid #1C7CF9;
  492. color: #165DFF;
  493. }
  494. &.primary {
  495. background: linear-gradient( 88deg, #0FB0FF 0%, #2260F6 100%);
  496. color: #fff;
  497. }
  498. }
  499. .add-sheet {
  500. padding: 0 32rpx calc(40rpx + env(safe-area-inset-bottom));
  501. background: #fff;
  502. }
  503. .sheet-title {
  504. text-align: center;
  505. font-weight: 500;
  506. font-size: 32rpx;
  507. color: #1D2129;
  508. padding: 56rpx 0 52rpx;
  509. }
  510. .sheet-card {
  511. background: linear-gradient( -216deg, #DEEEFF 0%, #FFFFFF 18%);
  512. border-radius: 24rpx;
  513. border: 1rpx solid #11ADFF;
  514. margin-bottom: 598rpx;
  515. .sheet-sub {
  516. font-size: 28rpx;
  517. color: #1d2129;
  518. font-weight: 500;
  519. padding: 32rpx 32rpx 24rpx;
  520. border-bottom: 1rpx solid #EEEEEE;
  521. }
  522. .range-row {
  523. display: flex;
  524. align-items: center;
  525. gap: 94rpx;
  526. padding: 32rpx;
  527. text-align: center;
  528. .range-input {
  529. flex: 1;
  530. font-weight: 500;
  531. font-size: 32rpx;
  532. color: #165DFF;
  533. }
  534. .range-dash {
  535. width: 40rpx;
  536. height: 2rpx;
  537. background: #C9CDD4;
  538. }
  539. .ph {
  540. font-size: 28rpx;
  541. color: #C9CDD4;
  542. }
  543. }
  544. }
  545. .sheet-btn {
  546. margin-top: 22rpx;
  547. }
  548. .tip-dialog {
  549. width: 560rpx;
  550. background: #fff;
  551. border-radius: 24rpx;
  552. overflow: hidden;
  553. padding: 48rpx;
  554. .tip-title {
  555. text-align: center;
  556. font-weight: 500;
  557. font-size: 32rpx;
  558. color: #000000;
  559. }
  560. .tip-content {
  561. text-align: center;
  562. padding: 68rpx 0 106rpx;
  563. font-size: 28rpx;
  564. color: #1D2129;
  565. }
  566. .tip-ok {
  567. display: flex;
  568. align-items: center;
  569. justify-content: center;
  570. font-weight: 500;
  571. font-size: 32rpx;
  572. color: #FFFFFF;
  573. height: 88rpx;
  574. background: linear-gradient( 92deg, #0FB0FF 0%, #2260F6 100%);
  575. border-radius: 140rpx;
  576. }
  577. }
  578. ::v-deep {
  579. .u-radio__label {
  580. margin: 0 0 0 16rpx;
  581. }
  582. }
  583. </style>