- AI chat page with SSE streaming - Knowledge base management with chunk/vector preview - Workflow definition list page - Chunked file upload (>5MB auto-slicing) - Removed demo menus - Added file request client proxy config
19 KiB
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.mdfor 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
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:
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)
// 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:
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:
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:
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:
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:
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:
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:
export * from './feature-name';
Step 2: Create Route Module
Create src/router/routes/modules/feature-name.ts:
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.
<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.
<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).
<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:
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
customRender: ({ text }: { text: string | null }) =>
text ? new Date(text).toLocaleString() : '-',
Pagination Template (copy-paste)
:pagination="{
current: pageIndex,
pageSize,
total,
showSizeChanger: true,
showTotal: (t: number) => `共 ${t} 条`,
}"
Modal + Form Template
<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
<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/loginwith{ 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:
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
computedimport split — keep all Vue imports in oneimport { ... } from 'vue'statement - No
Record<string, any>in templates — castrecordin#bodyCellif needed:record as XxxDto - No inline action API calls in
@click— always extract to a named function - No
useFormcomposable — form state is managed with plainref() - 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
himport block —hcomes from the mainimport { h, ... } from 'vue'on line 1