feat(studio): 个人中心 New-API 绑定摘要接入真后端(A 类:接已就绪后端)
A 类(后端就绪、前端 0 消费):02G GET /account/new-api-binding 后端就绪(curl code:0、
未绑定时 fail-closed 返 bindingStatus=unbound),但 studio 无任何展示(盘点 02G ❌ 缺口)。接入:
- useAccountNewApiBinding hook + openapi 导出 NewApiBindingSummary
- PersonalCenter「New-API 绑定」区:bindingStatus 中文徽标(已绑定/未绑定/同步失败/同步中)+ 上次同步时间
- mock handler + vitest 契约 + account-newapi-binding.spec.ts 真后端 e2e
验证:全量 e2e 45→46/0;vitest 80→81;tsc/eslint 绿。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bcb670ba4d
commit
7408615037
48
muse-studio/e2e/account-newapi-binding.spec.ts
Normal file
48
muse-studio/e2e/account-newapi-binding.spec.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* 个人中心 New-API 绑定摘要 e2e(真实后端,MSW off)——A 类:后端 getAppNewApiBinding 就绪、studio 此前 0 消费。
|
||||
*
|
||||
* 反假绿:盘点 02G ❌ "New-API 绑定摘要"缺口——后端就绪(curl code:0、未绑定时 fail-closed 返 bindingStatus=unbound),
|
||||
* 但前端无任何展示。本测进个人中心 → 等真实 GET /account/new-api-binding → 验 code:0 + 「New-API 绑定」区
|
||||
* 按真实 bindingStatus 渲染状态徽标。
|
||||
* 设计:不硬编码绑定态(随账户状态变),依真实响应数据断言——区块标题恒在,徽标按返回 bindingStatus 映射。
|
||||
*/
|
||||
const TOKEN = 'test1';
|
||||
|
||||
async function seedToken(page: Page): Promise<void> {
|
||||
await page.addInitScript((t) => {
|
||||
window.localStorage.setItem('accessToken', t);
|
||||
window.localStorage.setItem('tenantId', '1');
|
||||
}, TOKEN);
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
bound: '已绑定',
|
||||
unbound: '未绑定',
|
||||
sync_failed: '同步失败',
|
||||
pending: '同步中',
|
||||
};
|
||||
|
||||
test('个人中心渲染 New-API 绑定摘要(真实后端)', async ({ page }) => {
|
||||
await seedToken(page);
|
||||
|
||||
const bindingResp = page.waitForResponse(
|
||||
(r) =>
|
||||
/\/account\/new-api-binding$/.test(new URL(r.url()).pathname) &&
|
||||
r.request().method() === 'GET',
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
await page.goto('/account');
|
||||
const resp = await bindingResp;
|
||||
expect(resp.status()).toBe(200);
|
||||
const body = await resp.json();
|
||||
expect(body.code).toBe(0); // 后端就绪(未绑定时 fail-closed unbound,非报错)。
|
||||
|
||||
// 「New-API 绑定」区渲染(标题恒在) + 状态徽标按真实 bindingStatus 映射(证真后端态被 UI 消费)。
|
||||
await expect(page.getByRole('heading', { name: 'New-API 绑定' })).toBeVisible({ timeout: 10_000 });
|
||||
const expectedLabel = STATUS_LABELS[body.data?.bindingStatus] ?? body.data?.bindingStatus;
|
||||
if (expectedLabel) {
|
||||
await expect(page.getByText(expectedLabel).first()).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
});
|
||||
@ -127,4 +127,13 @@ export const accountHandlers = [
|
||||
http.get('/app-api/muse/account/usage', () => {
|
||||
return HttpResponse.json({ code: 0, msg: 'success', data: mockUsage });
|
||||
}),
|
||||
|
||||
// GET /app-api/muse/account/new-api-binding — New-API 绑定摘要(dev mock 默认未绑定)
|
||||
http.get('/app-api/muse/account/new-api-binding', () => {
|
||||
return HttpResponse.json({
|
||||
code: 0,
|
||||
msg: 'success',
|
||||
data: { bindingStatus: 'unbound', boundAt: null, lastSyncAt: null, canRecheck: false },
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import {
|
||||
useAccountEntitlements,
|
||||
useAccountLicenses,
|
||||
useAccountNewApiBinding,
|
||||
useAccountProfile,
|
||||
useAccountPublishRecords,
|
||||
useAccountPurchases,
|
||||
@ -19,6 +20,20 @@ function severityBadgeClass(severity: string): string {
|
||||
return 'bg-slate-100 text-slate-500';
|
||||
}
|
||||
|
||||
/** New-API 绑定状态 → 中文文案。 */
|
||||
const newApiBindingLabels: Record<string, string> = {
|
||||
bound: '已绑定',
|
||||
unbound: '未绑定',
|
||||
sync_failed: '同步失败',
|
||||
pending: '同步中',
|
||||
};
|
||||
/** New-API 绑定状态 → 徽标配色(bound 绿 / sync_failed 红 / 其余中性)。 */
|
||||
function newApiBindingBadgeClass(status: string): string {
|
||||
if (status === 'bound') return 'bg-emerald-50 text-emerald-700';
|
||||
if (status === 'sync_failed') return 'bg-rose-50 text-rose-700';
|
||||
return 'bg-slate-100 text-slate-500';
|
||||
}
|
||||
|
||||
/** 个人中心首页,聚合资料、用量、权益和账户状态。 */
|
||||
export default function PersonalCenter() {
|
||||
const { data: profile, isLoading: isProfileLoading } = useAccountProfile();
|
||||
@ -28,6 +43,7 @@ export default function PersonalCenter() {
|
||||
const { data: licenses } = useAccountLicenses();
|
||||
const { data: publishRecords } = useAccountPublishRecords();
|
||||
const { data: securityEvents } = useAccountSecurityEvents();
|
||||
const { data: newApiBinding } = useAccountNewApiBinding();
|
||||
const acknowledgeEvent = useAcknowledgeSecurityEvent();
|
||||
const updateProfile = useUpdateAccountProfile();
|
||||
const [nickname, setNickname] = useState('');
|
||||
@ -240,6 +256,23 @@ export default function PersonalCenter() {
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* New-API 绑定摘要:展示配额/计费外部账户绑定态(bound/unbound/sync_failed/pending)。后端就绪、此前 0 消费。 */}
|
||||
<section className="mt-6 rounded-3xl border border-slate-100 bg-white p-6 shadow-sm">
|
||||
<h2 className="text-base font-bold text-slate-800">New-API 绑定</h2>
|
||||
{!newApiBinding ? (
|
||||
<p className="mt-3 text-sm text-slate-400">暂无绑定信息</p>
|
||||
) : (
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<span className={`rounded-full px-3 py-1 text-xs font-bold ${newApiBindingBadgeClass(newApiBinding.bindingStatus)}`}>
|
||||
{newApiBindingLabels[newApiBinding.bindingStatus] ?? newApiBinding.bindingStatus}
|
||||
</span>
|
||||
{newApiBinding.lastSyncAt && (
|
||||
<span className="text-xs text-slate-400">上次同步:{newApiBinding.lastSyncAt}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@ import { resetMockDb } from '@/api/mocks/handlers/account';
|
||||
import { server } from '@/test/setup';
|
||||
import {
|
||||
useAccountEntitlements,
|
||||
useAccountNewApiBinding,
|
||||
useAccountProfile,
|
||||
useAccountPublishRecords,
|
||||
useAccountUsage,
|
||||
@ -33,6 +34,13 @@ describe('useAccount API Hooks 单元测试', () => {
|
||||
testQueryClient = createTestQueryClient();
|
||||
});
|
||||
|
||||
it('应能读取 New-API 绑定摘要(后端就绪、前端 0 消费缺口补齐)', async () => {
|
||||
const { result } = renderHook(() => useAccountNewApiBinding(), { wrapper });
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
// mock 默认未绑定;契约:bindingStatus 解出枚举值之一。
|
||||
expect(result.current.data?.bindingStatus).toBe('unbound');
|
||||
});
|
||||
|
||||
it('应能读取账户资料并保存昵称', async () => {
|
||||
const { result: profileHook } = renderHook(() => useAccountProfile(), { wrapper });
|
||||
await waitFor(() => expect(profileHook.current.isSuccess).toBe(true));
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/api/client';
|
||||
import type { AccountProfile, AppEntitlementDetail, AppUsageSummary, ProfileUpdateRequest } from '@/types/openapi';
|
||||
import type { AccountProfile, AppEntitlementDetail, AppUsageSummary, NewApiBindingSummary, ProfileUpdateRequest } from '@/types/openapi';
|
||||
|
||||
export type UsagePeriod = 'today' | 'week' | 'month' | 'billing_cycle';
|
||||
|
||||
@ -47,6 +47,18 @@ export function useAccountUsage(period: UsagePeriod = 'month') {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账户 New-API 绑定摘要(配额/计费外部账户绑定态)。后端 getAppNewApiBinding 就绪;
|
||||
* 未绑定时正确 fail-closed 返回 bindingStatus=unbound、canRecheck=false(New-API 服务不可用时不可重验)。此前 studio 0 消费。
|
||||
*/
|
||||
export function useAccountNewApiBinding() {
|
||||
return useQuery<NewApiBindingSummary, Error>({
|
||||
queryKey: ['accountNewApiBinding'],
|
||||
queryFn: () => api.get<NewApiBindingSummary>('/account/new-api-binding'),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** 账户购买记录条目(对齐后端 AppPurchaseRecordRespVO 关键字段)。 */
|
||||
export interface AccountPurchaseRecord {
|
||||
recordId: string;
|
||||
|
||||
@ -46,6 +46,7 @@ export type AccountProfile = AccountComponents['schemas']['AccountProfile'];
|
||||
export type AppEntitlementDetail = AccountComponents['schemas']['AppEntitlementDetail'];
|
||||
export type AppUsageSummary = AccountComponents['schemas']['AppUsageSummary'];
|
||||
export type ProfileUpdateRequest = AccountComponents['schemas']['ProfileUpdateRequest'];
|
||||
export type NewApiBindingSummary = AccountComponents['schemas']['NewApiBindingSummary'];
|
||||
// Planning / Meta-Projection(B5 planning 编辑器按 schema 字段填值)
|
||||
export type PlanningStructure = ContentComponents['schemas']['PlanningStructure'];
|
||||
export type PlanningSection = ContentComponents['schemas']['PlanningSection'];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user