shinnosuke
2026-09-10 65b57875700bf42ebd17eafa01a265e5690ce271
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
<template>
    <view class="page">
        <view class="page-header">
            <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" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
                <text class="nav-title">{{merchantName}}</text>
                <view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
            </view>
        </view>
 
        <view v-if="merchantLoading" class="state-block"><view class="loading-spinner"></view><text>正在加载洗车服务</text></view>
        <view v-else-if="merchantError" class="state-block error-state"><text>洗车服务加载失败</text><text class="retry-text" @tap="loadMerchantDetail">重新加载</text></view>
        <view v-else class="content">
            <view class="service-card">
                <view class="service-title-row">
                    <view class="service-icon"><image src="/static/merchant-assets/car-wash-service-orange.svg" mode="aspectFit"></image></view>
                    <text class="service-title">{{merchantName}}</text>
                </view>
                <view class="talent-tag"><text>{{applicableCards}}</text></view>
                <view v-if="externalLinkType != 'miniProgram'" class="rights-card">
                    <text class="rights-label">权益详情</text>
                    <text class="rights-content">{{rightsText}}</text>
                </view>
                <text v-else class="service-desc">服务点:{{servicePointText}}</text>
                <text v-if="externalLinkType != 'miniProgram' && tipText != ''" class="service-tip">{{tipText}}</text>
                <view v-if="!available" class="unavailable-note"><text>{{unavailableReason}}</text></view>
            </view>
            <text v-if="externalLinkType == 'miniProgram' && tipText != ''" class="service-tip mini-program-tip">{{tipText}}</text>
 
 
        </view>
 
        <view class="action-bar">
            <view class="wash-button" :class="!available || merchantLoading || merchantError ? 'is-disabled' : ''" @tap="openExternalService">
                <image src="/static/merchant-assets/car-wash-white.svg" mode="aspectFit"></image>
                <text v-if="externalLinkType == 'miniProgram' || externalLinkType == 'web'">去洗车</text>
                <text v-else>提交手机号</text>
            </view>
        </view>
 
        <view v-if="showPhoneForm" class="phone-mask" @tap="closePhoneForm">
            <view class="phone-dialog" @tap.stop>
                <view class="phone-dialog-close" @tap="closePhoneForm"><text>×</text></view>
                <text class="phone-title">填写手机号</text>
                <text class="phone-desc">提交后商家将根据手机号与您联系</text>
                <view class="phone-input-box"><input v-model="phone" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="phone-placeholder" /></view>
                <text v-if="phoneError != ''" class="phone-error">{{phoneError}}</text>
                <view class="phone-submit" @tap="submitPhone"><text>提交</text></view>
            </view>
        </view>
    </view>
</template>
 
