# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. > **Cross-repo rules** — see `/Users/wen/project/rag/CLAUDE.md` for full workspace conventions. Key integration: > - .NET backends share JWT key, response format: `{ code: 0, data, message: "ok" }` > - Pagination: request `{ pageIndex: 1, pageSize: 20 }`, response `{ items: T[], total: number }` > - userId from JWT: `useUserStore().userInfo?.userId` (GUID string) > - Other repos: `rag-backend` (5211), `im-system` (5212), `work-flow`, `file-system` (8080 Go) ## Build & Dev Commands ```bash pnpm dev:antd # Dev server (port 5666, API proxied to localhost:5211) pnpm build:antd # Production build pnpm lint # Lint (oxfmt + oxlint + eslint + stylelint) pnpm format # Format pnpm check:type # Type check (turbo typecheck) pnpm test:unit # Unit tests (vitest) pnpm test:unit -- --grep "pattern" # Single test by name ``` Node ^22.18.0, pnpm 10.33.0. If `pnpm` not in PATH: `corepack pnpm`. If turbo fails to find pnpm, run type check directly: ```bash cd apps/web-antd && npx vue-tsc --noEmit --skipLibCheck ``` --- ## Architecture Vue Vben Admin 5.7.0 monorepo. Single app: `apps/web-antd` + Ant Design Vue. `#/*` alias maps to `./src/*`. ### Three Backends, Three Request Clients All clients defined in `src/api/request.ts`: | Backend | Port | Client Variable | Proxy | Response Format | |---------|------|----------------|-------|-----------------| | Main .NET | 5211 | `requestClient` | `/api` → `localhost:5211` | `{ code: 0, data, message }` — interceptor strips to `data` | | IM .NET | 5212 | `imRequestClient` (in `im.ts`) | direct | Same as main | | File Go | 8080 | `fileRequestClient` | `/file-api` → `localhost:8080` | Raw response, PascalCase fields | Bearer token from `useAccessStore()`. Auth refresh uses `baseRequestClient` (no interceptors) to avoid recursion. ### Routing Route modules in `src/router/routes/modules/*.ts` are **auto-discovered** via `import.meta.glob('./modules/**/*.ts', { eager: true })`. No manual registration. Routes require authentication by default; core routes (auth, 404) bypass. ### State Management All stores use **Pinia setup store** pattern (function-based, not options API). `defineStore('name', () => { ... })` with `ref()` for state, `computed()` for derived, plain functions for actions. --- ## Code Conventions (MUST follow) ### Import Order (strict, 4 blocks) ```typescript // Block 1: Vue core — always first import { computed, h, onMounted, ref } from 'vue'; // Block 2: Vben framework import { Page } from '@vben/common-ui'; import { useUserStore } from '@vben/stores'; // Block 3: Ant Design Vue — destructure only what's used import { Button, Form, Input, message, Modal, Space, Table, Tag } from 'ant-design-vue'; // Block 4: App API/types — use #/ alias import { getXxxApi, SomeEnum, type XxxDto } from '#/api/core'; ``` Never split Vue imports across multiple import statements. Never place app imports before framework imports. ### API Function Naming `Api` — camelCase, always ends with `Api`: ```typescript getWorkflowDefinitionsApi() createWorkflowDefinitionApi(data) deleteWorkflowDefinitionApi(id) publishWorkflowDefinitionApi(id) ``` Resource IDs are bare `string` parameters, not wrapped in objects. ### Type Organization Use `export namespace XxxApi` to group types within an API module: ```typescript export namespace AiChatApi { export interface Conversation { id: string; title: string; } export interface Message { id: string; role: string; content: string; } } ``` Enums (for status codes) are exported at file top level, not inside namespaces: ```typescript export enum DefinitionStatus { Draft = 0, Published = 1, Disabled = 2 } ``` ### Error Handling in Pages Always use bare `catch` with explicit `message.error`. Always use `finally` for loading state in `loadData`: ```typescript async function loadData() { loading.value = true; try { const res = await getXxxApi(params); data.value = res.items ?? []; // always ?? [] fallback total.value = res.total ?? 0; } catch { message.error('加载xxx失败'); // Chinese error message } finally { loading.value = false; } } ``` Action handlers (create, delete, approve) — no `finally`, just try/catch: ```typescript async function handleCreate() { try { await createXxxApi(formState.value); message.success('创建成功'); createVisible.value = false; loadData(); } catch { message.error('创建失败'); } } ``` --- ## How to Add a New Feature Page ### Step 1: Create API Module Create `src/api/core/feature-name.ts`: ```typescript import { requestClient } from '#/api/request'; // Enums at top level export enum FeatureStatus { Active = 0, Inactive = 1 } // Types in namespace export namespace FeatureApi { export interface FeatureDto { id: string; name: string; status: number; createdAt: string; } } // Generic pagination result (reuse per module) export interface PagedFeatureResult { items: FeatureApi.FeatureDto[]; total: number; } // API functions export function getFeaturesApi(params?: { pageIndex?: number; pageSize?: number; status?: number }) { return requestClient.get('/features', { params }); } export function createFeatureApi(data: { name: string; description?: string }) { return requestClient.post('/features', data); } export function deleteFeatureApi(id: string) { return requestClient.delete(`/features/${id}`); } ``` Add to `src/api/core/index.ts`: ```typescript export * from './feature-name'; ``` ### Step 2: Create Route Module Create `src/router/routes/modules/feature-name.ts`: ```typescript import type { RouteRecordRaw } from 'vue-router'; const routes: RouteRecordRaw[] = [ { meta: { icon: 'lucide:some-icon', // use lucide: prefix order: 7, // controls menu sort title: '功能管理', }, name: 'Feature', path: '/feature', children: [ { name: 'FeatureList', path: '/feature/list', component: () => import('#/views/feature/list/index.vue'), meta: { title: '功能列表', icon: 'lucide:list' }, }, ], }, ]; export default routes; ``` ### Step 3: Create Vue Page Create `src/views/feature/list/index.vue` using the template below. --- ## Page Templates ### Template A: Table CRUD Page (most common) For list pages with pagination, create/edit modals, and row actions. ```vue ``` ### Template B: Dashboard / Monitor Page For statistics cards + table, no pagination on the table. ```vue ``` ### Template C: Approval / Action Page For task lists with multiple action modals (approve, reject, delegate). ```vue ``` --- ## Reusable Patterns ### Status Tag in customRender Always use this exact pattern for status columns: ```typescript customRender: ({ record }: { record: XxxDto }) => { const s = statusMap[record.status]; return h(Tag, { color: s?.color }, () => s?.text ?? '未知'); }, ``` The `h(Tag, { color }, () => text)` 3-arg form is required. Third argument must be an arrow function. ### Date Formatting in customRender ```typescript customRender: ({ text }: { text: string | null }) => text ? new Date(text).toLocaleString() : '-', ``` ### Pagination Template (copy-paste) ```html :pagination="{ current: pageIndex, pageSize, total, showSizeChanger: true, showTotal: (t: number) => `共 ${t} 条`, }" ``` ### Modal + Form Template ```html
``` ### Delete with Confirmation ```html ``` --- ## Backend Integration Context ### API Response Format (all .NET backends) Success: `{ code: 0, data: { ... }, message: "ok" }` — `requestClient` interceptor strips to just `data`. Error: `{ code: , message: "错误描述", data: null }` — displayed by `errorMessageResponseInterceptor`. ### Pagination Convention Backend uses page-based pagination: - Request: `{ pageIndex: 1, pageSize: 20, status?: number }` - Response: `{ items: T[], total: number }` - Page indices start at 1 (not 0) ### Authentication - Login: `POST /api/auth/login` with `{ username, password }` → `{ accessToken }` - Token: JWT Bearer, attached by request interceptor - userId: Extracted from JWT claims via `useUserStore().userInfo?.userId` (is a GUID string) - Access codes (permissions): `GET /api/auth/codes` → `string[]` ### Backend Port Map | Service | Dev Port | API Prefix | Proxy | |---------|----------|------------|-------| | rag-backend | 5211 | `/api` | vite proxy | | im-system | 5212 | `/api` | direct (no proxy) | | file-system | 8080 | `/` | `/file-api` → rewrite | | work-flow | configurable | `/api` | vite proxy (same as rag-backend) | ### SSE Streaming (AI Chat) Bypasses `requestClient` entirely. Uses native `fetch()` with Bearer token: ```typescript const response = await fetch(`${apiUrl}/chat/conversations/${id}/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ content }), }); return response.body; // ReadableStream ``` ### SignalR (IM Chat) Connects to `http://localhost:5212/hubs/chat` with `?access_token=` query parameter. Events: `OnMessageReceived`, `OnMessageRead`, `OnUserTyping`, `OnUserOnline`, `OnUserOffline`. ### File Upload Uses `fileRequestClient` with `FormData` + `multipart/form-data` Content-Type. `X-API-Key` header for Go backend auth. --- ## Things to Avoid - **No `computed` import split** — keep all Vue imports in one `import { ... } from 'vue'` statement - **No `Record` in templates** — cast `record` in `#bodyCell` if needed: `record as XxxDto` - **No inline action API calls in `@click`** — always extract to a named function - **No `useForm` composable** — form state is managed with plain `ref()` - **No options API stores** — always use `defineStore('name', () => { ... })` - **No hardcoded colors in status maps** — use Ant Design tag color names (blue, green, red, orange, cyan, purple) - **No separate `h` import block** — `h` comes from the main `import { h, ... } from 'vue'` on line 1