西藏巴青项目

index.vue 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. <template>
  2. <!-- 我的预约:见 doc/app/我的预约/我的预约接口说明.md -->
  3. <view :class="pageRootClass" class="tab-page mb-page" :style="pageStyle">
  4. <view class="mb-top">
  5. <view class="mb-card mb-card--tabs">
  6. <up-tabs
  7. :current="mainTabIndex"
  8. class="mb-tabs"
  9. :list="mainTabsList"
  10. key-name="name"
  11. :scrollable="false"
  12. line-color="#22C55E"
  13. :line-height="2"
  14. :item-style="tabsItemStyle"
  15. :active-style="{ color: '#15803d', fontWeight: '600', fontSize: '13px' }"
  16. :inactive-style="{ color: '#78716c', fontSize: '13px' }"
  17. @update:current="onMainTabUpdate"
  18. />
  19. </view>
  20. <scroll-view scroll-x class="mb-chip-scroll" :show-scrollbar="false" enable-flex>
  21. <view class="mb-chip-row">
  22. <view
  23. v-for="chip in subChips"
  24. :key="chip.key"
  25. class="mb-chip"
  26. :class="{ 'mb-chip--on': currentSubKey === chip.key }"
  27. role="button"
  28. @tap="setSubKey(chip.key)"
  29. >
  30. <text class="mb-chip__txt text-body">{{ $t(chip.labelKey) }}</text>
  31. </view>
  32. </view>
  33. </scroll-view>
  34. </view>
  35. <view class="mb-body">
  36. <view v-if="listLoading && !rows.length" class="mb-empty">
  37. <text class="text-body mb-empty__txt">{{ $t('myBookingPage.loading') }}</text>
  38. </view>
  39. <view v-else-if="!rows.length" class="mb-empty">
  40. <text class="text-body mb-empty__txt">{{ $t('myBookingPage.empty') }}</text>
  41. </view>
  42. <view v-else class="mb-list-wrap">
  43. <up-virtual-list
  44. ref="mbVList"
  45. class="mb-vlist"
  46. :list-data="rows"
  47. :item-height="slotHeightPx"
  48. :height="listHeightPx"
  49. :buffer="6"
  50. key-field="id"
  51. :scroll-top="vScrollTop"
  52. @update:scrollTop="vScrollTop = $event"
  53. @scroll="onVirtualListScroll"
  54. >
  55. <template #default="{ item }">
  56. <view class="mb-cell">
  57. <view class="mb-item" :style="{ height: rowBodyPx + 'px' }">
  58. <text class="mb-item__title text-body">{{ titleFor(item) }}</text>
  59. <text class="mb-item__line text-body">{{ timeLine(item) }}</text>
  60. <template v-if="item.kind !== 'org'">
  61. <text class="mb-item__line text-body">
  62. <text class="mb-item__k">{{ $t('myBookingPage.labelAddress') }}</text>
  63. {{ addressLine(item) }}
  64. </text>
  65. <text class="mb-item__line text-body">
  66. <text class="mb-item__k">{{ $t('myBookingPage.labelNeed') }}</text>
  67. {{ needLine(item) }}
  68. </text>
  69. </template>
  70. <view class="mb-item__row mb-item__row--status">
  71. <text class="mb-item__line text-body">
  72. <text class="mb-item__k">{{ $t('myBookingPage.labelStatus') }}</text>
  73. </text>
  74. <up-tag
  75. :text="statusLabel(item)"
  76. :type="statusTagType(item)"
  77. size="mini"
  78. plain
  79. plain-fill
  80. />
  81. </view>
  82. <view class="mb-item__row mb-item__row--end">
  83. <!-- :custom-style="cancelBtnStyle" -->
  84. <up-button
  85. v-if="showCancelBtn(item)"
  86. shape="circle"
  87. size="small"
  88. plain
  89. hairline
  90. type="primary"
  91. :text="$t('myBookingPage.btnCancelBook')"
  92. :custom-style="cancelBtnStyle"
  93. @click="onCancelBook(item)"
  94. />
  95. <template v-if="showExpertRateRow(item)">
  96. <up-button
  97. v-if="!item.hasReview"
  98. shape="circle"
  99. size="small"
  100. type="success"
  101. :custom-style="cancelBtnStyle"
  102. :text="$t('myBookingPage.btnReview')"
  103. @click="onReview(item)"
  104. />
  105. <up-button
  106. v-else
  107. shape="circle"
  108. size="small"
  109. plain
  110. hairline
  111. :custom-style="cancelBtnStyle"
  112. type="success"
  113. :text="$t('myBookingPage.btnViewReview')"
  114. @click="onViewReview(item)"
  115. />
  116. </template>
  117. </view>
  118. </view>
  119. </view>
  120. </template>
  121. </up-virtual-list>
  122. </view>
  123. </view>
  124. </view>
  125. </template>
  126. <script>
  127. import UTabs from 'uview-plus/components/u-tabs/u-tabs.vue'
  128. import UButton from 'uview-plus/components/u-button/u-button.vue'
  129. import UTag from 'uview-plus/components/u-tag/u-tag.vue'
  130. import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
  131. import tabPage from '@/mixins/tabPage'
  132. import { listMyAppointments, cancelMyAppointment } from '@/api/myAppointment'
  133. const ROW_GAP_RPX = 20
  134. const LIST_PAGE_SIZE = 20
  135. const MB_BODY_PAD_RPX = 40
  136. /** 主 Tab 索引 → providerType:1 兽医 / 3 专家 / 2 机构 */
  137. const PROVIDER_TYPES = [1, 3, 2]
  138. const STATUS_TAB_MAP = {
  139. all: 'all',
  140. upcoming: 'notStarted',
  141. todo_review: 'pendingReview',
  142. done: 'completed',
  143. cancelled: 'cancelled'
  144. }
  145. /** 状态码 → up-tag type:0 待确认 / 1 已确认 / 2 已拒绝 / 3 已取消 / 4 已完成 / 5 已预约 */
  146. const STATUS_TAG_TYPE = {
  147. 0: 'warning',
  148. 1: 'primary',
  149. 2: 'error',
  150. 3: 'info',
  151. 4: 'success',
  152. 5: 'primary'
  153. }
  154. function pad2(n) {
  155. return `${n}`.padStart(2, '0')
  156. }
  157. /** 按主 tab 取列表项内容区高度(rpx),与虚拟列表单项高度一致 */
  158. function rowBodyRpxForTab(mainTabIndex) {
  159. if (mainTabIndex === 2) return 280
  160. if (mainTabIndex === 1) return 400
  161. return 340
  162. }
  163. export default {
  164. components: {
  165. 'up-tabs': UTabs,
  166. 'up-button': UButton,
  167. 'up-tag': UTag,
  168. 'up-virtual-list': UVirtualList
  169. },
  170. mixins: [tabPage],
  171. data() {
  172. return {
  173. navTitleKey: 'myBookingPage.navTitle',
  174. mainTabIndex: 0,
  175. vetSubKey: 'all',
  176. expertSubKey: 'all',
  177. orgSubKey: 'all',
  178. rows: [],
  179. listLoading: false,
  180. listLoadingMore: false,
  181. listTotal: 0,
  182. pageNum: 1,
  183. pageSize: LIST_PAGE_SIZE,
  184. loadMoreTimer: null,
  185. rowBodyPx: 170,
  186. marginPx: 10,
  187. slotHeightPx: 180,
  188. listHeightPx: 400,
  189. pageHeightPx: 0,
  190. vScrollTop: 0,
  191. cancelBtnStyle: 'width:150rpx;flex-shrink:0;padding:0 20rpx;min-width:0;'
  192. }
  193. },
  194. computed: {
  195. tabsItemStyle() {
  196. return { height: '32px', padding: '0 8px' }
  197. },
  198. mainTabsList() {
  199. return [
  200. { name: this.$t('myBookingPage.tabVet') },
  201. { name: this.$t('myBookingPage.tabExpert') },
  202. { name: this.$t('myBookingPage.tabOrg') }
  203. ]
  204. },
  205. currentSubKey() {
  206. if (this.mainTabIndex === 0) return this.vetSubKey
  207. if (this.mainTabIndex === 1) return this.expertSubKey
  208. return this.orgSubKey
  209. },
  210. subChips() {
  211. if (this.mainTabIndex === 0) {
  212. return [
  213. { key: 'all', labelKey: 'myBookingPage.subAll' },
  214. { key: 'upcoming', labelKey: 'myBookingPage.subUpcoming' },
  215. { key: 'done', labelKey: 'myBookingPage.subDone' },
  216. { key: 'cancelled', labelKey: 'myBookingPage.subCancelled' }
  217. ]
  218. }
  219. if (this.mainTabIndex === 1) {
  220. return [
  221. { key: 'all', labelKey: 'myBookingPage.subAll' },
  222. { key: 'upcoming', labelKey: 'myBookingPage.subUpcoming' },
  223. { key: 'todo_review', labelKey: 'myBookingPage.subTodoReview' },
  224. { key: 'done', labelKey: 'myBookingPage.subDone' },
  225. { key: 'cancelled', labelKey: 'myBookingPage.subCancelled' }
  226. ]
  227. }
  228. return [
  229. { key: 'all', labelKey: 'myBookingPage.subAll' },
  230. { key: 'upcoming', labelKey: 'myBookingPage.subUpcoming' },
  231. { key: 'done', labelKey: 'myBookingPage.subDone' },
  232. { key: 'cancelled', labelKey: 'myBookingPage.subCancelled' }
  233. ]
  234. },
  235. listNoMore() {
  236. return this.listTotal > 0 && this.rows.length >= this.listTotal
  237. },
  238. pageStyle() {
  239. if (this.pageHeightPx > 0) {
  240. return { height: `${this.pageHeightPx}px` }
  241. }
  242. return {}
  243. }
  244. },
  245. watch: {
  246. mainTabIndex() {
  247. this.vScrollTop = 0
  248. this.applyRowMetrics()
  249. this.loadList(true)
  250. this.$nextTick(() => {
  251. this.calcLayoutHeights()
  252. })
  253. }
  254. },
  255. onShow() {
  256. this.loadList(true)
  257. },
  258. onReady() {
  259. this.applyRowMetrics()
  260. this.applyListHeightFallback()
  261. this.$nextTick(() => this.calcLayoutHeights())
  262. },
  263. onUnload() {
  264. if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
  265. },
  266. methods: {
  267. providerTypeForTab() {
  268. return PROVIDER_TYPES[this.mainTabIndex] || 1
  269. },
  270. statusTabForSub() {
  271. return STATUS_TAB_MAP[this.currentSubKey] || 'all'
  272. },
  273. mapRow(row) {
  274. const pt = Number(row.providerType)
  275. let kind = 'vet'
  276. if (pt === 2) kind = 'org'
  277. else if (pt === 3) kind = 'expert'
  278. return {
  279. id: row.id,
  280. kind,
  281. providerType: pt,
  282. providerName: row.providerName || '',
  283. appointDate: row.appointDate || '',
  284. timeStart: row.timeStart || '',
  285. timeEnd: row.timeEnd || '',
  286. serviceAddress: row.serviceAddress || '',
  287. serviceRequirement: row.serviceRequirement || '',
  288. status: row.status,
  289. statusLabel: row.statusLabel || '',
  290. canCancel: !!row.canCancel,
  291. hasReview: !!row.hasReview,
  292. rejectReason: row.rejectReason || ''
  293. }
  294. },
  295. loadList(reset = false) {
  296. if (!reset) {
  297. if (this.listLoading || this.listLoadingMore || this.listNoMore) {
  298. return Promise.resolve()
  299. }
  300. this.pageNum += 1
  301. this.listLoadingMore = true
  302. } else {
  303. this.pageNum = 1
  304. this.listLoading = true
  305. }
  306. return listMyAppointments({
  307. providerType: this.providerTypeForTab(),
  308. statusTab: this.statusTabForSub(),
  309. pageNum: this.pageNum,
  310. pageSize: this.pageSize
  311. })
  312. .then((res) => {
  313. const raw = res.rows || []
  314. this.listTotal = res.total != null ? Number(res.total) : 0
  315. const mapped = raw.map((row) => this.mapRow(row))
  316. this.rows = reset ? mapped : this.rows.concat(mapped)
  317. this.$nextTick(() => {
  318. this.calcLayoutHeights()
  319. })
  320. })
  321. .catch(() => {
  322. if (reset) {
  323. this.rows = []
  324. this.listTotal = 0
  325. } else {
  326. this.pageNum -= 1
  327. }
  328. })
  329. .finally(() => {
  330. if (reset) {
  331. this.listLoading = false
  332. } else {
  333. this.listLoadingMore = false
  334. }
  335. })
  336. },
  337. tryAutoLoadMore() {
  338. if (this.listLoading || this.listLoadingMore || this.listNoMore) return
  339. const totalH = this.rows.length * this.slotHeightPx
  340. const viewH = this.listHeightPx
  341. if (totalH > 0 && totalH <= viewH) {
  342. this.loadList(false)
  343. }
  344. },
  345. onVirtualListScroll(scrollTop) {
  346. if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
  347. this.loadMoreTimer = setTimeout(() => {
  348. this.loadMoreTimer = null
  349. this.tryLoadMoreOnScroll(typeof scrollTop === 'number' ? scrollTop : 0)
  350. }, 180)
  351. },
  352. tryLoadMoreOnScroll(scrollTop) {
  353. if (this.listLoading || this.listLoadingMore || this.listNoMore) return
  354. const totalH = this.rows.length * this.slotHeightPx
  355. const viewH = this.listHeightPx
  356. const threshold = Math.max(this.slotHeightPx * 2, 120)
  357. if (totalH <= viewH) {
  358. this.loadList(false)
  359. return
  360. }
  361. if (scrollTop + viewH >= totalH - threshold) {
  362. this.loadList(false)
  363. }
  364. },
  365. applyRowMetrics() {
  366. try {
  367. const rpx = rowBodyRpxForTab(this.mainTabIndex)
  368. this.marginPx = Math.ceil(uni.upx2px(ROW_GAP_RPX))
  369. this.rowBodyPx = Math.max(120, Math.ceil(uni.upx2px(rpx)))
  370. this.slotHeightPx = this.rowBodyPx + this.marginPx
  371. } catch (e) {
  372. this.marginPx = 10
  373. this.rowBodyPx = 170
  374. this.slotHeightPx = 180
  375. }
  376. },
  377. applyListHeightFallback() {
  378. const contentH = this.getViewportContentHeight()
  379. const topFallback = Math.ceil(uni.upx2px(200))
  380. const bodyPad = Math.ceil(uni.upx2px(MB_BODY_PAD_RPX))
  381. this.pageHeightPx = contentH
  382. this.listHeightPx = Math.max(240, contentH - topFallback - bodyPad)
  383. },
  384. getNavigationBarHeight() {
  385. const sys = uni.getSystemInfoSync()
  386. const statusBar = sys.statusBarHeight || 0
  387. // #ifdef MP-WEIXIN
  388. try {
  389. const menu = uni.getMenuButtonBoundingClientRect()
  390. if (menu && menu.height) {
  391. return statusBar + (menu.top - statusBar) * 2 + menu.height
  392. }
  393. } catch (e) {
  394. /* noop */
  395. }
  396. // #endif
  397. return statusBar + 44
  398. },
  399. getViewportContentHeight() {
  400. const sys = uni.getSystemInfoSync()
  401. const navH = this.getNavigationBarHeight()
  402. // #ifdef H5
  403. if (typeof window !== 'undefined' && window.innerHeight) {
  404. return Math.max(320, window.innerHeight - navH)
  405. }
  406. // #endif
  407. const screenH = sys.screenHeight || sys.windowHeight || 600
  408. return Math.max(320, screenH - navH)
  409. },
  410. calcLayoutHeights() {
  411. const contentH = this.getViewportContentHeight()
  412. const bodyPad = Math.ceil(uni.upx2px(MB_BODY_PAD_RPX))
  413. this.pageHeightPx = contentH
  414. uni.createSelectorQuery()
  415. .in(this)
  416. .select('.mb-top')
  417. .boundingClientRect((topRect) => {
  418. const topH =
  419. topRect && topRect.height > 0 ? Math.ceil(topRect.height) : Math.ceil(uni.upx2px(200))
  420. this.listHeightPx = Math.max(240, contentH - topH - bodyPad)
  421. this.$nextTick(() => {
  422. this.$refs.mbVList?.measureContainerHeight?.()
  423. this.tryAutoLoadMore()
  424. })
  425. })
  426. .exec()
  427. },
  428. onMainTabUpdate(idx) {
  429. this.mainTabIndex = typeof idx === 'number' ? idx : 0
  430. },
  431. setSubKey(key) {
  432. if (this.mainTabIndex === 0) this.vetSubKey = key
  433. else if (this.mainTabIndex === 1) this.expertSubKey = key
  434. else this.orgSubKey = key
  435. this.vScrollTop = 0
  436. this.loadList(true)
  437. },
  438. titleFor(item) {
  439. return item.providerName || '—'
  440. },
  441. parseAppointDate(dateStr) {
  442. if (!dateStr) return null
  443. const d = new Date(String(dateStr).replace(/-/g, '/'))
  444. return isNaN(d.getTime()) ? null : d
  445. },
  446. timeLine(item) {
  447. const d = this.parseAppointDate(item.appointDate)
  448. if (!d) {
  449. return this.$t('myBookingPage.timeTpl', {
  450. md: '—',
  451. wd: '',
  452. start: item.timeStart || '--:--',
  453. end: item.timeEnd || '--:--'
  454. })
  455. }
  456. const m = pad2(d.getMonth() + 1)
  457. const day = pad2(d.getDate())
  458. const wd = this.$t(`bookingServicePage.wd${d.getDay()}`)
  459. return this.$t('myBookingPage.timeTpl', {
  460. md: `${m}-${day}`,
  461. wd,
  462. start: item.timeStart || '--:--',
  463. end: item.timeEnd || '--:--'
  464. })
  465. },
  466. addressLine(item) {
  467. const s = (item.serviceAddress || '').trim()
  468. return s || this.$t('myBookingPage.noAddress')
  469. },
  470. needLine(item) {
  471. const s = (item.serviceRequirement || '').trim()
  472. return s || this.$t('myBookingPage.noNeed')
  473. },
  474. statusLabel(item) {
  475. if (item.statusLabel) return item.statusLabel
  476. const st = Number(item.status)
  477. const map = {
  478. 0: 'myBookingPage.vetStatusPending',
  479. 1: 'myBookingPage.vetStatusConfirmed',
  480. 2: 'myBookingPage.vetStatusRejected',
  481. 3: 'myBookingPage.vetStatusCancelled',
  482. 4: 'myBookingPage.vetStatusCompleted',
  483. 5: 'myBookingPage.orgStatusBooked'
  484. }
  485. return this.$t(map[st] || map[0])
  486. },
  487. statusTagType(item) {
  488. const st = Number(item.status)
  489. return STATUS_TAG_TYPE[st] || 'info'
  490. },
  491. showCancelBtn(item) {
  492. return !!item.canCancel
  493. },
  494. showExpertRateRow(item) {
  495. return item.kind === 'expert' && Number(item.status) === 4
  496. },
  497. onCancelBook(item) {
  498. uni.showModal({
  499. title: this.$t('myBookingPage.confirmCancelTitle'),
  500. content: this.$t('myBookingPage.confirmCancelBody'),
  501. confirmText: this.$t('myBookingPage.confirmOk'),
  502. cancelText: this.$t('myBookingPage.confirmNo'),
  503. success: (res) => {
  504. if (!res.confirm) return
  505. cancelMyAppointment(item.id)
  506. .then(() => {
  507. uni.showToast({ title: this.$t('myBookingPage.toastCancelled'), icon: 'none' })
  508. this.loadList(true)
  509. })
  510. .catch(() => {
  511. uni.showToast({ title: this.$t('myBookingPage.cancelFail'), icon: 'none' })
  512. })
  513. }
  514. })
  515. },
  516. reviewNavUrl(item, mode) {
  517. const q = [
  518. `appointmentId=${encodeURIComponent(item.id)}`,
  519. `mode=${encodeURIComponent(mode)}`,
  520. `providerName=${encodeURIComponent(item.providerName || '')}`
  521. ].join('&')
  522. return `/package-a/review-history/index?${q}`
  523. },
  524. onReview(item) {
  525. uni.navigateTo({ url: this.reviewNavUrl(item, 'submit') })
  526. },
  527. onViewReview(item) {
  528. uni.navigateTo({ url: this.reviewNavUrl(item, 'view') })
  529. }
  530. }
  531. }
  532. </script>
  533. <style lang="scss" scoped>
  534. @import '@/styles/morandi.scss';
  535. @import '@/styles/tab-page.scss';
  536. .mb-page {
  537. display: flex;
  538. flex-direction: column;
  539. min-width: 0;
  540. width: 100%;
  541. height: 100%;
  542. min-height: 100%;
  543. overflow: hidden;
  544. box-sizing: border-box;
  545. background: $morandi-bg-page;
  546. }
  547. .mb-top {
  548. flex-shrink: 0;
  549. min-width: 0;
  550. }
  551. .mb-card {
  552. background: #ffffff;
  553. padding: 16rpx 20rpx;
  554. box-sizing: border-box;
  555. }
  556. .mb-card--tabs {
  557. padding: 8rpx 12rpx 4rpx;
  558. }
  559. .mb-tabs {
  560. width: 100%;
  561. }
  562. .mb-tabs :deep(.u-tabs__wrapper__nav__item__text) {
  563. line-height: 1.2;
  564. }
  565. .mb-chip-scroll {
  566. width: 100%;
  567. white-space: nowrap;
  568. background: #ffffff;
  569. border-bottom: 1rpx solid $morandi-border-soft;
  570. }
  571. .mb-chip-row {
  572. display: inline-flex;
  573. flex-direction: row;
  574. align-items: center;
  575. gap: 16rpx;
  576. padding: 12rpx 20rpx 16rpx;
  577. min-width: min-content;
  578. }
  579. .mb-chip {
  580. flex-shrink: 0;
  581. padding: 10rpx 22rpx;
  582. border-radius: 999rpx;
  583. border: 1rpx solid #d6d3d1;
  584. background: #fafaf9;
  585. }
  586. .mb-chip--on {
  587. border-color: #22c55e;
  588. background: rgba(34, 197, 94, 0.08);
  589. }
  590. .mb-chip__txt {
  591. font-size: 24rpx;
  592. color: #78716c;
  593. font-weight: 500;
  594. }
  595. .mb-chip--on .mb-chip__txt {
  596. color: #15803d;
  597. font-weight: 600;
  598. }
  599. .mb-body {
  600. flex: 1;
  601. min-height: 0;
  602. height: 0;
  603. min-width: 0;
  604. display: flex;
  605. flex-direction: column;
  606. padding: 0 24rpx 16rpx;
  607. box-sizing: border-box;
  608. overflow: hidden;
  609. }
  610. .mb-list-wrap {
  611. flex: 1;
  612. min-height: 0;
  613. display: flex;
  614. flex-direction: column;
  615. min-width: 0;
  616. }
  617. .mb-empty {
  618. flex: 1;
  619. display: flex;
  620. align-items: center;
  621. justify-content: center;
  622. min-height: 0;
  623. padding: 48rpx 24rpx;
  624. box-sizing: border-box;
  625. }
  626. .mb-empty__txt {
  627. color: $morandi-text-muted;
  628. }
  629. .mb-vlist {
  630. flex: 1;
  631. min-height: 0;
  632. width: 100%;
  633. height: 100%;
  634. box-sizing: border-box;
  635. padding: 10rpx 0 0;
  636. }
  637. .mb-cell {
  638. box-sizing: border-box;
  639. padding-bottom: 10rpx;
  640. }
  641. .mb-item {
  642. display: flex;
  643. flex-direction: column;
  644. gap: 8rpx;
  645. padding: 20rpx;
  646. box-sizing: border-box;
  647. background: #ffffff;
  648. border-radius: 12rpx;
  649. border: 1rpx solid $morandi-border-soft;
  650. min-width: 0;
  651. }
  652. .mb-item__title {
  653. font-size: 30rpx;
  654. font-weight: 600;
  655. color: #111827;
  656. line-height: 1.35;
  657. word-break: break-word;
  658. }
  659. .mb-item__line {
  660. font-size: 24rpx;
  661. color: $morandi-text-muted;
  662. line-height: 1.45;
  663. word-break: break-word;
  664. }
  665. .mb-item__line--grow {
  666. flex: 1;
  667. min-width: 0;
  668. }
  669. .mb-item__k {
  670. color: $morandi-text-muted;
  671. font-weight: 500;
  672. }
  673. .mb-item__row {
  674. display: flex;
  675. flex-direction: row;
  676. align-items: center;
  677. justify-content: space-between;
  678. gap: 12rpx;
  679. margin-top: 4rpx;
  680. min-width: 0;
  681. }
  682. .mb-item__row--status {
  683. justify-content: flex-start;
  684. gap: 8rpx;
  685. }
  686. .mb-item__row--status :deep(.u-tag) {
  687. flex-shrink: 0;
  688. }
  689. .mb-item__row--end {
  690. width: 100%;
  691. justify-content: flex-end;
  692. flex-wrap: wrap;
  693. margin-top: 8rpx;
  694. }
  695. /* up-button 根节点默认易撑满行宽,需收窄后 flex-end 才能靠右 */
  696. .mb-item__row--end :deep(.u-button) {
  697. width: auto !important;
  698. flex-shrink: 0;
  699. }
  700. .mb-page.lang-bo {
  701. .mb-item__title {
  702. font-size: 26rpx;
  703. line-height: 1.65;
  704. }
  705. .mb-item__line {
  706. font-size: 22rpx;
  707. }
  708. }
  709. </style>