<script setup lang="uts">
    import { isApiConfigured } from '../../config/api.uts'
    import { getCarWashMerchantDetail, getTalentCardProfile, submitCarWashCredential } from '../../services/portal.uts'
    import { getSession } from '../../services/session.uts'
    import { safeBack } from '../../utils/navigation.uts'
 
    const navTop = ref(28)
    const navHeight = ref(32)
    const navWidth = ref(88)
    const navRight = ref(10)
    const merchantId = ref('')
    const merchantName = ref('自助洗车服务')
    const applicableCards = ref('')
    const servicePointText = ref('')
    const rightsText = ref('')
    const tipText = ref('')
    const available = ref(false)
    const unavailableReason = ref('洗车服务当前不可用')
    const externalLinkType = ref('')
    const externalMiniProgramShortLink = ref('')
    const externalMiniProgramPath = ref('')
    const externalWebUrl = ref('')
    const talentId = ref('')
    const cardId = ref('')
    const showPhoneForm = ref(false)
    const phone = ref('')
    const phoneSubmitting = ref(false)
    const phoneError = ref('')
    const merchantLoading = ref(true)
    const merchantError = ref(false)
 
    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
        if (options != null && options['merchantId'] != null) merchantId.value = options['merchantId'] as string
        if (options != null && options['name'] != null && merchantName.value == '自助洗车服务') merchantName.value = decodeURIComponent(options['name'] as string)
        const session = getSession()
        if (session != null && session.talentId != null) talentId.value = session.talentId as string
        if (talentId.value != '') {
            getTalentCardProfile(talentId.value, (data: any) => {
                const card = data != null ? data.card : null
                cardId.value = card != null && card.id != null ? '' + card.id : ''
                loadMerchantDetail()
            }, () => { loadMerchantDetail() })
        } else {
            loadMerchantDetail()
        }
    })
 
    const goBack = () => safeBack('/pages/transport-benefits/transport-benefits')
    const loadMerchantDetail = () => {
        merchantLoading.value = true
        merchantError.value = false
        available.value = false
        if (merchantId.value == '' || !isApiConfigured()) {
            merchantLoading.value = false
            merchantError.value = true
            return
        }
        getCarWashMerchantDetail(merchantId.value, cardId.value, (data: any) => {
            if (data == null) { merchantLoading.value = false; merchantError.value = true; return }
            merchantName.value = data.merchantName != null && data.merchantName != '' ? data.merchantName as string : merchantName.value
            const linkType = data.externalLinkType != null ? data.externalLinkType as string : ''
            if (linkType == '') {
                uni.redirectTo({ url: '/pages/merchant/merchant?merchantType=carWash&merchantId=' + encodeURIComponent(merchantId.value) + '&name=' + encodeURIComponent(merchantName.value) })
                return
            }
            applicableCards.value = data.applicableCards != null && data.applicableCards != '' ? data.applicableCards as string : '请咨询商家'
            servicePointText.value = data.servicePointText != null && data.servicePointText != '' ? data.servicePointText as string : '请咨询商家'
            rightsText.value = data.rightsText != null && data.rightsText != '' ? data.rightsText as string : servicePointText.value
            tipText.value = data.tipText != null ? data.tipText as string : ''
            available.value = data.available === true
            unavailableReason.value = data.unavailableReason != null && data.unavailableReason != '' ? data.unavailableReason as string : '洗车服务当前不可用'
            externalLinkType.value = linkType
            externalMiniProgramShortLink.value = data.externalMiniProgramShortLink != null ? data.externalMiniProgramShortLink as string : ''
            externalMiniProgramPath.value = data.externalMiniProgramPath != null ? data.externalMiniProgramPath as string : ''
            externalWebUrl.value = data.externalWebUrl != null ? data.externalWebUrl as string : ''
            merchantLoading.value = false
        }, () => { merchantLoading.value = false; merchantError.value = true })
    }
    const closePhoneForm = () => { showPhoneForm.value = false; phoneError.value = '' }
    const submitPhone = () => {
        if (phoneSubmitting.value) return
        const value = phone.value.trim()
        if (!/^1[3-9][0-9]{9}$/.test(value)) {
            phoneError.value = '请输入正确的 11 位手机号'
            return
        }
        if (merchantId.value == '' || talentId.value == '' || cardId.value == '') {
            phoneError.value = '请登录后提交手机号'
            return
        }
        phoneSubmitting.value = true
        phoneError.value = ''
        const key = 'car-wash-credential-' + merchantId.value + '-' + talentId.value + '-' + Date.now()
        submitCarWashCredential(merchantId.value, talentId.value, cardId.value, value, key, (data: any) => {
            phoneSubmitting.value = false
            showPhoneForm.value = false
            phone.value = ''
            uni.showToast({ title: '手机号已提交', icon: 'success' })
            setTimeout(() => safeBack('/pages/transport-benefits/transport-benefits'), 800)
        }, (error) => {
            phoneSubmitting.value = false
            phoneError.value = error.message
        })
    }
    const openExternalService = () => {
        if (merchantLoading.value || merchantError.value) return
        if (!available.value) { uni.showToast({ title: unavailableReason.value, icon: 'none' }); return }
        if (externalLinkType.value == 'miniProgram' && externalMiniProgramShortLink.value != '') {
            uni.navigateToMiniProgram({
                shortLink: externalMiniProgramShortLink.value,
                envVersion: 'release',
                fail: () => uni.showToast({ title: '暂时无法打开洗车服务', icon: 'none' })
            })
            return
        }
        if (externalLinkType.value == 'web' && externalWebUrl.value.startsWith('https://')) {
            uni.navigateTo({ url: '/pages/external-webview/external-webview?source=carWash&merchantId=' + encodeURIComponent(merchantId.value) + '&talentCardId=' + encodeURIComponent(cardId.value) })
            return
        }
        showPhoneForm.value = true
    }
</script>
 
