shinnosuke
4 天以前 5ce0d76154bf4c1d70543476cdfad9a30195b0f8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<template>
    <view class="page">
        <view class="custom-nav" :class="isSolidNav ? 'is-solid' : ''"><view class="custom-nav-bg"></view><view class="safe-space" :style="'height:' + navTop + 'px'"></view><view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"><view class="back-button" @tap="goBack"><image src="/static/food-assets/arrow-left-white.svg"></image></view><text class="nav-title">{{packageName}}</text><view class="capsule-space" :style="'width:' + navWidth + 'px'"></view></view></view>
        <scroll-view :show-scrollbar="false" class="content" scroll-y @scroll="handleScroll"><view class="hero"><image class="hero-image" :src="heroImage" mode="aspectFill" @error="replaceHero"></image><view class="hero-shade"></view></view><view class="package-panel">
            <view v-if="detailLoading" class="detail-state"><view class="loading-spinner"></view><text>正在加载体检套餐</text></view>
            <view v-else-if="detailError" class="detail-state error-state"><text>体检套餐加载失败</text><text class="retry-text" @tap="loadDetail">重新加载</text></view>
            <view v-else>
                <view class="package-info-card"><view class="package-title-row"><text class="package-title">{{packageName}}</text><text class="free-tag">{{freeLabel}}</text></view><view v-if="applicableCards != ''" class="service-tags"><text>{{applicableCards}}</text></view><text class="hospital-name">{{hospitalName}}</text><view class="address-row"><image class="address-icon" src="/static/benefits-assets/map-pin.svg"></image><text class="address-text">{{hospitalAddress}}</text><view class="address-actions"><view :class="canNavigate ? 'round-action' : 'round-action is-disabled'" @tap="openMap"><image src="/static/merchant-assets/navigation.svg"></image></view><view :class="hospitalPhone != '' ? 'round-action' : 'round-action is-disabled'" @tap="callHospital"><image src="/static/merchant-assets/phone.svg"></image></view></view></view><text v-if="!packageAvailable" class="unavailable-text">{{unavailableReason}}</text></view>
                <view class="tabs"><view class="tab-item active"><text>详情</text><view class="tab-line"></view></view></view>
                <view v-if="itemsLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载检查项目</text></view><view v-else-if="itemsError" class="list-state error-state"><text>检查项目加载失败</text><text class="retry-text" @tap="loadItems">重新加载</text></view><view v-else-if="itemsLoaded && packageItems.length == 0" class="list-state"><text>暂无体检检查项目</text></view>
                <view v-if="itemsLoaded && packageItems.length > 0" class="check-table"><view class="check-head"><view class="check-cell cell-item"><text>检查项目</text></view><view class="check-cell cell-meaning"><text>项目意义</text></view><view class="check-cell cell-gender"><text>男</text></view><view class="check-cell cell-gender cell-gender-end"><text>女</text></view></view><view class="check-row" v-for="item in packageItems" :key="item.id"><view class="check-cell cell-item"><text>{{item.name}}</text></view><view class="check-cell cell-meaning"><text>{{item.meaning}}</text></view><view class="check-cell cell-gender"><text>{{item.male ? '√' : ''}}</text></view><view class="check-cell cell-gender cell-gender-end"><text>{{item.female ? '√' : ''}}</text></view></view></view>
                <view v-if="detailImages.length > 0" class="detail-images"><image v-for="(imageUrl, index) in detailImages" :key="imageUrl + index" class="detail-image" :src="imageUrl" mode="widthFix" @error="dropDetailImage(index)"></image></view>
            </view><view class="content-bottom"></view>
        </view></scroll-view>
        <view class="action-bar"><view :class="canBook ? 'booking-button' : 'booking-button booking-disabled'" hover-class="booking-button-active" @tap="goBooking"><text>{{packageAvailable ? '预约' : '暂不可预约'}}</text></view></view>
    </view>
</template>
 
