| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683 |
- <template>
- <view class="hotel-customer-theme dd-page">
- <!-- 暂停接单提示横幅 -->
- <view v-if="isPaused" class="dd-pause-banner">
- <text class="dd-pause-icon">⏸️</text>
- <text class="dd-pause-text">餐厅已暂停接单,请稍后再试</text>
- </view>
- <!-- 大图轮播 -->
- <view class="dd-hero">
- <swiper
- v-if="allImages.length"
- class="dd-hero-swiper"
- :current="currentImageIndex"
- @change="onSwiperChange"
- :autoplay="true"
- :interval="5000"
- :circular="true"
- indicator-dots
- indicator-color="rgba(255, 255, 255, 0.4)"
- indicator-active-color="#fff"
- >
- <swiper-item v-for="(img, idx) in allImages" :key="idx">
- <image
- :src="img"
- mode="aspectFill"
- class="dd-hero-img"
- @click="previewAllImages(idx)"
- />
- </swiper-item>
- </swiper>
- <text v-else class="dd-hero-placeholder">🍽️</text>
-
- <!-- 后退按钮 -->
- <view class="dd-back-btn" @click="goBack">
- <text class="dd-back-icon">‹</text>
- </view>
-
- <!-- 页码指示器 -->
- <view v-if="allImages.length > 1" class="dd-image-counter">
- {{ currentImageIndex + 1 }} / {{ allImages.length }}
- </view>
- </view>
- <!-- 菜品信息 -->
- <view class="dd-body">
- <view class="dd-title-row">
- <text class="dd-title">{{ dish.name }}</text>
- <text class="dd-price">¥{{ dish.price }}</text>
- </view>
- <view class="dd-sales" v-if="dish.sales">月售 {{ dish.sales }} 份</view>
- <view class="dd-desc" v-if="dish.description">{{ dish.description }}</view>
- <!-- 附加图片 -->
- <view v-if="imageList.length" class="dd-images">
- <image
- v-for="(img, idx) in imageList"
- :key="idx"
- :src="img"
- mode="aspectFill"
- class="dd-image-thumb"
- @click="previewImage(idx)"
- />
- </view>
- <!-- 规格选择 -->
- <view v-for="group in dish.specGroups" :key="group.id" class="dd-spec-section">
- <view class="dd-spec-title">
- {{ group.groupName }}
- <text v-if="group.required" class="dd-spec-required">必选</text>
- </view>
- <view class="dd-spec-options">
- <view
- v-for="opt in group.options"
- :key="opt.id"
- class="dd-spec-opt"
- :class="{ 'dd-spec-opt--active': selectedSpecs[group.id] === opt.id }"
- @click="selectSpec(group, opt)"
- >
- {{ opt.optionName }}
- <text v-if="opt.priceExtra > 0" class="dd-spec-extra"> +¥{{ opt.priceExtra }}</text>
- </view>
- </view>
- </view>
- <!-- 加料选择 -->
- <view v-if="dish.additions && dish.additions.length" class="dd-spec-section">
- <view class="dd-spec-title">
- 加料
- <text class="dd-spec-optional">选配</text>
- </view>
- <view class="dd-spec-options">
- <view
- v-for="add in dish.additions"
- :key="add.id"
- class="dd-spec-opt"
- :class="{ 'dd-spec-opt--active': selectedAdditions.includes(add.id) }"
- @click="toggleAddition(add)"
- >
- {{ add.name }}
- <text class="dd-spec-extra"> +¥{{ add.extraPrice }}</text>
- </view>
- </view>
- </view>
- <!-- 备注 -->
- <view class="dd-spec-section">
- <view class="dd-spec-title">特殊要求</view>
- <textarea
- class="dd-note"
- v-model="itemNote"
- placeholder="如:少盐、忌口等..."
- :maxlength="200"
- />
- </view>
- </view>
- <!-- 底部操作栏 -->
- <view class="dd-bottom-bar">
- <view class="dd-qty-ctrl">
- <view class="dd-qty-btn" @click="changeQty(-1)">−</view>
- <text class="dd-qty-num">{{ quantity }}</text>
- <view class="dd-qty-btn dd-qty-btn--add" @click="changeQty(1)">+</view>
- </view>
- <view class="dd-add-btn" :class="{ 'dd-add-btn--disabled': isPaused }" @click="addToCart">
- {{ isPaused ? '暂停接单' : '加入购物车 ¥' + totalPrice }}
- </view>
- </view>
- </view>
- </template>
- <script setup>
- import { computed, onMounted, reactive, ref } from 'vue'
- import { useHotelOrderStore } from '@/store'
- import api from '@/api'
- import { getFileDownloadUrl } from '@/utils/file'
- const orderStore = useHotelOrderStore()
- const dish = ref({})
- const selectedSpecs = reactive({}) // { groupId: optionId }
- const selectedAdditions = ref([]) // [additionId, ...]
- const quantity = ref(1)
- const itemNote = ref('')
- const currentImageIndex = ref(0) // 当前轮播图片索引
- const isPaused = ref(false) // 暂停接单状态
- let dishId = ''
- /** 所有图片列表(主图 + 附加图) */
- const allImages = computed(() => {
- const images = []
- // 主图
- if (dish.value.image) {
- images.push(getImageUrl(dish.value))
- }
- // 附加图
- if (dish.value.images) {
- dish.value.images
- .split(',')
- .map(id => id.trim())
- .filter(id => id)
- .forEach(id => {
- images.push(getFileDownloadUrl(id))
- })
- }
- return images
- })
- /** 附加图片列表(解析逗号分隔的 fileId) */
- const imageList = computed(() => {
- if (!dish.value.images) return []
- return dish.value.images
- .split(',')
- .map(id => id.trim())
- .filter(id => id)
- .map(id => getFileDownloadUrl(id))
- })
- /** Swiper 切换事件 */
- function onSwiperChange(e) {
- currentImageIndex.value = e.detail.current
- }
- /** 预览所有图片(包括主图和附加图) */
- function previewAllImages(index) {
- uni.previewImage({
- urls: allImages.value,
- current: index,
- })
- }
- onMounted(async () => {
- const pages = getCurrentPages()
- const currentPage = pages[pages.length - 1]
- const options = currentPage?.$page?.options || currentPage?.options || {}
- dishId = options.id
- if (!dishId) {
- uni.showToast({ title: '菜品ID缺失', icon: 'none' })
- return
- }
- await loadDishDetail()
- loadPauseStatus()
- })
- async function loadDishDetail() {
- try {
- const res = await api.hotelDishDetail(dishId, orderStore.tenantId)
- const data = res?.data || res
- dish.value = data || {}
- // 默认选中每个必选规格组的第一个选项
- if (data.specGroups) {
- data.specGroups.forEach(group => {
- if (group.required && group.options?.length) {
- selectedSpecs[group.id] = group.options[0].id
- }
- })
- }
- } catch (e) {
- console.error('加载菜品详情失败:', e)
- uni.showToast({ title: '加载失败', icon: 'none' })
- }
- }
- function getImageUrl(d) {
- if (!d.image) return ''
- // getFileDownloadUrl 已兼容 http/data/blob 直链与 fileId,无需再判前缀
- return getFileDownloadUrl(d.image)
- }
- /** 加载暂停接单状态 */
- async function loadPauseStatus() {
- if (!orderStore.tenantId) return
- try {
- var res = await api.hotelPauseStatus(orderStore.tenantId)
- isPaused.value = res?.data ?? res ?? false
- } catch (e) {
- console.log('暂停状态查询失败:', e?.message || e)
- isPaused.value = false
- }
- }
- function goBack() {
- uni.navigateBack()
- }
- function previewImage(index) {
- uni.previewImage({
- urls: imageList.value,
- current: index,
- })
- }
- /** 选择规格(必选组不可取消,可选组点击已选项可取消) */
- function selectSpec(group, opt) {
- if (group.required) {
- // 必选组:直接选中
- selectedSpecs[group.id] = opt.id
- } else {
- // 可选组:点击已选项则取消,否则选中
- if (selectedSpecs[group.id] === opt.id) {
- delete selectedSpecs[group.id]
- } else {
- selectedSpecs[group.id] = opt.id
- }
- }
- }
- /** 切换加料 */
- function toggleAddition(add) {
- const idx = selectedAdditions.value.indexOf(add.id)
- if (idx > -1) {
- selectedAdditions.value.splice(idx, 1)
- } else {
- selectedAdditions.value.push(add.id)
- }
- }
- /** 数量变化 */
- function changeQty(delta) {
- const v = quantity.value + delta
- if (v >= 1) quantity.value = v
- }
- /** 计算总价 */
- const totalPrice = computed(() => {
- let price = Number(dish.value.price) || 0
- // 加上选中的规格加价
- if (dish.value.specGroups) {
- dish.value.specGroups.forEach(group => {
- const optId = selectedSpecs[group.id]
- if (optId && group.options) {
- const opt = group.options.find(o => o.id === optId)
- if (opt) price += Number(opt.priceExtra) || 0
- }
- })
- }
- // 加上选中的加料加价
- if (dish.value.additions) {
- selectedAdditions.value.forEach(addId => {
- const add = dish.value.additions.find(a => a.id === addId)
- if (add) price += Number(add.extraPrice) || 0
- })
- }
- return (price * quantity.value).toFixed(2)
- })
- /** 加入购物车 */
- function addToCart() {
- if (isPaused.value) {
- uni.showToast({ title: '餐厅已暂停接单', icon: 'none' })
- return
- }
- const specs = []
- const additions = []
- // 收集选中的规格
- if (dish.value.specGroups) {
- dish.value.specGroups.forEach(group => {
- const optId = selectedSpecs[group.id]
- if (optId && group.options) {
- const opt = group.options.find(o => o.id === optId)
- if (opt) {
- specs.push({
- optionId: opt.id,
- groupName: group.groupName,
- optionName: opt.optionName,
- priceExtra: Number(opt.priceExtra) || 0,
- })
- }
- }
- })
- }
- // 收集选中的加料
- if (dish.value.additions) {
- selectedAdditions.value.forEach(addId => {
- const add = dish.value.additions.find(a => a.id === addId)
- if (add) {
- additions.push({ id: add.id, name: add.name, extraPrice: Number(add.extraPrice) || 0 })
- }
- })
- }
- // 计算单价
- let unitPrice = Number(dish.value.price) || 0
- specs.forEach(s => { unitPrice += s.priceExtra })
- additions.forEach(a => { unitPrice += a.extraPrice })
- const specKey = [
- ...specs.map(s => s.optionName),
- ...additions.map(a => a.name),
- ].sort().join('|')
- orderStore.addToCartItem({
- dishId: dish.value.id,
- dishName: dish.value.name,
- image: dish.value.image || '',
- price: Number(dish.value.price),
- specs,
- additions,
- note: itemNote.value,
- quantity: quantity.value,
- unitPrice,
- specKey,
- })
- uni.showToast({ title: '已加入购物车', icon: 'success' })
- setTimeout(() => {
- uni.navigateBack()
- }, 800)
- }
- </script>
- <style lang="scss" scoped>
- .dd-page {
- min-height: 100vh;
- background: var(--h-bg, #F7F7F7);
- padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
- }
- /* 大图 */
- .dd-hero {
- width: 100%;
- height: 440rpx;
- background: var(--h-bg, #F7F7F7);
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: hidden;
- position: relative;
- }
- .dd-hero-swiper {
- width: 100%;
- height: 100%;
- }
- .dd-hero-img {
- width: 100%;
- height: 100%;
- }
- .dd-hero-placeholder {
- font-size: 120rpx;
- opacity: 0.3;
- }
- /* 后退按钮 */
- .dd-back-btn {
- position: absolute;
- top: 24rpx;
- left: 24rpx;
- width: 64rpx;
- height: 64rpx;
- border-radius: 50%;
- background: rgba(0, 0, 0, 0.4);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10;
- }
- .dd-back-icon {
- font-size: 48rpx;
- color: #fff;
- font-weight: 300;
- line-height: 1;
- margin-top: -4rpx;
- }
- /* 图片计数器 */
- .dd-image-counter {
- position: absolute;
- bottom: 24rpx;
- right: 24rpx;
- padding: 8rpx 20rpx;
- background: rgba(0, 0, 0, 0.5);
- border-radius: 32rpx;
- font-size: 24rpx;
- color: #fff;
- z-index: 10;
- }
- /* 内容区 */
- .dd-body {
- padding: 32rpx 28rpx;
- }
- .dd-title-row {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- margin-bottom: 8rpx;
- }
- .dd-title {
- font-size: 36rpx;
- font-weight: 700;
- color: var(--h-txt, #1A1A1A);
- flex: 1;
- }
- .dd-price {
- font-size: 44rpx;
- font-weight: 700;
- color: var(--h-warn, #D97706);
- white-space: nowrap;
- }
- .dd-sales {
- font-size: 22rpx;
- color: var(--h-txt-light, #999);
- margin-bottom: 16rpx;
- }
- .dd-desc {
- font-size: 26rpx;
- color: var(--h-txt-sub, #666);
- line-height: 1.8;
- margin-bottom: 24rpx;
- }
- /* 附加图片 */
- .dd-images {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- margin-top: 24rpx;
- }
- .dd-image-thumb {
- width: 200rpx;
- height: 200rpx;
- border-radius: 16rpx;
- overflow: hidden;
- }
- /* 规格 */
- .dd-spec-section {
- margin-top: 32rpx;
- }
- .dd-spec-title {
- font-size: 28rpx;
- font-weight: 600;
- color: var(--h-txt, #1A1A1A);
- margin-bottom: 16rpx;
- }
- .dd-spec-required {
- color: var(--h-dang, #DC2626);
- font-size: 22rpx;
- font-weight: 400;
- margin-left: 8rpx;
- }
- .dd-spec-optional {
- color: var(--h-txt-light, #999);
- font-size: 22rpx;
- font-weight: 400;
- margin-left: 8rpx;
- }
- .dd-spec-options {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- }
- .dd-spec-opt {
- padding: 16rpx 32rpx;
- border: 1rpx solid var(--h-bdr, #eee);
- border-radius: 40rpx;
- font-size: 24rpx;
- color: var(--h-txt-sub, #666);
- background: #fff;
- transition: all 0.15s;
- }
- .dd-spec-opt--active {
- background: var(--h-acc-lt, #E8F0E3);
- border-color: var(--h-pri, #2D5016);
- color: var(--h-pri, #2D5016);
- font-weight: 600;
- }
- .dd-spec-extra {
- font-size: 22rpx;
- color: var(--h-warn, #D97706);
- }
- /* 备注 */
- .dd-note {
- display: block;
- width: 100%;
- height: 140rpx;
- /* H5 无全局 border-box 重置,width:100% + padding + border 会把备注框撑出卡片右侧 */
- box-sizing: border-box;
- padding: 20rpx 24rpx;
- border: 1rpx solid var(--h-bdr, #eee);
- border-radius: 16rpx;
- font-size: 26rpx;
- line-height: 40rpx;
- resize: none;
- overflow: hidden;
- background: #fff;
- color: var(--h-txt, #1A1A1A);
- /* uni-app H5 的 textarea 会渲染成 uni-textarea > wrapper > textarea,内层需撑满外框 */
- :deep(.uni-textarea-wrapper),
- :deep(.uni-textarea-textarea) {
- width: 100%;
- height: 100%;
- box-sizing: border-box;
- font-size: 26rpx;
- line-height: 40rpx;
- border: none;
- background: transparent;
- resize: none;
- }
- :deep(.uni-textarea-placeholder) {
- font-size: 26rpx;
- line-height: 40rpx;
- color: var(--h-txt-light, #999);
- }
- }
- /* 底部操作栏 */
- .dd-bottom-bar {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20rpx 28rpx;
- padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
- background: #fff;
- border-top: 1rpx solid var(--h-bdr, #eee);
- z-index: 10;
- }
- .dd-qty-ctrl {
- display: flex;
- align-items: center;
- border-radius: 40rpx;
- border: 1rpx solid var(--h-bdr, #eee);
- overflow: hidden;
- }
- .dd-qty-btn {
- width: 64rpx;
- height: 64rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 36rpx;
- color: var(--h-txt-sub, #666);
- background: var(--h-bg, #F7F7F7);
- }
- .dd-qty-btn--add {
- color: var(--h-pri, #2D5016);
- font-weight: 700;
- }
- .dd-qty-num {
- width: 80rpx;
- text-align: center;
- font-size: 32rpx;
- font-weight: 600;
- color: var(--h-txt, #1A1A1A);
- border-left: 1rpx solid var(--h-bdr, #eee);
- border-right: 1rpx solid var(--h-bdr, #eee);
- line-height: 64rpx;
- }
- .dd-add-btn {
- padding: 20rpx 48rpx;
- border-radius: 48rpx;
- background: var(--h-pri, #2D5016);
- color: #fff;
- font-size: 28rpx;
- font-weight: 700;
- box-shadow: 0 8rpx 24rpx rgba(45, 80, 22, 0.3);
- }
- .dd-add-btn:active {
- opacity: 0.85;
- }
- .dd-add-btn--disabled {
- opacity: 0.5;
- background: var(--h-txt-sub, #999);
- }
- /* 暂停接单横幅 */
- .dd-pause-banner {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8rpx;
- padding: 20rpx 28rpx;
- background: #FEF3C7;
- color: #92400E;
- font-size: 26rpx;
- font-weight: 600;
- }
- .dd-pause-icon {
- font-size: 28rpx;
- }
- </style>
|