解决登录显示、首页显示bug

This commit is contained in:
2026-05-19 11:06:46 +08:00
parent 2f7e097e6c
commit 00a859c551
181 changed files with 55329 additions and 998 deletions

View File

@@ -1,8 +1,17 @@
<template>
<ServicePageScaffold title="提交服务申请" fallback-url="/pages/mall/consumer/home-service/index">
<ServicePanel title="提交服务申请" subtitle="先使用 mock 数据模拟申请受理流程。">
<view class="form-item">
<text class="label">选择服务</text>
<view class="apply-page">
<ServicePageScaffold title="提交服务申请" fallback-url="/pages/mall/consumer/home-service/index">
<view class="summary-card">
<text class="summary-title">快速预约上门服务</text>
<text class="summary-desc">保留现有申请逻辑,把旧入口调整为预约下单流程页体验。</text>
<view class="summary-tips-row">
<text class="summary-tip">平台认证</text>
<text class="summary-tip">可预约</text>
<text class="summary-tip">服务保障</text>
</view>
</view>
<ServicePanel title="Step1 选择服务" subtitle="先确认预约的居家服务项目。">
<view class="choice-wrap">
<view
v-for="item in services"
@@ -11,58 +20,109 @@
:class="selectedServiceId == item.id ? 'choice-active' : ''"
@click="selectService(item.id, item.name)"
>
<text class="choice-title">{{ item.name }}</text>
<text class="choice-desc">{{ item.durationText }} · ¥{{ item.price }}</text>
<view class="choice-header-row">
<text class="choice-title">{{ item.name }}</text>
<text class="choice-price">¥{{ item.price }}</text>
</view>
<text class="choice-desc">{{ item.durationText }} · {{ item.summary }}</text>
<text class="choice-suitable">适用对象:{{ item.suitableFor }}</text>
</view>
</view>
</view>
</ServicePanel>
<view class="form-item">
<text class="label">申请人</text>
<input v-model="form.applicantName" class="input" placeholder="请输入申请人姓名" />
</view>
<view class="form-item">
<text class="label">服务对象</text>
<input v-model="form.elderName" class="input" placeholder="请输入老人姓名" />
</view>
<view class="form-item">
<text class="label">年龄</text>
<input v-model="ageText" class="input" type="number" placeholder="请输入老人年龄" />
</view>
<view class="form-item">
<text class="label">联系电话</text>
<input v-model="form.phone" class="input" type="number" placeholder="请输入联系电话" />
</view>
<view class="form-item">
<text class="label">服务地址</text>
<textarea v-model="form.address" class="textarea" placeholder="请输入详细上门地址"></textarea>
</view>
<view class="form-item">
<text class="label">期望时间</text>
<input v-model="form.preferredTime" class="input" placeholder="例如 2026-05-14 上午" />
</view>
<view class="form-item">
<text class="label">需求说明</text>
<textarea v-model="form.demandSummary" class="textarea" placeholder="简要描述照护需求、病情重点、注意事项"></textarea>
</view>
<ServicePanel title="Step2 服务地址" subtitle="保持原有地址字段,先用预约卡形式承载。">
<view class="form-item">
<text class="label">服务地址</text>
<textarea v-model="form.address" class="textarea" placeholder="请输入详细上门地址"></textarea>
</view>
</ServicePanel>
<ServicePanel title="Step3 上门时间" subtitle="可直接选择推荐时段,也支持手动输入。">
<scroll-view class="booking-day-scroll" direction="horizontal" :show-scrollbar="false">
<view class="booking-day-row">
<view
v-for="item in bookingDays"
:key="item.id"
:class="['booking-day-card', selectedDayId == item.id ? 'booking-day-card-active' : '']"
@click="selectDay(item.id)"
>
<text :class="['booking-day-label', selectedDayId == item.id ? 'booking-day-label-active' : '']">{{ item.label }}</text>
<text :class="['booking-day-date', selectedDayId == item.id ? 'booking-day-date-active' : '']">{{ item.dateText }}</text>
</view>
</view>
</scroll-view>
<view class="booking-slot-grid">
<view
v-for="item in bookingSlots"
:key="item.id"
:class="['booking-slot-card', selectedSlotId == item.id ? 'booking-slot-card-active' : '', item.available ? '' : 'booking-slot-card-disabled']"
@click="selectSlot(item.id, item.available)"
>
<text :class="['booking-slot-label', selectedSlotId == item.id ? 'booking-slot-label-active' : '']">{{ item.label }}</text>
</view>
</view>
<view class="form-item form-item-last">
<text class="label">期望时间</text>
<input v-model="form.preferredTime" class="input" placeholder="例如 2026-05-14 上午" />
</view>
</ServicePanel>
<ServicePanel title="Step4 联系人信息" subtitle="保留现有字段,不改提交结构。">
<view class="form-item">
<text class="label">申请人</text>
<input v-model="form.applicantName" class="input" placeholder="请输入申请人姓名" />
</view>
<view class="form-item">
<text class="label">服务对象</text>
<input v-model="form.elderName" class="input" placeholder="请输入老人姓名" />
</view>
<view class="form-item">
<text class="label">年龄</text>
<input v-model="ageText" class="input" type="number" placeholder="请输入老人年龄" />
</view>
<view class="form-item">
<text class="label">联系电话</text>
<input v-model="form.phone" class="input" type="number" placeholder="请输入联系电话" />
</view>
<view class="form-item form-item-last">
<text class="label">需求说明</text>
<textarea v-model="form.demandSummary" class="textarea" placeholder="简要描述照护需求、病情重点、注意事项"></textarea>
</view>
</ServicePanel>
<view class="page-bottom-space"></view>
</ServicePageScaffold>
<view class="bottom-bar">
<view>
<text class="bottom-bar-label">预计金额</text>
<view class="bottom-bar-price-row">
<text class="bottom-bar-price-prefix">¥</text>
<text class="bottom-bar-price">{{ selectedPrice }}</text>
<text class="bottom-bar-price-unit">起</text>
</view>
</view>
<view class="submit-btn" @click="submitApplication">提交申请</view>
</ServicePanel>
</ServicePageScaffold>
</view>
</view>
</template>
<script setup lang="uts">
import { reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import { createHomeServiceApplication, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
import { HomeServiceApplicationDraftType, HomeServiceCatalogType } from '@/types/home-service.uts'
import { BookingDayOptionType, BookingTimeSlotType, getBookingDayOptions, getBookingTimeSlots } from '@/utils/homeServiceUiMock.uts'
const services = ref<Array<HomeServiceCatalogType>>([])
const selectedServiceId = ref('svc-001')
const selectedServiceName = ref('基础上门护理')
const ageText = ref('78')
const bookingDays = ref<Array<BookingDayOptionType>>([])
const bookingSlots = ref<Array<BookingTimeSlotType>>([])
const selectedDayId = ref('day-1')
const selectedSlotId = ref('slot-1')
const form = reactive({
serviceId: 'svc-001',
@@ -76,17 +136,61 @@ const form = reactive({
demandSummary: '老人需要基础照护、血压监测和跌倒风险提醒。'
} as HomeServiceApplicationDraftType)
const selectedPrice = computed((): string => {
for (let i = 0; i < services.value.length; i++) {
if (services.value[i].id == selectedServiceId.value) {
return services.value[i].price.toString()
}
}
return '0'
})
async function loadData() {
bookingDays.value = getBookingDayOptions()
bookingSlots.value = getBookingTimeSlots()
services.value = await fetchHomeServiceCatalog()
}
function selectService(serviceId: string, serviceName: string) {
selectedServiceId.value = serviceId
selectedServiceName.value = serviceName
form.serviceId = serviceId
form.serviceName = serviceName
}
function syncPreferredTime() {
let selectedDay = ''
for (let i = 0; i < bookingDays.value.length; i++) {
if (bookingDays.value[i].id == selectedDayId.value) {
selectedDay = bookingDays.value[i].label + ' ' + bookingDays.value[i].dateText
break
}
}
let selectedSlot = ''
for (let i = 0; i < bookingSlots.value.length; i++) {
if (bookingSlots.value[i].id == selectedSlotId.value) {
selectedSlot = bookingSlots.value[i].label
break
}
}
if (selectedDay != '' && selectedSlot != '') {
form.preferredTime = selectedDay + ' ' + selectedSlot
}
}
function selectDay(dayId: string) {
selectedDayId.value = dayId
syncPreferredTime()
}
function selectSlot(slotId: string, available: boolean) {
if (!available) {
uni.showToast({ title: '该时段暂不可约', icon: 'none' })
return
}
selectedSlotId.value = slotId
syncPreferredTime()
}
async function submitApplication() {
if (form.applicantName == '' || form.elderName == '' || form.phone == '' || form.address == '' || form.preferredTime == '') {
uni.showToast({ title: '请补全申请信息', icon: 'none' })
@@ -106,10 +210,66 @@ onLoad(() => {
</script>
<style scoped>
.apply-page {
background: #f4f8fb;
}
.summary-card {
background: linear-gradient(180deg, #eafbf7 0%, #eff6ff 100%);
border-radius: 32rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 12rpx 24rpx rgba(15, 118, 110, 0.08);
}
.summary-title {
font-size: 38rpx;
font-weight: 700;
color: #16324f;
}
.summary-desc,
.choice-desc,
.choice-suitable,
.bottom-bar-label {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #64748b;
}
.summary-tips-row,
.choice-header-row,
.bottom-bar,
.bottom-bar-price-row,
.booking-day-row {
flex-direction: row;
align-items: center;
}
.summary-tips-row {
flex-wrap: wrap;
margin-top: 20rpx;
}
.summary-tip {
padding: 10rpx 16rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.78);
font-size: 22rpx;
color: #476072;
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-item-last {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
font-weight: 700;
@@ -138,13 +298,13 @@ onLoad(() => {
}
.choice-wrap {
gap: 16rpx;
margin-top: 4rpx;
}
.choice-card {
padding: 22rpx;
background: #f8fbfc;
border-radius: 18rpx;
border-radius: 24rpx;
margin-bottom: 16rpx;
border-width: 2rpx;
border-style: solid;
@@ -156,25 +316,132 @@ onLoad(() => {
background: #effcf8;
}
.choice-header-row {
justify-content: space-between;
}
.choice-title {
font-size: 30rpx;
font-weight: 700;
color: #16324f;
}
.choice-price,
.bottom-bar-price-prefix,
.bottom-bar-price {
font-size: 34rpx;
font-weight: 700;
color: #0f766e;
}
.choice-desc {
margin-top: 8rpx;
}
.booking-day-scroll {
height: 132rpx;
margin-bottom: 18rpx;
}
.booking-day-row {
padding-right: 20rpx;
}
.booking-day-card {
width: 150rpx;
padding: 18rpx;
border-radius: 22rpx;
background: #f8fbfd;
margin-right: 16rpx;
box-sizing: border-box;
}
.booking-day-card-active {
background: #0f766e;
}
.booking-day-label,
.booking-day-date,
.booking-slot-label {
font-size: 24rpx;
color: #66788a;
line-height: 34rpx;
color: #476072;
}
.booking-day-date {
margin-top: 6rpx;
}
.booking-day-label-active,
.booking-day-date-active,
.booking-slot-label-active {
color: #ffffff;
font-weight: 700;
}
.booking-slot-grid {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 20rpx;
}
.booking-slot-card {
width: 48.5%;
height: 82rpx;
border-radius: 22rpx;
background: #f8fbfd;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
border-width: 2rpx;
border-style: solid;
border-color: transparent;
box-sizing: border-box;
}
.booking-slot-card-active {
background: #0f766e;
border-color: #0f766e;
}
.booking-slot-card-disabled {
background: #f1f5f9;
opacity: 0.55;
}
.page-bottom-space {
height: 150rpx;
}
.bottom-bar {
position: fixed;
left: 24rpx;
right: 24rpx;
bottom: 28rpx;
justify-content: space-between;
background: #ffffff;
border-radius: 30rpx;
padding: 20rpx 22rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.12);
}
.bottom-bar-price-unit {
font-size: 22rpx;
color: #64748b;
margin-left: 8rpx;
margin-bottom: 4rpx;
}
.submit-btn {
margin-top: 16rpx;
padding: 26rpx 0;
border-radius: 20rpx;
height: 84rpx;
padding: 0 36rpx;
border-radius: 999rpx;
background: #0f766e;
text-align: center;
font-size: 30rpx;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}

View File

@@ -1,32 +1,67 @@
<template>
<ServicePageScaffold title="居家上门服务" fallback-url="/pages/main/index">
<view class="hero-card">
<text class="hero-title">居家上门服务</text>
<text class="hero-desc">覆盖服务申请、上门评估、执行跟踪与验收反馈,先用 mock 数据跑通前端闭环。</text>
<view class="hero-actions">
<view class="primary-btn" @click="goApply">立即申请</view>
<view>
<text class="hero-title">居家服务大厅</text>
<text class="hero-desc">从服务选择、预约下单到服务单跟踪,整体体验升级为上门服务预约平台。</text>
</view>
<view class="hero-right-tag">
<text class="hero-right-tag-text">服务保障</text>
</view>
</view>
<ServicePanel title="推荐服务" subtitle="适老化信息更清晰,入口更聚焦。">
<view v-for="item in services" :key="item.id" class="service-card">
<view class="service-top">
<view>
<text class="service-name">{{ item.name }}</text>
<text class="service-meta">{{ item.category }} · {{ item.durationText }}</text>
</view>
<text class="service-price">¥{{ item.price }}</text>
<view class="service-category-grid">
<view v-for="item in categoryGrid" :key="item.id" class="service-category-card" @click="switchCategory(item.id)">
<view class="service-category-icon" :style="{ background: item.color }">
<text class="service-category-icon-text">{{ item.iconText }}</text>
</view>
<text class="service-category-name">{{ item.name }}</text>
</view>
</view>
<scroll-view class="promo-scroll" direction="horizontal" :show-scrollbar="false">
<view class="promo-row">
<view v-for="item in promoCards" :key="item.id" :class="['promo-card', 'promo-card-' + item.tone]" @click="consultService">
<text class="promo-card-title">{{ item.title }}</text>
<text class="promo-card-desc">{{ item.desc }}</text>
</view>
</view>
</scroll-view>
<ServicePanel title="推荐服务" subtitle="优先展示最常预约的居家服务项目。">
<view v-for="item in filteredServices" :key="item.id" class="recommend-card">
<view class="recommend-card-top">
<view class="recommend-card-cover">
<text class="recommend-card-cover-text">{{ item.imageText }}</text>
</view>
<view class="recommend-card-main">
<text class="recommend-card-title">{{ item.title }}</text>
<text class="recommend-card-subtitle">{{ item.subtitle }}</text>
<text class="recommend-card-suitable">适用对象:{{ item.suitableFor }}</text>
</view>
</view>
<view class="recommend-card-tags">
<text v-for="tag in item.tags" :key="item.id + '-' + tag" class="recommend-card-tag">{{ tag }}</text>
</view>
<view class="recommend-card-bottom">
<view>
<text class="recommend-card-price-prefix">¥</text>
<text class="recommend-card-price">{{ item.price }}</text>
<text class="recommend-card-unit">起 / {{ item.unit }}</text>
</view>
<view class="recommend-card-actions">
<view class="recommend-card-secondary" @click="goDetail(item.id)">查看详情</view>
<view class="recommend-card-primary" @click="goBooking(item.id)">立即预约</view>
</view>
</view>
<text class="service-summary">{{ item.summary }}</text>
<text class="service-suitable">适用对象:{{ item.suitableFor }}</text>
</view>
</ServicePanel>
<ServicePanel title="我的服务单" subtitle="先展示待派单和服务中的 mock 单据。">
<ServicePanel title="我的预约 / 服务单" subtitle="按服务预约单的方式呈现状态、机构和上门信息。">
<view v-if="cases.length == 0" class="empty-box">
<text class="empty-text">当前没有服务单</text>
</view>
<view v-for="item in cases" :key="item.id" class="case-card" @click="goDetail(item.id)">
<view v-for="item in cases" :key="item.id" class="case-card" @click="goOrderDetail(item.id)">
<view class="case-row">
<view>
<text class="case-title">{{ item.serviceName }}</text>
@@ -34,127 +69,354 @@
</view>
<ServiceStatusTag :text="item.statusText" :tone="item.statusTone"></ServiceStatusTag>
</view>
<text class="case-info">服务对象{{ item.elderName }}{{ item.age }}</text>
<text class="case-info">服务机构{{ item.staffName == '待分配' ? '待分配机构' : item.staffName }}</text>
<text class="case-info">上门时间:{{ item.serviceTime }}</text>
<text class="case-info">服务地址:{{ item.address }}</text>
<view class="case-action-row">
<view class="case-action-secondary" @click.stop="goDetailByName(item.serviceName)">再次预约</view>
<view class="case-action-primary" @click.stop="goOrderDetail(item.id)">查看服务单</view>
</view>
</view>
</ServicePanel>
</ServicePageScaffold>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue'
import { fetchConsumerHomeServiceCases, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
import { HomeServiceCatalogType, HomeServiceCaseType } from '@/types/home-service.uts'
import {
HomeServiceCategoryType,
HomeServiceItemType,
HomeServicePromoCardType,
getHomeServiceCategories,
getHomeServiceItems,
getHomeServicePromoCards
} from '@/utils/homeServiceUiMock.uts'
const services = ref<Array<HomeServiceCatalogType>>([])
const cases = ref<Array<HomeServiceCaseType>>([])
const categoryGrid = ref<Array<HomeServiceCategoryType>>([])
const promoCards = ref<Array<HomeServicePromoCardType>>([])
const selectedCategory = ref('all-services')
const filteredServices = computed((): Array<HomeServiceItemType> => {
const serviceItems = getHomeServiceItems(services.value)
if (selectedCategory.value == 'all-services') {
return serviceItems
}
const result: Array<HomeServiceItemType> = []
for (let i = 0; i < serviceItems.length; i++) {
if (serviceItems[i].category == selectedCategory.value) {
result.push(serviceItems[i])
}
}
if (result.length == 0) {
return serviceItems
}
return result
})
async function loadData() {
categoryGrid.value = getHomeServiceCategories()
promoCards.value = getHomeServicePromoCards()
services.value = await fetchHomeServiceCatalog()
cases.value = await fetchConsumerHomeServiceCases()
}
function goApply() {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/apply' })
function switchCategory(categoryId: string) {
selectedCategory.value = categoryId
}
function goDetail(caseId: string) {
function consultService() {
uni.showToast({ title: '即将接入专属客服入口', icon: 'none' })
}
function goDetail(serviceId: string) {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/service-detail?id=' + serviceId })
}
function goBooking(serviceId: string) {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/service-detail?id=' + serviceId + '&mode=booking' })
}
function goOrderDetail(caseId: string) {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + caseId })
}
onLoad(() => {
function goDetailByName(serviceName: string) {
if (serviceName == '康复训练指导') {
goBooking('svc-002')
return
}
if (serviceName == '慢病健康随访') {
goBooking('svc-003')
return
}
goBooking('svc-001')
}
onLoad((options) => {
const category = options['category']
if (category != null) {
selectedCategory.value = category as string
}
loadData()
})
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f3f7f9;
padding: 24rpx;
box-sizing: border-box;
}
.hero-card {
background: linear-gradient(135deg, #0f766e, #1d4ed8);
border-radius: 28rpx;
padding: 32rpx;
background: linear-gradient(180deg, #eafbf7 0%, #eff6ff 100%);
border-radius: 32rpx;
padding: 28rpx;
margin-bottom: 24rpx;
flex-direction: row;
justify-content: space-between;
}
.hero-title {
font-size: 40rpx;
font-weight: 700;
color: #ffffff;
line-height: 56rpx;
color: #16324f;
line-height: 54rpx;
}
.hero-desc {
margin-top: 16rpx;
font-size: 28rpx;
line-height: 40rpx;
color: rgba(255, 255, 255, 0.9);
font-size: 24rpx;
line-height: 36rpx;
color: #5f7284;
}
.hero-actions {
margin-top: 28rpx;
.hero-right-tag {
height: 56rpx;
padding: 0 22rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.76);
align-items: center;
justify-content: center;
}
.primary-btn {
background: #ffffff;
color: #0f3d66;
font-size: 30rpx;
.hero-right-tag-text {
font-size: 22rpx;
font-weight: 700;
text-align: center;
padding: 24rpx 0;
border-radius: 18rpx;
color: #0f766e;
}
.service-card,
.service-category-grid,
.promo-row,
.recommend-card-top,
.recommend-card-bottom,
.recommend-card-actions,
.case-card {
flex-direction: row;
align-items: center;
}
.service-category-grid {
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 12rpx;
}
.service-category-card {
width: 19%;
align-items: center;
margin-bottom: 24rpx;
}
.service-category-icon {
width: 88rpx;
height: 88rpx;
border-radius: 28rpx;
align-items: center;
justify-content: center;
}
.service-category-icon-text {
font-size: 28rpx;
font-weight: 700;
color: #16324f;
}
.service-category-name {
margin-top: 12rpx;
font-size: 22rpx;
line-height: 30rpx;
text-align: center;
color: #425466;
}
.promo-scroll {
height: 168rpx;
margin-bottom: 24rpx;
}
.promo-row {
padding-right: 20rpx;
}
.promo-card {
width: 240rpx;
height: 144rpx;
padding: 24rpx;
border-radius: 24rpx;
box-sizing: border-box;
margin-right: 16rpx;
}
.promo-card-green {
background: #eafbf7;
}
.promo-card-orange {
background: #fff7ed;
}
.promo-card-blue {
background: #eff6ff;
}
.promo-card-teal {
background: #ecfeff;
}
.promo-card-title {
font-size: 28rpx;
font-weight: 700;
color: #16324f;
}
.promo-card-desc {
margin-top: 12rpx;
font-size: 22rpx;
line-height: 32rpx;
color: #5f7284;
}
.recommend-card,
.case-card {
padding: 24rpx;
border-radius: 20rpx;
border-radius: 24rpx;
background: #f8fbfc;
margin-bottom: 20rpx;
}
.service-top,
.case-row {
.recommend-card-top,
.recommend-card-bottom,
.case-row,
.case-action-row {
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
align-items: center;
}
.service-name,
.recommend-card-cover {
width: 112rpx;
height: 112rpx;
border-radius: 28rpx;
background: linear-gradient(180deg, #eafbf7 0%, #eff6ff 100%);
align-items: center;
justify-content: center;
}
.recommend-card-cover-text {
font-size: 28rpx;
font-weight: 700;
color: #0f766e;
}
.recommend-card-main {
flex: 1;
min-width: 0;
margin-left: 20rpx;
}
.recommend-card-title,
.case-title {
font-size: 32rpx;
font-weight: 700;
color: #16324f;
}
.service-meta,
.recommend-card-subtitle,
.recommend-card-suitable,
.case-no,
.case-info,
.service-summary,
.service-suitable,
.empty-text {
margin-top: 10rpx;
font-size: 26rpx;
line-height: 38rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #66788a;
}
.service-price {
.recommend-card-tags {
flex-direction: row;
flex-wrap: wrap;
margin-top: 18rpx;
margin-bottom: 18rpx;
}
.recommend-card-tag {
padding: 10rpx 16rpx;
border-radius: 999rpx;
background: #eef2f7;
font-size: 22rpx;
color: #476072;
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.recommend-card-price-prefix,
.recommend-card-price {
font-size: 34rpx;
font-weight: 700;
color: #0f766e;
}
.recommend-card-unit {
font-size: 22rpx;
color: #64748b;
}
.recommend-card-secondary,
.recommend-card-primary,
.case-action-secondary,
.case-action-primary {
height: 68rpx;
padding: 0 24rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 700;
align-items: center;
justify-content: center;
margin-left: 12rpx;
}
.recommend-card-secondary,
.case-action-secondary {
background: #ffffff;
border-width: 2rpx;
border-style: solid;
border-color: #cbd5e1;
color: #476072;
}
.recommend-card-primary,
.case-action-primary {
background: #16a085;
color: #ffffff;
}
.case-action-row {
margin-top: 18rpx;
}
.empty-box {
padding: 40rpx 0;
align-items: center;

View File

@@ -4,36 +4,70 @@
<text class="empty-text">未找到对应服务单</text>
</view>
<view v-else>
<ServicePanel title="服务进度" subtitle="申请到执行的关键状态先以 mock 流程展示。">
<ServiceInfoCard
:title="detail.serviceName"
:code="detail.caseNo"
:status-text="detail.statusText"
:status-tone="detail.statusTone"
:items="[
{ label: '上门时间', value: detail.serviceTime },
{ label: '当前进度', value: '第 ' + detail.currentStep + ' / ' + detail.totalSteps + ' 步' },
{ label: '执行人员', value: detail.staffName + ' ' + detail.staffPhone }
]"
></ServiceInfoCard>
</ServicePanel>
<view class="summary-card">
<view class="summary-top-row">
<view>
<text class="summary-title">{{ detail.serviceName }}</text>
<text class="summary-case-no">服务单号:{{ detail.caseNo }}</text>
</view>
<ServiceStatusTag :text="detail.statusText" :tone="detail.statusTone"></ServiceStatusTag>
</view>
<text class="summary-desc">{{ detail.summary }}</text>
<view class="summary-price-row">
<text class="summary-price-prefix">¥</text>
<text class="summary-price">{{ detail.amount }}</text>
<text class="summary-price-unit">服务金额</text>
</view>
<view class="summary-meta-grid">
<view class="summary-meta-item">
<text class="summary-meta-label">上门时间</text>
<text class="summary-meta-value">{{ detail.serviceTime }}</text>
</view>
<view class="summary-meta-item">
<text class="summary-meta-label">当前进度</text>
<text class="summary-meta-value">第 {{ detail.currentStep }} / {{ detail.totalSteps }} 步</text>
</view>
<view class="summary-meta-item">
<text class="summary-meta-label">服务机构 / 人员</text>
<text class="summary-meta-value">{{ detail.staffName }}</text>
</view>
<view class="summary-meta-item">
<text class="summary-meta-label">联系电话</text>
<text class="summary-meta-value">{{ detail.staffPhone }}</text>
</view>
</view>
</view>
<ServicePanel title="服务对象信息">
<ServicePanel title="预约信息" subtitle="围绕联系人、地址和服务对象展示当前预约信息">
<ServiceInfoList
:items="[
{ label: '申请人:', value: detail.applicantName },
{ label: '联系人:', value: detail.applicantName },
{ label: '服务对象:', value: detail.elderName + '' + detail.age + ' 岁' },
{ label: '联系电话:', value: detail.phone },
{ label: '服务地址:', value: detail.address },
{ label: '需求说明', value: detail.summary }
{ label: '预约备注', value: detail.summary }
]"
></ServiceInfoList>
<view v-if="detail.status == 'pending_acceptance'" class="feedback-btn" @click="goFeedback">去验收反馈</view>
</ServicePanel>
<ServicePanel title="过程留痕" subtitle="后续可替换为真实时间线与上传凭证。">
<ServicePanel title="服务保障" subtitle="用户在预约后仍可看到平台保障与追溯承诺。">
<view class="guarantee-row">
<text class="guarantee-chip">平台认证</text>
<text class="guarantee-chip">明码标价</text>
<text class="guarantee-chip">服务可追溯</text>
<text class="guarantee-chip">异常可申诉</text>
</view>
</ServicePanel>
<ServicePanel title="服务过程" subtitle="当前以 mock 时间线展示预约受理、派单和上门过程。">
<ServiceTimeline :items="detail.timeline"></ServiceTimeline>
</ServicePanel>
<view class="action-row">
<view class="secondary-btn" @click="bookAgain">再次预约</view>
<view v-if="detail.status == 'pending_acceptance'" class="primary-btn" @click="goFeedback">去验收反馈</view>
<view v-else class="primary-btn" @click="contactService">联系客服</view>
</view>
</view>
</ServicePageScaffold>
</template>
@@ -42,9 +76,9 @@
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
import ServiceInfoCard from '@/components/homeService/ServiceInfoCard.uvue'
import ServiceInfoList from '@/components/homeService/ServiceInfoList.uvue'
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue'
import ServiceTimeline from '@/components/homeService/ServiceTimeline.uvue'
import { fetchConsumerHomeServiceCaseDetail } from '@/services/homeServiceService.uts'
import { HomeServiceCaseType } from '@/types/home-service.uts'
@@ -66,6 +100,24 @@ function goFeedback() {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/feedback?id=' + caseId.value })
}
function bookAgain() {
if (detail.value == null) {
return
}
let serviceTargetId = 'svc-001'
if (detail.value.serviceName == '康复训练指导') {
serviceTargetId = 'svc-002'
}
if (detail.value.serviceName == '慢病健康随访') {
serviceTargetId = 'svc-003'
}
uni.navigateTo({ url: '/pages/mall/consumer/home-service/service-detail?id=' + serviceTargetId + '&mode=booking' })
}
function contactService() {
uni.showToast({ title: '即将接入专属客服入口', icon: 'none' })
}
onLoad((options) => {
const id = options['id']
if (id != null) {
@@ -80,29 +132,129 @@ onShow(() => {
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f3f7f9;
padding: 24rpx;
.summary-card {
background: #ffffff;
border-radius: 32rpx;
padding: 28rpx;
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06);
margin-bottom: 24rpx;
}
.summary-top-row,
.summary-price-row,
.action-row,
.guarantee-row {
flex-direction: row;
align-items: center;
}
.summary-top-row,
.action-row {
justify-content: space-between;
}
.summary-title {
font-size: 34rpx;
font-weight: 700;
color: #16324f;
}
.summary-case-no,
.summary-desc,
.summary-meta-label,
.summary-meta-value,
.empty-text {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #66788a;
}
.summary-desc {
margin-top: 18rpx;
}
.summary-price-row {
margin-top: 18rpx;
align-items: flex-end;
}
.summary-price-prefix,
.summary-price {
font-size: 40rpx;
font-weight: 700;
color: #0f766e;
}
.summary-price-unit {
font-size: 22rpx;
color: #64748b;
margin-left: 10rpx;
margin-bottom: 6rpx;
}
.summary-meta-grid {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 22rpx;
}
.summary-meta-item {
width: 48%;
padding: 22rpx;
border-radius: 24rpx;
background: #f8fbfd;
box-sizing: border-box;
margin-bottom: 16rpx;
}
.guarantee-row {
flex-wrap: wrap;
}
.guarantee-chip {
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: #eef6ff;
font-size: 22rpx;
color: #476072;
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.action-row {
margin-top: 10rpx;
margin-bottom: 12rpx;
}
.secondary-btn,
.primary-btn {
width: 48%;
height: 78rpx;
border-radius: 999rpx;
font-size: 26rpx;
font-weight: 700;
align-items: center;
justify-content: center;
}
.secondary-btn {
background: #ffffff;
border-width: 2rpx;
border-style: solid;
border-color: #cbd5e1;
color: #476072;
}
.primary-btn {
background: #16a085;
color: #ffffff;
}
.empty-text {
margin-top: 10rpx;
font-size: 26rpx;
line-height: 38rpx;
color: #66788a;
}
.feedback-btn {
margin-top: 24rpx;
padding: 24rpx 0;
text-align: center;
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
background: #1d4ed8;
border-radius: 18rpx;
}
.empty-box {

View File

@@ -0,0 +1,628 @@
<template>
<view class="booking-detail-page">
<ServicePageScaffold title="预约服务" fallback-url="/pages/mall/consumer/home-service/index">
<view class="detail-service-summary">
<view class="detail-summary-cover">
<text class="detail-summary-cover-text">{{ serviceImageText }}</text>
</view>
<view class="detail-summary-main">
<text class="detail-summary-title">{{ serviceTitle }}</text>
<text class="detail-summary-desc">{{ serviceSubtitle }}</text>
<text class="detail-summary-duration">服务时长:{{ serviceDuration }}</text>
<view class="detail-summary-tags">
<text v-for="tag in guaranteeTags" :key="tag.id" class="detail-summary-tag">{{ tag.label }}</text>
</view>
<view class="detail-summary-price-row">
<text class="detail-summary-price-prefix">¥</text>
<text class="detail-summary-price">{{ servicePrice }}</text>
<text class="detail-summary-price-unit">起 / 次</text>
</view>
</view>
</view>
<ServicePanel title="服务说明卡" subtitle="围绕服务内容、适用人群和注意事项建立预约预期。">
<view class="detail-info-list">
<view class="detail-info-item">
<text class="detail-info-label">服务内容</text>
<text class="detail-info-value">{{ serviceSubtitle }}</text>
</view>
<view class="detail-info-item">
<text class="detail-info-label">适用人群</text>
<text class="detail-info-value">{{ serviceSuitableFor }}</text>
</view>
<view class="detail-info-item">
<text class="detail-info-label">服务时长</text>
<text class="detail-info-value">{{ serviceDuration }}</text>
</view>
<view class="detail-info-item">
<text class="detail-info-label">注意事项</text>
<text class="detail-info-value">预约成功后请保持电话畅通,首次上门建议家属在场。</text>
</view>
<view class="detail-info-item">
<text class="detail-info-label">不包含项目</text>
<text class="detail-info-value">{{ serviceExcludeText }}</text>
</view>
</view>
</ServicePanel>
<ServicePanel title="Step1 服务地址" subtitle="没有选过地址时先提示用户补全上门地址。">
<view class="booking-step-card">
<text class="step-card-title">上门服务地址</text>
<text v-if="addressText == ''" class="step-card-placeholder">请选择上门服务地址</text>
<text v-else class="step-card-value">{{ addressText }}</text>
<view class="step-card-action" @click="selectAddress">选择地址</view>
</view>
</ServicePanel>
<ServicePanel title="Step2 服务机构 / 服务人员" subtitle="当前先展示静态推荐机构,后续可接入真实人员选择。">
<view class="booking-step-card agency-card">
<view class="agency-card-top">
<text class="step-card-title">{{ agency.name }}</text>
<text class="agency-card-distance">{{ agency.distance }}</text>
</view>
<text class="agency-card-meta">评分 {{ agency.rating }}</text>
<text class="step-card-value">{{ agency.summary }}</text>
<text class="agency-card-todo">TODO后续接入真实服务机构 / 服务人员选择接口。</text>
</view>
</ServicePanel>
<ServicePanel title="Step3 上门时间" subtitle="先完成日期和时间段选择,再提交预约申请。">
<scroll-view class="booking-day-scroll" direction="horizontal" :show-scrollbar="false">
<view class="booking-day-row">
<view
v-for="item in bookingDays"
:key="item.id"
:class="['booking-day-card', selectedDayId == item.id ? 'booking-day-card-active' : '']"
@click="selectDay(item.id)"
>
<text :class="['booking-day-label', selectedDayId == item.id ? 'booking-day-label-active' : '']">{{ item.label }}</text>
<text :class="['booking-day-date', selectedDayId == item.id ? 'booking-day-date-active' : '']">{{ item.dateText }}</text>
<text :class="['booking-day-weekday', selectedDayId == item.id ? 'booking-day-weekday-active' : '']">{{ item.weekday }}</text>
</view>
</view>
</scroll-view>
<view class="booking-slot-grid">
<view
v-for="item in bookingSlots"
:key="item.id"
:class="['booking-slot-card', selectedSlotId == item.id ? 'booking-slot-card-active' : '', item.available ? '' : 'booking-slot-card-disabled']"
@click="selectSlot(item.id, item.available)"
>
<text :class="['booking-slot-label', selectedSlotId == item.id ? 'booking-slot-label-active' : '']">{{ item.label }}</text>
</view>
</view>
</ServicePanel>
<ServicePanel title="Step4 联系人信息" subtitle="使用现有 input 组件完成预约联系人填写。">
<view class="contact-form-item">
<text class="contact-form-label">联系人姓名</text>
<input v-model="contactName" class="contact-form-input" placeholder="请输入联系人姓名" />
</view>
<view class="contact-form-item">
<text class="contact-form-label">联系电话</text>
<input v-model="contactPhone" class="contact-form-input" type="number" placeholder="请输入联系电话" />
</view>
<view class="contact-form-item">
<text class="contact-form-label">性别</text>
<view class="gender-row">
<view :class="['gender-pill', contactGender == '先生' ? 'gender-pill-active' : '']" @click="contactGender = '先生'">先生</view>
<view :class="['gender-pill', contactGender == '女士' ? 'gender-pill-active' : '']" @click="contactGender = '女士'">女士</view>
</view>
</view>
<view class="contact-form-item">
<text class="contact-form-label">备注</text>
<textarea v-model="remarkText" class="contact-form-textarea" placeholder="如行动不便、术后照护重点、上门注意事项"></textarea>
</view>
</ServicePanel>
<ServicePanel title="用户保障卡" subtitle="让预约页更像服务平台而不是普通商品页。">
<view class="guarantee-grid">
<view v-for="item in guaranteeTags" :key="item.id" class="guarantee-grid-item">
<text class="guarantee-grid-text">{{ item.label }}</text>
</view>
</view>
</ServicePanel>
<view class="detail-page-bottom-space"></view>
</ServicePageScaffold>
<view class="booking-bottom-bar">
<view class="booking-bottom-main">
<view>
<text class="booking-bottom-price-prefix">¥</text>
<text class="booking-bottom-price">{{ servicePrice }}</text>
<text class="booking-bottom-unit">起</text>
</view>
<text class="booking-bottom-time">{{ selectedTimeText }}</text>
</view>
<view class="booking-submit-btn" @click="submitBooking">立即预约</view>
</view>
</view>
</template>
<script setup lang="uts">
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import { createHomeServiceApplication, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
import { HomeServiceApplicationDraftType, HomeServiceCatalogType } from '@/types/home-service.uts'
import {
BookingDayOptionType,
BookingTimeSlotType,
HomeServiceAgencyType,
HomeServiceGuaranteeItemType,
getBookingDayOptions,
getBookingTimeSlots,
getHomeServiceItems,
getRecommendedAgency,
getServiceExcludes,
getServiceGuarantees
} from '@/utils/homeServiceUiMock.uts'
const serviceId = ref('svc-001')
const serviceTitle = ref('基础上门护理')
const serviceSubtitle = ref('覆盖生命体征监测、基础照护、风险提醒。')
const servicePrice = ref(168)
const serviceDuration = ref('约 2 小时')
const serviceSuitableFor = ref('行动不便、术后恢复、慢病随访老人')
const serviceImageText = ref('照护')
const serviceExcludeText = ref('高风险处置、住院陪护、急诊陪诊')
const bookingDays = ref<Array<BookingDayOptionType>>([])
const bookingSlots = ref<Array<BookingTimeSlotType>>([])
const guaranteeTags = ref<Array<HomeServiceGuaranteeItemType>>([])
const agency = ref<HomeServiceAgencyType>({
id: 'agency-001',
name: '梅江居家护理服务中心',
distance: '距您 1.2km',
rating: '4.9',
summary: '提供基础照护、上门护理和长者陪伴服务。'
})
const addressText = ref('')
const selectedDayId = ref('day-1')
const selectedSlotId = ref('slot-1')
const contactName = ref('李晓兰')
const contactPhone = ref('13800138000')
const contactGender = ref('女士')
const remarkText = ref('老人需要基础照护与血压监测。')
const selectedTimeText = computed((): string => {
let selectedDayLabel = ''
for (let i = 0; i < bookingDays.value.length; i++) {
if (bookingDays.value[i].id == selectedDayId.value) {
selectedDayLabel = bookingDays.value[i].label + ' ' + bookingDays.value[i].dateText
break
}
}
let selectedSlotLabel = ''
for (let i = 0; i < bookingSlots.value.length; i++) {
if (bookingSlots.value[i].id == selectedSlotId.value) {
selectedSlotLabel = bookingSlots.value[i].label
break
}
}
if (selectedDayLabel == '' || selectedSlotLabel == '') {
return '请选择上门时间'
}
return selectedDayLabel + ' ' + selectedSlotLabel
})
async function loadData() {
bookingDays.value = getBookingDayOptions()
bookingSlots.value = getBookingTimeSlots()
guaranteeTags.value = getServiceGuarantees()
agency.value = getRecommendedAgency(serviceId.value)
serviceExcludeText.value = getServiceExcludes(serviceId.value).join('')
const catalog = await fetchHomeServiceCatalog()
let matchedService: HomeServiceCatalogType | null = null
for (let i = 0; i < catalog.length; i++) {
if (catalog[i].id == serviceId.value) {
matchedService = catalog[i]
break
}
}
if (matchedService == null) {
const fallbackItems = getHomeServiceItems(catalog)
if (fallbackItems.length > 0) {
serviceTitle.value = fallbackItems[0].title
serviceSubtitle.value = fallbackItems[0].subtitle
servicePrice.value = fallbackItems[0].price
serviceSuitableFor.value = fallbackItems[0].suitableFor
serviceImageText.value = fallbackItems[0].imageText
}
return
}
serviceTitle.value = matchedService.name
serviceSubtitle.value = matchedService.summary
servicePrice.value = matchedService.price
serviceDuration.value = matchedService.durationText
serviceSuitableFor.value = matchedService.suitableFor
const mappedItems = getHomeServiceItems([matchedService])
if (mappedItems.length > 0) {
serviceImageText.value = mappedItems[0].imageText
}
}
function selectAddress() {
if (addressText.value == '') {
addressText.value = '梅州市梅江区学海路 18 号 2 栋 602'
uni.showToast({ title: '已填入示例地址', icon: 'none' })
return
}
uni.navigateTo({ url: '/pages/mall/consumer/address-list' })
}
function selectDay(dayId: string) {
selectedDayId.value = dayId
}
function selectSlot(slotId: string, available: boolean) {
if (!available) {
uni.showToast({ title: '该时段暂不可约', icon: 'none' })
return
}
selectedSlotId.value = slotId
}
async function submitBooking() {
if (addressText.value == '') {
uni.showToast({ title: '请选择上门服务地址', icon: 'none' })
return
}
if (contactName.value == '' || contactPhone.value == '') {
uni.showToast({ title: '请补全联系人信息', icon: 'none' })
return
}
const draft: HomeServiceApplicationDraftType = {
serviceId: serviceId.value,
serviceName: serviceTitle.value,
applicantName: contactName.value,
elderName: contactName.value,
age: 78,
phone: contactPhone.value,
address: addressText.value,
preferredTime: selectedTimeText.value,
demandSummary: remarkText.value == '' ? serviceSubtitle.value : remarkText.value
}
const created = await createHomeServiceApplication(draft)
uni.showToast({ title: '预约已提交', icon: 'success' })
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
}
onLoad((options) => {
const id = options['id']
if (id != null) {
serviceId.value = id as string
}
const mode = options['mode']
if (mode != null && mode == 'booking') {
addressText.value = '梅州市梅江区学海路 18 号 2 栋 602'
}
loadData()
})
</script>
<style scoped>
.booking-detail-page {
background: #f4f8fb;
}
.detail-service-summary {
background: #ffffff;
border-radius: 32rpx;
padding: 28rpx;
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06);
flex-direction: row;
align-items: flex-start;
margin-bottom: 24rpx;
}
.detail-summary-cover {
width: 172rpx;
height: 172rpx;
border-radius: 32rpx;
background: linear-gradient(180deg, #eafbf7 0%, #eff6ff 100%);
align-items: center;
justify-content: center;
}
.detail-summary-cover-text {
font-size: 34rpx;
font-weight: 700;
color: #0f766e;
}
.detail-summary-main {
flex: 1;
min-width: 0;
margin-left: 24rpx;
}
.detail-summary-title {
font-size: 34rpx;
font-weight: 700;
color: #16324f;
}
.detail-summary-desc,
.detail-summary-duration,
.detail-info-value,
.step-card-value,
.step-card-placeholder,
.agency-card-meta,
.agency-card-todo,
.booking-bottom-time {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #64748b;
}
.detail-summary-tags,
.gender-row,
.guarantee-grid,
.agency-card-top,
.booking-bottom-bar,
.booking-bottom-main {
flex-direction: row;
align-items: center;
}
.detail-summary-tags {
flex-wrap: wrap;
margin-top: 16rpx;
}
.detail-summary-tag {
padding: 10rpx 16rpx;
border-radius: 999rpx;
background: #eff6ff;
font-size: 22rpx;
color: #476072;
margin-right: 10rpx;
margin-bottom: 10rpx;
}
.detail-summary-price-row {
margin-top: 14rpx;
flex-direction: row;
align-items: flex-end;
}
.detail-summary-price-prefix,
.detail-summary-price,
.booking-bottom-price-prefix,
.booking-bottom-price {
font-size: 38rpx;
font-weight: 700;
color: #0f766e;
}
.detail-summary-price-unit,
.booking-bottom-unit {
font-size: 22rpx;
color: #64748b;
margin-left: 8rpx;
margin-bottom: 4rpx;
}
.detail-info-item,
.contact-form-item {
margin-bottom: 24rpx;
}
.detail-info-label,
.contact-form-label,
.step-card-title {
font-size: 28rpx;
font-weight: 700;
color: #16324f;
}
.booking-step-card {
padding: 24rpx;
border-radius: 24rpx;
background: #f8fbfd;
}
.step-card-placeholder {
color: #94a3b8;
}
.step-card-action {
margin-top: 20rpx;
height: 68rpx;
border-radius: 999rpx;
background: #16a085;
color: #ffffff;
font-size: 24rpx;
font-weight: 700;
align-items: center;
justify-content: center;
}
.agency-card-distance {
font-size: 22rpx;
color: #0f766e;
}
.agency-card-todo {
font-size: 22rpx;
color: #94a3b8;
}
.booking-day-scroll {
height: 156rpx;
}
.booking-day-row {
flex-direction: row;
padding-right: 20rpx;
}
.booking-day-card {
width: 160rpx;
padding: 20rpx;
border-radius: 24rpx;
background: #f8fbfd;
margin-right: 16rpx;
box-sizing: border-box;
}
.booking-day-card-active {
background: #0f766e;
}
.booking-day-label,
.booking-day-date,
.booking-day-weekday {
font-size: 24rpx;
line-height: 34rpx;
color: #476072;
}
.booking-day-date,
.booking-day-weekday {
margin-top: 6rpx;
}
.booking-day-label-active,
.booking-day-date-active,
.booking-day-weekday-active {
color: #ffffff;
}
.booking-slot-grid {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 20rpx;
}
.booking-slot-card {
width: 48.5%;
height: 82rpx;
border-radius: 22rpx;
background: #f8fbfd;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
border-width: 2rpx;
border-style: solid;
border-color: transparent;
box-sizing: border-box;
}
.booking-slot-card-active {
background: #eafbf7;
border-color: #16a085;
}
.booking-slot-card-disabled {
background: #f1f5f9;
opacity: 0.55;
}
.booking-slot-label {
font-size: 24rpx;
color: #476072;
}
.booking-slot-label-active {
color: #0f766e;
font-weight: 700;
}
.contact-form-input,
.contact-form-textarea {
width: 100%;
background: #f8fbfd;
border-radius: 22rpx;
font-size: 26rpx;
color: #16324f;
box-sizing: border-box;
margin-top: 14rpx;
}
.contact-form-input {
height: 84rpx;
padding: 0 24rpx;
}
.contact-form-textarea {
height: 160rpx;
padding: 20rpx 24rpx;
}
.gender-pill {
height: 64rpx;
padding: 0 28rpx;
border-radius: 999rpx;
background: #f1f5f9;
font-size: 24rpx;
color: #476072;
align-items: center;
justify-content: center;
margin-right: 16rpx;
margin-top: 14rpx;
}
.gender-pill-active {
background: #eafbf7;
color: #0f766e;
font-weight: 700;
}
.guarantee-grid {
flex-wrap: wrap;
justify-content: space-between;
}
.guarantee-grid-item {
width: 48%;
height: 72rpx;
border-radius: 20rpx;
background: #f8fbfd;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.guarantee-grid-text {
font-size: 24rpx;
color: #476072;
}
.detail-page-bottom-space {
height: 160rpx;
}
.booking-bottom-bar {
position: fixed;
left: 24rpx;
right: 24rpx;
bottom: 28rpx;
background: #ffffff;
border-radius: 30rpx;
padding: 20rpx 22rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.12);
justify-content: space-between;
}
.booking-submit-btn {
height: 84rpx;
padding: 0 34rpx;
border-radius: 999rpx;
background: #16a085;
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
align-items: center;
justify-content: center;
}
</style>