feat(ECR-011): 昵称、首页贴士与档案 Self 唯一/合盘交叉校验
每账号仅一条 self(migration 40902);合盘只选 TA;账号昵称可改;首页穿衣/颜色/养生贴士。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import type { GrowthReport, Profile } from '@yuxingu/types'
|
||||
import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureOtherProfile } from '../lib/authSession'
|
||||
import type { SynastryMainTab } from '../lib/synastryChart'
|
||||
|
||||
type NearbyItem = { profile: Profile; distance_km: number }
|
||||
@@ -37,7 +38,7 @@ export function createSynastryPageActions(r: SynastryPageActionRefs) {
|
||||
r.profiles.value = res.items || []
|
||||
const self = r.profiles.value.find((p) => p.relation === 'self')
|
||||
if (self && !r.profileA.value) r.profileA.value = self.id
|
||||
const other = r.profiles.value.find((p) => p.id !== r.profileA.value)
|
||||
const other = r.profiles.value.find((p) => p.relation === 'other')
|
||||
if (other && !r.profileB.value) r.profileB.value = other.id
|
||||
} catch (e) {
|
||||
r.error.value = e instanceof Error ? e.message : '加载档案失败'
|
||||
@@ -57,8 +58,7 @@ export function createSynastryPageActions(r: SynastryPageActionRefs) {
|
||||
r.error.value = ''
|
||||
try {
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const p = await api.createProfile({
|
||||
relation: 'other',
|
||||
const p = await ensureOtherProfile({
|
||||
birth_date: birth,
|
||||
display_name: r.tName.value || 'TA',
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { AskMessage, AskQuota, Profile } from '@yuxingu/types'
|
||||
import { api } from '../api/client'
|
||||
import { ensureAccount } from '../lib/authSession'
|
||||
import { ensureAccount, pickableArchiveList } from '../lib/authSession'
|
||||
|
||||
export const askScenes = [
|
||||
{ key: 'self', label: '我想更了解自己的性格与互动风格', prompt: '我想更了解自己的性格与互动风格。' },
|
||||
@@ -51,7 +51,7 @@ export function useAskPage() {
|
||||
}
|
||||
try {
|
||||
const [list, q] = await Promise.all([api.listProfiles(), api.getAskQuota()])
|
||||
profiles.value = list.items || []
|
||||
profiles.value = pickableArchiveList(list.items || [])
|
||||
quota.value = q
|
||||
const self = profiles.value.find((p) => p.relation === 'self')
|
||||
profileId.value = self?.id || profiles.value[0]?.id || ''
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { HomeDailyTips as ApiTips } from '@yuxingu/types'
|
||||
import { api } from '../api/client'
|
||||
|
||||
export type ColorChip = { name: string; hex: string }
|
||||
|
||||
export type HomeDailyTips = {
|
||||
clothingIndex: number
|
||||
clothing: string
|
||||
colorNote: string
|
||||
palette: ColorChip[]
|
||||
wellness: string
|
||||
needBirth?: boolean
|
||||
guaName?: string
|
||||
source?: string
|
||||
}
|
||||
|
||||
function localFallback(): HomeDailyTips {
|
||||
const n = new Date()
|
||||
const seed = n.getFullYear() * 10000 + (n.getMonth() + 1) * 100 + n.getDate() + n.getHours()
|
||||
const palettes: ColorChip[][] = [
|
||||
[
|
||||
{ name: '米白', hex: '#F5F0E8' },
|
||||
{ name: '雾霾蓝', hex: '#A8C4D8' },
|
||||
],
|
||||
[
|
||||
{ name: '燕麦色', hex: '#E8DCC8' },
|
||||
{ name: '浅杏', hex: '#F0C9A8' },
|
||||
],
|
||||
[
|
||||
{ name: '浅灰', hex: '#D8D6D4' },
|
||||
{ name: '雾粉', hex: '#E8B8C4' },
|
||||
],
|
||||
]
|
||||
const i = seed % palettes.length
|
||||
return {
|
||||
clothingIndex: 62 + (seed % 31),
|
||||
clothing: '轻薄透气更舒服,外套可备一件薄衫应付温差。',
|
||||
colorNote: '清爽干净,不抢戏。',
|
||||
palette: palettes[i],
|
||||
wellness: '午后泡一杯温茶,给身心一点缓冲。',
|
||||
source: 'fallback',
|
||||
needBirth: true,
|
||||
}
|
||||
}
|
||||
|
||||
function fromApi(t: ApiTips): HomeDailyTips {
|
||||
return {
|
||||
clothingIndex: t.clothing_index,
|
||||
clothing: t.clothing,
|
||||
colorNote: t.color_note,
|
||||
palette: (t.palette || []).map((p) => ({ name: p.name, hex: p.hex })),
|
||||
wellness: t.wellness,
|
||||
needBirth: !!t.need_birth,
|
||||
guaName: t.gua_name,
|
||||
source: t.source,
|
||||
}
|
||||
}
|
||||
|
||||
/** Load daily tips from API (LLM + 易经种子); local fallback on failure. */
|
||||
export function useHomeDailyTips() {
|
||||
const tips = ref<HomeDailyTips>(localFallback())
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
tips.value = fromApi(await api.getHomeDailyTips())
|
||||
} catch {
|
||||
/* keep local fallback */
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
return { tips, loading }
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
const moodTexts = [
|
||||
'今天心态平稳,遇到小波折也能慢慢化解,适合把一件小事做完。',
|
||||
'精力在回升,适合温和推进计划,不必一次做完所有事。',
|
||||
'情绪有起伏很正常,给自己一点空隙,会更清楚下一步。',
|
||||
'今天利于沟通与整理思路,可以从身边亲近的人开始。',
|
||||
'节奏偏慢也没关系,把注意力放在身体感受上会更踏实。',
|
||||
]
|
||||
|
||||
const dimColors = ['#ff7a9a', '#ff9a5c', '#5b9cff', '#3ecfcf', '#a78bfa']
|
||||
|
||||
function daySeed(): number {
|
||||
const n = new Date()
|
||||
return n.getFullYear() * 10000 + (n.getMonth() + 1) * 100 + n.getDate()
|
||||
}
|
||||
|
||||
function clamp(n: number) {
|
||||
return Math.max(40, Math.min(95, n))
|
||||
}
|
||||
|
||||
function moodFromSeed(seed: number) {
|
||||
const score = 58 + (seed % 37)
|
||||
const text = moodTexts[seed % moodTexts.length]
|
||||
const base = [70, 62, 68, 64, 66]
|
||||
const dims = [
|
||||
{ key: 'love', label: '爱情', score: clamp(base[0] + (seed % 17) - 8), color: dimColors[0] },
|
||||
{ key: 'wealth', label: '财富', score: clamp(base[1] + ((seed >> 2) % 19) - 9), color: dimColors[1] },
|
||||
{ key: 'career', label: '事业', score: clamp(base[2] + ((seed >> 3) % 15) - 7), color: dimColors[2] },
|
||||
{ key: 'learn', label: '学习', score: clamp(base[3] + ((seed >> 4) % 21) - 10), color: dimColors[3] },
|
||||
{ key: 'social', label: '人际', score: clamp(base[4] + ((seed >> 5) % 13) - 6), color: dimColors[4] },
|
||||
]
|
||||
return { score, text, dims }
|
||||
}
|
||||
|
||||
/** Deterministic daily mood card for home self-card. */
|
||||
export function useHomeMood() {
|
||||
return computed(() => moodFromSeed(daySeed()))
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type HomeTool,
|
||||
} from '../lib/homeCatalog'
|
||||
import type { HomeToolIconName } from '../components/HomeToolIcon.vue'
|
||||
import { useHomeMood } from './useHomeMood'
|
||||
import { useHomeDailyTips } from './useHomeDailyTips'
|
||||
|
||||
const ICONS = new Set([
|
||||
'mbti', 'star', 'portrait', 'rhythm', 'synastry', 'astro',
|
||||
@@ -55,15 +55,23 @@ function mapTools(
|
||||
|
||||
export function useHomePage() {
|
||||
const router = useRouter()
|
||||
const profileLabel = ref('自己')
|
||||
const profileLabel = ref('访客')
|
||||
const plusOpen = ref(false)
|
||||
const mood = useHomeMood()
|
||||
const { tips, loading: tipsLoading } = useHomeDailyTips()
|
||||
const grid = reactive({
|
||||
gridRow1: [...homeGridRow1] as HomeTool[],
|
||||
gridRow2: [...homeGridRow2] as HomeTool[],
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void api
|
||||
.authMe()
|
||||
.then((me) => {
|
||||
if (me.nickname) profileLabel.value = me.nickname
|
||||
})
|
||||
.catch(() => {
|
||||
profileLabel.value = '访客'
|
||||
})
|
||||
void api
|
||||
.getHomeTools()
|
||||
.then((res) => {
|
||||
@@ -105,7 +113,8 @@ export function useHomePage() {
|
||||
router,
|
||||
profileLabel,
|
||||
plusOpen,
|
||||
mood,
|
||||
tips,
|
||||
tipsLoading,
|
||||
searchHint,
|
||||
...toRefs(grid),
|
||||
feeds: homeFeeds,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { GrowthReport } from '@yuxingu/types'
|
||||
import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount } from '../lib/authSession'
|
||||
import { ensureAccount, ensureSelfProfile } from '../lib/authSession'
|
||||
|
||||
export const promptRows = [
|
||||
{ icon: '🌊', label: '最近情绪有点乱,帮我理一理', scene: '情绪整理' },
|
||||
@@ -101,7 +101,7 @@ export function useImageCardPage() {
|
||||
drawing.value = true
|
||||
try {
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const profile = await api.createProfile({ relation: 'self', birth_date: birth, display_name: '我' })
|
||||
const profile = await ensureSelfProfile({ birth_date: birth, display_name: '我' })
|
||||
const out = await api.drawImageCard({
|
||||
profile_id: profile.id,
|
||||
scene: scene.value,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import type { WuxingBar } from '../components/WuxingBars.vue'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount, loadSelfLatest } from '../lib/authSession'
|
||||
import { ensureAccount, loadSelfLatest, ensureSelfProfile } from '../lib/authSession'
|
||||
|
||||
export const resultTabs = [
|
||||
{ key: 'overview' as const, label: '概览' },
|
||||
@@ -69,7 +69,7 @@ export function useLifeRhythmPage() {
|
||||
needBirth.value = false
|
||||
try {
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const profile = await api.createProfile({ relation: 'self', birth_date: birth, display_name: '我' })
|
||||
const profile = await ensureSelfProfile({ birth_date: birth, display_name: '我' })
|
||||
report.value = await api.createRhythm(profile.id)
|
||||
tab.value = 'overview'
|
||||
track(AnalyticsEvent.PortraitCompleted, { source: 'rhythm' })
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { GrowthReport } from '@yuxingu/types'
|
||||
import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount, loadSelfLatest } from '../lib/authSession'
|
||||
import { ensureAccount, loadSelfLatest, ensureSelfProfile } from '../lib/authSession'
|
||||
import type { PortraitSharePayload } from '../lib/shareLink'
|
||||
|
||||
export function usePortraitPage() {
|
||||
@@ -45,7 +45,7 @@ export function usePortraitPage() {
|
||||
needBirth.value = false
|
||||
try {
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const profile = await api.createProfile({ relation: 'self', birth_date: birth, display_name: '我' })
|
||||
const profile = await ensureSelfProfile({ birth_date: birth, display_name: '我' })
|
||||
report.value = await api.createPortrait(profile.id)
|
||||
track(AnalyticsEvent.PortraitCompleted, { source: 'form' })
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { Profile } from '@yuxingu/types'
|
||||
import { api } from '../api/client'
|
||||
import { ensureAccount } from '../lib/authSession'
|
||||
import {
|
||||
avatarInitial,
|
||||
@@ -16,6 +17,7 @@ export function useProfilePage() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const accountNickname = ref('')
|
||||
const items = ref<Profile[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
@@ -102,13 +104,25 @@ export function useProfilePage() {
|
||||
() => applyQueryFlags(),
|
||||
)
|
||||
|
||||
function onNicknameUpdated(n: string) {
|
||||
accountNickname.value = n
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!(await ensureAccount(router, route.fullPath))) return
|
||||
try {
|
||||
const me = await api.authMe()
|
||||
accountNickname.value = me.nickname || ''
|
||||
} catch {
|
||||
accountNickname.value = ''
|
||||
}
|
||||
await load()
|
||||
applyQueryFlags()
|
||||
})
|
||||
|
||||
return {
|
||||
accountNickname,
|
||||
onNicknameUpdated,
|
||||
items,
|
||||
loading,
|
||||
error,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { GrowthReport, Profile } from '@yuxingu/types'
|
||||
import type { GrowthReport } from '@yuxingu/types'
|
||||
import { api } from '../api/client'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount } from '../lib/authSession'
|
||||
import { ensureAccount, ensureOtherProfile, findSelfProfile } from '../lib/authSession'
|
||||
import type { RelationSharePayload } from '../lib/shareLink'
|
||||
|
||||
export const focusTabs = [
|
||||
@@ -93,13 +93,6 @@ export function useRelationPage() {
|
||||
}
|
||||
})
|
||||
|
||||
async function ensureSelf(): Promise<Profile> {
|
||||
const { items } = await api.listProfiles()
|
||||
const self = (items || []).find((p) => p.relation === 'self')
|
||||
if (self) return self
|
||||
throw new Error('请先在首页完成愈心解码,创建「我」的个人档案')
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (!(await ensureAccount(router, route.fullPath))) return
|
||||
loading.value = true
|
||||
@@ -111,10 +104,10 @@ export function useRelationPage() {
|
||||
const m = Number(om.value)
|
||||
const d = Number(od.value)
|
||||
if (!y || !m || !d) throw new Error('请填写 TA 的完整生日')
|
||||
const self = await ensureSelf()
|
||||
const self = await findSelfProfile()
|
||||
if (!self) throw new Error('请先在首页完成愈心解码,创建「我」的个人档案')
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const other = await api.createProfile({
|
||||
relation: 'other',
|
||||
const other = await ensureOtherProfile({
|
||||
birth_date: birth,
|
||||
display_name: otherName.value || 'TA',
|
||||
relation_type: relationType.value,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api } from '../api/client'
|
||||
import { ensureAccount } from '../lib/authSession'
|
||||
import { ensureAccount, findSelfProfile } from '../lib/authSession'
|
||||
import { clearScaleDraft, loadScaleDraft, saveScaleDraft } from '../lib/scaleDraft'
|
||||
import type { PortraitSharePayload } from '../lib/shareLink'
|
||||
|
||||
@@ -146,8 +146,7 @@ export function useScalePage() {
|
||||
for (const q of questions.value) {
|
||||
if (!answers[q.id]) throw new Error('请完成全部题目')
|
||||
}
|
||||
const { items } = await api.listProfiles()
|
||||
const self = (items || []).find((p) => p.relation === 'self')
|
||||
const self = await findSelfProfile()
|
||||
if (!self) {
|
||||
needProfile.value = true
|
||||
throw new Error('请先在首页完成性格探索,创建个人档案')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import type { WheelAspect, WheelPlanet } from '../components/NatalWheel.vue'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount, loadSelfLatest } from '../lib/authSession'
|
||||
import { ensureAccount, loadSelfLatest, ensureSelfProfile } from '../lib/authSession'
|
||||
import {
|
||||
ascLonFromSummary,
|
||||
fortuneBundleFrom,
|
||||
@@ -111,8 +111,7 @@ export function useStarProfilePage() {
|
||||
try {
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const bt = birthTime.value ? birthTime.value.slice(0, 5) : undefined
|
||||
const profile = await api.createProfile({
|
||||
relation: 'self',
|
||||
const profile = await ensureSelfProfile({
|
||||
birth_date: birth,
|
||||
display_name: '我',
|
||||
birth_time: bt,
|
||||
|
||||
@@ -56,7 +56,10 @@ export function useSynastryPage() {
|
||||
const showQuickAdd = ref(false)
|
||||
|
||||
const needsSubTab = computed(() => ['composite', 'davison', 'marks'].includes(mainTab.value))
|
||||
const pickableProfiles = computed(() => profiles.value.filter((p) => p.id !== profileA.value))
|
||||
const pickableProfiles = computed(() =>
|
||||
profiles.value.filter((p) => p.relation === 'other' && p.id !== profileA.value),
|
||||
)
|
||||
const selfProfiles = computed(() => profiles.value.filter((p) => p.relation === 'self'))
|
||||
const profileAName = computed(() => {
|
||||
const p = profiles.value.find((x) => x.id === profileA.value)
|
||||
return p?.display_name || ''
|
||||
@@ -178,6 +181,7 @@ export function useSynastryPage() {
|
||||
showQuickAdd,
|
||||
needsSubTab,
|
||||
pickableProfiles,
|
||||
selfProfiles,
|
||||
profileAName,
|
||||
selfInitial,
|
||||
profileInitial,
|
||||
|
||||
Reference in New Issue
Block a user