Files
digital-psychology/apps/admin-h5/src/api/client.ts
T
jackyu66gitandCursor b91806ba80 feat(ECR-026): OpsCMS ScheduledPublication 只读并 Closed
定时发布目录(ops_scheduled_publications),并加固 catalog 生成器。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 03:13:36 +08:00

525 lines
14 KiB
TypeScript
Raw 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.
/** Thin admin API client → /api/v1/admin (proxied to Go). */
export type ApiEnvelope<T> = { code: number; message: string; data?: T }
const TOKEN_KEY = 'yuxingu_admin_token'
/** Under /psy/admin use /psy/api; local vite uses /api. */
function adminAPIBase(): string {
if (typeof location !== 'undefined' && location.pathname.startsWith('/psy/admin')) {
return '/psy/api/v1/admin'
}
return '/api/v1/admin'
}
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}
export function setToken(token: string | null) {
if (token) localStorage.setItem(TOKEN_KEY, token)
else localStorage.removeItem(TOKEN_KEY)
}
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const headers: Record<string, string> = { Accept: 'application/json' }
if (body !== undefined) headers['Content-Type'] = 'application/json'
const token = getToken()
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(`${adminAPIBase()}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
})
const text = await res.text()
let env: ApiEnvelope<T>
try {
env = JSON.parse(text) as ApiEnvelope<T>
} catch {
throw new Error(
res.status === 404
? `接口不存在或后端未更新(${path}`
: `响应不是 JSONHTTP ${res.status}`,
)
}
if (!res.ok || env.code !== 0) {
throw new Error(env.message || `HTTP ${res.status}`)
}
return env.data as T
}
export type DashboardStats = {
users_total: number
membership_active: number
orders_today: number
paid_cents_today: number
ask_replies_today: number
profiles_total: number
reports_total: number
series?: Array<{
day: string
new_users: number
orders: number
paid_cents: number
ask_replies: number
}>
reports_by_type?: Array<{ type: string; count: number }>
}
export type UserListItem = {
id: string
status: string
phone?: string | null
nickname?: string | null
ask_paid_quota_left: number
profile_count: number
membership_active: boolean
membership_plan?: string | null
created_at: string
}
export type UserDetail = {
id: string
status: string
phone?: string | null
nickname?: string | null
ask_paid_quota_left: number
created_at: string
profiles: Array<{ id: string; relation: string; display_name: string; birth_date?: string }>
reports: Array<{ id: string; type: string; created_at: string }>
membership: {
active: boolean
plan?: string
status: string
expires_at?: string | null
ask_quota_left?: number
}
recent_orders: Array<{
id: string
kind: string
plan?: string
status: string
amount_cents: number
created_at: string
}>
}
export type AdminMe = {
id: string
username: string
role?: string
permissions: string[]
}
export const adminApi = {
login: (username: string, password: string) =>
request<{ token: string; admin: AdminMe }>('POST', '/auth/login', {
username,
password,
}),
logout: () => request<{ ok: boolean }>('POST', '/auth/logout'),
me: () => request<AdminMe>('GET', '/me'),
roles: () =>
request<{ items: Array<{ id: string; name: string; system: boolean }> }>('GET', '/roles'),
role: (id: string) =>
request<{ id: string; name: string; system: boolean; permissions: string[] }>('GET', `/roles/${id}`),
stats: () => request<DashboardStats>('GET', '/stats'),
users: (q = '') =>
request<{ items: UserListItem[] }>('GET', `/users?q=${encodeURIComponent(q)}`),
user: (id: string) => request<UserDetail>('GET', `/users/${id}`),
setUserStatus: (id: string, status: string, reason: string) =>
request<UserDetail>('POST', `/users/${id}/status`, { status, reason }),
statusTransitions: (id: string) =>
request<{
items: Array<{
id: string
from_status: string
to_status: string
admin_id: string
reason: string
created_at: string
}>
}>('GET', `/users/${id}/status-transitions`),
userInsight: (id: string) =>
request<{
user_id: string
profiles_count: number
reports_by_type: Array<{ type: string; count: number }>
recent_reports: Array<{ id: string; type: string; created_at: string }>
tags: Array<{ code: string; label: string }>
behavior: {
events: Array<{ name: string; page_path?: string; received_at: string }>
ask_thread_count: number
}
}>('GET', `/users/${id}/insight`),
userEntitlements: (id: string) =>
request<{
user_id: string
membership: {
plan?: string
status: string
expires_at?: string
ask_quota_left?: number
active: boolean
}
ask_paid_quota_left: number
flags: {
report_detail_via_membership: boolean
deep_access_count: number
}
deep_accesses: Array<{
id: string
report_id: string
report_type?: string
created_at: string
}>
}>('GET', `/users/${id}/entitlements`),
grant: (id: string, plan: string) =>
request<{ ok: boolean }>('POST', `/users/${id}/membership/grant`, { plan }),
grantAskQuota: (id: string, delta: number) =>
request<{ ok: boolean; ask_paid_quota_left: number }>('POST', `/users/${id}/ask-quota/grant`, {
delta,
}),
membershipPlans: () =>
request<{
items: Array<{
code: string
title: string
duration_days: number
amount_cents: number
active: boolean
updated_at: string
}>
}>('GET', '/membership-plans'),
updateMembershipPlan: (
code: string,
body: { title: string; duration_days: number; amount_cents: number; active: boolean },
) =>
request<{
code: string
title: string
duration_days: number
amount_cents: number
active: boolean
}>('PUT', `/membership-plans/${code}`, body),
createRedemptionBatch: (body: { label: string; plan_code: string; quantity: number }) =>
request<{
batch: { id: string; label: string; plan_code: string; quantity: number }
codes: Array<{ id: string; code: string; status: string }>
}>('POST', '/redemption-batches', body),
redemptionBatches: () =>
request<{ items: Array<{ id: string; label: string; plan_code: string; quantity: number; created_at: string }> }>(
'GET',
'/redemption-batches',
),
redemptionCodes: (batchId: string) =>
request<{ items: Array<{ id: string; code: string; status: string; plan_code: string }> }>(
'GET',
`/redemption-batches/${batchId}/codes`,
),
disableRedemptionCode: (id: string) => request<{ ok: boolean }>('POST', `/redemption-codes/${id}/disable`),
askThreads: (userId = '') => {
const q = userId ? `?user_id=${encodeURIComponent(userId)}` : ''
return request<{
items: Array<{
id: string
user_id: string
profile_id: string
scene?: string
message_count: number
created_at: string
updated_at: string
}>
}>('GET', `/ask/threads${q}`)
},
askThread: (id: string) =>
request<{
id: string
user_id: string
profile_id: string
scene?: string
message_count: number
created_at: string
updated_at: string
messages: Array<{ id: string; role: string; content: string; created_at: string }>
}>('GET', `/ask/threads/${id}`),
filterRules: () =>
request<{
items: Array<{
id: string
code: string
title: string
category: string
pattern: string
action: string
active: boolean
system: boolean
updated_at: string
}>
}>('GET', '/content-safety/filter-rules'),
filterRule: (id: string) =>
request<{
id: string
code: string
title: string
category: string
pattern: string
action: string
active: boolean
system: boolean
updated_at: string
}>('GET', `/content-safety/filter-rules/${id}`),
evaluateContent: (text: string) =>
request<{ matches: Array<{ code: string; title: string; category: string; action: string }> }>(
'POST',
'/content-safety/evaluate',
{ text },
),
askFeedback: () =>
request<{
items: Array<{
id: string
thread_id: string
message_id?: string
source: string
rating: number
tag?: string
note?: string
created_at: string
}>
}>('GET', '/ask/feedback'),
createAskFeedback: (threadId: string, body: { rating: number; tag?: string; note?: string; message_id?: string }) =>
request<{
id: string
thread_id: string
rating: number
source: string
}>('POST', `/ask/threads/${threadId}/feedback`, body),
systemPrompts: () =>
request<{
items: Array<{
id: string
code: string
title: string
scene?: string
body: string
version: number
active: boolean
system: boolean
updated_at: string
}>
}>('GET', '/ai/system-prompts'),
systemPrompt: (id: string) =>
request<{
id: string
code: string
title: string
scene?: string
body: string
version: number
active: boolean
system: boolean
updated_at: string
}>('GET', `/ai/system-prompts/${id}`),
knowledgeSources: () =>
request<{
items: Array<{
id: string
code: string
title: string
description?: string
source_kind: string
version: number
active: boolean
system: boolean
updated_at: string
}>
}>('GET', '/ai/knowledge-sources'),
knowledgeSource: (id: string) =>
request<{
id: string
code: string
title: string
description?: string
source_kind: string
version: number
active: boolean
system: boolean
updated_at: string
}>('GET', `/ai/knowledge-sources/${id}`),
crisisPolicies: () =>
request<{
items: Array<{
id: string
code: string
title: string
severity: string
pattern: string
action: string
helpline_text?: string
active: boolean
system: boolean
updated_at: string
}>
}>('GET', '/crisis/policies'),
crisisPolicy: (id: string) =>
request<{
id: string
code: string
title: string
severity: string
pattern: string
action: string
helpline_text?: string
active: boolean
system: boolean
updated_at: string
}>('GET', `/crisis/policies/${id}`),
evaluateCrisis: (text: string) =>
request<{
matches: Array<{
code: string
title: string
severity: string
action: string
helpline_text?: string
}>
}>('POST', '/crisis/evaluate', { text }),
banners: () =>
request<{
items: Array<{
id: string
code: string
title: string
placement: string
image_url?: string
link_path?: string
sort_order: number
active: boolean
system: boolean
updated_at: string
}>
}>('GET', '/cms/banners'),
banner: (id: string) =>
request<{
id: string
code: string
title: string
placement: string
image_url?: string
link_path?: string
sort_order: number
active: boolean
system: boolean
updated_at: string
}>('GET', `/cms/banners/${id}`),
feedSlots: () =>
request<{
items: Array<{
id: string
code: string
title: string
slot_key: string
placement: string
active: boolean
system: boolean
updated_at: string
}>
}>('GET', '/cms/feed-slots'),
feedSlot: (id: string) =>
request<{
id: string
code: string
title: string
slot_key: string
placement: string
active: boolean
system: boolean
updated_at: string
}>('GET', `/cms/feed-slots/${id}`),
publications: () =>
request<{ items: Array<Record<string, unknown>> }>('GET', '/cms/publications'),
publication: (id: string) =>
request<Record<string, unknown>>('GET', `/cms/publications/${id}`),
orders: () =>
request<{
items: Array<{
id: string
user_id: string
kind: string
plan?: string
amount_cents: number
status: string
created_at: string
}>
}>('GET', '/orders'),
audit: () =>
request<{
items: Array<{
id: string
admin_id: string
action: string
target_type: string
target_id: string
meta: unknown
created_at: string
}>
}>('GET', '/audit-logs'),
analyticsOverview: (from: string, to: string) =>
request<AnalyticsOverview>('GET', `/analytics/overview?from=${from}&to=${to}`),
analyticsPages: (from: string, to: string) =>
request<{ items: AnalyticsPageRow[] }>('GET', `/analytics/pages?from=${from}&to=${to}`),
analyticsExits: (from: string, to: string) =>
request<{ items: AnalyticsExitRow[] }>('GET', `/analytics/exits?from=${from}&to=${to}`),
analyticsClicks: (from: string, to: string) =>
request<{ items: AnalyticsClickRow[] }>('GET', `/analytics/clicks?from=${from}&to=${to}`),
analyticsFunnel: (from: string, to: string) =>
request<{ steps: AnalyticsFunnelStep[] }>('GET', `/analytics/funnel?from=${from}&to=${to}`),
homeTools: () => request<{ items: HomeToolAdmin[] }>('GET', '/home/tools'),
saveHomeTools: (items: HomeToolAdmin[]) =>
request<{ ok: boolean }>('PUT', '/home/tools', { items }),
scales: () => request<{ items: ScaleAdmin[] }>('GET', '/scales'),
patchScale: (id: string, status: 'published' | 'draft') =>
request<{ ok: boolean }>('PATCH', `/scales/${id}`, { status }),
}
export type HomeToolAdmin = {
id?: string
row_index: number
sort_order: number
path: string
icon: string
label: string
badge?: string | null
badge_tone?: string | null
enabled: boolean
}
export type ScaleAdmin = {
id: string
slug: string
title: string
description: string
status: string
}
export type AnalyticsOverview = {
dau: number
new_users: number
sessions: number
avg_session_ms: number
series?: Array<{ day: string; dau: number; sessions: number }>
}
export type AnalyticsPageRow = {
page_path: string
pv: number
uv: number
avg_dwell_ms: number
exit_count: number
}
export type AnalyticsExitRow = { exit_page: string; count: number }
export type AnalyticsClickRow = { element_id: string; count: number }
export type AnalyticsFunnelStep = { name: string; count: number }