DishOrderAdd.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. <template>
  2. <view class="dish-order-add">
  3. <view class="order-header">
  4. <view>
  5. <view class="room-name">{{ currentRoom.roomName || '-' }}</view>
  6. <view class="room-no">包房号:{{ currentRoom.roomNo || '-' }}</view>
  7. </view>
  8. <button class="mini-btn primary" @click="backOrderDetail">返回订单</button>
  9. </view>
  10. <view class="order-main">
  11. <view class="dish-panel">
  12. <view class="dish-search">
  13. <input class="search-input" v-model="dishSearchName" placeholder="搜索菜品名称" confirm-type="search"
  14. @confirm="searchDish" />
  15. <button class="search-btn" @click="searchDish">搜索</button>
  16. </view>
  17. <view class="dish-content">
  18. <scroll-view scroll-y class="category-list">
  19. <view v-for="item in categoryList" :key="item.id" class="category-item"
  20. :class="{ active: activeTypeId === item.id }" @click="changeType(item)">
  21. {{ item.name }}
  22. </view>
  23. </scroll-view>
  24. <scroll-view scroll-y class="dish-list">
  25. <view v-if="dishLoading" class="empty-box">加载中...</view>
  26. <view v-else-if="dishList.length === 0" class="empty-box">暂无菜品</view>
  27. <view v-else class="dish-grid">
  28. <view v-for="item in dishList" :key="item.id" class="dish-card">
  29. <image v-if="item.previewImageUrl" class="dish-image" :src="item.previewImageUrl"
  30. mode="aspectFill"></image>
  31. <view v-else class="dish-image dish-image-empty">暂无图片</view>
  32. <view class="dish-info">
  33. <view class="dish-name">{{ item.dishName }}</view>
  34. <view class="dish-desc">{{ item.spec || item.taste || item.remarks || '暂无说明' }}</view>
  35. <view class="dish-bottom">
  36. <text class="dish-price">¥{{ formatMoney(item.salePrice) }}</text>
  37. <button class="plain-btn" @click="addDish(item)">点菜</button>
  38. </view>
  39. </view>
  40. </view>
  41. </view>
  42. </scroll-view>
  43. </view>
  44. </view>
  45. <view class="ordered-panel">
  46. <view class="ordered-title">本次加菜</view>
  47. <scroll-view scroll-y class="ordered-list">
  48. <view v-if="orderLoading" class="ordered-empty">加载中...</view>
  49. <view v-else-if="orderedList.length === 0" class="ordered-empty">
  50. <view class="ordered-empty-title">暂未加菜</view>
  51. <view class="ordered-empty-sub">从左侧选择菜品后会显示在这里</view>
  52. </view>
  53. <view v-else>
  54. <view class="ordered-head">
  55. <text>菜品</text>
  56. <text>单价</text>
  57. <text>总价</text>
  58. <text>数量</text>
  59. </view>
  60. <view v-for="item in orderedList" :key="item.dishId" class="ordered-item">
  61. <view class="ordered-dish">
  62. <view class="ordered-dish-name">{{ item.dishName }}</view>
  63. </view>
  64. <view class="ordered-price">¥{{ formatMoney(item.salePrice) }}</view>
  65. <view class="ordered-total">¥{{ formatMoney(itemTotal(item)) }}</view>
  66. <view class="ordered-qty">
  67. <button class="qty-btn" @click="reduceDish(item)">-</button>
  68. <text>{{ item.quantity }}</text>
  69. <button class="qty-btn" @click="increaseOrderedDish(item)">+</button>
  70. </view>
  71. </view>
  72. </view>
  73. </scroll-view>
  74. <view class="ordered-footer">
  75. <view class="ordered-summary">
  76. <text>共 {{ orderedList.length }} 项 ,本次加菜金额</text>
  77. <strong>¥{{ formatMoney(orderTotal) }}</strong>
  78. </view>
  79. <button class="footer-btn primary" :disabled="submitting" @click="confirmSubmitOrder">下单</button>
  80. </view>
  81. </view>
  82. </view>
  83. </view>
  84. </template>
  85. <script>
  86. import DishOrderService from '@/api/psi/DishOrderService'
  87. import OSSSerivce from '@/api/sys/OSSService'
  88. export default {
  89. data() {
  90. return {
  91. roomId: '',
  92. orderId: '',
  93. currentRoom: {},
  94. currentOrder: null,
  95. categoryList: [],
  96. activeTypeId: 'all',
  97. dishSearchName: '',
  98. dishList: [],
  99. orderedList: [],
  100. existDishSortMap: {},
  101. maxDishSort: 0,
  102. dishLoading: false,
  103. orderLoading: false,
  104. submitting: false,
  105. dishOrderService: null,
  106. ossService: null
  107. }
  108. },
  109. computed: {
  110. orderTotal() {
  111. return this.orderedList.reduce((total, item) => total + this.itemTotal(item), 0)
  112. },
  113. orderQuantity() {
  114. return this.orderedList.reduce((total, item) => total + Number(item.quantity || 0), 0)
  115. },
  116. submitDetailList() {
  117. let appendIndex = 0
  118. return this.orderedList.map((item) => {
  119. let dishSort = this.existDishSortMap[item.dishId]
  120. if (!dishSort) {
  121. appendIndex += 1
  122. dishSort = this.maxDishSort + appendIndex
  123. }
  124. return {
  125. dishId: item.dishId,
  126. dishName: item.dishName,
  127. quantity: item.quantity,
  128. dishSort
  129. }
  130. })
  131. }
  132. },
  133. onLoad(option) {
  134. this.roomId = option && option.roomId ? option.roomId : ''
  135. this.orderId = option && option.orderId ? option.orderId : ''
  136. uni.setNavigationBarTitle({ title: '加菜' })
  137. this.dishOrderService = new DishOrderService()
  138. this.ossService = new OSSSerivce()
  139. this.initPage()
  140. },
  141. methods: {
  142. initPage() {
  143. if (!this.roomId) {
  144. this.backOrderDetail()
  145. return
  146. }
  147. this.orderedList = []
  148. this.activeTypeId = 'all'
  149. this.dishSearchName = ''
  150. this.loadRoom(this.roomId)
  151. this.loadCurrentOrder(this.roomId)
  152. this.loadCategoryList()
  153. this.loadDishList()
  154. },
  155. loadRoom(roomId) {
  156. this.dishOrderService.findRoomById(roomId).then((data) => {
  157. this.currentRoom = data || {}
  158. })
  159. },
  160. loadCurrentOrder(roomId) {
  161. this.orderLoading = true
  162. this.dishOrderService.currentOrder(roomId).then((data) => {
  163. this.currentOrder = data || null
  164. if (!this.currentOrder || !this.currentOrder.id) {
  165. uni.showToast({ title: '当前包房暂无未结账订单,请先下单', icon: 'none' })
  166. this.backOrderDetail()
  167. return
  168. }
  169. this.buildDishSortMap(this.currentOrder.detailList || [])
  170. }).finally(() => {
  171. this.orderLoading = false
  172. })
  173. },
  174. buildDishSortMap(detailList) {
  175. this.existDishSortMap = {}
  176. this.maxDishSort = 0
  177. detailList.forEach((item) => {
  178. const sort = Number(item.dishSort || 0)
  179. if (item.dishId && sort > 0) {
  180. this.existDishSortMap[item.dishId] = sort
  181. }
  182. if (sort > this.maxDishSort) {
  183. this.maxDishSort = sort
  184. }
  185. })
  186. },
  187. loadCategoryList() {
  188. this.dishOrderService.typeList().then((data) => {
  189. this.categoryList = [
  190. { id: 'all', name: '所有菜品' },
  191. ...(Array.isArray(data) ? data.map(item => ({ id: item.id, name: item.name })) : [])
  192. ]
  193. })
  194. },
  195. changeType(item) {
  196. this.activeTypeId = item.id
  197. this.dishSearchName = ''
  198. this.loadDishList()
  199. },
  200. searchDish() {
  201. this.activeTypeId = 'all'
  202. this.loadDishList()
  203. },
  204. loadDishList() {
  205. this.dishLoading = true
  206. this.dishOrderService.dishList({
  207. typeId: this.activeTypeId === 'all' ? '' : this.activeTypeId,
  208. dishName: this.dishSearchName
  209. }).then((data) => {
  210. this.dishList = Array.isArray(data) ? data : []
  211. this.loadDishImagePreview()
  212. }).finally(() => {
  213. this.dishLoading = false
  214. })
  215. },
  216. addDish(dish) {
  217. const row = this.orderedList.find(item => item.dishId === dish.id)
  218. if (row) {
  219. row.quantity += 1
  220. return
  221. }
  222. this.orderedList.push(this.toOrderedItem(dish, 1))
  223. },
  224. increaseOrderedDish(item) {
  225. item.quantity += 1
  226. },
  227. reduceDish(item) {
  228. if (item.quantity <= 1) {
  229. this.orderedList = this.orderedList.filter(row => row.dishId !== item.dishId)
  230. return
  231. }
  232. item.quantity -= 1
  233. },
  234. confirmSubmitOrder() {
  235. if (this.orderedList.length === 0) {
  236. uni.showToast({ title: '请先选择菜品', icon: 'none' })
  237. return
  238. }
  239. uni.showModal({
  240. title: '提示',
  241. content: '是否确认下单?',
  242. success: (res) => {
  243. if (res.confirm) {
  244. this.submitOrder()
  245. }
  246. }
  247. })
  248. },
  249. submitOrder() {
  250. this.submitting = true
  251. this.dishOrderService.submit({
  252. id: this.currentOrder ? this.currentOrder.id : this.orderId,
  253. roomId: this.roomId,
  254. settleFlag: '0',
  255. detailList: this.submitDetailList
  256. }).then((data) => {
  257. uni.showToast({ title: data || '下单成功', icon: 'none' })
  258. this.backOrderDetail()
  259. }).finally(() => {
  260. this.submitting = false
  261. })
  262. },
  263. toOrderedItem(item, quantity) {
  264. return {
  265. dishId: item.dishId || item.id,
  266. dishName: item.dishName,
  267. salePrice: Number(item.salePrice || 0),
  268. quantity
  269. }
  270. },
  271. itemTotal(item) {
  272. return Number(item.salePrice || 0) * Number(item.quantity || 0)
  273. },
  274. loadDishImagePreview() {
  275. this.dishList.forEach((row) => {
  276. const urls = this.getImageUrls(row.imageUrl)
  277. if (urls.length === 0) {
  278. this.$set ? this.$set(row, 'previewImageUrl', '') : row.previewImageUrl = ''
  279. this.$set ? this.$set(row, 'previewImageList', []) : row.previewImageList = []
  280. return
  281. }
  282. this.$set ? this.$set(row, 'previewImageUrl', urls[0]) : row.previewImageUrl = urls[0]
  283. this.$set ? this.$set(row, 'previewImageList', urls) : row.previewImageList = urls
  284. Promise.all(urls.map(url => this.getPreviewUrl(url))).then((list) => {
  285. row.previewImageUrl = list[0]
  286. row.previewImageList = list
  287. })
  288. })
  289. },
  290. getImageUrls(value) {
  291. if (!value) {
  292. return []
  293. }
  294. return value.split(',').map(item => item.trim()).filter(item => item)
  295. },
  296. getPreviewUrl(url) {
  297. if (!url) {
  298. return Promise.resolve('')
  299. }
  300. if (url.indexOf('http') === 0) {
  301. return Promise.resolve(url)
  302. }
  303. return this.ossService.getTemporaryUrl(url).catch(() => url)
  304. },
  305. formatMoney(value) {
  306. return Number(value || 0).toFixed(2)
  307. },
  308. backOrderDetail() {
  309. uni.navigateBack({
  310. fail: () => {
  311. uni.redirectTo({
  312. url: `/pages/psiManagement/dishManage/DishOrderDetail?roomId=${this.roomId}`
  313. })
  314. }
  315. })
  316. }
  317. }
  318. }
  319. </script>
  320. <style scoped>
  321. .dish-order-add {
  322. height: calc(100vh - var(--window-top, 0px));
  323. box-sizing: border-box;
  324. display: flex;
  325. flex-direction: column;
  326. overflow: hidden;
  327. padding: 20rpx;
  328. background: #f5f6fa;
  329. }
  330. .order-header {
  331. height: 92rpx;
  332. display: flex;
  333. align-items: center;
  334. justify-content: space-between;
  335. padding: 0 8rpx 18rpx;
  336. box-sizing: border-box;
  337. border-bottom: 1rpx solid #ebeef5;
  338. }
  339. .room-name {
  340. font-size: 34rpx;
  341. font-weight: 700;
  342. color: #303133;
  343. line-height: 44rpx;
  344. }
  345. .room-no {
  346. font-size: 24rpx;
  347. color: #909399;
  348. line-height: 34rpx;
  349. }
  350. .mini-btn,
  351. .plain-btn,
  352. .footer-btn {
  353. margin: 0;
  354. padding: 0;
  355. border-radius: 8rpx;
  356. font-size: 26rpx;
  357. }
  358. .mini-btn {
  359. width: 160rpx;
  360. height: 64rpx;
  361. line-height: 64rpx;
  362. color: #fff;
  363. }
  364. .primary,
  365. .footer-btn.primary {
  366. color: #fff;
  367. background: #2979ff;
  368. }
  369. .order-main {
  370. flex: 1;
  371. min-height: 0;
  372. display: grid;
  373. grid-template-columns: minmax(0, 1fr) 600rpx;
  374. gap: 18rpx;
  375. padding-top: 18rpx;
  376. }
  377. .dish-panel,
  378. .ordered-panel {
  379. min-height: 0;
  380. display: flex;
  381. flex-direction: column;
  382. border: 1rpx solid #ebeef5;
  383. border-radius: 10rpx;
  384. background: #fff;
  385. overflow: hidden;
  386. }
  387. .dish-search {
  388. display: grid;
  389. grid-template-columns: minmax(0, 1fr) 120rpx;
  390. gap: 12rpx;
  391. padding: 16rpx;
  392. border-bottom: 1rpx solid #ebeef5;
  393. }
  394. .search-input {
  395. height: 64rpx;
  396. box-sizing: border-box;
  397. padding: 0 18rpx;
  398. border: 1rpx solid #dcdfe6;
  399. border-radius: 8rpx;
  400. font-size: 26rpx;
  401. }
  402. .search-btn {
  403. height: 64rpx;
  404. margin: 0;
  405. padding: 0;
  406. border-radius: 8rpx;
  407. font-size: 26rpx;
  408. line-height: 64rpx;
  409. color: #fff;
  410. background: #2979ff;
  411. }
  412. .dish-content {
  413. flex: 1;
  414. min-height: 0;
  415. display: grid;
  416. grid-template-columns: 150rpx minmax(0, 1fr);
  417. }
  418. .category-list {
  419. height: 100%;
  420. background: #f7f9fb;
  421. border-right: 1rpx solid #ebeef5;
  422. }
  423. .category-item {
  424. min-height: 78rpx;
  425. display: flex;
  426. align-items: center;
  427. box-sizing: border-box;
  428. padding: 0 16rpx;
  429. border-left: 5rpx solid transparent;
  430. font-size: 26rpx;
  431. color: #606266;
  432. }
  433. .category-item.active {
  434. color: #2979ff;
  435. background: #fff;
  436. border-left-color: #2979ff;
  437. font-weight: 700;
  438. }
  439. .dish-list {
  440. height: 100%;
  441. box-sizing: border-box;
  442. padding: 16rpx;
  443. }
  444. .dish-grid {
  445. display: grid;
  446. grid-template-columns: repeat(4, minmax(0, 1fr));
  447. gap: 16rpx;
  448. }
  449. .dish-card {
  450. min-width: 0;
  451. border: 1rpx solid #ebeef5;
  452. border-radius: 10rpx;
  453. overflow: hidden;
  454. background: #fff;
  455. }
  456. .dish-image {
  457. width: 100%;
  458. height: 150rpx;
  459. background: #f2f3f5;
  460. }
  461. .dish-image-empty {
  462. display: flex;
  463. align-items: center;
  464. justify-content: center;
  465. font-size: 24rpx;
  466. color: #909399;
  467. }
  468. .dish-info {
  469. padding: 12rpx;
  470. }
  471. .dish-name,
  472. .ordered-dish-name {
  473. overflow: hidden;
  474. white-space: nowrap;
  475. text-overflow: ellipsis;
  476. }
  477. .dish-name {
  478. font-size: 28rpx;
  479. font-weight: 700;
  480. color: #303133;
  481. line-height: 38rpx;
  482. }
  483. .dish-desc {
  484. height: 42rpx;
  485. margin-top: 8rpx;
  486. font-size: 24rpx;
  487. color: #606266;
  488. line-height: 32rpx;
  489. overflow: hidden;
  490. }
  491. .dish-bottom {
  492. display: flex;
  493. align-items: center;
  494. justify-content: space-between;
  495. gap: 10rpx;
  496. margin-top: 12rpx;
  497. }
  498. .dish-price,
  499. .ordered-total {
  500. color: #fa3534;
  501. }
  502. .dish-price {
  503. font-size: 30rpx;
  504. font-weight: 700;
  505. }
  506. .plain-btn {
  507. width: 96rpx;
  508. height: 54rpx;
  509. line-height: 54rpx;
  510. color: #2979ff;
  511. background: #ecf5ff;
  512. }
  513. .ordered-title {
  514. height: 76rpx;
  515. display: flex;
  516. align-items: center;
  517. box-sizing: border-box;
  518. padding: 0 18rpx;
  519. border-bottom: 1rpx solid #ebeef5;
  520. font-size: 30rpx;
  521. font-weight: 700;
  522. color: #303133;
  523. }
  524. .ordered-list {
  525. flex: 1;
  526. min-height: 0;
  527. box-sizing: border-box;
  528. padding: 14rpx;
  529. }
  530. .ordered-head,
  531. .ordered-item {
  532. display: grid;
  533. grid-template-columns: minmax(0, 1.2fr) 84rpx 96rpx 132rpx;
  534. gap: 10rpx;
  535. align-items: center;
  536. }
  537. .ordered-head {
  538. height: 50rpx;
  539. padding: 0 10rpx;
  540. border-radius: 8rpx;
  541. background: #f7f9fb;
  542. font-size: 24rpx;
  543. color: #909399;
  544. }
  545. .ordered-item {
  546. min-height: 86rpx;
  547. box-sizing: border-box;
  548. padding: 12rpx 10rpx;
  549. border-bottom: 1rpx solid #ebeef5;
  550. font-size: 24rpx;
  551. color: #606266;
  552. }
  553. .ordered-dish-name {
  554. font-size: 26rpx;
  555. font-weight: 700;
  556. color: #303133;
  557. }
  558. .ordered-total {
  559. font-weight: 700;
  560. }
  561. .ordered-qty {
  562. display: flex;
  563. align-items: center;
  564. justify-content: flex-end;
  565. gap: 8rpx;
  566. }
  567. .qty-btn {
  568. width: 42rpx;
  569. height: 42rpx;
  570. margin: 0;
  571. padding: 0;
  572. border-radius: 50%;
  573. font-size: 28rpx;
  574. line-height: 42rpx;
  575. color: #2979ff;
  576. background: #ecf5ff;
  577. }
  578. .ordered-footer {
  579. padding: 16rpx;
  580. border-top: 1rpx solid #ebeef5;
  581. }
  582. .ordered-summary {
  583. display: flex;
  584. align-items: center;
  585. justify-content: space-between;
  586. margin-bottom: 14rpx;
  587. font-size: 26rpx;
  588. color: #606266;
  589. }
  590. .ordered-summary strong {
  591. font-size: 36rpx;
  592. color: #fa3534;
  593. }
  594. .footer-btn {
  595. width: 100%;
  596. height: 72rpx;
  597. line-height: 72rpx;
  598. }
  599. .ordered-empty,
  600. .empty-box {
  601. min-height: 240rpx;
  602. display: flex;
  603. flex-direction: column;
  604. align-items: center;
  605. justify-content: center;
  606. color: #909399;
  607. font-size: 26rpx;
  608. }
  609. .ordered-empty-title {
  610. font-size: 30rpx;
  611. color: #606266;
  612. }
  613. .ordered-empty-sub {
  614. margin-top: 8rpx;
  615. font-size: 24rpx;
  616. }
  617. </style>