index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. <template>
  2. <view class="page">
  3. <u-navbar title="团餐订单" :autoBack="true" :placeholder="true" :border-bottom="false">
  4. <view slot="right" class="navbar-right" @click="goToDetail">订单明细</view>
  5. </u-navbar>
  6. <view class="search-bar">
  7. <u-search
  8. v-model="keyword"
  9. placeholder="请输入商品名称"
  10. bg-color="#f7f8fc"
  11. search-icon-color="#9aa1ab"
  12. :show-action="true"
  13. action-text="搜索"
  14. :action-style="actionStyle"
  15. @search="onSearch"
  16. @custom="onSearch"
  17. ></u-search>
  18. </view>
  19. <view class="status-tabs">
  20. <view
  21. v-for="(tab, index) in orderTabs"
  22. :key="tab.name"
  23. :class="['status-tab', { active: currentTab === index }]"
  24. @click="onTabChange(index)"
  25. >
  26. <text>{{ tab.name }}</text>
  27. <image
  28. v-if="currentTab === index"
  29. class="arrow-down"
  30. src="/static/merchantDish/select-tab.png"
  31. mode="aspectFit"
  32. ></image>
  33. </view>
  34. </view>
  35. <view class="filter-row" v-if="currentTab !== 2">
  36. <view class="filter-item" v-if="currentTab === 0" @click="openDatePicker">
  37. <text>{{ dateLabel }}</text>
  38. <u-icon name="arrow-down" size="22" color="#8b929c"></u-icon>
  39. </view>
  40. <view class="filter-item meal-filter" @click="openMealPicker">
  41. <text>{{ mealTypeLabel }}</text>
  42. <u-icon name="arrow-down" size="22" color="#8b929c"></u-icon>
  43. </view>
  44. <view class="total">合计:<text>{{ total }}</text>份</view>
  45. </view>
  46. <view class="list-wrap">
  47. <ListScrollView
  48. ref="listScroll"
  49. :api="api"
  50. :init="initParams"
  51. :page-size="20"
  52. :down-enabled="false"
  53. :up-text-visible="false"
  54. empty-text="暂无订单"
  55. @afterFetch="afterFetch"
  56. >
  57. <template v-slot:list="{ data }">
  58. <view class="order-list">
  59. <view
  60. class="order-item"
  61. :class="{ 'order-item-selectable': currentTab === 1 }"
  62. v-for="item in data"
  63. :key="item.hostOrderId || item.id"
  64. @click="openOrder(item)"
  65. >
  66. <view class="order-summary">
  67. <view v-if="currentTab === 0" class="summary-row date-row">
  68. <view class="summary-label">
  69. <u-icon name="clock" color="#8b929c" size="27"></u-icon>
  70. <text>预约日期:{{ getReserveDate(item) }}</text>
  71. </view>
  72. <text :class="['meal-tag', `meal-tag-${getMealCategory(item)}`]">{{ getMealLabel(item) }}</text>
  73. </view>
  74. <view class="summary-row leader-row">
  75. <view class="summary-label">
  76. <u-icon name="account" color="#8b929c" size="27"></u-icon>
  77. <text>{{ item.headName || item.leaderName || '团长姓名' }}</text>
  78. </view>
  79. <view class="order-count">合计:<text>{{ item.buyNumber || 0 }}</text>份</view>
  80. </view>
  81. <view class="summary-label address-line">
  82. <u-icon name="map" color="#8b929c" size="27"></u-icon>
  83. <text>{{ item.address || '暂无地址' }}</text>
  84. </view>
  85. <view class="region">{{ item.region || '' }}</view>
  86. <view
  87. v-if="currentTab === 1"
  88. class="order-select summary-select"
  89. :class="{ checked: isOrderSelected(item) }"
  90. @click.stop="toggleOrder(item)"
  91. >
  92. <u-icon v-if="isOrderSelected(item)" name="checkmark" color="#fff" size="22"></u-icon>
  93. </view>
  94. </view>
  95. <view class="product-list">
  96. <view
  97. class="product-item"
  98. v-for="(product, productIndex) in (item.merchantPackageOrderDTOS || [])"
  99. :key="product.merchantOrderIds || product.id || productIndex"
  100. >
  101. <u-image
  102. width="80rpx"
  103. height="80rpx"
  104. border-radius="8rpx"
  105. :src="getProductImage(product.productImage)"
  106. mode="aspectFill"
  107. ></u-image>
  108. <view class="product-info">
  109. <text class="product-name">{{ product.packageName || '' }}</text>
  110. <text class="product-desc">{{ product.boxName || '默认餐盒'}}</text>
  111. </view>
  112. <text class="product-quantity">{{ product.numberUserCount || product.packageNumber || 0 }}份</text>
  113. </view>
  114. </view>
  115. </view>
  116. </view>
  117. </template>
  118. </ListScrollView>
  119. </view>
  120. <view v-if="currentTab === 1" class="footer-bar">
  121. <view class="select-all" @click="toggleAllOrders">
  122. <view class="order-select footer-select" :class="{ checked: isAllSelected }">
  123. <u-icon v-if="isAllSelected" name="checkmark" color="#fff" size="22"></u-icon>
  124. </view>
  125. <text>全选</text>
  126. </view>
  127. <view class="ready-button" :class="{ disabled: readySubmitting }" @click="handleReady">出餐</view>
  128. </view>
  129. <u-action-sheet
  130. :list="mealTypeOptions"
  131. v-model="mealPickerVisible"
  132. @click="onMealPick"
  133. ></u-action-sheet>
  134. <rangTime
  135. ref="rangeTime"
  136. :mode="1"
  137. title="选择日期"
  138. :default-start-date="startDate"
  139. :default-end-date="endDate"
  140. @onSubmit="onDateRangeConfirm"
  141. />
  142. </view>
  143. </template>
  144. <script>
  145. import ListScrollView from '@/components/list-scroll-view/index.vue'
  146. import rangTime from '@/components/dengrq-datetime-picker/dateSelector/index.vue'
  147. import merchantDishApi from '@/api/merchantDish.js'
  148. const MEAL_TYPE_OPTIONS = [
  149. { text: '全部', value: '' },
  150. { text: '早餐', value: 1 },
  151. { text: '午餐', value: 2 },
  152. { text: '晚餐', value: 3 }
  153. ]
  154. export default {
  155. components: {
  156. ListScrollView,
  157. rangTime
  158. },
  159. data() {
  160. return {
  161. api: {
  162. url: '/groupmeal/merchantPackageOrder/pageList',
  163. method: 'POST'
  164. },
  165. keyword: '',
  166. currentTab: 1,
  167. orderTabs: [
  168. { name: '预约单' },
  169. { name: '待出餐' },
  170. { name: '已出餐' }
  171. ],
  172. mealType: '',
  173. startDate: '',
  174. endDate: '',
  175. total: 0,
  176. selectedOrderKeys: [],
  177. readySubmitting: false,
  178. mealPickerVisible: false,
  179. mealTypeOptions: MEAL_TYPE_OPTIONS,
  180. actionStyle: {
  181. width: 'auto',
  182. color: '#333',
  183. padding: '0 20rpx',
  184. borderLeft: '1rpx solid #e5e7eb'
  185. }
  186. }
  187. },
  188. computed: {
  189. initParams() {
  190. const params = {
  191. orderStatus: this.currentTab
  192. }
  193. if (this.currentTab === 0) params.isReservation = 1
  194. if (this.keyword) params.queryName = this.keyword
  195. if (this.mealType) params.category = this.mealType
  196. if (this.currentTab === 0 && this.startDate && this.endDate) {
  197. params.reservationDateStart = this.startDate
  198. params.reservationDateEnd = this.endDate
  199. }
  200. return params
  201. },
  202. mealTypeLabel() {
  203. return (MEAL_TYPE_OPTIONS.find((item) => item.value === this.mealType) || MEAL_TYPE_OPTIONS[0]).text
  204. },
  205. dateLabel() {
  206. if (!this.startDate || !this.endDate) return '选择日期'
  207. return `${this.formatDateLabel(this.startDate)}-${this.formatDateLabel(this.endDate)}`
  208. },
  209. isAllSelected() {
  210. const orders = this.getVisibleOrders()
  211. const selectedCount = this.selectedOrderKeys.length
  212. return orders.length > 0 && selectedCount === orders.length && orders.every((item) => this.isOrderSelected(item))
  213. }
  214. },
  215. methods: {
  216. goToDetail() {
  217. uni.navigateTo({ url: '/pages/merchantDish/orders/order-blist' })
  218. },
  219. onSearch(value) {
  220. this.keyword = typeof value === 'string' ? value : this.keyword
  221. this.reloadList()
  222. },
  223. onTabChange(index) {
  224. this.currentTab = index
  225. this.selectedOrderKeys = []
  226. this.reloadList()
  227. },
  228. openMealPicker() {
  229. this.mealPickerVisible = true
  230. },
  231. onMealPick(event) {
  232. const value = event && typeof event === 'object'
  233. ? event.value
  234. : (MEAL_TYPE_OPTIONS[event] || {}).value
  235. if (value === undefined) return
  236. this.mealType = value
  237. this.reloadList()
  238. },
  239. openDatePicker() {
  240. const picker = this.$refs.rangeTime
  241. if (!picker) return
  242. picker.startDate = this.startDate
  243. picker.endDate = this.endDate
  244. picker.open()
  245. },
  246. onDateRangeConfirm(value) {
  247. this.startDate = value.startDate
  248. this.endDate = value.endDate
  249. this.reloadList()
  250. },
  251. formatDateLabel(value) {
  252. const parts = String(value || '').slice(0, 10).split('-')
  253. if (parts.length !== 3) return value
  254. return `${Number(parts[1])}.${Number(parts[2])}`
  255. },
  256. getOrderId(item) {
  257. return item && (item.hostOrderId || item.id || item.orderId)
  258. },
  259. openOrder(item) {
  260. const orderId = this.getOrderId(item)
  261. if (!orderId) return
  262. const reservationParam = this.currentTab === 0 ? '&isReservation=1' : ''
  263. uni.navigateTo({
  264. url: `/pages/merchantDish/orders/order-details?hostOrderId=${encodeURIComponent(orderId)}${reservationParam}`
  265. })
  266. },
  267. getOrderKey(item) {
  268. return String(this.getOrderId(item) || '')
  269. },
  270. isOrderSelected(item) {
  271. return this.selectedOrderKeys.includes(this.getOrderKey(item))
  272. },
  273. toggleOrder(item) {
  274. const key = this.getOrderKey(item)
  275. if (!key) return
  276. const index = this.selectedOrderKeys.indexOf(key)
  277. if (index >= 0) this.selectedOrderKeys.splice(index, 1)
  278. else this.selectedOrderKeys.push(key)
  279. },
  280. getVisibleOrders() {
  281. const list = this.$refs.listScroll
  282. return (list && Array.isArray(list.dataList)) ? list.dataList : []
  283. },
  284. toggleAllOrders() {
  285. if (this.isAllSelected) {
  286. this.selectedOrderKeys = []
  287. return
  288. }
  289. this.selectedOrderKeys = this.getVisibleOrders().map((item) => this.getOrderKey(item)).filter(Boolean)
  290. },
  291. getReadyOrderIds() {
  292. const selected = this.getVisibleOrders().filter((item) => this.isOrderSelected(item))
  293. return selected.reduce((ids, item) => {
  294. const products = Array.isArray(item.merchantPackageOrderDTOS) ? item.merchantPackageOrderDTOS : []
  295. products.forEach((product) => {
  296. String(product.merchantOrderIds || product.merchantOrderId || '')
  297. .split(',')
  298. .map((id) => id.trim())
  299. .filter(Boolean)
  300. .forEach((id) => ids.push(id))
  301. })
  302. return ids
  303. }, [])
  304. },
  305. handleReady() {
  306. if (this.readySubmitting) return
  307. const orderIds = [...new Set(this.getReadyOrderIds())]
  308. if (!orderIds.length) {
  309. uni.showToast({ title: '请先选择订单', icon: 'none' })
  310. return
  311. }
  312. uni.showModal({
  313. title: '提示',
  314. content: '是否确认已经出餐?',
  315. success: ({ confirm }) => {
  316. if (!confirm) return
  317. this.readySubmitting = true
  318. merchantDishApi.packageOrderReady(orderIds.join(',')).then((res) => {
  319. if (res.data.code !== 200) {
  320. uni.showToast({ title: res.data.message || '出餐失败', icon: 'none' })
  321. return
  322. }
  323. this.selectedOrderKeys = []
  324. uni.showToast({ title: res.data.message || '出餐成功', icon: 'success' })
  325. this.reloadList()
  326. }).catch(() => {
  327. uni.showToast({ title: '出餐失败', icon: 'none' })
  328. }).finally(() => {
  329. this.readySubmitting = false
  330. })
  331. }
  332. })
  333. },
  334. getReserveDate(item) {
  335. const value = item.reservationDate || ''
  336. return String(value).slice(0, 10).replace(/-/g, '.') || '--'
  337. },
  338. getMealLabel(item) {
  339. const products = item.merchantPackageOrderDTOS || []
  340. const firstProduct = products[0] || {}
  341. const text = item.mealTypeName || item.categoryName || item.mealName || firstProduct.mealTypeName || firstProduct.categoryName
  342. if (text) return text
  343. return ({ 1: '早餐', 2: '午餐', 3: '晚餐' })[this.getMealCategory(item)] || '团餐'
  344. },
  345. getMealCategory(item) {
  346. const products = item.merchantPackageOrderDTOS || []
  347. const firstProduct = products[0] || {}
  348. return Number(item.category || item.mealType || firstProduct.category || firstProduct.mealType) || 0
  349. },
  350. getProductImage(value) {
  351. return String(value || '').split(',').filter(Boolean)[0] || ''
  352. },
  353. reloadList() {
  354. this.selectedOrderKeys = []
  355. this.$nextTick(() => {
  356. const list = this.$refs.listScroll
  357. if (list && list.mescroll) list.reloadList(this.initParams)
  358. })
  359. },
  360. afterFetch(result) {
  361. if (result && result.totalBuyNumber != null) {
  362. this.total = Number(result.totalBuyNumber) || 0
  363. return
  364. }
  365. const summary = (result && result.summary) || {}
  366. const summaryTotal = summary.buyNumber || summary.copies || summary.totalCopies
  367. if (summaryTotal != null) {
  368. this.total = Number(summaryTotal) || 0
  369. return
  370. }
  371. this.$nextTick(() => {
  372. const list = this.$refs.listScroll
  373. const records = (list && list.dataList) || []
  374. this.total = records.reduce((sum, item) => sum + (Number(item.buyNumber) || 0), 0)
  375. })
  376. }
  377. }
  378. }
  379. </script>
  380. <style lang="scss" scoped>
  381. .page {
  382. display: flex;
  383. flex-direction: column;
  384. height: 100vh;
  385. box-sizing: border-box;
  386. background: #fff;
  387. padding: 0 32rpx;
  388. overflow: hidden;
  389. }
  390. .search-bar {
  391. width: 100%;
  392. height: 64rpx;
  393. box-sizing: border-box;
  394. padding: 12rpx 20rpx;
  395. background: #F7F8FC;
  396. margin-bottom: 12rpx;
  397. }
  398. .navbar-right {
  399. padding: 0 24rpx;
  400. color: #333;
  401. font-size: 28rpx;
  402. }
  403. .filter-row {
  404. display: flex;
  405. align-items: center;
  406. justify-content: space-between;
  407. height: 76rpx;
  408. background: #f7f8fc;
  409. color: #7d858f;
  410. font-size: 26rpx;
  411. }
  412. .filter-item {
  413. display: flex;
  414. align-items: center;
  415. gap: 6rpx;
  416. }
  417. .meal-filter {
  418. justify-content: center;
  419. }
  420. .total {
  421. white-space: nowrap;
  422. }
  423. .total text {
  424. color: #1677ff;
  425. }
  426. .list-wrap {
  427. flex: 1;
  428. height: 0;
  429. margin: 0 -32rpx;
  430. // padding: 20rpx 24rpx 0;
  431. background: #f7f8fc;
  432. overflow: hidden;
  433. }
  434. .order-item-selectable {
  435. position: relative;
  436. }
  437. .order-select {
  438. display: flex;
  439. align-items: center;
  440. justify-content: center;
  441. width: 34rpx;
  442. height: 34rpx;
  443. box-sizing: border-box;
  444. border: 1rpx solid #c9cdd4;
  445. border-radius: 50%;
  446. background: #fff;
  447. }
  448. .summary-select {
  449. position: absolute;
  450. right: 24rpx;
  451. top: 50%;
  452. transform: translateY(-50%);
  453. }
  454. .order-item-selectable .order-summary {
  455. padding-right: 80rpx;
  456. }
  457. .order-item-selectable .date-row {
  458. padding-right: 68rpx;
  459. }
  460. .order-select.checked {
  461. border-color: #1677ff;
  462. background: #1677ff;
  463. }
  464. .footer-bar {
  465. display: flex;
  466. flex-shrink: 0;
  467. align-items: center;
  468. justify-content: space-between;
  469. min-height: 112rpx;
  470. box-sizing: border-box;
  471. margin: 0 -32rpx;
  472. padding: 16rpx 48rpx calc(16rpx + env(safe-area-inset-bottom));
  473. background: #fff;
  474. box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
  475. }
  476. .select-all {
  477. display: flex;
  478. align-items: center;
  479. gap: 16rpx;
  480. color: #1d2129;
  481. font-size: 28rpx;
  482. }
  483. .footer-select {
  484. position: static;
  485. }
  486. .ready-button {
  487. display: flex;
  488. align-items: center;
  489. justify-content: center;
  490. width: 240rpx;
  491. height: 80rpx;
  492. border-radius: 80rpx;
  493. background: linear-gradient(93deg, #10abfe 0%, #2260f6 100%);
  494. color: #fff;
  495. font-size: 32rpx;
  496. }
  497. .ready-button.disabled {
  498. opacity: 0.6;
  499. }
  500. .order-list {
  501. padding-bottom: 24rpx;
  502. }
  503. .order-item {
  504. margin-bottom: 20rpx;
  505. background: #fff;
  506. border-radius: 8rpx;
  507. overflow: hidden;
  508. padding: 24rpx;
  509. padding-top: 0;
  510. }
  511. .order-summary {
  512. position: relative;
  513. padding: 20rpx 24rpx 16rpx;
  514. color: #7f8792;
  515. font-size: 26rpx;
  516. border-bottom: 1px solid #dddddd;
  517. }
  518. .summary-row {
  519. display: flex;
  520. align-items: center;
  521. }
  522. .date-row {
  523. padding-right: 92rpx;
  524. }
  525. .leader-row {
  526. gap: 50rpx;
  527. margin-top: 10rpx;
  528. }
  529. .summary-label {
  530. font-size: 28rpx;
  531. display: flex;
  532. align-items: center;
  533. gap: 8rpx;
  534. min-width: 0;
  535. }
  536. .summary-label text {
  537. overflow: hidden;
  538. text-overflow: ellipsis;
  539. white-space: nowrap;
  540. }
  541. .meal-tag {
  542. position: absolute;
  543. top: 0;
  544. right: 0;
  545. display: flex;
  546. align-items: center;
  547. justify-content: center;
  548. width: 88rpx;
  549. height: 36rpx;
  550. box-sizing: border-box;
  551. border-radius: 0 12rpx 0 12rpx;
  552. background: linear-gradient(89deg, #1aeeae 0%, #0bda84 100%);
  553. color: #fff;
  554. font-size: 22rpx;
  555. text-align: center;
  556. }
  557. .meal-tag-2 {
  558. background: linear-gradient(89deg, #ffbf1e 0%, #ff781f 100%);
  559. }
  560. .meal-tag-3 {
  561. background: linear-gradient(90deg, #10abfe 0%, #2260f6 100%);
  562. }
  563. .order-count {
  564. flex-shrink: 0;
  565. color: #7f8792;
  566. font-size: 24rpx;
  567. }
  568. .order-count text {
  569. color: #1677ff;
  570. }
  571. .address-line {
  572. margin-top: 10rpx;
  573. }
  574. .region {
  575. margin-top: 8rpx;
  576. color: #8a8f98;
  577. font-size: 24rpx;
  578. }
  579. .product-list {
  580. margin: 0 24rpx;
  581. border-top: 1rpx solid #e5e7eb;
  582. }
  583. .product-item {
  584. display: flex;
  585. align-items: end;
  586. gap: 18rpx;
  587. min-height: 80rpx;
  588. box-sizing: border-box;
  589. border-bottom: 1rpx solid #f0f1f3;
  590. margin-top: 20rpx;
  591. }
  592. .product-item:last-child {
  593. border-bottom: 0;
  594. }
  595. .product-info {
  596. min-height: 80rpx;
  597. height: 100%;
  598. display: flex;
  599. flex: 1;
  600. min-width: 0;
  601. flex-direction: column;
  602. justify-content: space-between;
  603. gap: 8rpx;
  604. }
  605. .product-name,
  606. .product-desc {
  607. overflow: hidden;
  608. text-overflow: ellipsis;
  609. white-space: nowrap;
  610. }
  611. .product-name {
  612. color: #303133;
  613. font-size: 26rpx;
  614. font-weight: 600;
  615. }
  616. .product-desc {
  617. color: #8a91a8;
  618. font-size: 22rpx;
  619. }
  620. .product-quantity {
  621. flex-shrink: 0;
  622. align-self: flex-end;
  623. margin-bottom: 4rpx;
  624. color: #333;
  625. font-size: 24rpx;
  626. }
  627. .status-tabs {
  628. display: flex;
  629. justify-content: space-between;
  630. height: 92rpx;
  631. border-bottom: 1rpx solid #f0f1f3;
  632. }
  633. .status-tab {
  634. position: relative;
  635. display: flex;
  636. align-items: center;
  637. justify-content: center;
  638. width: 140rpx;
  639. color: #8a8f98;
  640. font-size: 28rpx;
  641. }
  642. .status-tab.active {
  643. color: #1677ff;
  644. font-weight: 600;
  645. }
  646. .arrow-down {
  647. position: absolute;
  648. bottom: 8rpx;
  649. left: 50%;
  650. width: 48rpx;
  651. height: 8rpx;
  652. transform: translateX(-50%);
  653. }
  654. ::v-deep .u-search {
  655. border-radius: 4rpx;
  656. }
  657. </style>