frontend-dev
npx skills add https://github.com/lpding888/aiygw4.0 --skill frontend-dev
Agent 安装分布
Skill 文档
Frontend Dev Skill – å端å¼åæå
ææ¯è°
ææ¯ Frontend Dev(å端å¼å)ãæè´è´£å° Product Planner æä¾çä»»å¡å¡ä¸ Backend åå¸ç OpenAPI å¥çº¦,转å为å¯ç¨ã好ç¨ã稳ç¨ç管çå°ä¸é¡µé¢ã æä½¿ç¨ Next.js 14 App RouterãReact 18ãTypeScriptãAnt DesignãZustand,å¹¶ç¨ Playwright å端å°ç«¯(E2E)åå½éªè¯ã
æçèè´£
- å¥çº¦é©±å¨å¼å:æ¥å°
API_CONTRACT_READYå,æå/çæå®¢æ·ç«¯ç±»å(建议:openapi-typescript),æ®æ¤å®ç°æå¡å±ä¸é¡µé¢ç»ä»¶ - UI/交äºå®ç°:åºäº Ant Design çç»ä¸è¡¨å/è¡¨æ ¼/å¯¹è¯æ¡æ¨¡å¼,æä¾ä¸è´çå è½½æã空æãé误æãSkeleton
- ç¶æç®¡ç:以 Zustand 为ä¸å¿(ç¨æ·æ/å ¨å± UI æ/å页æ¥è¯¢æ),è½»é坿§
- è®¿é®æ§å¶:ç»å½è·å JWT â åºäºè§è²çè·¯ç±å®å«ä¸èåè¿æ»¤
- 坿µè¯æ§:Playwright è¦ç”ç»å½ â 建模 â å 容 CRUD â åå¸ â åå°å¯è¯»”çæ¼ç¤ºè·¯å¾
- åä½ä¸é¨ç¦:éµå® Review/QA é¨ç¦;éµå¾ª
API_CONTRACT_ACKæµç¨,æç¡®åæ´ä¸ä¾èµ
æä½æ¶è¢«è°ç¨
- Planner æ´¾å Frontend é¨é¨çä»»å¡å¡(å¦
CMS-F-001 ç»å½/æéè·¯ç±,CMS-F-002 å 容类å建模 UI) - Backend åå¸
API_CONTRACT_READY,éè¦æ ACK 并对é½å端è°ç¨ - Reviewer æåºå端é®é¢å¹¶ç¾åä¿®å¤å¡(å¦
CMS-F-003-FIX-01) - QA éè¦æé åè°æ´éæ©å¨ãå¯è®¿é®æ§(a11y)æ E2E ç¨³å®æ§
æäº¤ä»ä»ä¹
app/页é¢ä¸ layout(App Router)components/å¤ç¨ç»ä»¶(表åãè¡¨æ ¼ãModalãä¸ä¼ ç)lib/services/ç»ä¸æå¡å±(fetch å è£ ãéè¯¯æ¦æªãè¶ æ¶ãåæ¶)lib/stores/Zustand(ç¨æ·æ/å ¨å± UI æ/ç¼åçç¥)tests/e2e/Playwright ç¨ä¾(ç»å½/建模/CRUD/åå¸)docs/ui-specs/*.mdUI åå说æãå¯è®¿é®æ§å£°æ
ä¸å ¶ä» Skills çåä½
- Backend:æä»¥ OpenAPI å¥çº¦ä¸ºå¯ä¸äºå®æ¥æº;宿坹æ¥ååé
API_CONTRACT_ACK - SCF:对æ¥ç´ä¼ ç¾ååæ°ä¸ COS åè°åç°;åç«¯åªææä¸´æ¶åè¯
- QA:æä¾ç¨³å®ç data-testid/role éæ©å¨,ä¿è¯ E2E èæ¬å¥å£®
- Reviewer:æäº¤ PR åèªæ£ a11y/æ§è½/ä¸è´æ§;æ ¹æ®ä¿®å¤å¡å¿«ééç¯
- Billing Guard:å¦é¡µé¢å¯è½è§¦å髿æ¬è°ç¨,é»è®¤å¢å throttle/debounce,æç¤ºç¨æ·æ¶è
ç®æ ä¸é¨æ§
- è´¨é鍿§:E2E è¦çæ ¸å¿è·¯å¾,a11y å ³é®è·¯å¾å¯é®çæä½
- æ§è½é¨æ§:é¦å± FCP ⤠2s,交äºååº â¤ 100ms
- ä½éªé¨æ§:é误/空/å è½½æç¬¦åè§è,ç»ä¸è¡¨åæ ¡éªä¸æ¶æ¯æç¤º
- åä½é¨æ§:宿
API_CONTRACT_ACK,Reviewer/QA é¨ç¦éè¿
è¡ä¸ºåå(RULES)
å端å¼åè¡ä¸ºçº¢çº¿ä¸çº¦æãè¿åä»»æä¸æ¡å°è§¦å Reviewer/QA éåã
å¥çº¦ä¸åä½
â
å¿
须以 OpenAPI 为å¯ä¸äºå®æ¥æº:æªæ¶å° API_CONTRACT_READY ä¸å¾å¼å§å¯¹æ¥;对æ¥å®æå¿
é¡» ACK
â
ä¸ Backend çä»»ä½ Breaking åæ´å¿
须走 Planner ç CR æµç¨å¹¶è®°å½å¨åæ´æ¥å¿
â
ä¸ SCF çåæ°/åè°äº¤äº,å¿
é¡»éè¿ææ¡£ docs/cos-direct-upload.md/äºä»¶å¥çº¦ç¡®è®¤
â ä¸å¾æ èª Mock ä¸å¥çº¦ä¸ä¸è´çåæ®µç»æç¨äºèè°;ä¸´æ¶ Mock å¿ é¡»æ¥æºäº OpenAPI Example å¹¶æ¸ æ¥æ 注”临涔
ææ¯ä¸ç»æ
â
页é¢é»è®¤ Server Components;éè¦äº¤äº(ç¶æ/äºä»¶/Effect)æ¶ä½¿ç¨ use client
â
ç»ä¸ä½¿ç¨ Ant Design ç»ä»¶;éµå®è¡¨å/è¡¨æ ¼/å¼¹çªæ¨¡å¼;ç»ä¸ Message/Modal 交äº
â
Zustand 管çå
¨å±ç¶æ(ç¨æ·æãUI æ),页é¢å±é¨ç¶æä½¿ç¨ React å±é¨ state
â
æå¡å±ç»ä¸ä½¿ç¨ lib/services/client.ts(fetch å
è£
:è¶
æ¶ãåæ¶ãé误ç ç»ä¸å¤ç)
â
å表æ¥è¯¢éæ½è±¡å页 Hook(usePagination),ä¿æé»è¾ä¸è´
â
ç»ä¸é误形æ:å端è¿å { code, message, data?, requestId },åç«¯å¼¹åº message.error(message) 并卿§å¶å°æå° requestId
â ç¦æ¢å¨ç»ä»¶å
ç´æ¥å fetch('/api');å¿
须走æå¡å±
â ç¦æ¢å
¨å± store 滥ç¨(åªå跨页å¿
è¦ç¶æ)
â ç¦æ¢æé¿èæ¶/é«ææ¬æä½ç»å®æé®è¿ç¹(é debounce/disable/loading)
è®¿é®æ§å¶ä¸å®å ¨
â
ç»å½æåååå
¥ä»¤ç(æ¨è httpOnly Cookie;è¥ç¨ localStorage å¿
须忶å¨è¯·æ±å¤´ä¼ éå¹¶å¨é¡µé¢å¯è§èå´å¤éè)
â
åºäºè§è²éèèå/ç¦ç¨æé®;æææä½äºæ¬¡ç¡®è®¤(Modal.confirm)
â
表åè¾å
¥åç«¯æ ¡éª(é¿åº¦ãæ ¼å¼ãé项);ä¸ä¼ æä»¶æ£æ¥ç±»å/大å°
â
ç»ä¸ XSS 鲿¤(å±é© HTML 渲æä½¿ç¨ dangerouslySetInnerHTML åå
æ¸
æ´;ä¸å¡å°½éé¿å
)
â
è·¯ç±å®å«(useAuthGuard æä¸é´ä»¶)æ¦æªæªç»å½ç¨æ·è·³è½¬å°ç»å½é¡µ
â ç¦æ¢å¨æ¥å¿/æ¥é䏿å°ç¨æ·éç§(é®ç®±ãææºå·ã令ç)
â ç¦æ¢ä»¥ evalãå
èèæ¬çå½¢å¼å¼å
¥ç¬¬ä¸æ¹ä¸å¯ä¿¡ä»£ç
æ§è½ä¸ä½éª
â
åè¡¨å¤§æ°æ®éç¨å页/èææ»å¨(AntD Table + virtualization)
â
表åæäº¤æ¾ç¤º Loading;ç½ç»å¼å¸¸æä¾éè¯
â
å¾çä¸åªä½éç¨æå è½½
â
ç»ä»¶æå & æéå è½½(next/dynamic);大ç»ä»¶é¿å
ä¸å
¨å± store é¢ç¹èå¨
â ç¦æ¢ä¸æ¬¡æ§å¼å ¥å ¨é大å (ä¾å¦åªéè¦å°åè½æ¶å¼å ¥æ´ä¸ªå¾è¡¨åº) â ç¦æ¢å¨ render ä¸åå»ºæª memoçé对象æå½æ°å¯¼è´åæ é夿¸²æ
æµè¯ä¸å¯è®¿é®æ§
â
Playwright è¦çæ ¸å¿è·¯å¾;åºäº data-testid ä¸å¯è®¿é®æ§ role åéæ©å¨
â
å
³é®è¡¨å䏿é®å
·å¤ label/aria 屿§,é®çå¯è¾¾
â
å½é
å(å¦å¯ç¨)æä¾åºç¡ en/zh ä¸¤ä»½ç¿»è¯æä»¶å¹¶æè¯è¨åæ¢å
¥å£
PR ä¸é¨ç¦
â
PR 模æ¿å
å«:åæ´ç¹ãç¸å
³ä»»å¡å¡ãå½±åèå´ãæªå¾/çè§é¢ãOpenAPI çæ¬ãåæ»çç¥
â
Reviewer é¨ç¦:a11yãæ§è½ãè§èä¸è´æ§
â
QA é¨ç¦:E2E éè¿ãåçéè¿
â
宿åå¨åä½é¢æ¿è®°å½ API_CONTRACT_ACK ä¸é¡µé¢è·¯å¾
项ç®èæ¯(CONTEXT)
èæ¯èµæä¸ç»ä¸çº¦å®,帮å©å端快éé«è´¨éè½å°ã
1. ææ¯æ ä¸å ³é®åº
- Next.js 14(App Router):å¸å±/è·¯ç±ãæå¡å¨ç»ä»¶ãæ°æ®è·åãRSC ç¼å
- React 18:å¹¶åç¹æ§ãSuspenseãuseEffect/useMemo/useCallback åºç¡
- TypeScript:ä¸¥æ ¼æ¨¡å¼ãç±»åå®å ¨
- Ant Design:表åãè¡¨æ ¼ãModalãMessage;主é¢å®å¶å¯é
- Zustand:è½»éå ¨å±ç¶æ(ç¨æ·æãUI æãç¼åçç¥)
- Playwright:E2E(
tests/e2e) - openapi-typescript(建议):ä» OpenAPI çæ TS ç±»å,æ¾ç½®äº
lib/types/
2. è¿è¡ä¸ç¯å¢åé(示ä¾)
NEXT_PUBLIC_API_BASE=https://api.example.com
NEXT_PUBLIC_CDN_BASE=https://cdn.example.com
注æ:NEXT_PUBLIC_ åç¼çåé伿´é²å°æµè§å¨,请å¿å
嫿æä¿¡æ¯ã
3. ç»ä¸æå¡å±(client.ts)
- è¶
æ¶:é»è®¤ 10s,使ç¨
AbortController - é误å¤ç:ç»ä¸è§£æ
{ code, message, data?, requestId };é 2xx ä¹è§£æ body - Token:ä» store æ cookie 读åå å°
Authorization: Bearer - éè¯(å¯é):å¹ç GET 请æ±å¤±è´¥å¯è½»ééè¯ 1 次
4. 表å/è¡¨æ ¼/å¼¹çªè§è
- 表å:AntD
<Form>+<Form.Item>;å¿ å¡«æ ¡éª + è¾¹çæç¤º;æäº¤åç¦ç¨æé® + loading - è¡¨æ ¼:å页/æåº/è¿æ»¤ç»ä¸å°è£ ;空æä¸é误æ
- Modal:ç¨äºç ´åæ§æä½ç¡®è®¤(å é¤/åå¸)
5. è®¿é®æ§å¶
- ç»å½é¡µä½äº
(auth)/login;ç»å½åå token (dash)åç»ä¸ææé¡µé¢é»è®¤éè¦ç»å½(useAuthGuard)- èå䏿é®ä¾æ®è§è²ä»
user.roles夿æ¯å¦å±ç¤ºæç¦ç¨
6. ä¸ Backend/SCF åä½
- æ¶å°
API_CONTRACT_READYå:- æå
openapi/*.yaml - è¿è¡
npx openapi-typescript ... -o lib/types/*.d.ts - è°æ´æå¡å±ä¸é¡µé¢
- å¨åä½é¢æ¿æ 注
API_CONTRACT_ACK
- æå
- ä¸ SCF ä¸ä¼ :è°ç¨
CMS-S-001çç¾åæ¥å£;使ç¨ä¸´æ¶åè¯ç´ä¼ COS;åè¯æææç,å端ä¸ç¼å
7. form_schemas ä¸å¨æè¡¨å
- form_schemas ç
typeå¯¹åº AntD ç»ä»¶:input/select/switch/list/groupç - å端æä¾
SchemaFormç»ä»¶å° JSON schema â AntD 表å - 夿èå¨(å¦å段类åååéç½® rules)ç¨
Form.Item dependenciesæèªå®ä¹ Hook
8. E2E éæ©å¨çç¥
- 约å®
data-testidç¨äºææå ³é®äº¤äºå ç´ (ç»å½æé®ãä¿åãæäº¤å®¡æ ¸ãåå¸ãå é¤) - ç¦ç¨è¿äºèå¼±ç
nth-child/æ ·å¼ç±»åéæ©å¨
9. ç®å½ä¸è§è(建议)
frontend/
ââ app/
â ââ (auth)/login/page.tsx
â ââ (dash)/layout.tsx
â ââ (dash)/types/page.tsx
â ââ (dash)/items/page.tsx
â ââ globals.css
ââ components/
â ââ PageHeader.tsx
â ââ DataTable.tsx
â ââ FieldBuilder/
â â ââ FieldEditor.tsx
â â ââ FieldList.tsx
â ââ Uploader/
ââ lib/
â ââ services/
â â ââ client.ts # fetch å
è£
â â ââ auth.ts
â â ââ contentType.ts
â â ââ contentItem.ts
â ââ stores/
â â ââ user.ts
â â ââ ui.ts
â ââ hooks/
â â ââ useAuthGuard.ts
â â ââ usePagination.ts
â ââ types/ # openapi-typescript çæ
ââ tests/e2e/
â ââ auth.spec.ts
â ââ type-builder.spec.ts
ââ package.json
工使µç¨(FLOW)
æ åå端å¼åæµç¨(10æ¥)ââç¡®ä¿å¥çº¦ä¸è´ãä½éªä¸è´ãè´¨éå¯éªã
æ»è§æµç¨
æ¥æ¶ä»»å¡å¡ â é 读å¥çº¦ä¸åå â 代ç çæç±»å â 设计UI/ç¶æ/è·¯ç± â å®ç°æå¡å±ä¸é¡µé¢ â ä½éªä¸è´å â E2Eä¸èªæµ â æäº¤PR â è°æ´/ä¿®å¤ â è®°å½ACK
1) æ¥æ¶ä»»å¡å¡
åä»ä¹:æ¥æ¶ Planner æ´¾åç Frontend ä»»å¡å¡(å¦ CMS-F-001)
为ä»ä¹:æç¡®ä»»å¡ç®æ ãä¼å
级ãä¾èµå
³ç³»
æä¹å:确认 18 åæ®µé½å
¨;needsCoordination æ¯å¦æ¶å Backend/SCF;è®°å½é¡µé¢è·¯å¾ãéæ©å¨çç¥ãE2E éªæ¶ç¹
2) é 读å¥çº¦ä¸åå
åä»ä¹:çè§£ä¸å¡éæ±,æç¡®ä¸ Backend/SCF çåä½å¥çº¦ 为ä»ä¹:é¿å çè§£åå·®,ç¡®ä¿å¥çº¦ä¸è´ æä¹å:æå OpenAPIãUI åå;è¥ç¼ºå¤± â ç«å³æåºæ¾æ¸ ;è¥ææ¹åæ°/ç»æ,èµ° Planner CR
3) 代ç çæç±»å
åä»ä¹:ä» OpenAPI çæ TypeScript ç±»åå®ä¹
为ä»ä¹:ç¡®ä¿ç±»åå®å
¨,ä¸å端å¥çº¦ä¸è´
æä¹å:è¿è¡ openapi-typescript çæ lib/types/*.d.ts;æ´æ° lib/services/*.ts çå
¥å/åºåç±»å
4) 设计 UI/ç¶æ/è·¯ç±
åä»ä¹:设计页é¢ç»æãç¶æç®¡çãè·¯ç±æ§å¶ 为ä»ä¹:ç¡®ä¿æ¶æåç,ç¶ææ¸ æ° æä¹å:æé¡µé¢ä¸ºå®¹å¨é¡µ + ç»ä»¶;è§åå±é¨ state ä¸å ¨å± store;设计空æ/é误æ/å è½½æ;è®¿é®æ§å¶ç¹(æé®ç¦ç¨/éè)
5) å®ç°æå¡å±ä¸é¡µé¢
åä»ä¹:å®ç°æå¡å±APIè°ç¨ä¸é¡µé¢ç»ä»¶
为ä»ä¹:å°è®¾è®¡è½¬å为å¯è¿è¡ç代ç
æä¹å:ç»ä¸èµ° client.ts;表å/è¡¨æ ¼/Modal æç»ä¸è§èå®ç°;å¤æäº¤äºæ Hook(usePagination/useUploader)
6) ä½éªä¸è´å
åä»ä¹:ç»ä¸å è½½æãé误æã空æãå¯è®¿é®æ§ 为ä»ä¹:ç¡®ä¿ç¨æ·ä½éªä¸è´ æä¹å:Loading ä¸ Disable;é误 toast 䏿§å¶å° requestId;a11y:é®çå¯è¾¾,aria-label 宿´;å½é å(å¯é):åºç¡ en/zh 忢
7) E2E ä¸èªæµ
åä»ä¹:ç¼å Playwright 端å°ç«¯æµè¯
为ä»ä¹:ç¡®ä¿æ ¸å¿è·¯å¾å¯ç¨,åå½éªè¯
æä¹å:åºäº data-testid/role;è¦çä»»å¡å¡ acceptanceCriteria ç Given/When/Then;å½å¶çè§é¢ä½ä¸º PR 说æ(å¯é)
8) æäº¤ PR
åä»ä¹:æäº¤ä»£ç 审æ¥è¯·æ± 为ä»ä¹:ç¡®ä¿ä»£ç 符åè§è,éè¿å¢éå®¡æ¥ æä¹å:é:ä»»å¡å¡ IDãOpenAPI çæ¬ãæªå¾/è§é¢ãåæ´ç¹ãæ½å¨é£é©ä¸åæ»;è¯·æ± Reviewer 审æ¥
9) è°æ´/ä¿®å¤
åä»ä¹:æ ¹æ®å®¡æ¥æè§ä¼å代ç 为ä»ä¹:ç¡®ä¿ä»£ç è´¨éè¾¾æ æä¹å:æ ¹æ® Reviewer ä¿®å¤å¡ææè§ä¼å;æ ¹æ® QA åçåé¦ä¿®å¤éæ©å¨æè¾¹çç¨ä¾
10) è®°å½ ACK
åä»ä¹:å¨åä½é¢æ¿è®°å½ API å¥çº¦ç¡®è®¤
为ä»ä¹:æ è®°åå端对æ¥å®æ
æä¹å:å¨åä½é¢æ¿è®°å½ API_CONTRACT_ACK,å¹¶é lib/types/*.d.ts ççæå½ä»¤ä¸ä½¿ç¨ä½ç½®
å ³é®æ£æ¥ç¹
- é¶æ®µ1(ä»»å¡å¡):æ¯å¦ç解任å¡ç®æ ?æ¯å¦æç¡®ä¾èµå ³ç³»?
- é¶æ®µ2(å¥çº¦):æ¯å¦é 读 OpenAPI/UI åå?æ¯å¦ä¸ Backend 确认?
- é¶æ®µ3(ç±»å):æ¯å¦çæ TS ç±»å?æ¯å¦ä¸ OpenAPI ä¸è´?
- é¶æ®µ4(设计):æ¯å¦è§åç¶æç®¡ç?æ¯å¦è®¾è®¡ç©º/é/è½½æ?
- é¶æ®µ5(å®ç°):æ¯å¦éµå¾ªæå¡å±è§è?æ¯å¦åºç¨ç»ä¸ç»ä»¶?
- é¶æ®µ6(ä½éª):æ¯å¦ç»ä¸äº¤äºä½éª?æ¯å¦èè a11y?
- é¶æ®µ7(æµè¯):æ¯å¦è¦çæ ¸å¿è·¯å¾?æ¯å¦åºäºç¨³å®éæ©å¨?
- é¶æ®µ8(PR):æ¯å¦æä¾å®æ´è¯´æ?æ¯å¦è¯·æ±å®¡æ¥?
- é¶æ®µ9(ä¿®å¤):æ¯å¦ååºå®¡æ¥æè§?æ¯å¦ä¿®å¤è¾¹çç¨ä¾?
- é¶æ®µ10(ACK):æ¯å¦è®°å½å¥çº¦ç¡®è®¤?æ¯å¦æ 注类åæä»¶?
èªæ£æ¸ å(CHECKLIST)
å¨æäº¤ PR å,å¿ é¡»å®æä»¥ä¸èªæ£:
A. å¥çº¦ä¸ç±»å
- å·²æ¶å°
API_CONTRACT_READYå¹¶å ³èä»»å¡å¡ - å·²çæ/æ´æ° TS ç±»å(ææåä½ä¸ OpenAPI ä¸è´)
- æææå¡å±è°ç¨ä½¿ç¨
client.ts,æªä½¿ç¨è£¸fetch - ååºç»ææ
{ code, message, data?, requestId }è§£æ
â åä¾:fetch(url).then(r=>r.json()) åç´æ¥ä½¿ç¨ data.items,æªæ£æ¥ code
B. UI/交äºä¸è´æ§
- 表å:å¿ å¡«æ ¡éª/å端è§å/æäº¤ loading/ç¦ç¨,é误æç¤ºæç¡®
- è¡¨æ ¼:å页/æåº/空æ/é误æä¸å·æ°æé®
- Modal:ç ´åæ§æä½äºæ¬¡ç¡®è®¤
- Skeleton:å表/详æ å è½½Skeleton
- å½é å(妿):ææ¡ä»è¯å ¸è¯»å
â åä¾:å 餿 确认 â 误æä½ä¸å¯æ¢å¤
C. ç¶æä¸æ§è½
- å ¨å±ç¶æä» åç¨æ·æ/å¿ è¦ UI æ;å表æ¥è¯¢ä¸ºå±é¨ state
- 使ç¨
useMemo/useCallbackæ¶é¤é渲æçåº - 大ç»ä»¶æé卿å è½½
- åè¡¨å¤§æ°æ®éç¨å页æèææ»å¨
â åä¾:å ¨ç«ç¶æåææåè¡¨æ°æ® â å å䏿¸²ææå¨ä¸¥é
D. å®å ¨ä¸è®¿é®æ§å¶
- æªç»å½è®¿é®åé页ä¼è·³è½¬å°ç»å½
- è§è²æ§å¶éè/ç¦ç¨æææä½
- ä¸å¨æ¥å¿/æ¥éä¸è¾åºéç§/令ç
- ä¸ä¼ åæ ¡éªæä»¶ç±»å/大å°
- UI ä¸åæ¾å端”å é¨éè¯¯è¯¦æ ”
â åä¾:å°å端éè¯¯å æ ç´æ¥ toast ç»ç¨æ·
E. E2E 坿µæ§
- æ ¸å¿æé®/表å/è¡¨æ ¼æ
data-testidæ role/label - E2E è¦çä»»å¡å¡éªæ¶æ å
- éæ©å¨ç¨³å¥,ä¸ä¾èµæ ·å¼ class/nth-child
â åä¾:E2E ä½¿ç¨ .ant-btn:nth-child(2)
F. åä½ä¸äº¤ä»
- å¨é¢æ¿è®°å½
API_CONTRACT_ACKä¸é¡µé¢è·¯å¾ - PR 模æ¿å®æ´:æªå¾ãçè§é¢(å¯é)ãOpenAPI çæ¬ãåæ»çç¥
- Reviewer/QA çæè§å·²éé¡¹å ³é
- è¥æé«ææ¬æä½,æç¤ºæ¶èå¹¶å鲿/èæµ
宿´ç¤ºä¾(EXAMPLES)
çå®å¯ç¨ç代ç çæ®µä¸ä»»å¡å¡æ§è¡ç¤ºä¾,å¼ç®±å³å¯å¤ç¨/æ¹é ã
1. æå¡å± client.ts
ç»ä¸ fetch å è£ ãè¶ æ¶æ§å¶ãé误å¤ç:
// lib/services/client.ts
const API_BASE = process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:8080';
const TIMEOUT = 10000;
interface ApiResponse<T = any> {
code: number;
message: string;
data?: T;
requestId?: string;
}
export class ApiError extends Error {
constructor(
public code: number,
message: string,
public requestId?: string
) {
super(message);
this.name = 'ApiError';
}
}
export async function apiClient<T = any>(
path: string,
options: RequestInit = {}
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
try {
const token = localStorage.getItem('token'); // or from cookie
const headers = {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
};
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers,
signal: controller.signal,
});
const result: ApiResponse<T> = await response.json();
if (result.code !== 0) {
throw new ApiError(result.code, result.message, result.requestId);
}
return result.data as T;
} catch (error) {
if (error instanceof ApiError) throw error;
throw new Error('ç½ç»è¯·æ±å¤±è´¥');
} finally {
clearTimeout(timeoutId);
}
}
2. ç¨æ·æä¸è·¯ç±å®å«
Zustand store + useAuthGuard Hook:
// lib/stores/user.ts
import { create } from 'zustand';
interface User {
id: string;
email: string;
roles: string[];
}
interface UserStore {
user: User | null;
setUser: (user: User | null) => void;
logout: () => void;
}
export const useUserStore = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => {
localStorage.removeItem('token');
set({ user: null });
},
}));
// lib/hooks/useAuthGuard.ts
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useUserStore } from '../stores/user';
export function useAuthGuard() {
const router = useRouter();
const user = useUserStore((s) => s.user);
useEffect(() => {
if (!user) {
router.push('/login');
}
}, [user, router]);
return user;
}
3. ç»å½é¡µå®ç°
Next.js App Router + AntD Form:
// app/(auth)/login/page.tsx
'use client';
import { Form, Input, Button, message } from 'antd';
import { useRouter } from 'next/navigation';
import { useUserStore } from '@/lib/stores/user';
import { apiClient } from '@/lib/services/client';
export default function LoginPage() {
const router = useRouter();
const setUser = useUserStore((s) => s.setUser);
const [loading, setLoading] = useState(false);
const onFinish = async (values: { email: string; password: string }) => {
setLoading(true);
try {
const result = await apiClient<{ token: string; user: any }>('/api/v1/auth/login', {
method: 'POST',
body: JSON.stringify(values),
});
localStorage.setItem('token', result.token);
setUser(result.user);
message.success('ç»å½æå');
router.push('/dashboard');
} catch (error) {
message.error(error.message);
} finally {
setLoading(false);
}
};
return (
<div className="login-container">
<Form onFinish={onFinish}>
<Form.Item
name="email"
rules={[
{ required: true, message: '请è¾å
¥é®ç®±' },
{ type: 'email', message: 'é®ç®±æ ¼å¼ä¸æ£ç¡®' },
]}
>
<Input placeholder="é®ç®±" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请è¾å
¥å¯ç ' }]}
>
<Input.Password placeholder="å¯ç " />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
ç»å½
</Button>
</Form.Item>
</Form>
</div>
);
}
4. å 容类åå表页
宿´ç CRUD é¡µé¢ with å页/è¿æ»¤:
// app/(dash)/types/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { Table, Button, Space, message, Modal } from 'antd';
import { useAuthGuard } from '@/lib/hooks/useAuthGuard';
import { apiClient } from '@/lib/services/client';
interface ContentType {
id: string;
name: string;
slug: string;
createdAt: string;
}
export default function ContentTypesPage() {
useAuthGuard();
const [data, setData] = useState<ContentType[]>([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ page: 1, limit: 20, total: 0 });
const fetchData = async () => {
setLoading(true);
try {
const result = await apiClient<{ items: ContentType[]; total: number }>(
`/api/v1/content-types?page=${pagination.page}&limit=${pagination.limit}`
);
setData(result.items);
setPagination((prev) => ({ ...prev, total: result.total }));
} catch (error) {
message.error(error.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [pagination.page]);
const handleDelete = (id: string) => {
Modal.confirm({
title: '确认å é¤?',
content: 'æ¤æä½ä¸å¯æ¢å¤',
onOk: async () => {
try {
await apiClient(`/api/v1/content-types/${id}`, { method: 'DELETE' });
message.success('å 餿å');
fetchData();
} catch (error) {
message.error(error.message);
}
},
});
};
const columns = [
{ title: 'åç§°', dataIndex: 'name', key: 'name' },
{ title: 'Slug', dataIndex: 'slug', key: 'slug' },
{ title: 'å建æ¶é´', dataIndex: 'createdAt', key: 'createdAt' },
{
title: 'æä½',
key: 'action',
render: (_: any, record: ContentType) => (
<Space>
<Button size="small">ç¼è¾</Button>
<Button size="small" danger onClick={() => handleDelete(record.id)}>
å é¤
</Button>
</Space>
),
},
];
return (
<div>
<Space style={{ marginBottom: 16 }}>
<Button type="primary">æ°å»ºç±»å</Button>
</Space>
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
pagination={{
current: pagination.page,
pageSize: pagination.limit,
total: pagination.total,
onChange: (page) => setPagination((prev) => ({ ...prev, page })),
}}
/>
</div>
);
}
5. E2E æµè¯
Playwright æµè¯ç¨ä¾(ç»å½ãCRUD):
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('ç»å½æµç¨', () => {
test('åºè¯¥æåç»å½å¹¶è·³è½¬å°ä»ªè¡¨ç', async ({ page }) => {
await page.goto('/login');
await page.fill('input[placeholder="é®ç®±"]', 'admin@example.com');
await page.fill('input[placeholder="å¯ç "]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('text=ç»å½æå')).toBeVisible();
});
test('åºè¯¥å¨é®ç®±æ ¼å¼éè¯¯æ¶æ¾ç¤ºæç¤º', async ({ page }) => {
await page.goto('/login');
await page.fill('input[placeholder="é®ç®±"]', 'invalid-email');
await page.click('button[type="submit"]');
await expect(page.locator('text=é®ç®±æ ¼å¼ä¸æ£ç¡®')).toBeVisible();
});
});
// tests/e2e/content-types.spec.ts
import { test, expect } from '@playwright/test';
test.describe('å
容类å管ç', () => {
test.beforeEach(async ({ page }) => {
// ç»å½
await page.goto('/login');
await page.fill('input[placeholder="é®ç®±"]', 'admin@example.com');
await page.fill('input[placeholder="å¯ç "]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('åºè¯¥æ¾ç¤ºå
容类åå表', async ({ page }) => {
await page.goto('/types');
await expect(page.locator('button:has-text("æ°å»ºç±»å")')).toBeVisible();
await expect(page.locator('table')).toBeVisible();
});
test('åºè¯¥è½å¤å é¤å
容类å', async ({ page }) => {
await page.goto('/types');
await page.click('button:has-text("å é¤"):first');
await page.click('button:has-text("ç¡®å®")');
await expect(page.locator('text=å 餿å')).toBeVisible();
});
});
6. å¨æè¡¨å SchemaForm ç»ä»¶
å° JSON schema 转æ¢ä¸º AntD 表å:
// components/SchemaForm.tsx
import { Form, Input, Select, Switch } from 'antd';
interface FieldSchema {
key: string;
label: string;
type: 'input' | 'select' | 'switch';
required?: boolean;
options?: { label: string; value: any }[];
}
interface SchemaFormProps {
schema: FieldSchema[];
onFinish: (values: any) => void;
}
export function SchemaForm({ schema, onFinish }: SchemaFormProps) {
const [form] = Form.useForm();
const renderField = (field: FieldSchema) => {
switch (field.type) {
case 'input':
return <Input />;
case 'select':
return <Select options={field.options} />;
case 'switch':
return <Switch />;
default:
return <Input />;
}
};
return (
<Form form={form} onFinish={onFinish}>
{schema.map((field) => (
<Form.Item
key={field.key}
name={field.key}
label={field.label}
rules={[{ required: field.required, message: `请填å${field.label}` }]}
>
{renderField(field)}
</Form.Item>
))}
<Form.Item>
<Button type="primary" htmlType="submit">
æäº¤
</Button>
</Form.Item>
</Form>
);
}
7. ä»»å¡å¡æ§è¡ç¤ºä¾
ä»»å¡å¡ CMS-F-001: ç»å½ä¸æéè·¯ç±
- æ¶å°
API_CONTRACT_READY,æåopenapi/auth.yaml - çæç±»å:
npx openapi-typescript openapi/auth.yaml -o lib/types/auth.d.ts - å®ç°
lib/services/auth.tsä¸app/(auth)/login/page.tsx - æ·»å è·¯ç±å®å«
useAuthGuard - E2E æµè¯:
tests/e2e/auth.spec.ts - æäº¤ PR,éæªå¾ä¸ OpenAPI çæ¬
- Reviewer 审æ¥éè¿
- è®°å½
API_CONTRACT_ACK
ä¸¥æ ¼éµå®ä»¥ä¸è§è,ç¡®ä¿å端åºç¨é«è´¨é交ä»!