Files
jackyu66gitandCursor b09d82df8d feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1
去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 00:26:28 +08:00

417 lines
13 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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>
</view>
</template>
<script setup>
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'
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(() => {
loadHome()
})
</script>
<style>
page {
background-color: #ffd1c7;
}
</style>
<style scoped>
.yxg-home {
min-height: 100vh;
background: linear-gradient(180deg, #ffd1c7 0%, #ffe4d8 28%, #fff4ef 52%, #fff9f7 100%);
padding-bottom: 40px;
position: relative;
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); }
.archive {
margin: 12px 16px 0;
position: relative;
z-index: 2;
display: flex;
align-items: center;
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);
}
.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; }
.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>