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:
+164
-3
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user