shinnosuke
2026-08-12 f40c2939c6f784fc6721554794154a336d12bc3a
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
<template>
    <view class="page">
        <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" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg"></image></view><text class="nav-title">人才政策</text><view class="capsule-space" :style="'width:' + navWidth + 'px'"></view></view></view>
        <scroll-view class="content" scroll-y>
            <view v-if="initialLoading" class="detail-state"><view class="loading-spinner"></view><text>正在加载政策详情</text></view>
            <view v-else-if="policyUnavailable" class="detail-state error-state"><text>政策已失效</text><text class="retry-text" @tap="goBack">返回政策列表</text></view>
            <view v-else-if="initialError" class="detail-state error-state"><text>政策详情加载失败</text><text class="retry-text" @tap="loadPolicy">重新加载</text></view>
            <view v-else>
                <image class="detail-hero" :src="policyCover" mode="aspectFill" @error="useHeroFallback"></image>
                <view class="policy-summary"><view class="summary-title-row"><text class="summary-title">{{policyTitle}}</text><text class="summary-tag">{{policyCategory}}</text></view><text class="summary-desc">{{policySummary}}</text></view>
                <view class="explanation-card"><image v-for="(imageUrl, index) in policyContentImages" :key="imageUrl + index" class="content-image" :src="imageUrl" mode="aspectFill" @error="useContentImageFallback(index)"></image><text class="explanation-title" :class="policyContentImages.length == 0 ? 'explanation-title-first' : ''">政策说明</text><text v-if="policyContent.length > 0" class="explanation-text">{{policyContent}}</text><text v-else class="explanation-empty">暂无政策说明</text></view>
            </view>
            <view class="content-bottom"></view>
        </scroll-view>
    </view>
</template>
 
<script setup lang="uts">
    import { getPolicyDetail } 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 policyTitle = ref('安家补助政策')
    const policySummary = ref('最高30万安家补助,按人才类别分档申请。')
    const policyCategory = ref('东区政策')
    const policyContent = ref('')
    const policyContentImages = ref<string[]>([])
    const policyCover = ref('/static/home-assets/home-banner-v2.jpg')
    const policyId = ref('')
    const initialLoading = ref(true)
    const initialError = ref(false)
    const policyUnavailable = ref(false)
    const policyContentText = (data: any): string => {
        const extracted = data != null && data.contentText != null ? (data.contentText as string).trim() : ''
        if (extracted != '') return extracted
        const raw = data != null && data.content != null ? data.content as string : ''
        return raw.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim()
    }
 
    onLoad((options: any) => {
        if (options != null && options.id != null) policyId.value = decodeURIComponent(options.id as string)
        // #ifdef MP-WEIXIN
        const capsule = uni.getMenuButtonBoundingClientRect()
        const systemInfo = uni.getSystemInfoSync()
        navTop.value = capsule.top
        navHeight.value = capsule.height
        navWidth.value = capsule.width
        navRight.value = systemInfo.windowWidth - capsule.right
        // #endif
        loadPolicy()
    })
    const loadPolicy = () => {
        if (policyId.value.length == 0) { initialLoading.value = false; initialError.value = true; return }
        initialLoading.value = true
        initialError.value = false
        policyUnavailable.value = false
        getPolicyDetail(policyId.value, (data: any) => {
            policyTitle.value = data != null && data.title != null ? data.title as string : '人才政策'
            policySummary.value = data != null && data.subtitle != null ? data.subtitle as string : ''
            policyCategory.value = data != null && data.subtitle != null && (data.subtitle as string).trim() != '' ? data.subtitle as string : '人才政策'
            // 正文直接取后台政策内容;contentText 为空时兼容后台仅返回富文本 content 的情况。
            policyContent.value = policyContentText(data)
            policyCover.value = '/static/home-assets/home-banner-v2.jpg'
            const heroImage = data != null && data.heroImage != null ? data.heroImage : (data != null ? data.coverUrl : null)
            if (heroImage != null && (heroImage as string).length > 0) policyCover.value = resolveApiUrl(heroImage as string)
            const images: string[] = []
            if (data != null && data.contentImages instanceof Array) {
                const sourceImages = data.contentImages as any[]
                for (let index = 0; index < sourceImages.length; index++) {
                    if (sourceImages[index] != null && (sourceImages[index] as string).length > 0) images.push(resolveApiUrl(sourceImages[index] as string))
                }
            }
            policyContentImages.value = images
            initialLoading.value = false
        }, (error: any) => {
            initialLoading.value = false
            policyUnavailable.value = error != null && error.statusCode == 404
            initialError.value = !policyUnavailable.value
        })
    }
    const useHeroFallback = () => { policyCover.value = '/static/home-assets/home-banner-v2.jpg' }
    const useContentImageFallback = (index: number) => {
        const next = policyContentImages.value.slice()
        next[index] = '/static/policy-assets/policy-explanation.jpg'
        policyContentImages.value = next
    }
    const goBack = () => safeBack('/pages/policy/policy')
</script>
 
<style>
    .page { display: flex; width: 100%; height: 100vh; background-color: #FFFFFF; flex-direction: column; }
    .custom-nav { flex-shrink: 0; padding-bottom: 30rpx; background-image: linear-gradient(180deg, #FFE4BC 0%, #FFF1DA 100%); }
    .safe-space { height: 28px; }
    .nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 26rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
    .back-button { display: flex; width: 60rpx; height: 60rpx; margin-left: -8rpx; justify-content: center; align-items: center; }
    .back-button image { width: 38rpx; height: 38rpx; }
    .back-button-active { opacity: .55; }
    .nav-title { position: absolute; left: 50%; top: 50%; color: #181A1E; font-size: 36rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
    .capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
    .content { min-height: 0; flex: 1; background-color: #FFFFFF; }
    .detail-state { display: flex; height: 520rpx; flex-direction: row; justify-content: center; align-items: center; color: #A0A4AA; font-size: 25rpx; }
    .detail-state.error-state { flex-direction: column; }
    .retry-text { margin-top: 22rpx; 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: policy-detail-loading 720ms linear infinite; }
    @keyframes policy-detail-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
    .detail-hero { width: 100%; height: 320rpx; background-color: #8BB9CB; }
    .policy-summary { padding: 70rpx 57rpx 43rpx; }
    .summary-title-row { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
    .summary-title { color: #24262A; font-size: 34rpx; font-weight: 700; }
    .summary-tag { padding: 9rpx 18rpx; border-radius: 22rpx; background-color: #FFF4E8; color: #FF761D; font-size: 22rpx; }
    .summary-desc { display: block; margin-top: 34rpx; color: #808794; font-size: 27rpx; line-height: 42rpx; }
    .explanation-card { margin: 0 57rpx; padding: 34rpx 34rpx 50rpx; border-radius: 29rpx; background-color: #FFFFFF; box-shadow: 0 9rpx 28rpx rgba(64,73,86,.08); }
    .content-image { display: block; width: 100%; height: 324rpx; margin-bottom: 22rpx; border-radius: 10rpx; background-color: #D3D7DA; }
    .explanation-title { display: block; margin-top: 38rpx; color: #24262A; font-size: 30rpx; font-weight: 700; }
    .explanation-title-first { margin-top: 0; }
    .explanation-text { display: block; margin-top: 22rpx; color: #717987; font-size: 27rpx; line-height: 50rpx; white-space: pre-wrap; }
    .explanation-empty { display: block; margin-top: 22rpx; color: #A5A8AD; font-size: 27rpx; line-height: 50rpx; }
    .content-bottom { height: 48rpx; height: calc(48rpx + env(safe-area-inset-bottom)); }
</style>