diff --git a/src/i18n/pages/daily-menu/en.ts b/src/i18n/pages/daily-menu/en.ts
index 7fcc877..a1c8397 100644
--- a/src/i18n/pages/daily-menu/en.ts
+++ b/src/i18n/pages/daily-menu/en.ts
@@ -161,7 +161,7 @@ export default {
dailyMenuBadge: "Daily menu",
unnamed: "Unnamed dish",
perUnit: "/ea",
- sold: "Sold {count}",
+ sold: "Ordered {count}",
mealSupply: "{meal} service",
productInfo: "Product info",
supplyDate: "Supply date",
diff --git a/src/i18n/pages/daily-menu/zh-cn.ts b/src/i18n/pages/daily-menu/zh-cn.ts
index 162f4e0..c05fe55 100644
--- a/src/i18n/pages/daily-menu/zh-cn.ts
+++ b/src/i18n/pages/daily-menu/zh-cn.ts
@@ -161,7 +161,7 @@ export default {
dailyMenuBadge: "每日菜谱",
unnamed: "未命名菜品",
perUnit: "/份",
- sold: "已售 {count}",
+ sold: "已订 {count}",
mealSupply: "{meal}供应",
productInfo: "商品信息",
supplyDate: "供应日期",
diff --git a/src/i18n/pages/daily-menu/zh-hk.ts b/src/i18n/pages/daily-menu/zh-hk.ts
index dde9cc9..29558af 100644
--- a/src/i18n/pages/daily-menu/zh-hk.ts
+++ b/src/i18n/pages/daily-menu/zh-hk.ts
@@ -161,7 +161,7 @@ export default {
dailyMenuBadge: "每日菜譜",
unnamed: "未命名菜品",
perUnit: "/份",
- sold: "已售 {count}",
+ sold: "已訂 {count}",
mealSupply: "{meal}供應",
productInfo: "商品信息",
supplyDate: "供應日期",
diff --git a/src/pages/hotel/confirm.vue b/src/pages/hotel/confirm.vue
index d97b47a..2b433be 100644
--- a/src/pages/hotel/confirm.vue
+++ b/src/pages/hotel/confirm.vue
@@ -89,7 +89,28 @@
共住
- {{ nightCount }}晚 · {{ form.rooms }}间房
+ {{ nightCount }}晚
+
+
+ 间数
+
+
+ −
+
+ {{ form.rooms }}
+
+ +
+
+ 最多{{ maxRooms }}间
+
房型
@@ -151,22 +172,65 @@
-
+
- 入住人信息
-
- 姓名
-
+
+ 入住人信息
+ 必填主要联系人;订多间时可继续添加
-
- 手机号
-
+
+
+
+
+
+
+ {{ idx === 0 ? "主要联系人" : `入住人 ${idx + 1}` }}
+
+ 必填
+
+
+
+ 删除
+
+
+
+
+ 姓名
+
+
+
+ 手机
+
+
+
+
+
+
+ 添加入住人
+ 还可加 {{ form.rooms - guests.length }} 人
@@ -174,7 +238,7 @@
费用明细
- 房费({{ nightCount }}晚)
+ 房费({{ nightCount }}晚 · {{ form.rooms }}间)
¥{{ roomFee }}
@@ -246,11 +310,11 @@
入住
- {{ stayRangeText }}
+ {{ stayRangeText }} · {{ form.rooms }}间
入住人
- {{ form.contactName.trim() }}
+ {{ guestSummaryText }}
@@ -291,6 +355,8 @@ const DATE_PICKER_MIN = dayjs().format("YYYY-MM-DD")
const DATE_PICKER_MAX = dayjs().add(365, "day").format("YYYY-MM-DD")
/** 连住上限:最多 30 晚(约一个月) */
const MAX_STAY_NIGHTS = 30
+/** 与后端 App 下单一致:单次最多 10 间 */
+const MAX_ROOMS = 10
const userStore = useUserStore()
const submitting = ref(false)
@@ -303,6 +369,8 @@ const hotelMetaLoaded = ref(false)
const calendarLoaded = ref(false)
/** 产品未来日历行(含可售) */
const calendarRows = ref([])
+/** 入住人列表:至少 1 位,最多 = 间数;可只填主要联系人 */
+const guests = ref>([{ name: "", mobile: "" }])
/** 支付拉起后用户取消等:保留待支付单,重试只走支付不再创单 */
const pendingPay = ref<{
orderId: string | number
@@ -313,14 +381,6 @@ const pendingPay = ref<{
productName: string
} | null>(null)
-// 房型/日期/间数变更后丢弃缓存,避免付到旧单
-watch(
- () => [form.hotelId, form.productCode, form.checkIn, form.checkOut, form.rooms],
- () => {
- pendingPay.value = null
- },
-)
-
const form = reactive({
hotelId: "",
/** 酒店所属部门:pay-card / 收款商户均按此解析 */
@@ -337,15 +397,118 @@ const form = reactive({
checkIn: dayjs().format("YYYY-MM-DD"),
checkOut: dayjs().add(1, "day").format("YYYY-MM-DD"),
rooms: 1,
- contactName: "",
- contactPhone: "",
})
+// 房型/日期/间数变更后丢弃缓存,避免付到旧单
+watch(
+ () => [form.hotelId, form.productCode, form.checkIn, form.checkOut, form.rooms],
+ () => {
+ pendingPay.value = null
+ },
+)
+
const nightCount = computed(() => {
const n = dayjs(form.checkOut).diff(dayjs(form.checkIn), "day")
return n > 0 ? n : 1
})
+/** 所选区间每晚可售的最小值,作为间数上限(未加载日历时按 10) */
+const maxRooms = computed(() => {
+ if (!calendarLoaded.value || !stayNightList.value.length) return MAX_ROOMS
+ let minRemain = MAX_ROOMS
+ for (const ymd of stayNightList.value) {
+ const row = calendarRows.value.find((r) => stayYmd(r?.stayDate) === ymd)
+ if (!row || Number(row.dailyStatus) !== 1) {
+ minRemain = 0
+ break
+ }
+ const remain =
+ row.remainStock != null
+ ? Number(row.remainStock)
+ : Math.max(0, Number(row.stock || 0) - Number(row.sold || 0))
+ minRemain = Math.min(minRemain, remain)
+ }
+ return Math.max(1, Math.min(MAX_ROOMS, Math.floor(minRemain) || 1))
+})
+
+const changeRooms = (delta: number) => {
+ const next = Math.max(1, Math.min(maxRooms.value, (Number(form.rooms) || 1) + delta))
+ if (next === form.rooms) {
+ if (delta > 0 && form.rooms >= maxRooms.value) {
+ uni.showToast({ title: `当前日期最多可订${maxRooms.value}间`, icon: "none" })
+ }
+ return
+ }
+ form.rooms = next
+ // 间数减少时裁掉多余入住人;增加时不自动加行(用户点「添加入住人」)
+ if (guests.value.length > next) {
+ guests.value = guests.value.slice(0, next)
+ }
+}
+
+const addGuest = () => {
+ if (guests.value.length >= form.rooms) {
+ uni.showToast({ title: `最多可填${form.rooms}位入住人`, icon: "none" })
+ return
+ }
+ guests.value.push({ name: "", mobile: "" })
+}
+
+const removeGuest = (idx: number) => {
+ if (idx <= 0 || guests.value.length <= 1) return
+ guests.value.splice(idx, 1)
+}
+
+const guestSummaryText = computed(() => {
+ const filled = guests.value.filter((g) => g.name?.trim())
+ if (!filled.length) return "-"
+ if (filled.length === 1) return filled[0].name.trim()
+ return `${filled[0].name.trim()} 等${filled.length}人`
+})
+
+/** 提交用:联系人取首位;guestList 含所有已填完整的入住人 */
+const buildGuestPayload = () => {
+ const primary = guests.value[0] || { name: "", mobile: "" }
+ const list = guests.value
+ .filter((g) => g.name?.trim() && g.mobile?.trim())
+ .map((g) => ({ name: g.name.trim(), mobile: g.mobile.trim() }))
+ return {
+ contactName: primary.name.trim(),
+ contactPhone: primary.mobile.trim(),
+ guestList: list.length ? list : [{ name: primary.name.trim(), mobile: primary.mobile.trim() }],
+ }
+}
+
+const validateGuests = () => {
+ const primary = guests.value[0]
+ if (!primary?.name?.trim()) {
+ uni.showToast({ title: "请填写主要入住人姓名", icon: "none" })
+ return false
+ }
+ if (!/^1\d{10}$/.test(primary.mobile || "")) {
+ uni.showToast({ title: "请填写正确的主要入住人手机号", icon: "none" })
+ return false
+ }
+ for (let i = 1; i < guests.value.length; i++) {
+ const g = guests.value[i]
+ const name = g.name?.trim() || ""
+ const mobile = g.mobile?.trim() || ""
+ if (!name && !mobile) {
+ uni.showToast({ title: `请完善入住人${i + 1},或删除该行`, icon: "none" })
+ return false
+ }
+ if (!name) {
+ uni.showToast({ title: `请填写入住人${i + 1}姓名`, icon: "none" })
+ return false
+ }
+ if (!/^1\d{10}$/.test(mobile)) {
+ uni.showToast({ title: `请填写入住人${i + 1}正确手机号`, icon: "none" })
+ return false
+ }
+ }
+ return true
+}
+
const checkOutPickerMin = computed(() => dayjs(form.checkIn).add(1, "day").format("YYYY-MM-DD"))
const checkOutPickerMax = computed(() => dayjs(form.checkIn).add(MAX_STAY_NIGHTS, "day").format("YYYY-MM-DD"))
@@ -363,8 +526,24 @@ const toPickerValue = (ymd: string) => `${ymd} 00:00:00`
const normalizeDate = (value: string) => dayjs(String(value || "").replace(/\//g, "-")).format("YYYY-MM-DD")
const roomFeeNum = computed(() => {
- const unit = Number(form.sellPrice) || 0
- return unit * nightCount.value * (form.rooms || 1)
+ const rooms = Math.max(1, Number(form.rooms) || 1)
+ // 优先按所选每晚日历卖价累加,再 × 间数
+ if (calendarLoaded.value && stayNightList.value.length) {
+ let nightSum = 0
+ let priced = 0
+ for (const ymd of stayNightList.value) {
+ const hit = bookableNightSet.value.get(ymd)
+ const p = hit ? Number(hit.sellPrice) : 0
+ if (p > 0) {
+ nightSum += p
+ priced++
+ } else {
+ nightSum += Number(form.sellPrice) || 0
+ }
+ }
+ if (priced > 0 || nightSum > 0) return nightSum * rooms
+ }
+ return (Number(form.sellPrice) || 0) * nightCount.value * rooms
})
const roomFee = computed(() => roomFeeNum.value.toFixed(2))
@@ -598,7 +777,19 @@ const loadCalendar = async () => {
}
/** 按已加载日历刷新单价:仅用日历行自身卖价/成本取均价;没有有效价时保留产品标价 */
+const clampRoomsByStock = () => {
+ if (!calendarLoaded.value) return
+ const cap = maxRooms.value
+ if (form.rooms > cap) {
+ form.rooms = cap
+ if (guests.value.length > cap) {
+ guests.value = guests.value.slice(0, cap)
+ }
+ }
+}
+
const refreshStayPriceFromCalendar = () => {
+ clampRoomsByStock()
if (!form.checkIn || !form.checkOut) return
const prices: number[] = []
stayNightList.value.forEach((ymd) => {
@@ -667,14 +858,7 @@ const openConfirmDialog = async () => {
}
const ok = await hooks.useCheckLogin()
if (!ok) return
- if (!form.contactName?.trim()) {
- uni.showToast({ title: "请填写入住人姓名", icon: "none" })
- return
- }
- if (!/^1\d{10}$/.test(form.contactPhone || "")) {
- uni.showToast({ title: "请填写正确手机号", icon: "none" })
- return
- }
+ if (!validateGuests()) return
const today = dayjs().format("YYYY-MM-DD")
if (form.checkIn < today) {
uni.showToast({ title: "入住日期不能早于今天", icon: "none" })
@@ -712,6 +896,7 @@ const submit = async () => {
try {
// 1) 无缓存待支付单时才创单;支付失败重试只走支付,避免连点刷单
if (!orderId) {
+ const guestPayload = buildGuestPayload()
const res: any = await hotelApi.createOrder({
hotelId: form.hotelId,
productCode: form.productCode,
@@ -719,8 +904,9 @@ const submit = async () => {
checkInDate: form.checkIn,
checkOutDate: form.checkOut,
count: form.rooms,
- contactName: form.contactName.trim(),
- contactPhone: form.contactPhone.trim(),
+ contactName: guestPayload.contactName,
+ contactPhone: guestPayload.contactPhone,
+ guestList: guestPayload.guestList,
totalAmount: totalAmount.value,
})
if (res?.code !== 0) {
@@ -841,11 +1027,15 @@ onLoad((opt: any) => {
form.productCode = opt?.productCode || ""
form.checkIn = opt?.checkIn || form.checkIn
form.checkOut = opt?.checkOut || form.checkOut
- form.rooms = Number(opt?.rooms || 1) || 1
+ form.rooms = Math.max(1, Math.min(MAX_ROOMS, Number(opt?.rooms || 1) || 1))
clampStayRange(false)
const u: any = userStore.userInfo || {}
- form.contactName = u?.sysUser?.name || u?.name || ""
- form.contactPhone = u?.sysUser?.phone || u?.phone || ""
+ guests.value = [
+ {
+ name: u?.sysUser?.name || u?.name || "",
+ mobile: u?.sysUser?.phone || u?.phone || "",
+ },
+ ]
layout()
fillHotelMeta().then(() => loadCalendar())
})
@@ -1227,9 +1417,172 @@ onLoad((opt: any) => {
align-items: center;
gap: 2px;
}
-.guest-sec,
+.room-stepper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.room-stepper__btn {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ border: 1px solid #d0d5dd;
+ background: #fff;
+ color: #344054;
+ font-size: 18px;
+ line-height: 26px;
+ text-align: center;
+}
+.room-stepper__btn:active {
+ background: #f2f4f7;
+ border-color: #98a2b3;
+}
+.room-stepper__btn.disabled {
+ opacity: 0.35;
+ pointer-events: none;
+}
+.room-stepper__num {
+ min-width: 24px;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #101828;
+}
+.room-stepper__hint {
+ margin-left: 2px;
+ font-size: 11px;
+ color: #98a2b3;
+}
+.guest-sec {
+ padding: 16px;
+}
.pay-sec {
margin: 0 12px 8px;
+ padding: 14px 12px 12px;
+ border-radius: 12px;
+ background: #fff;
+}
+.guest-sec__hd {
+ margin-bottom: 14px;
+}
+.guest-sec__title {
+ display: block;
+ font-size: 15px;
+ font-weight: 600;
+ color: #101828;
+ line-height: 1.3;
+}
+.guest-sec__sub {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+ color: #98a2b3;
+ line-height: 1.4;
+}
+.guest-block {
+ margin-bottom: 12px;
+ padding: 4px 0 2px;
+ border-radius: 12px;
+ background: #f8fafc;
+ overflow: hidden;
+}
+.guest-block__bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 12px 6px;
+}
+.guest-block__who {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+.guest-block__dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: #cbd5e1;
+ flex-shrink: 0;
+}
+.guest-block__dot.is-main {
+ background: #1677ff;
+}
+.guest-block__name {
+ font-size: 13px;
+ font-weight: 600;
+ color: #344054;
+}
+.guest-block__badge {
+ font-size: 10px;
+ line-height: 1;
+ padding: 3px 6px;
+ border-radius: 4px;
+ background: #eff4ff;
+ color: #1677ff;
+}
+.guest-block__del {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ font-size: 12px;
+ color: #98a2b3;
+ padding: 4px 0 4px 8px;
+}
+.guest-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin: 0 8px 8px;
+ padding: 0 12px;
+ min-height: 44px;
+ border-radius: 10px;
+ background: #fff;
+ border: 1px solid #e4e7ec;
+}
+.guest-row--last {
+ margin-bottom: 10px;
+}
+.guest-row__lab {
+ flex-shrink: 0;
+ width: 36px;
+ font-size: 14px;
+ color: #667085;
+}
+.guest-row__input {
+ flex: 1;
+ min-width: 0;
+ height: 44px;
+ font-size: 15px;
+ color: #101828;
+ background: transparent;
+}
+.guest-row__ph {
+ color: #c0c5ce;
+}
+.guest-add {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ margin-top: 2px;
+ min-height: 44px;
+ border-radius: 10px;
+ border: 1px dashed #b2d0ff;
+ background: #f7fbff;
+}
+.guest-add--active {
+ background: #eef5ff;
+ border-color: #1677ff;
+}
+.guest-add__txt {
+ font-size: 14px;
+ font-weight: 500;
+ color: #1677ff;
+}
+.guest-add__cap {
+ font-size: 12px;
+ color: #8fb3e8;
}
.pay-sec__tip {
padding: 24rpx 16rpx;
@@ -1248,24 +1601,6 @@ onLoad((opt: any) => {
color: #222;
margin-bottom: 12px;
}
-.guest-field {
- margin-bottom: 12px;
-}
-.guest-field--last {
- margin-bottom: 0;
-}
-.guest-field__label {
- display: block;
- font-size: 13px;
- color: #999;
- margin-bottom: 4px;
-}
-.guest-field__input {
- font-size: 15px;
- color: #222;
- padding: 8px 0;
- border-bottom: 1px solid #f0f0f0;
-}
.fee-row {
display: flex;
justify-content: space-between;
diff --git a/src/pages/hotel/detail.vue b/src/pages/hotel/detail.vue
index 542447d..4af21b9 100644
--- a/src/pages/hotel/detail.vue
+++ b/src/pages/hotel/detail.vue
@@ -42,28 +42,27 @@
{{ hotel.score }}
分
- 电话 {{ hotel.phone }}
-
-
-
-
- 酒店介绍
-
-
-
-
-
- 设施服务
-
-
-
- {{ f }}
+
+
+
+ 联系电话
+ {{ hotel.phone }}
+
+
+ 拨打
+
-
+
可选房型
@@ -84,6 +83,23 @@
预订
+
+
+
+ 酒店介绍
+
+
+
+
+
+ 设施服务
+
+
+
+ {{ f }}
+
+
+
@@ -106,6 +122,7 @@ import { onLoad } from "@dcloudio/uni-app"
import dayjs from "dayjs"
import { hotelApi } from "@@/api"
import { router } from "@@/index"
+import { useMakePhoneCall } from "@@/hooks"
import { filePath } from "@@/utils/file"
const STAR_MAP: Record = { 1: "民宿", 2: "经济", 3: "舒适", 4: "高档", 5: "五星" }
@@ -272,6 +289,10 @@ const layout = () => {
scrollHeight.value = `${sys.windowHeight || 667}px`
}
+const callHotel = () => {
+ useMakePhoneCall(hotel.value?.phone)
+}
+
const onBack = () => {
uni.navigateBack({ fail: () => router.jump("/pages/hotel/list", {}, 2) })
}
@@ -432,11 +453,53 @@ onLoad((opt: any) => {
font-size: 22rpx;
color: #1570ef;
}
-.ht-phone {
- margin-left: 20rpx;
+.ht-call-row {
+ margin-top: 20rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16rpx;
+ padding: 18rpx 4rpx 4rpx;
+ border-top: 1rpx solid #f1f5f9;
+}
+.ht-call-row--active .ht-call-row__num {
+ color: #1570ef;
+}
+.ht-call-row__left {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+ padding: 8rpx 0;
+}
+.ht-call-row__lab {
font-size: 22rpx;
color: #94a3b8;
}
+.ht-call-row__num {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #1570ef;
+ line-height: 1.2;
+ letter-spacing: 0.5rpx;
+}
+.ht-call-row__btn {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 14rpx 24rpx;
+ border-radius: 999rpx;
+ background: #1570ef;
+ box-shadow: 0 6rpx 16rpx rgba(21, 112, 239, 0.28);
+}
+.ht-call-row__btn-txt {
+ font-size: 26rpx;
+ font-weight: 600;
+ color: #fff;
+ line-height: 1;
+}
.ht-sec {
font-size: 30rpx;
font-weight: 700;
diff --git a/src/pages/hotel/list.vue b/src/pages/hotel/list.vue
index baa5aa6..713e6ae 100644
--- a/src/pages/hotel/list.vue
+++ b/src/pages/hotel/list.vue
@@ -269,10 +269,14 @@
scroll-y
class="ht-scroll"
:style="{ height: scrollHeight }"
+ :refresher-enabled="true"
+ :refresher-triggered="refreshing"
+ refresher-default-style="black"
+ @refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
- 加载中…
+ 加载中…
暂无符合条件的酒店
@@ -332,6 +336,7 @@ import { filePath } from "@@/utils/file"
const statusBarHeight = ref(20)
const scrollHeight = ref("100vh")
+const refreshing = ref(false)
const panel = ref<"" | "area" | "price" | "score" | "more">("")
const filterTabs = [
@@ -907,6 +912,18 @@ const fetchPage = async (reset = false) => {
const reload = () => fetchPage(true)
const loadMore = () => fetchPage(false)
+const onRefresh = async () => {
+ if (refreshing.value) return
+ refreshing.value = true
+ try {
+ // 下拉刷新允许打断「加载更多」等待态
+ state.loading = false
+ await fetchPage(true)
+ } finally {
+ refreshing.value = false
+ }
+}
+
const goDetail = (item: any) => {
router.jump("/pages/hotel/detail", {
id: String(item.id),
diff --git a/src/pages/hotel/order-detail.vue b/src/pages/hotel/order-detail.vue
index 8fb332a..731d710 100644
--- a/src/pages/hotel/order-detail.vue
+++ b/src/pages/hotel/order-detail.vue
@@ -107,7 +107,15 @@
酒店电话
- {{ order.phone }}
+
+ {{ order.phone }}
+
+
订单金额
@@ -145,7 +153,8 @@ import { reactive, ref, computed } from "vue"
import { onLoad, onShow, onHide, onUnload } from "@dcloudio/uni-app"
import dayjs from "dayjs"
import { hotelApi } from "@@/api"
-import { hooks, router } from "@@/index"
+import { router } from "@@/index"
+import { useMakePhoneCall } from "@@/hooks"
import { filePath } from "@@/utils/file"
import { AggregationPayTool } from "@@/utils/pay-tool"
import { waitHotelOrderAfterPay } from "@@/utils/hotel-order"
@@ -399,8 +408,7 @@ const copyNo = () => {
}
const callHotel = () => {
- if (!order.phone) return
- uni.makePhoneCall({ phoneNumber: String(order.phone).replace(/\s/g, "") })
+ useMakePhoneCall(order.phone)
}
const goHotel = () => {
@@ -721,6 +729,26 @@ onUnload(() => stopCountdown())
.kv__v--link {
color: #1677ff;
}
+.ht-call {
+ display: inline-flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 8rpx 16rpx;
+ border-radius: 999rpx;
+ background: #eff6ff;
+ border: 1rpx solid #93c5fd;
+ max-width: 70%;
+}
+.ht-call--active {
+ opacity: 0.72;
+ background: #dbeafe;
+}
+.ht-call__text {
+ font-size: 26rpx;
+ color: #1570ef;
+ font-weight: 600;
+ line-height: 1;
+}
.kv__v--fail {
color: #cf1322;
text-align: right;
diff --git a/src/sheep/components/hotel/hotel-order-panel.vue b/src/sheep/components/hotel/hotel-order-panel.vue
index f66c62d..13859e4 100644
--- a/src/sheep/components/hotel/hotel-order-panel.vue
+++ b/src/sheep/components/hotel/hotel-order-panel.vue
@@ -18,10 +18,14 @@
scroll-y
class="ht-scroll"
:style="{ height: scrollHeight }"
+ :refresher-enabled="true"
+ :refresher-triggered="refreshing"
+ refresher-default-style="black"
+ @refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
-
+
加载中…
@@ -138,6 +142,7 @@ const tabs = [
]
const scrollHeight = ref("70vh")
+const refreshing = ref(false)
const remainMap = reactive>({})
let countdownTimer: ReturnType | null = null
@@ -341,6 +346,17 @@ const fetchPage = async (reset = false) => {
const reload = () => fetchPage(true)
const loadMore = () => fetchPage(false)
+const onRefresh = async () => {
+ if (refreshing.value) return
+ refreshing.value = true
+ try {
+ state.loading = false
+ await fetchPage(true)
+ } finally {
+ refreshing.value = false
+ }
+}
+
const switchTab = (v: string) => {
if (state.status === v) return
state.status = v
diff --git a/src/sheep/hooks/use-phone.ts b/src/sheep/hooks/use-phone.ts
index e4776c3..249cfb4 100644
--- a/src/sheep/hooks/use-phone.ts
+++ b/src/sheep/hooks/use-phone.ts
@@ -1,4 +1,47 @@
-export const useMakePhoneCall = (str: string | undefined) => {
- if (str == undefined) return;
- uni.makePhoneCall({ phoneNumber: str });
-};
+/**
+ * 拨打电话:打开系统拨号盘填入号码(不直接呼叫,避免无 CALL_PHONE 权限时静默失败)。
+ * 与 service-text 客服电话同一套入口。
+ */
+export const useMakePhoneCall = (raw: string | undefined | null) => {
+ const phoneNumber = String(raw || "")
+ .replace(/[^\d+]/g, "")
+ .trim()
+ if (!phoneNumber) {
+ uni.showToast({ title: "暂无联系电话", icon: "none" })
+ return
+ }
+
+ const plusObj = (typeof globalThis !== "undefined" && (globalThis as any).plus) || null
+
+ // App:优先 tel: 打开拨号界面(不直接播出)
+ if (plusObj?.runtime?.openURL) {
+ try {
+ plusObj.runtime.openURL(`tel:${phoneNumber}`)
+ return
+ } catch (e) {
+ console.warn("plus.runtime.openURL tel failed", e)
+ }
+ }
+ // 带确认框的 dial(true=弹确认,不强制直接呼叫权限)
+ if (plusObj?.device?.dial) {
+ try {
+ plusObj.device.dial(phoneNumber, true)
+ return
+ } catch (e) {
+ console.warn("plus.device.dial failed", e)
+ }
+ }
+
+ uni.makePhoneCall({
+ phoneNumber,
+ fail: (err) => {
+ const msg = String((err as any)?.errMsg || "")
+ if (/cancel|取消/i.test(msg)) return
+ console.warn("makePhoneCall fail", err)
+ uni.showToast({
+ title: msg.replace(/^makePhoneCall:fail\s*/i, "") || "无法打开拨号盘",
+ icon: "none",
+ })
+ },
+ })
+}