shinnosuke
2026-08-12 fa232086731fbabcb6df070246d1b09f68930096
fix(config): 更新开发环境基础URL并优化界面显示

- 将开发环境基础URL从 http://127.0.0.1:8080 更新为 http://221.182.45.100:11451
- 优化银行卡选择页面导航栏样式,调整标题居中和返回按钮位置
- 修复搜索页面景点图片路径错误,统一使用默认占位图
- 修正人才卡页面文字描述,将"一卡三类"改为"一二三类"
- 调整对话框确认按钮尺寸,宽度从240rpx改为200rpx
- 优化申请条件页面条件行处理逻辑,新增编号条件样式
- 更新首页界面元素,移除视频播放功能和相关图片资源
- 修正首页新闻资讯数据结构,增加封面图片URL字段
- 删除商户端提现功能入口,简化设置卡片布局
10个文件已删除
25个文件已修改
130 ■■■■■ 已修改文件
rencaika/config/api.uts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/benefit-search/benefit-search.uvue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/card-application-conditions/card-application-conditions.uvue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/card/card.uvue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/index/index.uvue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/policy-detail/policy-detail.uvue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/premium-benefits/premium-benefits.uvue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/services/portal.uts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/benefits-assets/benefits-hero.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/card-assets/card-elite.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/card-assets/card-excellence.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/card-assets/card-outstanding.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/card-assets/talent-card-background.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/food-assets/food-1.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/food-assets/food-2.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/city-golden.webp 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/city-industry.webp 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/city-intro.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/city-livable.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/city-potential.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/home-banner-v2.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/home-title-left-v3.png 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/news-mascot-x3.png 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/talent-east-flower.png 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/talent-east.png 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/home-assets/top.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/policy-assets/policy-explanation.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/policy-assets/policy-hero.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/stay-assets/hotel-hampton.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/static/stay-assets/hotel-vienna.jpg 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/bank-card-select/bank-card-select.uvue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/merchant/merchant.uvue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/auth/WeChatMiniProgramClient.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/config/api.uts
@@ -6,7 +6,7 @@
// 后台提供合法域名后仅在此处填写,业务页面不得硬编码域名。
// 微信开发者工具运行在本机,使用回环地址可避免公共网络防火墙拦截局域网转发请求。
const developmentBaseUrl = 'http://127.0.0.1:8080'
const developmentBaseUrl = 'http://221.182.45.100:11451'
const stagingBaseUrl = ''
const productionBaseUrl = ''
rencaika/pages/benefit-search/benefit-search.uvue
@@ -90,10 +90,10 @@
        { id: 101, name: '乡菜郎·攀枝花特色延边菜', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg', sort: 10 },
        { id: 102, name: '攀枝花希尔顿欢朋酒店', category: '舒心安居', merchantType: 'hotel', discount: '6折', address: '东区机场路10号', image: '/static/stay-assets/hotel-hampton.jpg', sort: 20 },
        { id: 103, name: '攀枝花万达影城', category: '怡乐生活', merchantType: 'cinema', discount: '免费券', address: '东区攀枝花大道东段699号', image: '/static/entertainment-assets/wanda-cinema.webp', sort: 30 },
        { id: 104, name: '攀枝花中国三线建设博物馆', category: '悦览花城', merchantType: 'scenic', discount: '优惠票', address: '仁和区花城新区干坝塘村', image: '/static/home-assets/city-intro.jpg', sort: 40 },
        { id: 104, name: '攀枝花中国三线建设博物馆', category: '悦览花城', merchantType: 'scenic', discount: '优惠票', address: '仁和区花城新区干坝塘村', image: '/static/common/image-fallback.svg', sort: 40 },
        { id: 105, name: '金府酒楼(西区店)', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg', sort: 50 },
        { id: 106, name: '中国石油东区新福路加油站', category: '畅行无忧', merchantType: 'fuel', discount: '9.4折', address: '东区新福路20号附3-21号', image: '/static/home-assets/top.jpg', sort: 60 },
        { id: 107, name: '渡口公司管辖停车场', category: '畅行无忧', merchantType: 'parking', discount: '免费', address: '攀枝花市东区指定停车场', image: '/static/home-assets/city-intro.jpg', sort: 70 },
        { id: 107, name: '渡口公司管辖停车场', category: '畅行无忧', merchantType: 'parking', discount: '免费', address: '攀枝花市东区指定停车场', image: '/static/common/image-fallback.svg', sort: 70 },
        { id: 108, name: '攀商未来城', category: '惠享好物', merchantType: 'shopping', discount: '购房优惠3%', address: '东区榕树街55号', image: '/static/home-assets/top.jpg', sort: 80 },
        { id: 109, name: '攀枝花市中心医院', category: '臻享礼遇', merchantType: 'medical', discount: '免费体检', address: '东区攀枝花大道中段益康街34号', image: '/static/stay-assets/hotel-vienna.jpg', sort: 90 },
        { id: 110, name: '人才金融服务中心', category: '臻享礼遇', merchantType: 'finance', discount: '专属金融方案', address: '东区人民街中心广场', image: '/static/stay-assets/hotel-hampton.jpg', sort: 100 },
rencaika/pages/card-application-conditions/card-application-conditions.uvue
@@ -97,16 +97,25 @@
        // #endif
        recipient.value = '经区人才服务专窗认定的高层次人才,根据人才层次发放卓越人才卡、杰出人才卡或菁英人才卡。'
        conditions.value = '卓越人才卡\n发放对象:经区人才服务专窗认定的一至三类高层次人才。\n\n' + excellentConditions + '\n\n杰出人才卡\n发放对象:经区人才服务专窗认定的四至五类高层次人才。\n\n' + outstandingConditions + '\n\n菁英人才卡\n发放对象:经区人才服务专窗认定的六至七类高层次人才。\n\n' + eliteConditions
        conditionLines.value = conditions.value
            .replace(/(([0-9]+))/g, '\n($1)')
            .split('\n')
            .map((line: string): string => line.trim())
            .filter((line: string): boolean => line != '')
        conditionLines.value = normalizeConditionLines(conditions.value)
    })
        const normalizeConditionLines = (value: string): string[] => {
            const result = [] as string[]
            for (const sourceLine of value.split('\n')) {
                const line = sourceLine.trim()
                if (line == '') continue
                for (const piece of line.split(/(?=[((][0-90-9]+[))])/)) {
                    const normalized = piece.trim()
                    if (normalized != '') result.push(normalized)
                }
            }
            return result
        }
    const conditionLineClass = (line: string): string => {
        if (line == '卓越人才卡' || line == '杰出人才卡' || line == '菁英人才卡') return 'condition-line card-heading'
        if (line.indexOf('发放对象:') == 0) return 'condition-line recipient-heading'
        if (/^[一二三四五六七]类人才/.test(line)) return 'condition-line category-heading'
        if (/^\d+\.\s*[^((]+[。.]$/.test(line)) return 'condition-line numbered-heading'
        return 'condition-line'
    }
    const goBack = () => safeBack('/pages/card/card')
@@ -124,7 +133,8 @@
    .condition-line { display: block; color: #535B66; font-size: 27rpx; line-height: 48rpx; }
    .card-heading { margin-top: 34rpx; color: #E84B35; font-size: 32rpx; font-weight: 700; }
    .conditions-content .card-heading:first-child { margin-top: 0; }
    .recipient-heading { margin: 15rpx 0 22rpx; color: #34383E; font-weight: 700; text-align: center; }
    .category-heading { margin-top: 19rpx; color: #34383E; font-weight: 700; }
    .recipient-heading { margin: 15rpx 0 22rpx; color: #E84B35; font-weight: 700; text-align: center; }
    .category-heading { margin-top: 19rpx; color: #34383E; font-weight: 700; text-align: center; }
    .numbered-heading { margin-top: 19rpx; color: #34383E; font-weight: 700; }
    .content-bottom { height: 52rpx; height: calc(52rpx + env(safe-area-inset-bottom)); }
</style>
rencaika/pages/card/card.uvue
@@ -10,7 +10,7 @@
        <scroll-view class="content" scroll-y="true" show-scrollbar="false">
            <view v-if="isGuest" class="guest-content">
                <view class="guest-intro"><text class="guest-intro-title">东区人才卡</text><text class="guest-intro-copy">面向区内不同层次人才推出的“一卡三类人才卡”,提供消费优惠、便捷政务、安家补助、子女教育、创业支持等优质服务,增强人才归属感,提升人才引育用留质效,加快建设区域人才集聚高地。</text><view class="apply-button" hover-class="apply-button-active" @tap="openApplyNotice"><text>立即申请</text></view></view>
                <view class="guest-intro"><text class="guest-intro-title">东区人才卡</text><text class="guest-intro-copy">面向区内不同层次人才推出的一二三类人才卡,提供消费优惠、便捷政务、安家补助、子女教育、创业支持等优质服务,增强人才归属感,提升人才引育用留质效,加快建设区域人才集聚高地。</text><view class="apply-button" hover-class="apply-button-active" @tap="openApplyNotice"><text>立即申请</text></view></view>
                <view class="guest-card-item" v-for="item in guestCardTypes" :key="item.title">
                    <view class="guest-card-title"><view></view><text>{{item.title}}</text><view></view></view>
                    <view class="guest-card-preview"><image class="guest-card-image" :src="item.image" mode="aspectFit"></image></view>
@@ -294,8 +294,8 @@
    .dialog-title { display: block; color: #386BB0; font-size: 43rpx; line-height: 60rpx; font-weight: 700; text-align: center; }
    .dialog-copy { display: block; margin-top: 44rpx; color: #737F91; font-size: 27rpx; line-height: 42rpx; font-weight: 600; }
    .dialog-phone { display: block; margin-top: 16rpx; color: #FF8957; font-size: 27rpx; line-height: 39rpx; font-weight: 700; }
    .dialog-confirm { display: flex; width: 240rpx; height: 68rpx; margin: 39rpx auto 0; border-radius: 35rpx; background-image: linear-gradient(90deg, #FF4A32 0%, #FF8A1B 100%); justify-content: center; align-items: center; }
    .dialog-confirm text { color: #FFFFFF; font-size: 28rpx; font-weight: 700; }
    .dialog-confirm { display: flex; width: 200rpx; height: 60rpx; margin: 39rpx auto 0; border-radius: 31rpx; background-image: linear-gradient(90deg, #FF4A32 0%, #FF8A1B 100%); justify-content: center; align-items: center; }
    .dialog-confirm text { color: #FFFFFF; font-size: 27rpx; font-weight: 700; }
    .dialog-confirm-active { opacity: .78; }
    @keyframes card-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
rencaika/pages/index/index.uvue
@@ -2,27 +2,24 @@
    <view class="page">
        <scroll-view class="scroll-page" scroll-y @scroll="handleScroll">
            <view class="hero">
                <image class="top-image" src="/static/home-assets/home-banner-v2.jpg" mode="aspectFill"></image>
                <view class="hero-gradient"></view>
                <view class="play-button" @tap="playVideo"><image src="/static/home-assets/play.svg"></image><text>向东看</text></view>
                <view v-if="homeVideoUrl != ''" class="play-button" @tap="playVideo"><text>向东看</text></view>
            </view>
            <view class="city-panel">
                <image class="city-mascot" src="/static/home-assets/talent-east.png" mode="aspectFit"></image>
                <image class="city-flower" src="/static/home-assets/talent-east-flower.png" mode="aspectFit"></image>
                <view class="city-heading"><text>才聚东区,共富未来</text><view class="city-label" @tap="openIntro"><text>幸福东区</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
                <view class="city-grid">
                    <view class="city-card city-intro" @tap="openIntro"><image class="city-photo" src="/static/home-assets/city-intro.jpg" mode="aspectFill"></image><view class="city-shade"></view><text class="intro-name">东区简介</text><view class="intro-button"><text>查看更多</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
                    <view class="city-side"><view class="city-card city-potential"><image class="city-photo" src="/static/home-assets/city-potential.jpg" mode="aspectFill"></image><view class="city-shade"></view><text>潜力城</text></view><view class="city-card city-golden"><image class="city-photo" src="/static/home-assets/city-golden.webp" mode="aspectFill"></image><view class="city-shade"></view><text>黄金城</text></view></view>
                    <view class="city-side"><view class="city-card city-industry"><image class="city-photo" src="/static/home-assets/city-industry.webp" mode="aspectFill"></image><view class="city-shade"></view><text>兴业城</text></view><view class="city-card city-livable"><image class="city-photo" src="/static/home-assets/city-livable.jpg" mode="aspectFill"></image><view class="city-shade"></view><text>宜居城</text></view></view>
                    <view class="city-card city-intro" @tap="openIntro"><text class="intro-name">东区简介</text><view class="intro-button"><text>查看更多</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
                    <view class="city-side"><view class="city-card city-potential"><text>潜力城</text></view><view class="city-card city-golden"><text>黄金城</text></view></view>
                    <view class="city-side"><view class="city-card city-industry"><text>兴业城</text></view><view class="city-card city-livable"><text>宜居城</text></view></view>
                </view>
            </view>
            <view class="section-head"><view class="section-title"><image src="/static/home-assets/news-mascot-x3.png"></image><text>人才资讯</text></view><text class="more" @tap="goNews">查看更多</text></view>
            <view class="section-head"><view class="section-title"><text>人才资讯</text></view><text class="more" @tap="goNews">查看更多</text></view>
            <view v-if="homeLoading" class="news-state"><text>资讯加载中...</text></view>
            <view v-else-if="homeError" class="news-state" @tap="loadHome"><text>{{homeError}}</text><text class="news-retry">点击重试</text></view>
            <view v-else>
                <view class="news-featured" @tap="openNews(featuredNews.id)"><image src="/static/home-assets/home-banner-v2.jpg" mode="aspectFill"></image><view class="news-shade"></view><view class="top-badge"><text>置顶</text></view><view class="featured-copy"><text>{{featuredNews.title}}</text><text>{{featuredNews.date}}</text></view></view>
                <view class="news-featured" @tap="openNews(featuredNews.id)"><image v-if="featuredNews.coverUrl != ''" :src="featuredNews.coverUrl" mode="aspectFill"></image><view class="news-shade"></view><view class="top-badge"><text>置顶</text></view><view class="featured-copy"><text>{{featuredNews.title}}</text><text>{{featuredNews.date}}</text></view></view>
                <view class="news-list"><view class="news-row" v-for="item in news" :key="item.id" @tap="openNews(item.id)"><text class="news-name">{{item.title}}</text><view class="news-meta"><text v-if="item.isNew" class="new-badge">最新</text><text>{{item.date}}</text></view></view></view>
            </view>
            <view class="news-more" @tap="goNews"><text>查看更多</text></view>
@@ -32,7 +29,7 @@
        <view class="sticky-nav" :class="isStickyNav ? 'is-solid' : ''">
            <view class="sticky-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="nav-title"><image src="/static/home-assets/home-title-left-v3.png" mode="aspectFit"></image></view><view class="capsule-reserve" :style="'width:' + navWidth + 'px'"></view></view>
            <view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"><view class="nav-title"><text>攀枝花市东区人才服务平台</text></view><view class="capsule-reserve" :style="'width:' + navWidth + 'px'"></view></view>
        </view>
        <view class="tab-bar">
@@ -72,7 +69,7 @@
        loadHome()
    })
    const featuredNews = ref({ id: 'news-20260603-senior-talent', title: '攀枝花市东区事业单位2026年直接考核招聘副高职称及以上人才公告', date: '2026-06-03 09:19' })
    const featuredNews = ref({ id: 'news-20260603-senior-talent', title: '攀枝花市东区事业单位2026年直接考核招聘副高职称及以上人才公告', date: '2026-06-03 09:19', coverUrl: '' })
    const news = ref([
        { id: 'news-20260601-score-review', title: '关于攀枝花市东区事业单位2026年上半年公开考试招聘工作人员东区岗位加分情况、笔试总成绩及岗位排名、面试资格复审有关事宜的公告', date: '2026-06-01 16:36', isNew: true },
        { id: 'news-20260529-teacher-review', title: '攀枝花市2026年上半年公开考试招聘中小学教师东区岗位面试资格复审及领取面试通知书公告', date: '2026-05-29 10:42', isNew: false },
@@ -95,7 +92,7 @@
            const articles: any[] = data != null && data.news instanceof Array ? data.news as any[] : []
            if (articles.length == 0) return
            const first: any = articles[0]
            featuredNews.value = { id: '' + first.id, title: first.title != null ? first.title as string : '人才资讯', date: formatNoticeDate(first.publishTime) }
            featuredNews.value = { id: '' + first.id, title: first.title != null ? first.title as string : '人才资讯', date: formatNoticeDate(first.publishTime), coverUrl: first.coverUrl != null && first.coverUrl != '' ? resolveApiUrl(first.coverUrl as string) : '' }
            const nextItems: any[] = []
            for (let index = 1; index < articles.length; index++) {
                const item: any = articles[index]
@@ -144,31 +141,29 @@
    .page { height: 100%; background-color: #FFFFFF; }
    .scroll-page { height: calc(100% - 126rpx); height: calc(100% - 126rpx - env(safe-area-inset-bottom)); background-color: #FFFFFF; }
    .hero { height: 468rpx; background-color: #FFFFFF; position: relative; overflow: hidden; }
    .top-image { position: absolute; z-index: 0; top: 0; left: 0; width: 100%; height: 468rpx; border-bottom-left-radius: 48rpx; border-bottom-right-radius: 48rpx; }
    .hero-gradient { position: absolute; z-index: 1; top: 0; left: 0; right: 0; height: 300rpx; background-image: linear-gradient(180deg, rgba(239,81,39,.92) 0%, rgba(239,81,39,.68) 42%, rgba(239,81,39,0) 100%); }
    .hero-gradient { position: absolute; z-index: 1; top: 0; left: 0; right: 0; bottom: 0; border-bottom-left-radius: 48rpx; border-bottom-right-radius: 48rpx; background-image: linear-gradient(145deg, #F45A2A 0%, #FF9E4B 55%, #FFE0AC 100%); }
    .safe-space { height: 28px; position: relative; z-index: 2; }
    .nav-row { height: 32px; padding: 0 32rpx; position: relative; z-index: 2; flex-direction: row; align-items: center; justify-content: space-between; }
    .sticky-nav { position: absolute; z-index: 30; top: 0; left: 0; right: 0; padding-bottom: 20rpx; }
    .sticky-nav-bg { position: absolute; z-index: 0; top: 0; left: 0; right: 0; bottom: 0; opacity: 0; background-image: linear-gradient(180deg, #F26A38 0%, #EE542B 100%); transition: opacity 280ms ease-out; }
    .sticky-nav.is-solid .sticky-nav-bg { opacity: 1; }
    .nav-title { position: absolute; z-index: 1; left: 0; top: 10rpx; width: 750rpx; height: 100%; padding: 0; flex-direction: row; align-items: center; justify-content: flex-start; }
    .nav-title image { width: 750rpx; height: 66rpx; }
    .nav-title { position: absolute; z-index: 1; left: 30rpx; top: 10rpx; height: 100%; flex-direction: row; align-items: center; justify-content: flex-start; }
    .nav-title text { color: #FFFFFF; font-size: 28rpx; font-weight: 700; opacity: .95; }
    .capsule-reserve { width: 88px; height: 100%; flex-shrink: 0; }
    .play-button { position: absolute; z-index: 2; bottom: 20rpx; left: 50%; margin-left: -78rpx; width: 156rpx; height: 56rpx; border-radius: 28rpx; background-image: linear-gradient(180deg, #FFB75A 0%, #FF6A24 100%); border-width: 2rpx; border-color: rgba(255,255,255,.82); box-shadow: 0 7rpx 14rpx rgba(234,92,26,.26); flex-direction: row; justify-content: center; align-items: center; }
    .play-button image { width: 24rpx; height: 24rpx; margin-right: 9rpx; } .play-button text { color: #FFFFFF; font-size: 23rpx; }
    .play-button text { color: #FFFFFF; font-size: 23rpx; }
    .city-panel { margin: -10rpx 16rpx 38rpx; padding: 22rpx 0 0; overflow: visible; background-color: #FFFFFF; border-radius: 26rpx; position: relative; box-shadow: 0 -8rpx 14rpx rgba(255, 103, 39, .27); }
    .city-mascot { position: absolute; z-index: 2; top: 45rpx; right: 174rpx; width: 112rpx; height: 86rpx; } .city-flower { position: absolute; z-index: 2; top: 6rpx; right: 6rpx; width: 46rpx; height: 72rpx; }
    .city-heading { height: 58rpx; margin: 0 22rpx; flex-direction: row; justify-content: space-between; align-items: center; }
    .city-heading > text { color: #F16624; font-size: 35rpx; font-weight: 700; letter-spacing: 1rpx; background-image: linear-gradient(180deg, #F45A2A 0%, #FFB257 100%); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
    .city-label { margin-right: 48rpx; padding: 10rpx 13rpx; border-radius: 18rpx; background-image: linear-gradient(180deg, #FFB85E 0%, #FF6726 100%); box-shadow: 0 5rpx 10rpx rgba(239,101,34,.22); flex-direction: row; align-items: center; } .city-label text { color: #FFFFFF; font-size: 20rpx; } .city-label image { width: 20rpx; height: 20rpx; margin-left: 7rpx; }
    .city-grid { box-sizing: border-box; height: 292rpx; margin-top: 18rpx; padding: 22rpx; overflow: visible; background-image: linear-gradient(180deg, #FF5F2D 0%, #FFB36B 100%); border-bottom-left-radius: 26rpx; border-bottom-right-radius: 26rpx; flex-direction: row; } .city-card { overflow: hidden; border-radius: 17rpx; position: relative; justify-content: flex-end; background-color: #455A71; } .city-photo { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } .city-shade { position: absolute; left: 0; right: 0; bottom: 0; height: 92rpx; background-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.62) 100%); }
    .city-grid { box-sizing: border-box; height: 292rpx; margin-top: 18rpx; padding: 22rpx; overflow: visible; background-image: linear-gradient(180deg, #FF5F2D 0%, #FFB36B 100%); border-bottom-left-radius: 26rpx; border-bottom-right-radius: 26rpx; flex-direction: row; } .city-card { overflow: hidden; border-radius: 17rpx; position: relative; justify-content: flex-end; background-image: linear-gradient(135deg, #E96C3D 0%, #F8AA56 100%); }
    .city-intro { width: 242rpx; height: 308rpx; box-sizing: border-box; margin-right: 14rpx; padding: 22rpx; } .intro-name { position: absolute; z-index: 1; top: 50%; left: 0; right: 0; transform: translateY(-50%); color: #FFFFFF; font-size: 37rpx; font-weight: 700; text-align: center; } .intro-button { position: relative; z-index: 1; align-self: center; box-sizing: border-box; width: 146rpx; height: 42rpx; margin-top: 13rpx; padding: 0 12rpx; border-radius: 21rpx; background-image: linear-gradient(90deg, rgba(255,207,138,.92) 0%, rgba(255,101,36,.68) 100%); border-width: 1rpx; border-color: rgba(255,255,255,.42); flex-direction: row; align-items: center; justify-content: space-between; } .intro-button text { color: #FFFFFF; font-size: 19rpx; } .intro-button image { width: 19rpx; height: 19rpx; margin-left: 8rpx; }
    .city-side { height: 308rpx; flex: 1; margin-right: 14rpx; justify-content: space-between; } .city-side:last-child { margin-right: 0; } .city-side .city-card { height: 147rpx; padding: 0; } .city-side .city-card > text { position: absolute; z-index: 1; left: 0; right: 0; bottom: 0; padding: 10rpx 12rpx; background-image: linear-gradient(90deg, rgba(236,76,118,.96) 0%, rgba(247,94,135,.82) 45%, rgba(255,169,187,0) 72%, rgba(255,169,187,0) 100%); color: #FFFFFF; font-size: 22rpx; font-weight: 700; text-shadow: 0 2rpx 8rpx rgba(0,0,0,.35); }
    .city-side { height: 308rpx; flex: 1; margin-right: 14rpx; justify-content: space-between; } .city-side:last-child { margin-right: 0; } .city-side .city-card { height: 147rpx; padding: 0; } .city-side .city-card > text { position: absolute; z-index: 1; left: 0; right: 0; bottom: 0; padding: 10rpx 12rpx; color: #FFFFFF; font-size: 22rpx; font-weight: 700; }
    .section-head { height: 93rpx; padding: 0 31rpx; flex-direction: row; align-items: center; justify-content: space-between; } .section-title { flex-direction: row; align-items: center; } .section-title image { width: 40rpx; height: 40rpx; margin-right: 12rpx; } .section-title text { color: #353535; font-size: 31rpx; font-weight: 700; } .more { color: #5C5C5C; font-size: 22rpx; }
    .section-head { height: 93rpx; padding: 0 31rpx; flex-direction: row; align-items: center; justify-content: space-between; } .section-title { flex-direction: row; align-items: center; } .section-title text { color: #353535; font-size: 31rpx; font-weight: 700; } .more { color: #5C5C5C; font-size: 22rpx; }
    .news-state { min-height: 150rpx; margin: 0 31rpx; justify-content: center; align-items: center; color: #8B8B8B; font-size: 24rpx; } .news-retry { margin-top: 12rpx; color: #F16624; }
    .news-featured { height: 205rpx; margin: 0 19rpx; border-radius: 18rpx; overflow: hidden; position: relative; } .news-featured > image { width: 100%; height: 100%; } .news-shade { position: absolute; left: 0; right: 0; bottom: 0; height: 118rpx; background-color: rgba(0,0,0,.44); } .top-badge { position: absolute; left: 20rpx; top: 18rpx; padding: 7rpx 14rpx; border-radius: 12rpx; background-color: #FFF3D4; } .top-badge text { color: #F2722E; font-size: 20rpx; font-weight: 700; } .featured-copy { position: absolute; left: 20rpx; right: 20rpx; bottom: 17rpx; } .featured-copy text:first-child { display: block; color: #FFFFFF; font-size: 24rpx; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .featured-copy text:last-child { display: block; margin-top: 8rpx; color: #FFFFFF; opacity: .85; font-size: 19rpx; }
    .news-featured { height: 205rpx; margin: 0 19rpx; border-radius: 18rpx; overflow: hidden; position: relative; background-image: linear-gradient(135deg, #F26A38 0%, #F9A551 100%); } .news-featured > image { width: 100%; height: 100%; } .news-shade { position: absolute; left: 0; right: 0; bottom: 0; height: 118rpx; background-color: rgba(0,0,0,.44); } .top-badge { position: absolute; left: 20rpx; top: 18rpx; padding: 7rpx 14rpx; border-radius: 12rpx; background-color: #FFF3D4; } .top-badge text { color: #F2722E; font-size: 20rpx; font-weight: 700; } .featured-copy { position: absolute; left: 20rpx; right: 20rpx; bottom: 17rpx; } .featured-copy text:first-child { display: block; color: #FFFFFF; font-size: 24rpx; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .featured-copy text:last-child { display: block; margin-top: 8rpx; color: #FFFFFF; opacity: .85; font-size: 19rpx; }
    .news-list { margin: 0 31rpx; } .news-row { min-height: 116rpx; padding: 22rpx 0 16rpx; border-bottom-width: 1rpx; border-bottom-color: #EEEEEE; } .news-name { color: #363636; font-size: 25rpx; line-height: 36rpx; } .news-meta { margin-top: 12rpx; flex-direction: row; align-items: center; color: #98A2B3; font-size: 19rpx; } .new-badge { margin-right: 14rpx; padding: 4rpx 10rpx; border-radius: 12rpx; background-color: #FFF0D9; color: #FE7930; font-size: 18rpx; }
    .news-more { height: 87rpx; justify-content: center; align-items: center; color: #545454; font-size: 22rpx; } .content-bottom { height: 22rpx; }
    .tab-bar { height: 126rpx; height: calc(126rpx + constant(safe-area-inset-bottom)); height: calc(126rpx + env(safe-area-inset-bottom)); padding: 8rpx 14rpx; padding-bottom: calc(8rpx + constant(safe-area-inset-bottom)); padding-bottom: calc(8rpx + env(safe-area-inset-bottom)); box-sizing: border-box; overflow: visible; background-color: #FFFFFF; border-top-width: 1rpx; border-top-color: #F0F0F0; flex-direction: row; justify-content: space-between; } .tab { width: 20%; padding-top: 4rpx; align-items: center; justify-content: flex-start; color: #708099; font-size: 24rpx; } .tab-icon-wrap { width: 66rpx; height: 66rpx; margin-bottom: 13rpx; border-radius: 33rpx; justify-content: center; align-items: center; } .tab-icon-wrap image { width: 42rpx; height: 42rpx; } .active-icon { background-color: #FFF1F1; } .tab.active { color: #FF6030; font-weight: 700; } .card-tab { margin-top: -16rpx; padding-top: 0; justify-content: flex-start; } .card-icon { width: 160rpx; height: 160rpx; position: relative; justify-content: center; align-items: center; } .card-icon image { width: 160rpx; height: 160rpx; } .card-label { position: absolute; left: 0; right: 0; bottom: 42rpx; z-index: 1; color: #FFFFFF; font-size: 18rpx; font-weight: 700; text-align: center; }
rencaika/pages/policy-detail/policy-detail.uvue
@@ -29,7 +29,7 @@
    const policyCategory = ref('东区政策')
    const policyContent = ref('')
    const policyContentImages = ref<string[]>([])
    const policyCover = ref('/static/home-assets/home-banner-v2.jpg')
    const policyCover = ref('')
    const policyId = ref('')
    const initialLoading = ref(true)
    const initialError = ref(false)
@@ -64,7 +64,7 @@
            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'
            policyCover.value = ''
            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[] = []
@@ -82,7 +82,7 @@
            initialError.value = !policyUnavailable.value
        })
    }
    const useHeroFallback = () => { policyCover.value = '/static/home-assets/home-banner-v2.jpg' }
    const useHeroFallback = () => { policyCover.value = '' }
    const useContentImageFallback = (index: number) => {
        const next = policyContentImages.value.slice()
        next[index] = '/static/policy-assets/policy-explanation.jpg'
rencaika/pages/premium-benefits/premium-benefits.uvue
@@ -40,8 +40,8 @@
        getPremiumTopic((data: any) => {
            topicError.value = false; if (data != null && data.title != null && data.title != '') topicTitle.value = data.title as string; if (data != null && data.description != null) topicDescription.value = data.description as string
            const imageRecords: any[] = data != null && data.images instanceof Array ? data.images as any[] : []; const nextScenes: any[] = []; for (let index = 0; index < imageRecords.length; index++) { const item = imageRecords[index]; if (item.imageUrl != null && item.imageUrl != '') nextScenes.push({ id: '' + item.id, name: item.name != null ? item.name as string : '', image: resolveApiUrl(item.imageUrl as string) }) }; if (nextScenes.length > 0) { scenes.value = nextScenes; sceneTrackWidth.value = 644 + nextScenes.length * 137; selectScene(Math.min(activeIndex.value, nextScenes.length - 1)) }
            const categoryRecords: any[] = data != null && data.categories instanceof Array ? data.categories as any[] : []; const nextTabs: any[] = []; for (let index = 0; index < categoryRecords.length; index++) { const item = categoryRecords[index]; if (item.id != null && item.name != null && item.name != '') nextTabs.push({ id: '' + item.id, name: item.name as string }) }; serviceTabs.value = nextTabs
            if (nextTabs.length == 0) { initialLoading.value = false; hasLoaded.value = true; visibleServices.value = []; return }; selectTab(nextTabs[0].id)
            const categoryRecords: any[] = data != null && data.categories instanceof Array ? data.categories as any[] : []; const nextTabs: any[] = []; for (let index = 0; index < categoryRecords.length; index++) { const item = categoryRecords[index]; if (item.name != null && item.name != '') nextTabs.push({ id: item.name as string, name: item.name as string }) }; serviceTabs.value = nextTabs
            if (nextTabs.length == 0) { initialLoading.value = false; hasLoaded.value = true; visibleServices.value = []; return }; selectTab(nextTabs[0].name)
        }, () => { topicError.value = true; initialLoading.value = false; initialError.value = true })
    }
    const selectTab = (categoryId: string) => { if (categoryId == '') return; activeTab.value = categoryId; loadInitialServices() }
rencaika/services/portal.uts
@@ -47,7 +47,7 @@
export const getShoppingTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appShoppingTopic'), method: 'GET', skipAuth: true }, onSuccess, onError)
export const getShoppingTopicMerchants = (categoryId: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appShoppingTopicMerchants'), method: 'GET', skipAuth: true, data: { categoryId: categoryId, page: page, size: size } }, onSuccess, onError)
export const getPremiumTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appPremiumTopic'), method: 'GET', skipAuth: true }, onSuccess, onError)
export const getPremiumTopicServices = (categoryId: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appPremiumTopicServices'), method: 'GET', skipAuth: true, data: { categoryId: categoryId, page: page, size: size } }, onSuccess, onError)
export const getPremiumTopicServices = (type: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appPremiumTopicServices'), method: 'GET', skipAuth: true, data: { type: type, page: page, size: size } }, onSuccess, onError)
export const getSpouseEmploymentBenefit = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appSpouseEmploymentBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true }, onSuccess, onError)
export const submitSpouseEmploymentApplication = (serviceId: string, payload: any, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appSpouseEmploymentBenefit') + encodeURIComponent(serviceId) + '/applications', method: 'POST', skipAuth: true, data: payload, idempotencyKey: idempotencyKey }, onSuccess, onError)
export const getAdminApprovalBenefit = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appAdminApprovalBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true }, onSuccess, onError)
rencaika/static/benefits-assets/benefits-hero.jpg

rencaika/static/card-assets/card-elite.jpg

rencaika/static/card-assets/card-excellence.jpg

rencaika/static/card-assets/card-outstanding.jpg

rencaika/static/card-assets/talent-card-background.jpg

rencaika/static/food-assets/food-1.jpg

rencaika/static/food-assets/food-2.jpg

rencaika/static/home-assets/city-golden.webp
Binary files differ
rencaika/static/home-assets/city-industry.webp
Binary files differ
rencaika/static/home-assets/city-intro.jpg
Binary files differ
rencaika/static/home-assets/city-livable.jpg
Binary files differ
rencaika/static/home-assets/city-potential.jpg
Binary files differ
rencaika/static/home-assets/home-banner-v2.jpg
Binary files differ
rencaika/static/home-assets/home-title-left-v3.png
Binary files differ
rencaika/static/home-assets/news-mascot-x3.png
Binary files differ
rencaika/static/home-assets/talent-east-flower.png
Binary files differ
rencaika/static/home-assets/talent-east.png
Binary files differ
rencaika/static/home-assets/top.jpg

rencaika/static/policy-assets/policy-explanation.jpg

rencaika/static/policy-assets/policy-hero.jpg

rencaika/static/stay-assets/hotel-hampton.jpg

rencaika/static/stay-assets/hotel-vienna.jpg

rencaika_merchant/pages/bank-card-select/bank-card-select.uvue
@@ -1,5 +1,5 @@
<template>
  <view class="page"><view class="nav"><view class="nav-back" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text>选择银行卡</text><view></view></view><view class="add" @tap="addCard">添加银行卡</view><scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载银行卡</view><view v-else-if="cards.length == 0" class="state">暂无银行卡</view><view v-for="card in cards" :key="card.id" class="card" @tap="selectCard(card)"><view class="row"><text class="name">{{card.bankName}}</text><text :class="'status ' + card.status">{{statusText(card.status)}}</text></view><text class="number">{{card.cardMasked}}</text><text class="branch">{{card.branchName}}</text><text v-if="card.reason.length > 0" class="reason">{{card.reason}}</text></view></scroll-view></view>
  <view class="page"><view class="nav"><view class="nav-back" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="nav-title">选择银行卡</text><view class="nav-right-placeholder"></view></view><view class="add" @tap="addCard">添加银行卡</view><scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载银行卡</view><view v-else-if="cards.length == 0" class="state">暂无银行卡</view><view v-for="card in cards" :key="card.id" class="card" @tap="selectCard(card)"><view class="row"><text class="name">{{card.bankName}}</text><text :class="'status ' + card.status">{{statusText(card.status)}}</text></view><text class="number">{{card.cardMasked}}</text><text class="branch">{{card.branchName}}</text><text v-if="card.reason.length > 0" class="reason">{{card.reason}}</text></view></scroll-view></view>
</template>
<script setup lang="uts">
import { loadBankCards, SettlementBankCard } from '../../services/settlement.uts'
@@ -12,4 +12,4 @@
const load = () => { loading.value = true; loadBankCards((data) => { cards.value = data; loading.value = false }, (error: any) => { loading.value = false; uni.showToast({ title: error.message || '银行卡加载失败', icon: 'none' }) }) }
onShow(() => load())
</script>
<style>.page{height:100vh;background:#f4f6f9}.nav{height:174rpx;padding:68rpx 38rpx 0;display:flex;justify-content:space-between;align-items:center;box-sizing:border-box;color:#202b3e;font-size:32rpx;font-weight:700}.nav view{width:80rpx;font-size:25rpx}.nav-back{display:flex;align-items:center;justify-content:center}.back-icon{width:44rpx;height:44rpx}.add{height:88rpx;margin:0 34rpx 28rpx;border-radius:44rpx;background:#ff7625;color:#fff;text-align:center;line-height:88rpx;font-weight:700}.scroll{height:calc(100vh - 290rpx);padding:0 34rpx;box-sizing:border-box}.card{padding:28rpx;margin-bottom:22rpx;background:#fff;border-radius:28rpx}.row{display:flex;justify-content:space-between}.name{font-size:30rpx;font-weight:700;color:#2c3748}.number{display:block;margin-top:20rpx;font-size:28rpx;color:#3a4556}.branch,.reason{display:block;margin-top:10rpx;color:#8993a1;font-size:23rpx}.reason{color:#e95b5b}.status{font-size:23rpx}.approved{color:#249765}.pending{color:#e58b28}.rejected{color:#e95b5b}.state{padding:150rpx 0;text-align:center;color:#8993a1}</style>
<style>.page{height:100vh;background:#f4f6f9}.nav{height:174rpx;padding:68rpx 38rpx 0;display:flex;position:relative;justify-content:center;align-items:center;box-sizing:border-box;color:#202b3e}.nav-back{display:flex;width:64rpx;height:64rpx;position:absolute;left:38rpx;top:89rpx;align-items:center;justify-content:center;z-index:2}.back-icon{width:38rpx;height:38rpx}.nav-title{width:100%;font-size:34rpx;line-height:48rpx;text-align:center;font-weight:700}.nav-right-placeholder{width:64rpx;height:64rpx;position:absolute;right:38rpx;top:89rpx}.add{height:88rpx;margin:0 34rpx 28rpx;border-radius:44rpx;background:#ff7625;color:#fff;text-align:center;line-height:88rpx;font-weight:700}.scroll{height:calc(100vh - 290rpx);padding:0 34rpx;box-sizing:border-box}.card{padding:28rpx;margin-bottom:22rpx;background:#fff;border-radius:28rpx}.row{display:flex;justify-content:space-between}.name{font-size:30rpx;font-weight:700;color:#2c3748}.number{display:block;margin-top:20rpx;font-size:28rpx;color:#3a4556}.branch,.reason{display:block;margin-top:10rpx;color:#8993a1;font-size:23rpx}.reason{color:#e95b5b}.status{font-size:23rpx}.approved{color:#249765}.pending{color:#e58b28}.rejected{color:#e95b5b}.state{padding:150rpx 0;text-align:center;color:#8993a1}</style>
rencaika_merchant/pages/merchant/merchant.uvue
@@ -19,7 +19,6 @@
            </view>
            <view class="setting-card">
                <view class="setting-row" @tap="handleMenuAction('商户提现')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/wallet-cards-gray.svg" mode="aspectFit"></image><text class="setting-label">商户提现</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view>
                <view class="setting-row">
                    <view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/store.svg" mode="aspectFit"></image><text class="setting-label">营业状态</text></view>
                    <view class="business-state" :class="businessUpdating ? 'business-state-disabled' : ''" @tap="toggleBusiness"><text class="business-state-text">{{isBusinessOpen ? '营业中' : '休息中'}}</text><view class="state-switch" :class="isBusinessOpen ? 'open' : ''"><view class="state-thumb"></view></view></view>
@@ -27,6 +26,7 @@
                <view class="setting-row" @tap="handleMenuAction('营业时间')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/clock-3.svg" mode="aspectFit"></image><text class="setting-label">营业时间</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view>
                <view class="setting-row" @tap="handleMenuAction('商户权益')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/badge-check.svg" mode="aspectFit"></image><text class="setting-label">商户权益</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view>
                <view class="setting-row" @tap="handleMenuAction('商户协议')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/file-text.svg" mode="aspectFit"></image><text class="setting-label">商户协议</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view>
                <view class="setting-row" @tap="handleMenuAction('商户提现')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/wallet-cards-gray.svg" mode="aspectFit"></image><text class="setting-label">商户提现</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view>
                <view class="setting-row" @tap="handleMenuAction('设置')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/settings.svg" mode="aspectFit"></image><text class="setting-label">设置</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view>
            </view>
            </view>
@@ -146,9 +146,9 @@
    .setting-chevron { width: 28rpx; height: 28rpx; }
    .business-state { display: flex; flex-direction: row; align-items: center; }
    .business-state-disabled { opacity: 0.58; }
    .business-state-text { color: #FF5A42; font-size: 27rpx; line-height: 38rpx; font-weight: 600; }
    .state-switch { display: flex; width: 78rpx; height: 48rpx; padding: 4rpx; margin-left: 18rpx; border-radius: 28rpx; align-items: center; background-color: #CDD5E0; box-sizing: border-box; }
    .business-state-text { color: #FF5A42; font-size: 25rpx; line-height: 35rpx; font-weight: 600; }
    .state-switch { display: flex; width: 68rpx; height: 40rpx; padding: 3rpx; margin-left: 12rpx; border-radius: 22rpx; align-items: center; background-color: #CDD5E0; box-sizing: border-box; }
    .state-switch.open { justify-content: flex-end; background-color: #FF5A42; }
    .state-thumb { width: 40rpx; height: 40rpx; border-radius: 20rpx; background-color: #FFFFFF; box-shadow: 0 2rpx 7rpx rgba(36, 41, 49, 0.18); }
    .state-thumb { width: 34rpx; height: 34rpx; border-radius: 17rpx; background-color: #FFFFFF; box-shadow: 0 2rpx 7rpx rgba(36, 41, 49, 0.18); }
</style>
talent-card-server/src/main/java/com/talentcard/server/business/auth/WeChatMiniProgramClient.java
@@ -45,8 +45,14 @@
    }
    public String exchangePhoneNumber(String code) {
        String token = clientAccessToken();
        String token = clientAccessToken(false);
        Map<String, Object> response = post("/wxa/business/getuserphonenumber?access_token=" + encode(token), Map.of("code", code.trim()));
        // 微信返回 40001 时,缓存的 access_token 可能已被其它实例刷新。使用稳定
        // 令牌接口强制刷新一次并重试,避免把令牌问题误报成手机号授权失效。
        if (number(response.get("errcode"), 0) == 40001) {
            token = clientAccessToken(true);
            response = post("/wxa/business/getuserphonenumber?access_token=" + encode(token), Map.of("code", code.trim()));
        }
        ensureSuccess(response, "手机号授权已失效,请重新授权");
        Object phoneInfo = response.get("phone_info");
        if (!(phoneInfo instanceof Map<?, ?> raw)) throw new BusinessException(ErrorCode.CONFLICT, "未获取到微信手机号,请重新授权");
@@ -56,16 +62,24 @@
        return phone;
    }
    private String clientAccessToken() {
    private String clientAccessToken(boolean forceRefresh) {
        long now = System.currentTimeMillis();
        if (!accessToken.isBlank() && now < accessTokenExpiresAt) return accessToken;
        if (!forceRefresh && !accessToken.isBlank() && now < accessTokenExpiresAt) return accessToken;
        synchronized (this) {
            now = System.currentTimeMillis();
            if (!accessToken.isBlank() && now < accessTokenExpiresAt) return accessToken;
            Map<String, Object> response = get("/cgi-bin/token", Map.of(
            if (!forceRefresh && !accessToken.isBlank() && now < accessTokenExpiresAt) return accessToken;
            Map<String, Object> response = post("/cgi-bin/stable_token", Map.of(
                    "appid", required(properties.getAppId()),
                    "secret", required(properties.getAppSecret()),
                    "grant_type", "client_credential",
                    "force_refresh", forceRefresh));
            // 兼容旧微信接口/网关:稳定令牌接口不可用时再回退到旧接口。
            if (number(response.get("errcode"), 0) != 0 && !forceRefresh) {
                response = get("/cgi-bin/token", Map.of(
                    "grant_type", "client_credential",
                    "appid", required(properties.getAppId()),
                    "secret", required(properties.getAppSecret())));
            }
            ensureSuccess(response, "微信服务暂不可用,请稍后重试");
            String token = text(response.get("access_token"));
            long expiresIn = number(response.get("expires_in"), 7200);
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalController.java
@@ -169,11 +169,11 @@
    @GetMapping("/topics/premium/services")
    public ApiResponse<PageResult<Map<String, Object>>> premiumServices(
            @RequestParam String categoryId,
            @RequestParam String type,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "4") int size
    ) {
        return ApiResponse.success(portalService.premiumServices(categoryId, page, size));
        return ApiResponse.success(portalService.premiumServices(type, page, size));
    }
    @GetMapping("/merchants/{id}")
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
@@ -512,7 +512,8 @@
    }
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> premiumServices(String categoryId, int page, int size) {
    public PageResult<Map<String, Object>> premiumServices(String type, int page, int size) {
        String categoryId = jdbcTemplate.query("select category_key from premium_topic_category where category_name = ? and enabled = 1 and deleted = 0 limit 1", rs -> rs.next() ? rs.getString("category_key") : null, type == null ? "" : type.trim());
        if (categoryId == null || categoryId.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择臻享服务分类");
        Integer categoryCount = jdbcTemplate.queryForObject("""
                select count(*) from premium_topic_category