feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具

落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-03 11:37:53 +08:00
co-authored by Cursor
parent 15a9db374a
commit bd22d9dddd
248 changed files with 26309 additions and 842 deletions
+164 -3
View File
@@ -1,4 +1,12 @@
import type { ApiResponse, GrowthReport, Profile } from '@yuxingu/types'
import type {
ApiResponse,
AskMessage,
AskQuota,
AskThread,
GrowthReport,
MembershipMe,
Profile,
} from '@yuxingu/types'
/** Platform adapters so H5 and mini-program share one client. */
export interface RequestOptions {
@@ -57,10 +65,141 @@ export function createClient(opts: CreateClientOptions) {
birth_date: string
display_name?: string
relation_type?: string
birth_time?: string
birth_place?: string
}) => call<Profile>('/api/v1/profiles', { method: 'POST', body }),
updateProfile: (
id: string,
body: {
display_name?: string
birth_date?: string
relation_type?: string
birth_time?: string
birth_place?: string
geo_lat?: number
geo_lng?: number
geo_visible?: boolean
},
) => call<Profile>(`/api/v1/profiles/${id}`, { method: 'PATCH', body }),
deleteProfile: (id: string) =>
call<{ deleted: boolean }>(`/api/v1/profiles/${id}`, { method: 'DELETE' }),
createPortrait: (profile_id: string) =>
call<GrowthReport>('/api/v1/reports/portrait', { method: 'POST', body: { profile_id } }),
createStar: (profile_id: string) =>
call<GrowthReport>('/api/v1/reports/star', { method: 'POST', body: { profile_id } }),
createSynastry: (profile_id_a: string, profile_id_b: string, as_of?: string) =>
call<GrowthReport>('/api/v1/reports/synastry', {
method: 'POST',
body: { profile_id_a, profile_id_b, ...(as_of ? { as_of } : {}) },
}),
listSynastryNearby: (lat: number, lng: number, radius_km = 50) =>
call<{ items: { profile: Profile; distance_km: number }[] }>(
`/api/v1/synastry/nearby?lat=${lat}&lng=${lng}&radius_km=${radius_km}`,
),
createSynastryInvite: (profile_id: string) =>
call<{ token: string; expires_at: string; path: string }>('/api/v1/synastry/invites', {
method: 'POST',
body: { profile_id },
}),
getSynastryInvite: (token: string) =>
call<{
token: string
expires_at: string
host_name: string
already_accepted: boolean
}>(`/api/v1/synastry/invites/${token}`),
acceptSynastryInvite: (token: string, body: {
display_name?: string
birth_date: string
birth_time?: string
birth_place?: string
}) =>
call<GrowthReport>(`/api/v1/synastry/invites/${token}/accept`, { method: 'POST', body }),
createRhythm: (profile_id: string) =>
call<GrowthReport>('/api/v1/reports/rhythm', { method: 'POST', body: { profile_id } }),
listImageCardScenes: () =>
call<{ items: { key: string; label: string }[] }>('/api/v1/image-cards/scenes'),
getImageCardQuota: () =>
call<{ remaining: number; daily_free: number; unlimited: boolean }>('/api/v1/image-cards/quota'),
drawImageCard: (body: { profile_id: string; scene?: string; depth?: boolean }) =>
call<{
report: GrowthReport
scene: string
cards: { id: string; title: string; imagery: string; prompt: string; tip: string }[]
quota_left: number
}>('/api/v1/image-cards/draw', { method: 'POST', body }),
getSolarTermsToday: () =>
call<{ name: string; tip: string; date: string }>('/api/v1/solar-terms/today'),
saveMood: (body: { score?: number; note?: string; day?: string }) =>
call<{ id: string; day: string; score?: number; note?: string }>('/api/v1/moods', {
method: 'POST',
body,
}),
getMoodToday: () =>
call<{ mood: { id: string; day: string; score?: number; note?: string } | null }>(
'/api/v1/moods/today',
),
getMoodsRecent: () =>
call<{ items: { id: string; day: string; score?: number; note?: string }[] }>(
'/api/v1/moods/recent',
),
getExploreCatalog: () =>
call<{
categories: {
key: string
title: string
description: string
icon: string
tone: string
items: {
key: string
title: string
description: string
path: string
icon: string
tone: string
badge?: string
}[]
}[]
}>('/api/v1/explore/catalog'),
getExploreCategory: (key: string) =>
call<{
key: string
title: string
description: string
icon: string
tone: string
items: {
key: string
title: string
description: string
path: string
icon: string
tone: string
badge?: string
}[]
}>(`/api/v1/explore/catalog/${key}`),
listGrowthPlans: () =>
call<{ items: { id: string; title: string; focus: string; status: string }[] }>(
'/api/v1/growth/plans',
),
createGrowthPlan: (body: { title: string; focus?: string }) =>
call<{ id: string; title: string; focus: string }>('/api/v1/growth/plans', {
method: 'POST',
body,
}),
growthCheckin: (planId: string, body?: { note?: string }) =>
call<{ id: string; day: string }>(`/api/v1/growth/plans/${planId}/checkin`, {
method: 'POST',
body: body || {},
}),
listGrowthCheckins: (planId: string) =>
call<{ items: { id: string; day: string; note?: string }[] }>(
`/api/v1/growth/plans/${planId}/checkins`,
),
listReports: () => call<{ items: GrowthReport[] }>('/api/v1/reports'),
getReport: (id: string) => call<GrowthReport>(`/api/v1/reports/${id}`),
getMembership: () => call<MembershipMe>('/api/v1/membership/me'),
createOrder: (body: { kind: 'membership' | 'deep_access'; plan?: string; report_id?: string }) =>
call<{ order_id: string }>('/api/v1/orders', { method: 'POST', body }),
payMock: (orderId: string) =>
@@ -87,6 +226,20 @@ export function createClient(opts: CreateClientOptions) {
method: 'POST',
body: { profile_id, answers },
}),
getAskQuota: () => call<AskQuota>('/api/v1/ask/quota'),
createAskThread: (body: { profile_id: string; scene?: string }) =>
call<AskThread>('/api/v1/ask/threads', { method: 'POST', body }),
listAskMessages: (threadId: string) =>
call<{ items: AskMessage[] }>(`/api/v1/ask/threads/${threadId}/messages`),
sendAskMessage: (threadId: string, content: string) =>
call<{
user_message: AskMessage
assistant_message: AskMessage
quota: AskQuota
}>(`/api/v1/ask/threads/${threadId}/messages`, {
method: 'POST',
body: { content },
}),
}
}
@@ -94,7 +247,7 @@ export function createClient(opts: CreateClientOptions) {
export function createBrowserAdapters(): ClientAdapters {
const deviceKeyStorage = 'yxg_device_key'
return {
request: async <T>({ method = 'GET', path, body, headers }) => {
request: async <T>({ method = 'GET', path, body, headers }: RequestOptions) => {
const res = await fetch(path, {
method,
headers: {
@@ -103,7 +256,15 @@ export function createBrowserAdapters(): ClientAdapters {
},
body: body === undefined ? undefined : JSON.stringify(body),
})
const json = (await res.json()) as ApiResponse<T>
const text = await res.text()
let json: ApiResponse<T>
try {
json = JSON.parse(text) as ApiResponse<T>
} catch {
throw new Error(
`接口返回异常(HTTP ${res.status}),请确认 API 已重启且代理指向 :8080`,
)
}
return Object.assign(json, { headers: res.headers })
},
getToken: () => localStorage.getItem('yxg_token'),
+40
View File
@@ -12,7 +12,12 @@ export interface Profile {
relation: 'self' | 'other'
display_name: string
birth_date: string
birth_time?: string | null
birth_place?: string | null
relation_type?: string | null
geo_lat?: number | null
geo_lng?: number | null
geo_visible?: boolean
created_at: string
}
@@ -36,3 +41,38 @@ export interface ScaleSummary {
questionCount: number
description?: string
}
/** 成长会员当前权益 */
export interface MembershipMe {
active: boolean
plan?: string
status: string
expires_at?: string | null
ask_quota_left?: number
}
/** 问答线程 */
export interface AskThread {
id: string
user_id: string
profile_id: string
scene?: string | null
created_at: string
}
/** 问答消息 */
export interface AskMessage {
id: string
thread_id: string
role: 'user' | 'assistant'
content: string
created_at: string
}
/** 问答额度 */
export interface AskQuota {
active_membership: boolean
remaining: number
free_limit: number
source: 'membership' | 'free' | string
}
+7
View File
@@ -49,10 +49,17 @@
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-sheet: 22px;
--radius-pill: 999px;
/* ── Typography ── */
--font-sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif;
--font-display: "Noto Serif SC", "Songti SC", "PingFang SC", serif;
/* ── Shadow / layout ── */
--shadow-card: 0 2px 10px rgba(0, 0, 0, 0.04);
--shadow-hero: 0 8px 28px rgba(229, 77, 66, 0.12);
--shadow-sheet: 0 -4px 24px rgba(180, 100, 80, 0.06);
--shadow-nav: 0 -2px 8px rgba(0, 0, 0, 0.03);
--layout-max-user: 430px;
--layout-nav-h: 56px;