<style>
    .page { display: flex; width: 100%; height: 100vh; background-color: #F4F7FB; flex-direction: column; }
    .page-header { position: relative; z-index: 10; flex-shrink: 0; padding-bottom: 20rpx; background-image: linear-gradient(180deg, #FFE6BD 0%, #FFF0D9 100%); }
    .safe-space { height: 28px; }
    .nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
    .back-button { display: flex; width: 70rpx; height: 64rpx; margin-left: -4rpx; justify-content: center; align-items: center; }
    .back-button image { width: 36rpx; height: 36rpx; }
    .back-button-active { opacity: .55; }
    .nav-title { position: absolute; left: 50%; top: 50%; color: #171B22; font-size: 37rpx; line-height: 48rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
    .capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
    .state-block { display: flex; min-height: 360rpx; color: #8E98A6; font-size: 26rpx; flex-direction: column; justify-content: center; align-items: center; }
    .error-state { flex-direction: column; }
    .retry-text { margin-top: 22rpx; color: #F26A38; }
    .loading-spinner { width: 28rpx; height: 28rpx; margin-right: 14rpx; box-sizing: border-box; border-width: 3rpx; border-style: solid; border-color: #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: wash-loading 720ms linear infinite; }
    @keyframes wash-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
    .content { min-height: 0; box-sizing: border-box; padding: 30rpx 40rpx 30rpx; flex: 1; overflow-y: auto; }
    .service-card { box-sizing: border-box; padding: 31rpx 34rpx 27rpx; border-radius: 28rpx; background-color: #FFFFFF; box-shadow: 0 13rpx 30rpx rgba(63,74,89,.07); }
    .service-title-row { display: flex; flex-direction: row; align-items: center; }
    .service-icon { display: flex; width: 80rpx; height: 80rpx; border-radius: 50%; background-color: #FFF3E6; justify-content: center; align-items: center; flex-shrink: 0; }
    .service-icon image { width: 48rpx; height: 48rpx; }
    .service-title { min-width: 0; flex: 1; margin-left: 21rpx; color: #202631; font-size: 34rpx; line-height: 47rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .talent-tag { display: flex; max-width: 100%; width: fit-content; min-height: 47rpx; margin-top: 25rpx; box-sizing: border-box; padding: 8rpx 17rpx; border-radius: 24rpx; background-image: linear-gradient(90deg, #FFF0E5 0%, #FFE8D7 100%); justify-content: center; align-items: center; }
    .talent-tag text { max-width: 540rpx; color: #D86B35; font-size: 23rpx; line-height: 31rpx; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .service-desc { display: block; margin-top: 24rpx; color: #6F7887; font-size: 25rpx; line-height: 39rpx; }
    .rights-card { display: flex; margin-top: 24rpx; box-sizing: border-box; padding: 24rpx; border-radius: 20rpx; background-color: #FFF8F2; flex-direction: column; }
    .rights-label { color: #D86B35; font-size: 24rpx; line-height: 34rpx; font-weight: 700; }
    .rights-content { margin-top: 10rpx; color: #4D5663; font-size: 26rpx; line-height: 40rpx; }
    .service-tip { display: block; margin-top: 16rpx; color: #9CA6B5; font-size: 22rpx; line-height: 34rpx; }
    .mini-program-tip { margin: 27rpx 40rpx 0; text-align: center; }
    .unavailable-note { margin-top: 16rpx; text-align: center; }
    .unavailable-note text { color: #D86B35; font-size: 23rpx; line-height: 34rpx; }
    .phone-mask { display: flex; position: fixed; z-index: 50; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0,0,0,.45); justify-content: center; align-items: center; }
    .phone-dialog { display: flex; width: 590rpx; box-sizing: border-box; padding: 44rpx 40rpx 36rpx; border-radius: 26rpx; background-color: #FFFFFF; position: relative; flex-direction: column; align-items: center; }
    .phone-dialog-close { display: flex; width: 60rpx; height: 60rpx; position: absolute; top: 12rpx; right: 12rpx; justify-content: center; align-items: center; }
    .phone-dialog-close text { color: #9AA3AE; font-size: 36rpx; line-height: 60rpx; }
    .phone-title { color: #262D36; font-size: 31rpx; font-weight: 700; }
    .phone-desc { margin-top: 12rpx; color: #8A94A1; font-size: 24rpx; }
    .phone-input-box { display: flex; width: 100%; height: 88rpx; margin-top: 28rpx; box-sizing: border-box; padding: 0 24rpx; border-radius: 16rpx; background-color: #F5F7FA; justify-content: center; align-items: center; }
    .phone-input-box input { width: 100%; color: #2D333C; font-size: 28rpx; }
    .phone-placeholder { color: #BBC1CA; }
    .phone-error { margin-top: 12rpx; color: #F05C3A; font-size: 22rpx; }
    .phone-submit { display: flex; width: 100%; height: 84rpx; margin-top: 28rpx; border-radius: 42rpx; background-image: linear-gradient(90deg, #FF4B32 0%, #FF8A1E 100%); justify-content: center; align-items: center; }
    .phone-submit text { color: #FFFFFF; font-size: 28rpx; font-weight: 600; }
    .action-bar { display: flex; flex-shrink: 0; box-sizing: border-box; padding: 17rpx 40rpx calc(17rpx + env(safe-area-inset-bottom)); background-color: #FFFFFF; box-shadow: 0 -5rpx 16rpx rgba(0,0,0,.04); }
    .wash-button { display: flex; width: 100%; height: 88rpx; border-radius: 45rpx; background-image: linear-gradient(90deg, #FF4B32 0%, #FF8A1E 100%); flex-direction: row; justify-content: center; align-items: center; }
    .wash-button.is-disabled { background-image: none; background-color: #C7CDD5; }
    .wash-button image { width: 36rpx; height: 36rpx; margin-right: 18rpx; }
    .wash-button text { color: #FFFFFF; font-size: 30rpx; font-weight: 600; }
</style>