shinnosuke
2026-09-16 c5a63165aca6468157e0b4807d511e828de2076a
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
<template>
    <view class="page">
        <image class="hero-image" :src="heroImage" mode="aspectFill" @error="replaceHero"></image>
        <view class="hero-shade"></view>
        <view class="custom-nav">
            <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" mode="aspectFit"></image></view>
                <view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
            </view>
        </view>
 
        <view class="content-sheet">
            <view class="service-row"><text class="service-name">{{benefitTitle}}</text></view>
            <view class="section-title"><text>权益详情</text><view></view></view>
            <scroll-view class="org-scroll" scroll-y :show-scrollbar="false">
                <view v-if="listLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载服务机构</text></view>
                <view v-else-if="listError" class="list-state error-state"><text>服务机构加载失败</text><text class="retry-text" @tap="loadOrgs">重新加载</text></view>
                <view v-else-if="listLoaded && orgs.length == 0" class="list-state"><text>暂无行政审批服务机构</text></view>
                <text v-if="listLoaded && !benefitAvailable" class="unavailable-text">{{unavailableReason}}</text>
                <view v-for="item in orgs" :key="item.id" class="org-card">
                    <text class="org-name">{{item.name}}</text>
                    <view class="org-address"><image src="/static/premium-assets/map-pin.png" mode="aspectFit"></image><text>{{item.address}}</text></view>
                    <view class="org-desc"><text>{{item.description}}</text></view>
                    <view class="org-footer">
                        <view v-if="item.phone != ''" class="phone-row" @tap="callOrg(item)"><image src="/static/premium-assets/phone.png" mode="aspectFit"></image><text>咨询电话-{{item.phone}}</text></view>
                        <view v-if="item.hasWindowInfo" class="window-button" hover-class="window-button-active" @tap="openWindowInfo(item)"><text>查看窗口信息</text></view>
                    </view>
                </view>
                <view class="bottom-space"></view>
            </scroll-view>
        </view>
 
    </view>
</template>
 
