# 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

`<verb><Resource>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<PagedFeatureResult>('/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
<script lang="ts" setup>
import { h, onMounted, ref } from 'vue';

import { Page } from '@vben/common-ui';

import {
  Button,
  Form,
  Input,
  message,
  Modal,
  Popconfirm,
  Space,
  Table,
  Tag,
} from 'ant-design-vue';

import {
  createFeatureApi,
  deleteFeatureApi,
  getFeaturesApi,
  FeatureStatus,
  type FeatureApi,
} from '#/api/core';

// Pagination state (always this exact set of 5 refs)
const loading = ref(false);
const data = ref<FeatureApi.FeatureDto[]>([]);
const total = ref(0);
const pageIndex = ref(1);
const pageSize = ref(20);

// Modal state
const createVisible = ref(false);
const formState = ref({ name: '', description: '' });

// Status map (always Record<number, { color: string; text: string }>)
const statusMap: Record<number, { color: string; text: string }> = {
  [FeatureStatus.Active]: { color: 'green', text: '启用' },
  [FeatureStatus.Inactive]: { color: 'red', text: '禁用' },
};

// Columns (plain const, not ref/computed)
const columns = [
  { title: '名称', dataIndex: 'name', key: 'name' },
  {
    title: '状态',
    dataIndex: 'status',
    key: 'status',
    width: 100,
    customRender: ({ record }: { record: FeatureApi.FeatureDto }) => {
      const s = statusMap[record.status];
      return h(Tag, { color: s?.color }, () => s?.text ?? '未知');
    },
  },
  { title: '操作', key: 'actions', width: 200 },
];

async function loadData() {
  loading.value = true;
  try {
    const res = await getFeaturesApi({
      pageIndex: pageIndex.value,
      pageSize: pageSize.value,
    });
    data.value = res.items ?? [];
    total.value = res.total ?? 0;
  } catch {
    message.error('加载数据失败');
  } finally {
    loading.value = false;
  }
}

function handleTableChange(pagination: any) {
  pageIndex.value = pagination.current ?? 1;
  pageSize.value = pagination.pageSize ?? 20;
  loadData();
}

function openCreate() {
  formState.value = { name: '', description: '' };
  createVisible.value = true;
}

async function handleCreate() {
  try {
    await createFeatureApi(formState.value);
    message.success('创建成功');
    createVisible.value = false;
    loadData();
  } catch {
    message.error('创建失败');
  }
}

async function handleDelete(id: string) {
  try {
    await deleteFeatureApi(id);
    message.success('删除成功');
    loadData();
  } catch {
    message.error('删除失败');
  }
}

onMounted(() => loadData());
</script>

<template>
  <Page auto-content-height>
    <div class="mb-4 flex justify-between">
      <h3 class="text-lg font-semibold">功能管理</h3>
      <Button type="primary" @click="openCreate">新建</Button>
    </div>

    <Table
      :columns="columns"
      :data-source="data"
      :loading="loading"
      :pagination="{
        current: pageIndex,
        pageSize,
        total,
        showSizeChanger: true,
        showTotal: (t: number) => `共 ${t} 条`,
      }"
      row-key="id"
      @change="handleTableChange"
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'actions'">
          <Space>
            <Popconfirm title="确定删除？" @confirm="handleDelete(record.id)">
              <Button size="small" danger>删除</Button>
            </Popconfirm>
          </Space>
        </template>
      </template>
    </Table>

    <Modal v-model:open="createVisible" title="新建" @ok="handleCreate">
      <Form layout="vertical">
        <Form.Item label="名称" required>
          <Input v-model:value="formState.name" placeholder="请输入名称" />
        </Form.Item>
        <Form.Item label="描述">
          <Input.TextArea v-model:value="formState.description" placeholder="可选" :rows="3" />
        </Form.Item>
      </Form>
    </Modal>
  </Page>
</template>
```

### Template B: Dashboard / Monitor Page

For statistics cards + table, no pagination on the table.