<script setup lang="uts">
    import { getMedicalPackage, getMedicalPackageItems } from '../../services/portal.uts'
    import { resolveApiUrl } from '../../services/http.uts'
    import { safeBack } from '../../utils/navigation.uts'
    const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const isSolidNav = ref(false)
    const packageId = ref(''); const hospitalId = ref(''); const routeApplicableCards = ref(''); const detailLoading = ref(true); const detailError = ref(false); const itemsLoading = ref(false); const itemsLoaded = ref(false); const itemsError = ref(false)
    const packageName = ref(''); const heroImage = ref(''); const freeLabel = ref('免费'); const applicableCards = ref(''); const packageAvailable = ref(false); const unavailableReason = ref('该套餐暂不可预约')
    const hospitalName = ref(''); const hospitalAddress = ref(''); const hospitalPhone = ref(''); const latitude = ref(0); const longitude = ref(0); const packageItems = ref<any[]>([])
    // 商户在「图文详情」上传的图片(可能多张),按顺序铺在检查项目表格下面。
    const detailImages = ref<string[]>([])
    const canNavigate = computed(() => latitude.value > 0 && longitude.value > 0)
    const canBook = computed(() => !detailLoading.value && !detailError.value && packageAvailable.value)
    const decodeRouteText = (value: any): string => {
        const text = value == null ? '' : String(value)
        try { return decodeURIComponent(text) } catch (error) { return text }
    }
    onLoad((options: any) => {
        // #ifdef MP-WEIXIN
        const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right
        // #endif
        packageId.value = options != null && options['packageId'] != null ? options['packageId'] as string : ''; hospitalId.value = options != null && options['hospitalId'] != null ? options['hospitalId'] as string : ''; routeApplicableCards.value = options != null ? decodeRouteText(options['applicableCards']) : ''; applicableCards.value = routeApplicableCards.value; loadDetail()
    })
    const loadDetail = () => {
        if (packageId.value == '') { detailLoading.value = false; detailError.value = true; return }
        detailLoading.value = true; detailError.value = false
        getMedicalPackage(packageId.value, (data: any) => { const hospital: any = data.hospital || {}; packageName.value = data.name || ''; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''; freeLabel.value = data.freeLabel || '免费'; const apiApplicableCards = data.applicableCards != null ? String(data.applicableCards).trim() : ''; applicableCards.value = apiApplicableCards != '' ? apiApplicableCards : routeApplicableCards.value; packageAvailable.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; hospitalId.value = String(hospital.hospitalId || hospitalId.value); hospitalName.value = hospital.name || ''; hospitalAddress.value = hospital.address || '医院暂未配置地址'; hospitalPhone.value = hospital.mobile || ''; latitude.value = Number(hospital.latitude); longitude.value = Number(hospital.longitude); const images: string[] = []; const sourceImages: any[] = data.detailImages instanceof Array ? data.detailImages as any[] : []; for (let index = 0; index < sourceImages.length; index++) { const value = sourceImages[index] == null ? '' : ('' + sourceImages[index]).trim(); if (value.length > 0) images.push(resolveApiUrl(value)) }; detailImages.value = images; detailLoading.value = false; loadItems() }, () => { detailLoading.value = false; detailError.value = true })
    }
    const loadItems = () => { if (packageId.value == '') return; itemsLoading.value = true; itemsLoaded.value = false; itemsError.value = false; getMedicalPackageItems(packageId.value, (data: any) => { packageItems.value = data instanceof Array ? data as any[] : []; itemsLoading.value = false; itemsLoaded.value = true }, () => { itemsLoading.value = false; itemsError.value = true }) }
    const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
    const handleScroll = (event: any) => { const show = event.detail.scrollTop > 64; if (isSolidNav.value != show) isSolidNav.value = show }
    const replaceHero = () => { heroImage.value = '' }
    const dropDetailImage = (index: number) => { const next = detailImages.value.slice(); next.splice(index, 1); detailImages.value = next }
    const openMap = () => { if (!canNavigate.value) { uni.showToast({ title: '医院暂未配置位置', icon: 'none' }); return }; uni.openLocation({ latitude: latitude.value, longitude: longitude.value, name: hospitalName.value, address: hospitalAddress.value, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) }
    const callHospital = () => { if (hospitalPhone.value == '') { uni.showToast({ title: '医院暂未配置联系电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: hospitalPhone.value, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) }
    const goBooking = () => { if (!canBook.value) { if (!detailLoading.value && !detailError.value) uni.showToast({ title: unavailableReason.value, icon: 'none' }); return }; uni.navigateTo({ url: '/pages/medical-booking/medical-booking?packageId=' + encodeURIComponent(packageId.value) + '&hospitalId=' + encodeURIComponent(hospitalId.value) + '&package=' + encodeURIComponent(packageName.value) + '&hospital=' + encodeURIComponent(hospitalName.value) }) }
</script>
 
<style>
    .page { width:100%; height:100vh; background-color:#F6F6F6; }.content { height:calc(100% - 150rpx); height:calc(100% - 150rpx - env(safe-area-inset-bottom)); background-color:#F6F6F6; }.hero { height:445rpx; position:relative; overflow:hidden; background-color:#3B3028; }.hero-image { position:absolute; left:0; top:0; width:100%; height:445rpx; }.hero-shade { position:absolute; left:0; right:0; top:0; height:190rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.58),rgba(0,0,0,0)); }.custom-nav { position:fixed; z-index:20; left:0; right:0; top:0; padding-bottom:20rpx; }.custom-nav-bg { position:absolute; z-index:0; left:0; right:0; top:0; bottom:0; opacity:0; background-image:linear-gradient(180deg,#F26A38,#EE542B); transition:opacity 280ms ease-out; }.custom-nav.is-solid .custom-nav-bg { opacity:1; }.safe-space { height:28px; position:relative; z-index:2; }.nav-row { display:flex; box-sizing:border-box; padding-left:24rpx; position:relative; z-index:2; flex-direction:row; align-items:center; justify-content:space-between; }.back-button { display:flex; width:54rpx; height:54rpx; border-radius:50%; background-color:rgba(45,45,45,.52); justify-content:center; align-items:center; }.custom-nav.is-solid .back-button { background-color:rgba(45,45,45,0); }.back-button image { width:34rpx; height:34rpx; }.nav-title { position:absolute; left:105rpx; right:105rpx; opacity:0; color:#FFF; font-size:29rpx; font-weight:600; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.custom-nav.is-solid .nav-title { opacity:1; }.capsule-space { height:100%; flex-shrink:0; }.package-panel { position:relative; z-index:3; min-height:600rpx; margin-top:-22rpx; border-radius:27rpx 27rpx 0 0; overflow:hidden; background-color:#F6F6F6; }.package-info-card { padding:25rpx 38rpx 20rpx; }.package-title-row { display:flex; min-height:50rpx; align-items:center; justify-content:space-between; }.package-title { min-width:0; flex:1; color:#292929; font-size:35rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.free-tag { margin-left:15rpx; color:#FF7C2B; font-size:26rpx; }.service-tags { display:flex; margin-top:16rpx; }.service-tags text { padding:6rpx 11rpx; border:1rpx solid #FF9B70; border-radius:5rpx; color:#EF7046; font-size:24rpx; }.hospital-name { display:block; margin-top:20rpx; color:#292929; font-size:30rpx; font-weight:700; }.address-row { display:flex; min-height:72rpx; margin-top:12rpx; align-items:flex-start; }.address-icon { width:27rpx; height:27rpx; margin-top:4rpx; margin-right:8rpx; flex-shrink:0; }.address-text { width:420rpx; min-width:0; color:#7B8491; font-size:24rpx; line-height:34rpx; }.address-actions { display:flex; margin-left:auto; flex-shrink:0; }.round-action { display:flex; width:52rpx; height:52rpx; border-radius:50%; background-color:#F2F3F5; justify-content:center; align-items:center; }.round-action + .round-action { margin-left:30rpx; }.round-action image { width:27rpx; height:27rpx; }.is-disabled { opacity:.42; }.unavailable-text { display:block; margin-top:10rpx; color:#D96B3B; font-size:23rpx; }.tabs { display:flex; height:85rpx; padding:0 38rpx; align-items:center; }.tab-item { display:flex; height:85rpx; position:relative; color:#303030; font-size:32rpx; font-weight:600; align-items:center; }.tab-line { position:absolute; left:50%; bottom:0; width:59rpx; height:7rpx; margin-left:-30rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4D36,#FFBF3A); }.detail-state,.list-state { display:flex; height:330rpx; margin:16rpx 25rpx 0; color:#969CA5; font-size:24rpx; align-items:center; justify-content:center; }.detail-state { margin-top:0; }.error-state { flex-direction:column; }.retry-text { margin-top:20rpx; color:#F26A38; }.loading-spinner { width:28rpx; height:28rpx; margin-right:13rpx; box-sizing:border-box; border:3rpx solid #FFD6C4; border-top-color:#FF6C2B; border-radius:50%; animation:medical-loading 720ms linear infinite; } @keyframes medical-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }.check-table { margin:16rpx 25rpx 0; border:1rpx solid #E3E6EB; border-radius:8rpx; overflow:hidden; background-color:#FFF; }.check-head,.check-row { display:flex; }.check-head { background-color:#F5F6F8; }.check-row { border-top:1rpx solid #E3E6EB; }.check-cell { display:flex; box-sizing:border-box; padding:18rpx 14rpx; border-left:1rpx solid #E3E6EB; align-items:center; justify-content:center; }.cell-item { width:180rpx; border-left:0; flex-shrink:0; }.cell-meaning { min-width:0; flex:1; justify-content:flex-start; }.cell-gender { width:84rpx; flex-shrink:0; }.check-head .check-cell text { color:#30343A; font-size:24rpx; font-weight:700; }.check-cell text { color:#4A5159; font-size:22rpx; line-height:32rpx; }.cell-gender text { color:#30343A; font-size:26rpx; }.content-bottom { height:35rpx; }.action-bar { display:flex; height:150rpx; height:calc(150rpx + env(safe-area-inset-bottom)); box-sizing:border-box; padding:20rpx 34rpx 2rpx; padding-bottom:calc(2rpx + env(safe-area-inset-bottom)); background:#FFF; box-shadow:0 -5rpx 15rpx rgba(0,0,0,.06); align-items:flex-end; }.booking-button { display:flex; width:100%; height:90rpx; border-radius:46rpx; background-image:linear-gradient(90deg,#FF4D32,#FF8A20); justify-content:center; align-items:center; }.booking-button text { color:#FFF; font-size:31rpx; }.booking-disabled { background-image:none; background-color:#C6CBD2; }.booking-button-active { opacity:.82; transform:scale(.99); }
    .package-title-row,
    .service-tags,
    .address-row,
    .address-actions,
    .tabs,
    .tab-item,
    .detail-state,
    .list-state,
    .check-head,
    .check-row {
        display: flex;
        flex-direction: row;
    }
    .service-tags {
        align-items: flex-start;
    }
    .service-tags text {
        flex-shrink: 0;
        white-space: nowrap;
    }
    .error-state {
        flex-direction: column;
    }
    .check-head,
    .check-row {
        width: 100%;
        align-items: stretch;
    }
    .check-cell {
        display: flex;
        flex-direction: row;
    }
    /* 商户在「图文详情」上传的多张图片:整组铺在检查项目表格下面,按原图比例完整显示。 */
    .detail-images {
        margin: 16rpx 25rpx 0;
    }
    .detail-image {
        display: block;
        width: 100%;
        margin-bottom: 16rpx;
        border-radius: 8rpx;
        background-color: #F2F4F7;
    }
</style>