feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1

去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-09-15 00:26:28 +08:00
co-authored by Cursor
parent 6e97f01378
commit b09d82df8d
40 changed files with 3852 additions and 285 deletions
+383 -107
View File
@@ -1,140 +1,416 @@
<template>
<!-- 顶栏在 web-view 外层页面级H5 只在下方 web-view -->
<view class="yxg-page">
<view class="yxg-nav-bar" :style="navBarStyle">
<view class="yxg-nav-row" :style="navRowStyle">
<image
class="yxg-nav-logo"
:src="YXG_MP_LOGO_URL"
mode="aspectFit"
:style="logoStyle"
/>
<view class="yxg-home">
<yxg-nav :logo="true" :show-back="false" />
<view class="atm a1" />
<view class="atm a2" />
<view class="atm a3" />
<view class="archive">
<view class="av-self" @click="yxgGo('/profile')">
<view class="av">
<image class="av-img" :src="avatarSrc" mode="aspectFill" />
</view>
<text class="av-name">{{ selfLabel }}</text>
</view>
<view class="av-add" @click="yxgGo('/profile', { add: '1' })">
<text class="plus-dot">+</text>
<text class="av-name">添加</text>
</view>
<view class="archive-tail" @click="yxgGo('/mine')">
<text class="chev">我的 </text>
</view>
</view>
<view class="self-card">
<view class="self-head">
<view class="who-btn" @click="yxgGo('/profile')">
<text class="who-name">{{ profileLabel }}</text>
<text class="caret"></text>
</view>
<text class="who-day">{{ dayLabel }}</text>
<text class="more" @click="yxgGo('/profile')">更多 </text>
</view>
<view v-if="tipsLoading" class="tips-loading">
<text>正在根据生日与此刻节律整理</text>
</view>
<view v-else class="self-body">
<view class="duo">
<view class="wear-panel">
<text class="wear-title">穿衣指数</text>
<view class="wear-ring">
<text class="ring-num">{{ tips.clothingIndex }}</text>
</view>
<text class="wear-text">{{ tips.clothing }}</text>
</view>
<view class="block color-block">
<text class="block-title">颜色搭配</text>
<view class="swatches">
<view v-for="c in tips.palette" :key="c.name" class="swatch">
<view class="chip" :style="{ background: c.hex }" />
<text class="chip-name">{{ c.name }}</text>
</view>
</view>
<text class="block-note">{{ tips.colorNote }}</text>
</view>
</view>
<view class="block wellness-block">
<text class="block-title">养生推荐</text>
<text class="wellness">{{ tips.wellness }}</text>
</view>
<text v-if="tips.needBirth" class="birth-hint">完善生日档案后会按你的生日与此刻节律个性化</text>
</view>
</view>
<view class="sheet">
<view class="tool-grid">
<view
v-for="t in tools"
:key="t.to + t.label"
class="tool-item"
@click="yxgGo(t.to)"
>
<yxg-tool-icon :name="t.icon" :size="44" />
<text class="label">{{ t.label }}</text>
<text v-if="t.badge" class="badge" :class="'b-' + (t.badgeTone || 'hot')">{{ t.badge }}</text>
</view>
</view>
<view class="promo-pair">
<view class="promo p-plaza" @click="yxgGo('/explore')">
<text class="p-title">探索广场</text>
<text class="p-sub">测评 · 工具 · 自我理解</text>
<text class="p-chip"></text>
</view>
<view class="promo p-vip" @click="yxgGo('/membership')">
<text class="p-title">成长会员</text>
<text class="p-sub">深度报告与全年陪伴</text>
<text class="p-chip soft"></text>
</view>
</view>
<text class="ai-note">部分内容由 AI 生成仅供参考</text>
<view v-if="feedsVisible" class="feed-sec">
<view class="sec-head">
<text class="title">今日推荐</text>
<text class="more" @click="yxgGo('/explore')">更多 </text>
</view>
<view class="feed-grid">
<view
v-for="f in feeds"
:key="f.to + f.title"
class="feed-card"
:class="f.tone"
@click="yxgGo(f.to)"
>
<text v-if="f.tag" class="feed-tag">{{ f.tag }}</text>
<view class="feed-cover">
<yxg-tool-icon :name="f.icon" :size="40" />
</view>
<view class="feed-body">
<text class="name">{{ f.title }}</text>
<text class="meta">{{ f.meta }}</text>
<view class="stat-row">
<text class="stat">{{ f.stat }}</text>
<text class="go"></text>
</view>
</view>
</view>
</view>
</view>
</view>
<web-view
class="yxg-webview"
:style="webviewStyle"
:src="h5Url"
@load="onWebLoad"
@error="onWebError"
/>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { YXG_H5_BASE, YXG_H5_VERSION, YXG_MP_LOGO_URL } from '@/util/yxgConfig.js'
import YxgNav from '@/components/yxg/yxg-nav.vue'
import YxgToolIcon from '@/components/yxg/yxg-tool-icon.vue'
import { yxgApi, assetURL } from '@/util/yxgApi.js'
import { setAccountNickname } from '@/util/yxgAuth.js'
import {
homeFeeds,
homeGridRow1,
homeGridRow2,
mapHomeBannersToFeeds,
mapHomeTools,
} from '@/util/yxgCatalog.js'
import { YXG_DEFAULT_AVATAR } from '@/util/yxgConfig.js'
import { yxgGo } from '@/util/yxgNav.js'
import { localFallbackTips, tipsFromApi } from '@/util/yxgTips.js'
function clearNavTitle() {
// #ifdef MP-WEIXIN
uni.setNavigationBarTitle({ title: '\u200b' })
// #endif
const profileLabel = ref('访客')
const avatarUrl = ref('')
const tips = ref(localFallbackTips())
const tipsLoading = ref(true)
const gridRow1 = ref([...homeGridRow1])
const gridRow2 = ref([...homeGridRow2])
const feeds = ref([...homeFeeds])
const feedsVisible = ref(true)
const tools = computed(() => [...gridRow1.value, ...gridRow2.value])
const selfLabel = computed(() => {
const n = (profileLabel.value || '').trim()
if (!n) return '访客'
return n.length > 4 ? `${n.slice(0, 4)}` : n
})
const avatarSrc = computed(() => assetURL(avatarUrl.value) || YXG_DEFAULT_AVATAR)
const dayLabel = computed(() => {
if (tipsLoading.value || !tips.value.asOf) return ''
return `今日 ${tips.value.asOf.slice(5)}`
})
function loadHome() {
yxgApi
.authMe()
.then((me) => {
if (me.nickname) {
profileLabel.value = me.nickname
setAccountNickname(me.nickname)
}
avatarUrl.value = me.avatar_url || ''
})
.catch(() => {
profileLabel.value = '访客'
avatarUrl.value = ''
})
yxgApi
.getHomeDailyTips()
.then((t) => {
tips.value = tipsFromApi(t)
})
.catch(() => {
tips.value = localFallbackTips()
})
.finally(() => {
tipsLoading.value = false
})
yxgApi
.getHomeTools()
.then((res) => {
const mapped = mapHomeTools(res.items || [])
if (mapped.row1.length || mapped.row2.length) {
gridRow1.value = mapped.row1
gridRow2.value = mapped.row2
}
})
.catch(() => {})
yxgApi
.getHomeBanners('home')
.then((res) => {
const mapped = mapHomeBannersToFeeds(res.items || [])
if (mapped.length) feeds.value = mapped
})
.catch(() => {})
yxgApi
.getHomeFeedSlots('home')
.then((res) => {
feedsVisible.value = (res.items || []).length > 0
})
.catch(() => {
feedsVisible.value = true
})
}
onShow(() => {
clearNavTitle()
loadHome()
})
clearNavTitle()
function readWindow() {
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && wx.getWindowInfo) {
return wx.getWindowInfo()
}
// #endif
return uni.getSystemInfoSync()
}
const win = readWindow()
const statusBarH = win.statusBarHeight || 0
const windowH = win.windowHeight || win.screenHeight || 667
const screenW = win.windowWidth || win.screenWidth || 375
let navBarH = 44
let menuH = 32
// #ifdef MP-WEIXIN
try {
const menu = uni.getMenuButtonBoundingClientRect()
if (menu?.height) {
navBarH = (menu.top - statusBarH) * 2 + menu.height
menuH = menu.height
}
} catch (e) {
console.warn('[yxg-magic] menuButton rect failed', e)
}
// #endif
const headH = statusBarH + navBarH
const webviewH = windowH - headH
const logoH = Math.min(menuH, 30)
const logoW = Math.min(Math.round((logoH * 598) / 130), Math.floor(screenW * 0.4))
const navBarStyle = `height:${headH}px;padding-top:${statusBarH}px;box-sizing:border-box;`
const navRowStyle = `height:${navBarH}px;`
const logoStyle = `width:${logoW}px;height:${logoH}px;`
const webviewStyle = `top:${headH}px;height:${webviewH}px;`
const h5Url = computed(
() =>
`${YXG_H5_BASE}?mp=1&nh=1&sbh=4&v=${YXG_H5_VERSION}#wechat_redirect`,
)
function onWebLoad() {
// web-view loaded
}
function onWebError(e) {
console.error('[yxg-magic] web-view error', e?.detail || e)
uni.showToast({
title: 'H5加载失败,请检查业务域名',
icon: 'none',
duration: 3000,
})
}
</script>
<style>
page {
background-color: #ffd1c7;
height: 100%;
}
</style>
<style scoped>
.yxg-page {
.yxg-home {
min-height: 100vh;
background: linear-gradient(180deg, #ffd1c7 0%, #ffe4d8 28%, #fff4ef 52%, #fff9f7 100%);
padding-bottom: 40px;
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #ffd1c7;
overflow-x: hidden;
}
.atm {
position: absolute;
pointer-events: none;
border-radius: 50%;
z-index: 0;
}
.a1 { width: 160px; height: 160px; top: 88px; right: -40px; background: rgba(255, 255, 255, 0.5); }
.a2 { width: 100px; height: 100px; top: 148px; left: -28px; background: rgba(255, 200, 180, 0.45); }
.a3 { width: 70px; height: 70px; top: 220px; right: 40px; background: rgba(255, 230, 210, 0.5); }
.yxg-nav-bar {
.archive {
margin: 12px 16px 0;
position: relative;
z-index: 2;
flex-shrink: 0;
width: 100%;
box-sizing: border-box;
background-color: #ffd1c7;
}
.yxg-nav-row {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
gap: 14px;
padding: 10px 12px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 6px 20px rgba(229, 77, 66, 0.08);
}
.yxg-nav-logo {
display: block;
.av-self, .av-add {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.av {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
background: linear-gradient(145deg, #ffe8e0, #ffd0c4);
border: 2px solid rgba(229, 77, 66, 0.35);
}
.av-img { width: 40px; height: 40px; }
.av-name {
font-size: 11px;
font-weight: 600;
color: #8a4a3a;
max-width: 3.2em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plus-dot {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 236, 230, 0.95);
border: 1.5px dashed rgba(229, 77, 66, 0.35);
color: #e54d42;
font-size: 22px;
line-height: 38px;
text-align: center;
}
.archive-tail { margin-left: auto; padding: 8px 4px; }
.chev { font-size: 13px; color: #c4a090; }
.yxg-webview {
position: absolute;
left: 0;
width: 100%;
z-index: 1;
.self-card {
margin: 12px 16px 0;
position: relative;
z-index: 2;
padding: 14px;
background: #fff;
border-radius: 20px;
box-shadow: 0 8px 28px rgba(229, 77, 66, 0.12);
}
.self-head {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.who-btn { display: flex; align-items: center; gap: 4px; min-width: 0; }
.who-name { font-size: 16px; font-weight: 700; color: #333; max-width: 7em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.caret { font-size: 10px; color: #bbb; }
.who-day { flex: 1; text-align: center; font-size: 11px; color: #bbb; }
.more { font-size: 12px; color: #bbb; }
.tips-loading { padding: 20px 8px; text-align: center; font-size: 12px; color: #bbb; }
.self-body { display: flex; flex-direction: column; gap: 10px; }
.duo { display: flex; gap: 10px; }
.wear-panel, .block {
flex: 1;
min-width: 0;
padding: 12px 10px;
border-radius: 16px;
}
.wear-panel { background: linear-gradient(160deg, #fff8f4 0%, #ffe8e0 55%, #fff5f0 100%); display: flex; flex-direction: column; align-items: center; gap: 8px; }
.wear-title, .block-title { font-size: 12px; font-weight: 700; color: #333; align-self: flex-start; }
.wear-ring {
width: 64px; height: 64px; border-radius: 50%;
border: 6px solid rgba(229, 77, 66, 0.12);
display: flex; align-items: center; justify-content: center;
}
.ring-num { font-size: 20px; font-weight: 700; color: #e54d42; }
.wear-text, .block-note { font-size: 11px; color: #7a6f6a; line-height: 1.45; text-align: center; }
.color-block { background: linear-gradient(160deg, #f7f9fc 0%, #eef4fa 100%); }
.wellness-block { background: linear-gradient(160deg, #f5faf6 0%, #eaf5ee 100%); }
.swatches { display: flex; justify-content: center; gap: 10px; margin: 8px 0; }
.swatch { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.chip { width: 36px; height: 36px; border-radius: 11px; }
.chip-name { font-size: 10px; font-weight: 600; color: #999; }
.wellness { font-size: 13px; color: #333; font-weight: 500; margin-top: 8px; display: block; }
.birth-hint { font-size: 11px; color: #bbb; text-align: center; }
.sheet {
margin-top: 14px;
background: #fff;
border-radius: 22px 22px 0 0;
padding: 8px 0 28px;
box-shadow: 0 -4px 24px rgba(180, 100, 80, 0.06);
position: relative;
z-index: 3;
}
.tool-grid {
display: flex;
flex-wrap: wrap;
padding: 8px 8px 2px;
}
.tool-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px 0 10px;
position: relative;
}
.label { font-size: 11px; color: #555; }
.badge {
position: absolute; top: 2px; right: 10px;
font-size: 9px; color: #fff; padding: 2px 5px; border-radius: 7px;
}
.b-hot { background: linear-gradient(135deg, #ff7a6e, #e54d42); }
.b-new { background: linear-gradient(135deg, #6eb6ff, #4a90e2); }
.promo-pair { display: flex; gap: 10px; padding: 14px 16px 0; }
.promo {
flex: 1; position: relative; border-radius: 16px; padding: 16px 14px; min-height: 96px;
display: flex; flex-direction: column; justify-content: center;
}
.p-plaza { background: linear-gradient(145deg, #ffe9e2, #ffc8ba); }
.p-vip { background: linear-gradient(145deg, #fff4e0, #ffd98a); }
.p-title { font-size: 15px; font-weight: 700; color: #333; }
.p-sub { font-size: 11px; color: rgba(0,0,0,.42); margin-top: 5px; }
.p-chip {
position: absolute; top: 10px; right: 10px;
font-size: 9px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, #ff7a6e, #e54d42);
padding: 2px 6px; border-radius: 6px;
}
.p-chip.soft { background: linear-gradient(135deg, #6eb6ff, #4a90e2); }
.ai-note { display: block; margin-top: 10px; font-size: 10px; color: #ccc; text-align: center; }
.feed-sec { padding: 18px 16px 0; }
.sec-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; }
.sec-head .title { font-size: 17px; font-weight: 700; color: #333; }
.feed-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.feed-card {
width: calc(50% - 5px);
border-radius: 16px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,.04);
position: relative;
}
.feed-cover { height: 86px; display: flex; align-items: center; justify-content: center; }
.fc-a .feed-cover { background: linear-gradient(155deg, #ffd8d0, #ffb4a8); }
.fc-b .feed-cover { background: linear-gradient(155deg, #e8dff8, #cbb8f0); }
.fc-c .feed-cover { background: linear-gradient(155deg, #d8eaff, #b4d2f5); }
.fc-e .feed-cover { background: linear-gradient(155deg, #ffe9cc, #f5cc8a); }
.feed-body { padding: 10px 11px 12px; display: flex; flex-direction: column; gap: 4px; }
.name { font-size: 13px; font-weight: 700; color: #333; }
.meta { font-size: 11px; color: #999; line-height: 1.4; }
.stat-row { display: flex; justify-content: space-between; margin-top: 4px; }
.stat { font-size: 10px; color: #c4c4c4; }
.go { font-size: 12px; color: #e54d42; }
.feed-tag {
position: absolute; top: 8px; left: 8px; z-index: 1;
font-size: 10px; color: #fff; background: #e54d42;
padding: 2px 7px; border-radius: 7px;
}
</style>