```vue
<script lang="ts" setup>
import { computed, h, onMounted, ref } from 'vue';

import { Page } from '@vben/common-ui';

import { Card, message, Statistic, Table } from 'ant-design-vue';

import { getMonitorApi, type MonitorDto, type OverdueItemDto } from '#/api/core';

const loading = ref(false);
const monitor = ref<MonitorDto>({ /* default zeros */ });
const overdueData = ref<OverdueItemDto[]>([]);

const stats = computed(() => [
  { title: '总数', value: monitor.value.total, color: undefined },
  { title: '运行中', value: monitor.value.running, color: '#52c41a' },
]);

const columns = [
  { title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
  { title: '处理人', dataIndex: 'assigneeId', key: 'assigneeId', width: 200 },
];

async function loadMonitor() {
  loading.value = true;
  try { monitor.value = await getMonitorApi(); }
  catch { message.error('加载监控数据失败'); }
  finally { loading.value = false; }
}

async function loadOverdue() {
  try { const res = await getOverdueApi(); overdueData.value = res.items ?? []; }
  catch { message.error('加载逾期数据失败'); }
}

onMounted(() => { loadMonitor(); loadOverdue(); });
</script>

<template>
  <Page auto-content-height>
    <div class="mb-4">
      <h3 class="text-lg font-semibold">监控</h3>
    </div>
    <div class="mb-6 grid grid-cols-4 gap-4">
      <Card v-for="stat in stats" :key="stat.title" :loading="loading">
        <Statistic
          :title="stat.title"
          :value="stat.value"
          :value-style="stat.color ? { color: stat.color } : undefined"
        />
      </Card>
    </div>
    <Table :columns="columns" :data-source="overdueData" :loading="loading" :pagination="false" row-key="id" />
  </Page>
</template>
```

### Template C: Approval / Action Page

For task lists with multiple action modals (approve, reject, delegate).

```vue
<script lang="ts" setup>
import { h, onMounted, ref } from 'vue';

import { Page } from '@vben/common-ui';

import { useUserStore } from '@vben/stores';

import { Button, Form, Input, message, Modal, Space, Table, Tag } from 'ant-design-vue';

import {
  approveTaskApi,
  rejectTaskApi,
  getPendingTasksApi,
  type TaskDto,
} from '#/api/core';

const userStore = useUserStore();
const userId = computed(() => userStore.userInfo?.userId ?? '');
import { computed } from 'vue';

const loading = ref(false);
const data = ref<TaskDto[]>([]);
const total = ref(0);
const pageIndex = ref(1);
const pageSize = ref(20);

// Each action gets its own visible ref + form refs
const approveVisible = ref(false);
const rejectVisible = ref(false);
const taskId = ref('');
const commentText = ref('');

const columns = [
  { title: '任务标题', dataIndex: 'title', key: 'title', ellipsis: true },
  { title: '操作', key: 'actions', width: 280 },
];

async function loadData() {
  loading.value = true;
  try {
    const res = await getPendingTasksApi({
      userId: userId.value,
      pageIndex: pageIndex.value,
      pageSize: pageSize.value,
    });
    data.value = res.items ?? [];
    total.value = res.total ?? 0;
  } catch {
    message.error('加载任务失败');
  } finally {
    loading.value = false;
  }
}

function openApprove(id: string) {
  taskId.value = id;
  commentText.value = '';
  approveVisible.value = true;
}

async function handleApprove() {
  try {
    await approveTaskApi(taskId.value, userId.value, commentText.value || undefined);
    message.success('审批通过');
    approveVisible.value = false;
    loadData();
  } catch {
    message.error('审批失败');
  }
}

// ... openReject / handleReject follow same pattern

function handleTableChange(pagination: any) {
  pageIndex.value = pagination.current ?? 1;
  pageSize.value = pagination.pageSize ?? 20;
  loadData();
}

onMounted(() => loadData());
</script>

<template>
  <Page auto-content-height>
    <div class="mb-4">
      <h3 class="text-lg font-semibold">我的待办</h3>
    </div>
    <Table
      :columns="columns" :data-source="data" :loading="loading"
      :pagination="{ current: pageIndex, pageSize, total, showSizeChanger: true, showTotal: (t: number) => `共 ${t} 条` }"
      row-key="id" @change="handleTableChange"
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'actions'">
          <Space>
            <Button size="small" type="primary" @click="openApprove(record.id)">审批</Button>
            <Button size="small" danger @click="openReject(record.id)">拒绝</Button>
          </Space>
        </template>
      </template>
    </Table>

    <Modal v-model:open="approveVisible" title="审批通过" @ok="handleApprove">
      <Form layout="vertical">
        <Form.Item label="审批意见">
          <Input.TextArea v-model:value="commentText" placeholder="可选" :rows="3" />
        </Form.Item>
      </Form>
    </Modal>
  </Page>
</template>
```

---

## 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
<Modal v-model:open="someVisible" title="标题" @ok="handleSome">
  <Form layout="vertical">
    <Form.Item label="字段名" required>
      <Input v-model:value="formState.field" placeholder="请输入" />
    </Form.Item>
  </Form>
</Modal>
```

### Delete with Confirmation

```html
<Popconfirm title="确定删除？" @confirm="handleDelete(record.id)">
  <Button size="small" danger>删除</Button>
</Popconfirm>
```

---

## Backend Integration Context

### API Response Format (all .NET backends)

Success: `{ code: 0, data: { ... }, message: "ok" }` — `requestClient` interceptor strips to just `data`.
Error: `{ code: <httpStatus>, 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<Uint8Array>
```

### 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<string, any>` 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