<script setup lang="uts">
    import { getAdminApprovalBenefit } 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 serviceId = ref('')
    const heroImage = ref('')
    const benefitTitle = ref('行政审批')
    const benefitAvailable = ref(true)
    const unavailableReason = ref('该权益暂未开放')
    const orgs = ref<any[]>([])
    const listLoading = ref(true)
    const listLoaded = ref(false)
    const listError = ref(false)
 
    const uploadUrl = (value: any): string => {
        if (value == null) return ''
        if (typeof value == 'object') {
            const keys = ['url', 'path', 'fileUrl', 'file_url', 'downloadUrl', 'download_url']
            for (const key of keys) {
                if (value[key] != null && String(value[key]).trim() != '') return String(value[key]).trim()
            }
            return ''
        }
        return String(value).trim()
    }
    const normalizeOrganization = (source: any): any => {
        const item = source != null ? source : {}
        const keys = ['windowInfoUrl', 'window_info_url', 'pdfFile', 'pdf_file', 'windowInfo', 'window_info', 'windowFile', 'window_file', 'fileUrl', 'file_url']
        let windowInfoUrl = ''
        for (const key of keys) {
            const candidate = uploadUrl(item[key])
            if (candidate != '') { windowInfoUrl = candidate; break }
        }
        const normalized: any = { ...item }
        normalized.windowInfoUrl = windowInfoUrl
        normalized.hasWindowInfo = windowInfoUrl != ''
        if (normalized.phone == null) normalized.phone = ''
        return normalized
    }
 
    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
        serviceId.value = options != null && options['serviceId'] != null ? options['serviceId'] as string : ''
        loadOrgs()
    })
    const loadOrgs = () => {
        listLoading.value = true
        listLoaded.value = false
        listError.value = false
        if (serviceId.value == '') { listLoading.value = false; listError.value = true; return }
        getAdminApprovalBenefit(serviceId.value, (data: any) => {
            benefitTitle.value = data.title || '行政审批'
            heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''
            benefitAvailable.value = data.available === true
            unavailableReason.value = data.unavailableReason || '该权益暂未开放'
            orgs.value = data.organizations instanceof Array ? (data.organizations as any[]).map(normalizeOrganization) : []
            listLoading.value = false
            listLoaded.value = true
        }, () => { listLoading.value = false; listError.value = true })
    }
    const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
    const replaceHero = () => { heroImage.value = '' }
    const callOrg = (item: any) => { if (item.phone == null || item.phone == '') { uni.showToast({ title: '机构暂未提供咨询电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: item.phone, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) }
    const openWindowInfo = (item: any) => {
        const normalized = normalizeOrganization(item)
        const source = normalized.windowInfoUrl
        if (source == '') {
            uni.showToast({ title: '窗口信息文件暂未配置', icon: 'none' })
            return
        }
        const url = resolveApiUrl(source.indexOf('/') == 0 || source.indexOf('http://') == 0 || source.indexOf('https://') == 0 ? source : '/' + source)
        uni.showLoading({ title: '正在打开文件', mask: true })
        uni.downloadFile({
            url: url,
            success: (result: any) => {
                if (result.statusCode != 200 || result.tempFilePath == null || result.tempFilePath == '') {
                    uni.showToast({ title: '窗口信息文件加载失败', icon: 'none' })
                    return
                }
                uni.openDocument({
                    filePath: result.tempFilePath as string,
                    fileType: 'pdf',
                    showMenu: true,
                    fail: () => uni.showToast({ title: '暂时无法打开 PDF 文件', icon: 'none' })
                })
            },
            fail: () => uni.showToast({ title: '窗口信息文件加载失败', icon: 'none' }),
            complete: () => uni.hideLoading()
        })
    }
</script>
 
<style>
    .page { width:100%; height:100vh; position:relative; overflow:hidden; background-color:#F5F5F5; }
    .hero-image { position:absolute; left:0; right:0; top:0; width:100%; height:425rpx; background-color:#C6D2DE; }
    .hero-shade { position:absolute; z-index:1; left:0; right:0; top:0; height:210rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.55),rgba(0,0,0,0)); }
    .custom-nav { position:absolute; z-index:10; left:0; right:0; top:0; }
    .safe-space { height:28px; }
    .nav-row { display:flex; box-sizing:border-box; padding-left:24rpx; 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,.38); justify-content:center; align-items:center; }
    .back-button image { width:38rpx; height:38rpx; }
    .capsule-space { height:100%; flex-shrink:0; }
    .content-sheet { display:flex; position:absolute; z-index:3; left:0; right:0; top:405rpx; bottom:0; box-sizing:border-box; padding:28rpx 25rpx 0; border-radius:30rpx 30rpx 0 0; background-color:#F5F5F5; flex-direction:column; }
    .service-row { display:flex; height:64rpx; padding:0 10rpx; flex-direction:row; justify-content:space-between; align-items:center; flex-shrink:0; }
    .service-name { color:#252525; font-size:35rpx; line-height:48rpx; font-weight:700; }
    .free-label { color:#FF7A27; font-size:24rpx; }
    .section-title { display:flex; height:84rpx; margin-top:15rpx; padding-left:20rpx; flex-direction:column; align-items:flex-start; flex-shrink:0; }
    .section-title text { color:#756B67; font-size:31rpx; line-height:42rpx; font-weight:600; }
    .section-title view { width:61rpx; height:7rpx; margin:10rpx 0 0 28rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4C56,#FFD43A); }
    .org-scroll { min-height:0; flex:1; }
    .list-state { display:flex; height:360rpx; flex-direction:row; justify-content:center; align-items:center; color:#969CA5; font-size:25rpx; }
    .error-state { flex-direction:column; }
    .retry-text { margin-top:20rpx; color:#FF742A; }
    .unavailable-text { display:block; margin:0 20rpx 20rpx; color:#D96B3B; font-size:24rpx; line-height:34rpx; }
    .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:org-loading 720ms linear infinite; }
    @keyframes org-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
    .org-card { display:flex; margin:0 0 22rpx; box-sizing:border-box; padding:28rpx 26rpx; border-radius:15rpx; background-color:#FFFFFF; flex-direction:column; }
    .org-name { color:#252525; font-size:30rpx; line-height:42rpx; font-weight:700; }
    .org-address { display:flex; margin-top:16rpx; box-sizing:border-box; padding:7rpx 10rpx; border-radius:7rpx; background-image:linear-gradient(90deg,rgba(239,240,242,.98),rgba(242,243,245,.76) 58%,rgba(255,255,255,0)); flex-direction:row; align-items:center; }
    .org-address image { width:26rpx; height:26rpx; margin-right:8rpx; flex-shrink:0; }
    .org-address text { min-width:0; flex:1; color:#7E8490; font-size:24rpx; line-height:34rpx; }
    .org-desc { margin-top:18rpx; box-sizing:border-box; padding:20rpx 22rpx; border-radius:12rpx; background-color:#F5F6F8; }
    .org-desc text { color:#727983; font-size:25rpx; line-height:40rpx; }
    .org-footer { display:flex; min-height:56rpx; margin-top:20rpx; flex-direction:row; justify-content:space-between; align-items:center; }
    .phone-row { display:flex; flex-direction:row; align-items:center; }
    .phone-row image { width:28rpx; height:28rpx; margin-right:10rpx; flex-shrink:0; }
    .phone-row text { color:#FF7A27; font-size:26rpx; line-height:36rpx; }
    .window-button { display:flex; box-sizing:border-box; padding:10rpx 26rpx; border:2rpx solid #FF8A3C; border-radius:30rpx; background-color:#FFFFFF; justify-content:center; align-items:center; }
    .window-button text { color:#FF7A27; font-size:24rpx; line-height:32rpx; }
    .window-button-active { opacity:.7; }
    .bottom-space { height:calc(30rpx + env(safe-area-inset-bottom)); }
</style>