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", + }) + }, + }) +}