index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <template>
  2. <view class="page">
  3. <u-navbar title="点餐管理" :autoBack="true" :placeholder="true" :border-bottom="false"></u-navbar>
  4. <!-- 下单类型 -->
  5. <view class="mode-card">
  6. <text class="mode-label">下单类型</text>
  7. <u-radio-group v-model="orderMode" active-color="#5B9EFF" @change="onModeChange">
  8. <u-radio name="orderOnly" active-color="#5B9EFF">仅下单</u-radio>
  9. <u-radio name="orderPay" active-color="#5B9EFF">下单和支付</u-radio>
  10. </u-radio-group>
  11. </view>
  12. <view class="table-card" :style="{ paddingBottom: tableList.length ? '24rpx' : '0' }">
  13. <view class="table-head">
  14. <text class="table-title">桌号·共{{ tableList.length }}桌</text>
  15. <view v-if="tableList.length" class="add-link" @click="openAdd">
  16. <u-icon name="plus" color="#5B9EFF" size="26"></u-icon>
  17. <text>添加桌号</text>
  18. </view>
  19. </view>
  20. <!-- 空态 -->
  21. <view v-if="!tableList.length" class="empty-box">
  22. <view class="empty-illust">
  23. <view class="box-lid"></view>
  24. <view class="box-body"></view>
  25. </view>
  26. <text class="empty-text">您的门店暂无桌号哦~</text>
  27. <view class="empty-btn" @click="openAdd">去添加桌号</view>
  28. </view>
  29. <!-- 列表 -->
  30. <view v-else class="table-list">
  31. <view class="list-row" v-for="item in tableList" :key="item.no">
  32. <text class="no-text">{{ item.no }}-桌号</text>
  33. <view class="qr-entry" @click="openQr(item)">
  34. <image class="qr-ico" src="/static/scan/qrcode.png" mode="aspectFit"></image>
  35. <text>二维码</text>
  36. </view>
  37. </view>
  38. </view>
  39. </view>
  40. <view class="footer" v-if="tableList.length">
  41. <view class="btn soft" @click="goDownload">下载链接</view>
  42. </view>
  43. <!-- 添加桌号:底部弹层 -->
  44. <u-popup v-model="addVisible" mode="bottom" border-radius="24" :mask-close-able="true">
  45. <view class="add-sheet">
  46. <view class="sheet-title">门店桌号</view>
  47. <view class="sheet-sub">桌号·共{{ tableList.length }}桌</view>
  48. <view class="range-row">
  49. <input
  50. class="range-input"
  51. type="number"
  52. v-model="startNo"
  53. placeholder="请输入开始桌号"
  54. placeholder-class="ph"
  55. />
  56. <text class="range-dash">-</text>
  57. <input
  58. class="range-input"
  59. type="number"
  60. v-model="endNo"
  61. placeholder="请输入结束桌号"
  62. placeholder-class="ph"
  63. />
  64. </view>
  65. <view class="btn primary sheet-btn" @click="confirmAdd">确认添加</view>
  66. </view>
  67. </u-popup>
  68. <!-- 校验提示弹窗 -->
  69. <u-popup v-model="tipVisible" mode="center" border-radius="24" :mask-close-able="false">
  70. <view class="tip-dialog">
  71. <view class="tip-title">提示</view>
  72. <view class="tip-content">{{ tipMessage }}</view>
  73. <view class="tip-ok" @click="tipVisible = false">确定</view>
  74. </view>
  75. </u-popup>
  76. <!-- 查看二维码:底部弹层 -->
  77. <u-popup v-model="qrVisible" mode="bottom" border-radius="24" :mask-close-able="true">
  78. <view class="qr-sheet">
  79. <view class="sheet-title">{{ currentTable.no }}桌号二维码</view>
  80. <view class="qr-block">
  81. <image class="qr-img" :src="miniQrUrl" mode="aspectFit"></image>
  82. <text class="qr-label">小程序二维码</text>
  83. </view>
  84. <view class="qr-block" v-if="officialQrUrl">
  85. <image class="qr-img" :src="officialQrUrl" mode="aspectFit"></image>
  86. <text class="qr-label">公众号二维码</text>
  87. </view>
  88. <view class="btn primary sheet-btn" @click="saveQr">保存二维码</view>
  89. </view>
  90. </u-popup>
  91. </view>
  92. </template>
  93. <script>
  94. import { USER_INFO } from '@/common/util/constants.js'
  95. const STORAGE_LIST = 'ordering_table_list'
  96. const STORAGE_MODE = 'ordering_order_mode'
  97. function padTableNo(num) {
  98. const intVal = parseInt(String(num).trim(), 10)
  99. if (Number.isNaN(intVal) || intVal < 1) return ''
  100. if (intVal >= 1 && intVal <= 99) return String(intVal).padStart(3, '0')
  101. return String(intVal)
  102. }
  103. function buildQrContent(merchantId, tableNo) {
  104. return `${merchantId || ''}桌号${tableNo}`
  105. }
  106. function buildQrImageUrl(content) {
  107. return `https://api.qrserver.com/v1/create-qr-code/?size=280x280&data=${encodeURIComponent(content)}`
  108. }
  109. export default {
  110. data() {
  111. return {
  112. orderMode: 'orderOnly',
  113. tableList: [],
  114. addVisible: false,
  115. startNo: '',
  116. endNo: '',
  117. qrVisible: false,
  118. currentTable: { no: '' },
  119. miniQrUrl: '',
  120. officialQrUrl: '',
  121. merchantId: '',
  122. tipVisible: false,
  123. tipMessage: ''
  124. }
  125. },
  126. onShow() {
  127. const info = uni.getStorageSync(USER_INFO) || {}
  128. this.merchantId = info.merchantId || ''
  129. this.loadData()
  130. },
  131. methods: {
  132. storageKey(base) {
  133. return `${base}_${this.merchantId || 'default'}`
  134. },
  135. loadData() {
  136. const mode = uni.getStorageSync(this.storageKey(STORAGE_MODE))
  137. if (mode === 'orderOnly' || mode === 'orderPay') this.orderMode = mode
  138. const list = uni.getStorageSync(this.storageKey(STORAGE_LIST))
  139. this.tableList = Array.isArray(list) ? list : []
  140. },
  141. saveList() {
  142. uni.setStorageSync(this.storageKey(STORAGE_LIST), this.tableList)
  143. },
  144. showTip(msg) {
  145. this.tipMessage = msg
  146. this.tipVisible = true
  147. },
  148. onModeChange(val) {
  149. this.orderMode = val
  150. uni.setStorageSync(this.storageKey(STORAGE_MODE), val)
  151. },
  152. openAdd() {
  153. this.startNo = ''
  154. this.endNo = ''
  155. this.addVisible = true
  156. },
  157. confirmAdd() {
  158. const startRaw = String(this.startNo).trim()
  159. const endRaw = String(this.endNo).trim()
  160. if (!startRaw || !endRaw) {
  161. this.showTip('桌号不能为空')
  162. return
  163. }
  164. const start = parseInt(startRaw, 10)
  165. const end = parseInt(endRaw, 10)
  166. if (Number.isNaN(start) || Number.isNaN(end) || start < 1 || end < 1) {
  167. this.showTip('桌号不能为空')
  168. return
  169. }
  170. if (start > end) {
  171. this.showTip('开始桌号不能大于结束桌号')
  172. return
  173. }
  174. if (end - start + 1 > 200) {
  175. this.showTip('单次最多添加200个桌号')
  176. return
  177. }
  178. const existSet = new Set(this.tableList.map((i) => i.no))
  179. const toAdd = []
  180. for (let i = start; i <= end; i++) {
  181. const no = padTableNo(i)
  182. if (!no) continue
  183. if (existSet.has(no)) {
  184. this.showTip(`桌号${parseInt(no, 10)}已存在,请重新输入`)
  185. return
  186. }
  187. toAdd.push({ no, createTime: Date.now() })
  188. }
  189. this.tableList = this.tableList.concat(toAdd).sort((a, b) => parseInt(a.no, 10) - parseInt(b.no, 10))
  190. this.saveList()
  191. this.addVisible = false
  192. uni.showToast({ title: '添加成功', icon: 'success' })
  193. },
  194. goDownload() {
  195. uni.navigateTo({ url: '/pages/orderingSet/download' })
  196. },
  197. openQr(item) {
  198. this.currentTable = item
  199. const content = buildQrContent(this.merchantId, item.no)
  200. this.miniQrUrl = buildQrImageUrl(content)
  201. this.officialQrUrl = ''
  202. this.qrVisible = true
  203. },
  204. saveQr() {
  205. const url = this.miniQrUrl
  206. if (!url) {
  207. uni.showToast({ title: '暂无二维码', icon: 'none' })
  208. return
  209. }
  210. uni.showLoading({ title: '保存中' })
  211. uni.downloadFile({
  212. url,
  213. success: (res) => {
  214. if (res.statusCode !== 200) {
  215. uni.hideLoading()
  216. uni.showToast({ title: '下载失败', icon: 'none' })
  217. return
  218. }
  219. uni.saveImageToPhotosAlbum({
  220. filePath: res.tempFilePath,
  221. success: () => {
  222. uni.hideLoading()
  223. uni.showToast({ title: '已保存到相册', icon: 'success' })
  224. },
  225. fail: () => {
  226. uni.hideLoading()
  227. uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' })
  228. }
  229. })
  230. },
  231. fail: () => {
  232. uni.hideLoading()
  233. uni.showToast({ title: '下载失败', icon: 'none' })
  234. }
  235. })
  236. }
  237. }
  238. }
  239. </script>
  240. <style lang="scss" scoped>
  241. $theme: #5b9eff;
  242. $theme-soft: #eaf3ff;
  243. $page-bg: #f5f6f8;
  244. .page {
  245. min-height: 100vh;
  246. background: $page-bg;
  247. padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
  248. box-sizing: border-box;
  249. }
  250. .mode-card {
  251. margin: 24rpx 28rpx 0;
  252. padding: 32rpx 28rpx;
  253. background: #fff;
  254. border-radius: 20rpx;
  255. display: flex;
  256. align-items: center;
  257. justify-content: space-between;
  258. .mode-label {
  259. font-size: 30rpx;
  260. color: #1d2129;
  261. font-weight: 500;
  262. }
  263. }
  264. .table-card {
  265. margin: 24rpx 28rpx 0;
  266. background: #fff;
  267. border-radius: 20rpx;
  268. overflow: hidden;
  269. background-image: linear-gradient(180deg, #eef5ff 0%, #ffffff 120rpx);
  270. }
  271. .table-head {
  272. display: flex;
  273. align-items: center;
  274. justify-content: space-between;
  275. padding: 28rpx 28rpx 12rpx;
  276. .table-title {
  277. font-size: 30rpx;
  278. color: #1d2129;
  279. font-weight: 600;
  280. }
  281. .add-link {
  282. display: flex;
  283. align-items: center;
  284. gap: 4rpx;
  285. font-size: 28rpx;
  286. color: $theme;
  287. }
  288. }
  289. .empty-box {
  290. display: flex;
  291. flex-direction: column;
  292. align-items: center;
  293. padding: 80rpx 40rpx 72rpx;
  294. .empty-illust {
  295. width: 180rpx;
  296. height: 140rpx;
  297. position: relative;
  298. margin-bottom: 28rpx;
  299. .box-lid {
  300. position: absolute;
  301. left: 20rpx;
  302. top: 8rpx;
  303. width: 140rpx;
  304. height: 36rpx;
  305. border: 8rpx solid #d0d5dd;
  306. border-radius: 8rpx 8rpx 0 0;
  307. border-bottom: none;
  308. transform: rotate(-8deg);
  309. transform-origin: left bottom;
  310. }
  311. .box-body {
  312. position: absolute;
  313. left: 24rpx;
  314. bottom: 10rpx;
  315. width: 132rpx;
  316. height: 88rpx;
  317. border: 8rpx solid #d0d5dd;
  318. border-radius: 8rpx;
  319. background: linear-gradient(180deg, #f7f8fa 0%, #eef0f3 100%);
  320. }
  321. }
  322. .empty-text {
  323. font-size: 28rpx;
  324. color: #86909c;
  325. margin-bottom: 40rpx;
  326. }
  327. .empty-btn {
  328. min-width: 280rpx;
  329. height: 72rpx;
  330. padding: 0 40rpx;
  331. border-radius: 40rpx;
  332. border: 2rpx solid #a8cbff;
  333. color: $theme;
  334. font-size: 28rpx;
  335. display: flex;
  336. align-items: center;
  337. justify-content: center;
  338. background: #fff;
  339. }
  340. }
  341. .table-list {
  342. padding: 0 8rpx 8rpx;
  343. }
  344. .list-row {
  345. display: flex;
  346. align-items: center;
  347. justify-content: space-between;
  348. padding: 34rpx 20rpx;
  349. margin: 0 12rpx;
  350. border-bottom: 1rpx solid #f0f2f5;
  351. &:last-child {
  352. border-bottom: none;
  353. }
  354. .row-left {
  355. display: flex;
  356. align-items: center;
  357. gap: 20rpx;
  358. }
  359. .no-text {
  360. font-size: 30rpx;
  361. color: #1d2129;
  362. }
  363. .qr-entry {
  364. display: flex;
  365. align-items: center;
  366. gap: 8rpx;
  367. font-size: 26rpx;
  368. color: #86909c;
  369. .qr-ico {
  370. width: 32rpx;
  371. height: 32rpx;
  372. }
  373. }
  374. }
  375. .footer {
  376. position: fixed;
  377. left: 0;
  378. right: 0;
  379. bottom: 0;
  380. padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
  381. background: #fff;
  382. z-index: 20;
  383. }
  384. .btn {
  385. height: 88rpx;
  386. border-radius: 44rpx;
  387. display: flex;
  388. align-items: center;
  389. justify-content: center;
  390. font-size: 30rpx;
  391. font-weight: 500;
  392. &.soft {
  393. background: $theme-soft;
  394. color: $theme;
  395. }
  396. &.primary {
  397. background: $theme;
  398. color: #fff;
  399. }
  400. }
  401. .add-sheet,
  402. .qr-sheet {
  403. padding: 36rpx 40rpx calc(40rpx + env(safe-area-inset-bottom));
  404. background: #fff;
  405. }
  406. .sheet-title {
  407. text-align: center;
  408. font-size: 34rpx;
  409. font-weight: 600;
  410. color: #1d2129;
  411. }
  412. .sheet-sub {
  413. margin-top: 28rpx;
  414. font-size: 28rpx;
  415. color: #1d2129;
  416. font-weight: 500;
  417. }
  418. .range-row {
  419. margin-top: 28rpx;
  420. display: flex;
  421. align-items: center;
  422. gap: 16rpx;
  423. .range-input {
  424. flex: 1;
  425. height: 80rpx;
  426. padding: 0 24rpx;
  427. background: #f5f6f8;
  428. border-radius: 12rpx;
  429. font-size: 28rpx;
  430. color: #333;
  431. text-align: center;
  432. }
  433. .range-dash {
  434. color: #86909c;
  435. font-size: 32rpx;
  436. }
  437. .ph {
  438. color: #c0c4cc;
  439. font-size: 26rpx;
  440. }
  441. }
  442. .sheet-btn {
  443. margin-top: 48rpx;
  444. }
  445. .tip-dialog {
  446. width: 560rpx;
  447. background: #fff;
  448. border-radius: 24rpx;
  449. overflow: hidden;
  450. .tip-title {
  451. text-align: center;
  452. padding-top: 40rpx;
  453. font-size: 32rpx;
  454. font-weight: 600;
  455. color: #1d2129;
  456. }
  457. .tip-content {
  458. text-align: center;
  459. padding: 28rpx 40rpx 36rpx;
  460. font-size: 28rpx;
  461. color: #4e5969;
  462. line-height: 1.5;
  463. }
  464. .tip-ok {
  465. height: 96rpx;
  466. border-top: 1rpx solid #f0f2f5;
  467. display: flex;
  468. align-items: center;
  469. justify-content: center;
  470. font-size: 30rpx;
  471. color: $theme;
  472. font-weight: 500;
  473. }
  474. }
  475. .qr-block {
  476. margin-top: 28rpx;
  477. border: 1rpx solid #e5e6eb;
  478. border-radius: 16rpx;
  479. padding: 32rpx 24rpx;
  480. display: flex;
  481. flex-direction: column;
  482. align-items: center;
  483. .qr-img {
  484. width: 280rpx;
  485. height: 280rpx;
  486. background: #f5f6f8;
  487. }
  488. .qr-label {
  489. margin-top: 20rpx;
  490. font-size: 26rpx;
  491. color: #4e5969;
  492. }
  493. }
  494. </style>