更新 2026-04-18 21:39
This commit is contained in:
parent
07ad42ea3b
commit
a6bd72f0d4
25
package-lock.json
generated
25
package-lock.json
generated
@ -3260,7 +3260,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3274,7 +3273,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3288,7 +3286,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3302,7 +3299,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3316,7 +3312,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3330,7 +3325,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3344,7 +3338,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3358,7 +3351,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3372,7 +3364,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3386,7 +3377,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3400,7 +3390,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3414,7 +3403,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3428,7 +3416,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3442,7 +3429,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3456,7 +3442,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3470,7 +3455,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3484,7 +3468,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3498,7 +3481,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3512,7 +3494,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3526,7 +3507,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3540,7 +3520,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3554,7 +3533,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3568,7 +3546,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3582,7 +3559,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -3596,7 +3572,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
28
src/App.vue
28
src/App.vue
@ -9,6 +9,23 @@ const isPublicRoute = (route) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLoginRedirectPath() {
|
||||||
|
try {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const cur = pages[pages.length - 1]
|
||||||
|
if (!cur) return ''
|
||||||
|
let path = cur.route || ''
|
||||||
|
if (!path.startsWith('/')) path = '/' + path
|
||||||
|
const opts = cur.options || {}
|
||||||
|
const keys = Object.keys(opts)
|
||||||
|
if (keys.length === 0) return path
|
||||||
|
const qs = keys.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(opts[k])}`).join('&')
|
||||||
|
return `${path}?${qs}`
|
||||||
|
} catch (_) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleAuthGuard() {
|
function scheduleAuthGuard() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
@ -18,7 +35,16 @@ function scheduleAuthGuard() {
|
|||||||
const cur = pages[pages.length - 1]
|
const cur = pages[pages.length - 1]
|
||||||
const route = cur && cur.route ? cur.route : ''
|
const route = cur && cur.route ? cur.route : ''
|
||||||
if (isPublicRoute(route)) return
|
if (isPublicRoute(route)) return
|
||||||
uni.reLaunch({ url: '/pages/login/Login' })
|
const redirect = buildLoginRedirectPath()
|
||||||
|
const safe =
|
||||||
|
redirect &&
|
||||||
|
redirect.startsWith('/pages/') &&
|
||||||
|
!redirect.includes('..') &&
|
||||||
|
!redirect.includes('//')
|
||||||
|
const loginUrl = safe
|
||||||
|
? `/pages/login/Login?redirect=${encodeURIComponent(redirect)}`
|
||||||
|
: '/pages/login/Login'
|
||||||
|
uni.reLaunch({ url: loginUrl })
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
uni.reLaunch({ url: '/pages/login/Login' })
|
uni.reLaunch({ url: '/pages/login/Login' })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,11 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else-if="empty" class="aps-panel aps-empty">
|
<view v-else-if="empty" class="aps-panel aps-empty">
|
||||||
<text class="aps-emoji">{{ emptyEmoji }}</text>
|
<slot name="empty">
|
||||||
<text class="aps-title">{{ emptyTitle }}</text>
|
<text class="aps-emoji">{{ emptyEmoji }}</text>
|
||||||
<text v-if="emptyHint" class="aps-hint">{{ emptyHint }}</text>
|
<text class="aps-title">{{ emptyTitle }}</text>
|
||||||
|
<text v-if="emptyHint" class="aps-hint">{{ emptyHint }}</text>
|
||||||
|
</slot>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
|
|||||||
@ -12,7 +12,19 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="c-form-card">
|
<view class="c-form-card">
|
||||||
<view class="c-field">
|
<view v-if="useCompactStoreRow" class="c-field">
|
||||||
|
<text class="app-form-label">预约门店</text>
|
||||||
|
<view class="c-store-summary-row">
|
||||||
|
<text class="c-store-summary-name">{{ currentStoreDisplayName }}</text>
|
||||||
|
<text
|
||||||
|
v-if="orderedStores.length > 1 && storeLockedCompact"
|
||||||
|
class="c-store-change-link"
|
||||||
|
@click="expandStorePicker"
|
||||||
|
>更换门店</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="storeHintLine" class="c-store-hint">{{ storeHintLine }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="c-field">
|
||||||
<text class="app-form-label">预约门店</text>
|
<text class="app-form-label">预约门店</text>
|
||||||
<picker
|
<picker
|
||||||
mode="selector"
|
mode="selector"
|
||||||
@ -27,6 +39,11 @@
|
|||||||
<text class="app-form-arrow">›</text>
|
<text class="app-form-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
</picker>
|
</picker>
|
||||||
|
<text
|
||||||
|
v-if="showCollapseStoreLink"
|
||||||
|
class="c-store-collapse"
|
||||||
|
@click="onCollapseStorePicker"
|
||||||
|
>收起</text>
|
||||||
<text v-if="storeHintLine" class="c-store-hint">{{ storeHintLine }}</text>
|
<text v-if="storeHintLine" class="c-store-hint">{{ storeHintLine }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="c-field">
|
<view class="c-field">
|
||||||
@ -85,6 +102,11 @@
|
|||||||
</picker>
|
</picker>
|
||||||
</view>
|
</view>
|
||||||
<text class="c-store-hint">{{ bookingWindowHint }}</text>
|
<text class="c-store-hint">{{ bookingWindowHint }}</text>
|
||||||
|
<button
|
||||||
|
v-if="showTryTomorrow"
|
||||||
|
class="app-btn-sm app-btn-soft c-try-tomorrow"
|
||||||
|
@click="tryPickTomorrow"
|
||||||
|
>试选明天</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="c-field">
|
<view class="c-field">
|
||||||
<text class="app-form-label">备注(可选)</text>
|
<text class="app-form-label">备注(可选)</text>
|
||||||
@ -97,6 +119,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import {
|
import {
|
||||||
createAppointment,
|
createAppointment,
|
||||||
getServiceTypeList,
|
getServiceTypeList,
|
||||||
@ -116,10 +139,57 @@ const LAST_SUBMITTED_PET_KEY = `petstore_last_submitted_pet_${userInfo.id || 'gu
|
|||||||
|
|
||||||
const BOOKING_STORE_ID_KEY = 'petstore_booking_store_id'
|
const BOOKING_STORE_ID_KEY = 'petstore_booking_store_id'
|
||||||
|
|
||||||
|
const lastServiceStorageKey = (storeId) =>
|
||||||
|
`petstore_last_service_${userInfo.id || 'guest'}_${storeId}`
|
||||||
|
|
||||||
|
/** 扫码/深链带入的门店 id,用于预填并支持折叠展示 */
|
||||||
|
const routeStoreId = ref('')
|
||||||
|
const storeExpanded = ref(false)
|
||||||
|
|
||||||
const orderedStores = ref([])
|
const orderedStores = ref([])
|
||||||
const selectedStoreIndex = ref(0)
|
const selectedStoreIndex = ref(0)
|
||||||
const hasUserLocation = ref(false)
|
const hasUserLocation = ref(false)
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options?.storeId != null && String(options.storeId).trim() !== '') {
|
||||||
|
routeStoreId.value = String(options.storeId).trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const storeLockedCompact = computed(() => {
|
||||||
|
if (!routeStoreId.value || !orderedStores.value.length) return false
|
||||||
|
const cur = orderedStores.value[selectedStoreIndex.value]
|
||||||
|
return !!(cur && String(cur.id) === String(routeStoreId.value))
|
||||||
|
})
|
||||||
|
|
||||||
|
const useCompactStoreRow = computed(() => {
|
||||||
|
if (!orderedStores.value.length) return false
|
||||||
|
if (orderedStores.value.length === 1) return true
|
||||||
|
return storeLockedCompact.value && !storeExpanded.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const showCollapseStoreLink = computed(
|
||||||
|
() =>
|
||||||
|
!!routeStoreId.value &&
|
||||||
|
orderedStores.value.length > 1 &&
|
||||||
|
storeLockedCompact.value &&
|
||||||
|
storeExpanded.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentStoreDisplayName = computed(() => {
|
||||||
|
const row = orderedStores.value[selectedStoreIndex.value]
|
||||||
|
if (!row) return orderedStores.value.length ? '请选择' : '暂无可选门店'
|
||||||
|
return row.pickerLabel || row.name || '—'
|
||||||
|
})
|
||||||
|
|
||||||
|
const expandStorePicker = () => {
|
||||||
|
storeExpanded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCollapseStorePicker = () => {
|
||||||
|
storeExpanded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const navSafeStyle = (() => {
|
const navSafeStyle = (() => {
|
||||||
const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
|
const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
|
||||||
let navHeight = statusBarHeight + 44
|
let navHeight = statusBarHeight + 44
|
||||||
@ -215,7 +285,14 @@ const initStoresAndLocation = async () => {
|
|||||||
const savedId = uni.getStorageSync(BOOKING_STORE_ID_KEY)
|
const savedId = uni.getStorageSync(BOOKING_STORE_ID_KEY)
|
||||||
const sid = storeInfo?.id ?? userInfo?.storeId
|
const sid = storeInfo?.id ?? userInfo?.storeId
|
||||||
let pickIdx = 0
|
let pickIdx = 0
|
||||||
if (savedId && orderedStores.value.some((x) => String(x.id) === String(savedId))) {
|
const qrIdx =
|
||||||
|
routeStoreId.value && orderedStores.value.length
|
||||||
|
? orderedStores.value.findIndex((x) => String(x.id) === String(routeStoreId.value))
|
||||||
|
: -1
|
||||||
|
if (qrIdx >= 0) {
|
||||||
|
pickIdx = qrIdx
|
||||||
|
storeExpanded.value = false
|
||||||
|
} else if (savedId && orderedStores.value.some((x) => String(x.id) === String(savedId))) {
|
||||||
pickIdx = orderedStores.value.findIndex((x) => String(x.id) === String(savedId))
|
pickIdx = orderedStores.value.findIndex((x) => String(x.id) === String(savedId))
|
||||||
} else if (sid && orderedStores.value.some((x) => String(x.id) === String(sid))) {
|
} else if (sid && orderedStores.value.some((x) => String(x.id) === String(sid))) {
|
||||||
pickIdx = orderedStores.value.findIndex((x) => String(x.id) === String(sid))
|
pickIdx = orderedStores.value.findIndex((x) => String(x.id) === String(sid))
|
||||||
@ -313,6 +390,25 @@ const selectedSlotRangeLabel = computed(() =>
|
|||||||
appointmentTime.value ? formatHalfHourSlotRange(appointmentTime.value) : ''
|
appointmentTime.value ? formatHalfHourSlotRange(appointmentTime.value) : ''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const showTryTomorrow = computed(
|
||||||
|
() =>
|
||||||
|
!loadingSlots.value &&
|
||||||
|
!!selectedDateStr.value &&
|
||||||
|
availableTimes.value.length === 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const tryPickTomorrow = () => {
|
||||||
|
const cur = selectedDateStr.value
|
||||||
|
if (!cur) return
|
||||||
|
const parts = cur.split('-').map((x) => parseInt(x, 10))
|
||||||
|
if (parts.length !== 3 || parts.some((n) => Number.isNaN(n))) return
|
||||||
|
const d = new Date(parts[0], parts[1] - 1, parts[2])
|
||||||
|
if (Number.isNaN(d.getTime())) return
|
||||||
|
d.setDate(d.getDate() + 1)
|
||||||
|
selectedDateStr.value = ymdLocal(d)
|
||||||
|
loadAvailableSlots()
|
||||||
|
}
|
||||||
|
|
||||||
const loadAvailableSlots = async () => {
|
const loadAvailableSlots = async () => {
|
||||||
const row = orderedStores.value[selectedStoreIndex.value]
|
const row = orderedStores.value[selectedStoreIndex.value]
|
||||||
const storeId = row?.id
|
const storeId = row?.id
|
||||||
@ -390,6 +486,14 @@ const loadServiceTypesForSelected = async () => {
|
|||||||
const res = await getServiceTypeList(sid ?? undefined)
|
const res = await getServiceTypeList(sid ?? undefined)
|
||||||
if (res.code === 200 && Array.isArray(res.data)) {
|
if (res.code === 200 && Array.isArray(res.data)) {
|
||||||
serviceTypes.value = res.data.map((s) => ({ label: s.name, value: s.name }))
|
serviceTypes.value = res.data.map((s) => ({ label: s.name, value: s.name }))
|
||||||
|
if (sid) {
|
||||||
|
try {
|
||||||
|
const last = uni.getStorageSync(lastServiceStorageKey(sid))
|
||||||
|
if (last && serviceTypes.value.some((s) => s.value === last)) {
|
||||||
|
newAppt.value.serviceType = last
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,6 +577,9 @@ const confirmNewAppt = async () => {
|
|||||||
creatingAppt.value = false
|
creatingAppt.value = false
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
rememberLastSubmittedPet(ensuredPet)
|
rememberLastSubmittedPet(ensuredPet)
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(lastServiceStorageKey(storeId), a.serviceType)
|
||||||
|
} catch (_) {}
|
||||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||||
setTimeout(() => uni.navigateBack(), 600)
|
setTimeout(() => uni.navigateBack(), 600)
|
||||||
} else {
|
} else {
|
||||||
@ -573,4 +680,38 @@ onMounted(async () => {
|
|||||||
color: #666660;
|
color: #666660;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
.c-store-summary-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #f5f5f2;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.c-store-summary-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
.c-store-change-link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2db96d;
|
||||||
|
padding: 6px 0 6px 8px;
|
||||||
|
}
|
||||||
|
.c-store-collapse {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2db96d;
|
||||||
|
}
|
||||||
|
.c-try-tomorrow {
|
||||||
|
margin-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -8,7 +8,10 @@
|
|||||||
|
|
||||||
<view class="c-hero">
|
<view class="c-hero">
|
||||||
<text class="c-hero-title">服务预约</text>
|
<text class="c-hero-title">服务预约</text>
|
||||||
<text class="c-hero-sub">查看预约进度,点击右下角加号新建预约</text>
|
<text class="c-hero-sub">一键预约洗美,到店与进度随时可查</text>
|
||||||
|
<button class="app-btn-main app-btn-primary c-hero-cta" hover-class="c-hero-cta--hover" @click="openCustCreateAppt">
|
||||||
|
预约服务
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="home-dash">
|
<view class="home-dash">
|
||||||
@ -64,6 +67,12 @@
|
|||||||
:empty-hint="listEmptyHint"
|
:empty-hint="listEmptyHint"
|
||||||
@retry="fetchAppointments"
|
@retry="fetchAppointments"
|
||||||
>
|
>
|
||||||
|
<template v-if="custEmptyProminent" #empty>
|
||||||
|
<text class="c-empty-emoji">📅</text>
|
||||||
|
<text class="c-empty-title">还没有预约</text>
|
||||||
|
<text class="c-empty-hint">创建预约后,待开始与进行中的单会出现在下方列表</text>
|
||||||
|
<button class="app-btn-main app-btn-primary c-empty-cta" @click="openCustCreateAppt">立即预约</button>
|
||||||
|
</template>
|
||||||
<view class="order-list">
|
<view class="order-list">
|
||||||
<view
|
<view
|
||||||
v-for="item in ordersForList"
|
v-for="item in ordersForList"
|
||||||
@ -86,7 +95,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</AppPageState>
|
</AppPageState>
|
||||||
|
|
||||||
<view class="fab-appt" @click="openCustCreateAppt" hover-class="fab-appt--hover">
|
<view class="fab-appt fab-appt--cust-secondary" @click="openCustCreateAppt" hover-class="fab-appt--hover">
|
||||||
<text class="fab-appt-icon">+</text>
|
<text class="fab-appt-icon">+</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -437,17 +446,26 @@ const showListPlaceholder = computed(
|
|||||||
() => !appointmentsLoading.value && !appointmentsError.value && ordersForList.value.length === 0
|
() => !appointmentsLoading.value && !appointmentsError.value && ordersForList.value.length === 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 宠主 Tab 列表为空:用自定义空态突出主按钮(与首屏 CTA 一致) */
|
||||||
|
const custEmptyProminent = computed(
|
||||||
|
() => userInfo.role === 'customer' && listMode.value === 'tab' && showListPlaceholder.value
|
||||||
|
)
|
||||||
|
|
||||||
const listEmptyTitle = computed(() => {
|
const listEmptyTitle = computed(() => {
|
||||||
if (listMode.value === 'today') return '今日暂无预约'
|
if (listMode.value === 'today') return '今日暂无预约'
|
||||||
if (listMode.value === 'week') return '本周暂无已完成'
|
if (listMode.value === 'week') return '本周暂无已完成'
|
||||||
return userInfo.role === 'customer' ? '暂无预约' : '暂无数据'
|
return userInfo.role === 'customer' ? '暂无预约' : '暂无数据'
|
||||||
})
|
})
|
||||||
|
|
||||||
const listEmptyHint = computed(() =>
|
const listEmptyHint = computed(() => {
|
||||||
listMode.value === 'today' || listMode.value === 'week'
|
if (listMode.value === 'today' || listMode.value === 'week') {
|
||||||
? '可返回列表切换日期或 Tab'
|
return '可返回列表切换日期或 Tab'
|
||||||
: '切换上方状态或新建预约'
|
}
|
||||||
)
|
if (userInfo.role === 'customer') {
|
||||||
|
return '切换上方 Tab 查看不同状态,或新建预约'
|
||||||
|
}
|
||||||
|
return '切换上方状态或新建预约'
|
||||||
|
})
|
||||||
|
|
||||||
const loadStaffAvailableSlots = async () => {
|
const loadStaffAvailableSlots = async () => {
|
||||||
if (!storeInfo?.id) {
|
if (!storeInfo?.id) {
|
||||||
@ -733,6 +751,51 @@ onShow(() => {
|
|||||||
color: #999993;
|
color: #999993;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
.c-hero-cta {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
.c-hero-cta--hover {
|
||||||
|
opacity: 0.94;
|
||||||
|
}
|
||||||
|
.c-empty-emoji {
|
||||||
|
display: block;
|
||||||
|
font-size: 40px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.c-empty-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.c-empty-hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999993;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
.c-empty-cta {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
margin: 24px auto 0;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
}
|
||||||
|
.fab-appt--cust-secondary {
|
||||||
|
background: #e8f8ef;
|
||||||
|
box-shadow: 0 4px 14px rgba(45, 185, 109, 0.22);
|
||||||
|
}
|
||||||
|
.fab-appt--cust-secondary .fab-appt-icon {
|
||||||
|
color: #2db96d;
|
||||||
|
}
|
||||||
.c-form-card {
|
.c-form-card {
|
||||||
margin: 0 16px;
|
margin: 0 16px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|||||||
@ -154,6 +154,38 @@ const safeLine = (s, fallback = '—') => {
|
|||||||
|
|
||||||
const isHttpOk = (res) => res && (res.code === 200 || res.code === '200')
|
const isHttpOk = (res) => res && (res.code === 200 || res.code === '200')
|
||||||
|
|
||||||
|
function parseRedirectParam(raw) {
|
||||||
|
if (raw == null || raw === '') return ''
|
||||||
|
let s = String(raw)
|
||||||
|
try {
|
||||||
|
s = decodeURIComponent(s)
|
||||||
|
if (/%[0-9A-Fa-f]{2}/.test(s)) s = decodeURIComponent(s)
|
||||||
|
} catch (_) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafeLoginRedirect(path) {
|
||||||
|
return (
|
||||||
|
typeof path === 'string' &&
|
||||||
|
path.startsWith('/pages/') &&
|
||||||
|
!path.includes('..') &&
|
||||||
|
!path.includes('//')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectAfterLogin = ref('')
|
||||||
|
|
||||||
|
function navigateAfterLogin() {
|
||||||
|
const r = redirectAfterLogin.value
|
||||||
|
if (isSafeLoginRedirect(r)) {
|
||||||
|
uni.reLaunch({ url: r })
|
||||||
|
} else {
|
||||||
|
openAppPage('home')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
view.value = 'login'
|
view.value = 'login'
|
||||||
const raw = options && (options.phone ?? options.prefillPhone)
|
const raw = options && (options.phone ?? options.prefillPhone)
|
||||||
@ -161,6 +193,8 @@ onLoad((options) => {
|
|||||||
if (/^1[3-9]\d{9}$/.test(p)) {
|
if (/^1[3-9]\d{9}$/.test(p)) {
|
||||||
loginForm.phone = p
|
loginForm.phone = p
|
||||||
}
|
}
|
||||||
|
const red = options && options.redirect != null ? options.redirect : ''
|
||||||
|
redirectAfterLogin.value = parseRedirectParam(red)
|
||||||
})
|
})
|
||||||
|
|
||||||
const backToLoginForm = () => { view.value = 'login' }
|
const backToLoginForm = () => { view.value = 'login' }
|
||||||
@ -235,7 +269,7 @@ const handleLogin = async () => {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
setUserSession(res.data.user)
|
setUserSession(res.data.user)
|
||||||
setStoreSession(res.data.store)
|
setStoreSession(res.data.store)
|
||||||
openAppPage('home')
|
navigateAfterLogin()
|
||||||
} else {
|
} else {
|
||||||
showToast(res.message || '登录失败')
|
showToast(res.message || '登录失败')
|
||||||
}
|
}
|
||||||
@ -279,7 +313,7 @@ const onWxGetPhoneNumber = async (e) => {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
setUserSession(res.data.user)
|
setUserSession(res.data.user)
|
||||||
setStoreSession(res.data.store)
|
setStoreSession(res.data.store)
|
||||||
openAppPage('home')
|
navigateAfterLogin()
|
||||||
} else {
|
} else {
|
||||||
showToast(res.message || '登录失败')
|
showToast(res.message || '登录失败')
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user