feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1
去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+383
-107
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user