前端组件与工具开发文档

本文档整理 Chatbuddy AI 前端中已经自研或二次封装过的组件与工具。它不是第三方库介绍书,而是告诉开发者:项目里已有的封装在哪里、适合解决什么问题、怎么接入、什么时候不要重复造。

范围与导入规则

类型位置导入方式介绍
跨项目 UI 组件packages/@chatbuddy-ai/web/ui/src/components@chatbuddy-ai/ui/components/...可被主应用和扩展共同使用
跨项目 UI hookspackages/@chatbuddy-ai/web/ui/src/hooks@chatbuddy-ai/ui/hooks/...偏 UI 行为,如确认弹窗、分页、上传
页面级 hookspackages/@chatbuddy-ai/web/hooks/src@chatbuddy-ai/hooks页面 head、复制、刷新用户配置、配置弹窗上下文
求封装packages/@chatbuddy-ai/web/http/srcpackages/@chatbuddy-ai/web/services/src@chatbuddy-ai/http@chatbuddy-ai/services/...HTTP client、业务 API hooks、上传服务
全局状态packages/@chatbuddy-ai/web/stores/src@chatbuddy-ai/storesZustand store 与持久化
主应用业务组件packages/client/src/components主应用内相对路径或@/components/...依赖主应用业务接口和状态,不默认给扩展使用
扩展前端封装packages/@chatbuddy-ai/web/core/src@chatbuddy-ai/web-core扩展路由、Vite 配置
建议:
页面开发优先使用@chatbuddy-ai/ui/components/ui/*基础组件。
上传优先使用ImageUploadUploaduseUploadFile,不要在页面里手写 FormData。
列表接口优先封装成useXxxQuery/useXxxMutation,不要在组件里直接调用http.get
通用 AI 展示组件放到
@chatbuddy-ai/ui/components/ai-elements;依赖主应用会话、反馈、历史消息的组件留在
packages/client/src/components/ask-assistant-ui

基础增强 UI

这些组件多数基于 shadcn/Radix/lucide 做了项目统一样式与行为扩展。
组件导入适用场景项目封装点
Button@chatbuddy-ai/ui/components/ui/button所有按钮增加loading、统一variant/size、支持asChild
Spinner@chatbuddy-ai/ui/components/ui/spinner局部加载状态统一 loading SVG
Empty系列@chatbuddy-ai/ui/components/ui/empty空列表、空结果、缺省态组合式空状态结构
Field系列@chatbuddy-ai/ui/components/ui/field表单字段布局统一 label、description、error、横纵向布局
StatusBadge@chatbuddy-ai/ui/components/ui/status-badge启用/禁用、状态显示内置图标、文案、颜色变体
TimeText@chatbuddy-ai/ui/components/ui/time-text时间展示支持时间戳、字符串、Date、相对时间和 i18n locale
InputGroup系列@chatbuddy-ai/ui/components/ui/input-group带前后缀、按钮、图标的输入框统一 addon/button/input/textarea 组合
Combobox系列@chatbuddy-ai/ui/components/ui/combobox搜索选择、多选 chips基于 Base UI 的项目样式封装
DataTableFacetedFilter@chatbuddy-ai/ui/components/ui/data-table-faceted-filter表格单选筛选Popover + Command + Badge 的固定交互

Button

关键 props:
prop介绍
variantdefaultoutlinesecondaryghostdestructivelink
sizedefaultxssmlgiconicon-xsicon-smicon-lg
loading为 true 时显示Spinner,并自动 disabled
asChild使用 Radix Slot,让按钮样式应用到子元素
import { Button } from "@chatbuddy-ai/ui/components/ui/button"; <Button type="submit" variant="outline" size="sm" loading={isSubmitting}> 提交
</Button>;
使用建议:
异步提交用loading,不要额外手写禁用和 spinner。
纯图标按钮用size="icon"icon-sm,并补aria-label
链接样式按钮可用asChild包一层Link

Empty

import { Empty, EmptyContent, EmptyDescription, EmptyMedia, EmptyTitle,
} from "@chatbuddy-ai/ui/components/ui/empty"; <Empty> <EmptyMedia variant="icon"> <Search className="size-5" /> </EmptyMedia> <EmptyContent> <EmptyTitle>暂无数据</EmptyTitle> <EmptyDescription>调整筛选条件后再试。</EmptyDescription> </EmptyContent>
</Empty>;
适用:
列表没有数据。
搜索无结果。
页面还未配置关键资源。

Field

Field是表单布局封装,不负责校验。校验仍由react-hook-formzod或页面逻辑处理。
import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel,
} from "@chatbuddy-ai/ui/components/ui/field"; <FieldGroup> <Field> <FieldLabel htmlFor="title">标题</FieldLabel> <Input id="title" value={title} onChange={(e) => setTitle(e.target.value)} /> <FieldDescription>展示在列表和详情页。</FieldDescription> {error ? <FieldError>{error}</FieldError> : null} </Field> <Field orientation="horizontal"> <Switch checked={enabled} onCheckedChange={setEnabled} /> <FieldContent> <FieldLabel>启用</FieldLabel> <FieldDescription>停用后用户侧不可见。</FieldDescription> </FieldContent> </Field>
</FieldGroup>;

StatusBadge

import { StatusBadge } from "@chatbuddy-ai/ui/components/ui/status-badge"; <StatusBadge active={record.isEnabled} />;
<StatusBadge active={false} inactiveText="已下架" inactiveVariant="muted" />;

TimeText

import { TimeText } from "@chatbuddy-ai/ui/components/ui/time-text"; <TimeText value={item.createdAt} />;
<TimeText value={item.createdAt} variant="date" />;
<TimeText value={item.updatedAt} variant="relative" />;
<TimeText value={item.createdAt} format="YYYY年MM月DD日 HH:mm" />;
variant
variant输出
datetimeYYYY-MM-DD HH:mm:ss
dateYYYY-MM-DD
timeHH:mm:ss
relative根据当前语言显示“几分钟前/几小时前”

IconPicker 与 LucideIcon

import { IconPicker } from "@chatbuddy-ai/ui/components/icon-picker";
import { LucideIcon } from "@chatbuddy-ai/ui/components/lucide-icon"; <IconPicker value={icon} onChange={setIcon} placeholder="选择菜单图标" />;
<LucideIcon name="settings" className="size-4" />;
IconPicker内部做了虚拟滚动和搜索,适合后台菜单、功能入口、插件配置选择图标。

上传组件与上传工具

ImageUpload

位置:packages/@chatbuddy-ai/web/ui/src/components/ui/image-upload.tsx
导入:
import { ImageUpload } from "@chatbuddy-ai/ui/components/ui/image-upload";
适用:
用户头像。
应用 Logo。
文章封面。
配置项里的单图字段。
关键 props:
prop类型默认值介绍
valuestring | undefined-受控 URL
defaultValuestring-非受控默认 URL
onChange(url, result) => void-上传成功或清除时触发
variantdefault | outlinedefault外观
sizesm | default | lg | xldefault尺寸
shapecircle | roundedrounded圆形或圆角
acceptstringimage/*文件类型
maxSizenumber5 * 1024 * 1024文件大小限制
forceMobilebooleanfalse桌面也使用移动端移除按钮
import { ImageUpload } from "@chatbuddy-ai/ui/components/ui/image-upload"; <ImageUpload value={form.cover} size="xl" onChange={(url, result) => { setForm((prev) => ({ ...prev, cover: url ?? "" })); console.log(result?.id); }} onUploadStart={ => setUploading(true)} onUploadError={(error) => toast.error(error.message)}
/>;
内部逻辑:
系统未初始化时使用uploadInitFile,无需登录态。
系统初始化后使用useUpload,最终走uploadFileAuto
有图时桌面 hover 显示替换/移除;移动端显示右上角移除按钮。

Upload

位置:packages/@chatbuddy-ai/web/ui/src/components/upload.tsx
导入:
import { Upload, UploadDropzone, UploadFileList, UploadRoot, UploadTrigger, useUpload,
} from "@chatbuddy-ai/ui/components/upload";
核心类型:
type UploadStatus = "idle" | "uploading" | "success" | "error"; interface UploadFile { id: string; file: File; status: UploadStatus; progress: number; result?: UploadFileResult; error?: string;
}
预设 UI:
<Upload multiple maxFiles={5} maxSize={20 * 1024 * 1024} accept=".pdf,.docx,.txt" params={{ description: "知识库文档", extensionId }} onUploadStart={(files) => console.log(files.length)} onUploadProgress={(file) => console.log(file.progress)} onUploadSuccess={(file, result) => console.log(file.file.name, result.url)} onUploadError={(file, error) => toast.error(`${file.file.name}: ${error.message}`)} onUploadComplete={(files) => console.log(files)}
/>
组合式 UI:
<UploadRoot multiple accept="image/*" maxFiles={3}> <UploadTrigger>选择图片</UploadTrigger> <UploadDropzone className="rounded-md border border-dashed p-8" activeClassName="border-primary bg-primary/5" > {({ isDragOver, isUploading }) => isUploading ? "上传中..." : isDragOver ? "松开上传" : "拖拽文件到这里" } </UploadDropzone> <UploadFileList className="space-y-2" />
</UploadRoot>
只复用逻辑:
const { files, upload, isUploading, removeFile, clearFiles, getRootProps, getInputProps } = useUpload({ multiple: true, onUploadComplete: (items) => console.log(items), }); return ( <div {...getRootProps}> <input {...getInputProps} /> {files.map((file) => ( <button key={file.id} onClick={ => removeFile(file.id)}> {file.file.name} </button> ))} </div>
);

上传服务

位置:
packages/@chatbuddy-ai/web/services/src/shared/upload.ts
packages/@chatbuddy-ai/web/services/src/shared/upload-oss.ts
导入:
import { invalidateStorageConfigCache, uploadFile, uploadFileAuto, uploadFiles, uploadFilesAuto, uploadInitFile,
} from "@chatbuddy-ai/services/shared";
选择规则:
函数场景
uploadFileAuto业务单文件上传,自动判断本地或 OSS,优先使用
uploadFilesAuto业务多文件上传,自动判断本地或 OSS,优先使用
uploadFile强制走后端本地上传接口
uploadFiles强制走后端本地多文件上传接口
uploadInitFile系统初始化阶段,无登录态上传
invalidateStorageConfigCache修改存储配置后清空前端缓存
const result = await uploadFileAuto( file, { description: "头像" }, { onUploadProgress: (event) => { const percent = event.total ? Math.round((event.loaded / event.total) * 100) : 0; setProgress(percent); }, },
);
注意:
页面层一般不直接调用上传服务,优先用ImageUploadUploaduseUploadFile
如果后台切换了存储类型,需调用invalidateStorageConfigCache,否则前端可能继续使用旧的 active
storage 配置。

列表与滚动

InfiniteScroll

向下加载更多,适合卡片列表、表格外层列表。
import { InfiniteScroll } from "@chatbuddy-ai/ui/components/infinite-scroll"; <InfiniteScroll loading={query.isFetchingNextPage} hasMore={hasNextPage} threshold={120} onLoadMore={ => fetchNextPage}
> {items.map((item) => ( <Item key={item.id} item={item} /> ))}
</InfiniteScroll>;

InfiniteScrollTop

向上加载历史,适合聊天消息。它会处理底部吸附和 prepend 后的滚动保持。
import { InfiniteScrollTop } from "@chatbuddy-ai/ui/components/infinite-scroll-top"; <InfiniteScrollTop hasMore={hasMoreMessages} isLoadingMore={isLoadingMoreMessages} onLoadMore={loadMoreMessages} prependKey={oldestMessageId} topThreshold={32}
> {messages.map((message) => ( <MessageItem key={message.id} message={message} /> ))}
</InfiniteScrollTop>;
使用注意:
prependKey传入最老消息 id 或列表长度,用于识别“前面插入了历史消息”。
内容很少但仍希望撑满容器时用forceFullHeight
不需“回到底部”按钮时配置hideScrollToBottomButton

富文本修改器

位置:packages/@chatbuddy-ai/web/ui/src/components/editor
导入:
import { Editor, EditorContainer, EditorKit, markdownToValue, Plate, serializeEditorToMarkdown, usePlateEditor,
} from "@chatbuddy-ai/ui/components/editor";
封装内容:
EditorKit:项目默认 Plate 插件组合。
BaseEditorKit:较基础的插件组合。
markdownToValue:Markdown 转修改器 value。
serializeEditorToMarkdown:修改器内容转 Markdown。
EditorContainerEditor:项目统一修改器 UI。
EditorContentRenderer:只读渲染内容。
修改示例:
const editor = usePlateEditor({ plugins: EditorKit, id: "article-content", value: markdownToValue(initialMarkdown),
}); <Plate editor={editor} onValueChange={ => { const markdown = serializeEditorToMarkdown(editor); setContent(markdown); }}
> <EditorContainer className="min-h-80 rounded-lg border"> <Editor variant="default" /> </EditorContainer>
</Plate>;
使用注意:
修改器实例id要稳定,避免多个修改器状态串扰。
初始值建议用useMemo( => markdownToValue(content), [contentId]),不要每次输入都重新构造。
表单提交时存 Markdown,详情页渲染时再转或用只读渲染器。

鉴权与权限组件

AuthGuard

导入:
import AuthGuard from "@chatbuddy-ai/ui/components/auth/auth-guard";
适用:路由层保护。未登录时跳转登录页;扩展路径会处理插件场景的重定向。
{ element: <AuthGuard />, children: [{ path: "/console", element: <ConsoleLayout /> }],
}

PermissionGuard

导入:
import { PermissionGuard } from "@chatbuddy-ai/ui/components/auth/permission-guard";
关键 props:
prop介绍
permissions单个权限码或权限码数组
any为 true 时任意一个权限满足就能;默认需全部满足
blockOnly为 true 时不隐藏元素,只阻止交互并提示
fallback无权限且非blockOnly时展示
toastMessage阻止交互时的提示
<PermissionGuard permissions="role:create"> <Button>创建角色</Button>
</PermissionGuard> <PermissionGuard permissions={["role:update", "role:delete"]} any blockOnly> <Button variant="destructive">批量操作</Button>
</PermissionGuard>

RootOnly

import { RootOnly } from "@chatbuddy-ai/ui/components/auth/root-only"; <RootOnly fallback={null}> <Button variant="destructive">系统维护</Button>
</RootOnly> <RootOnly reverse> <span>普通用户可见</span>
</RootOnly>

主题、图标与展示工具

ThemeProvider 与 ThemeToggle

import { ThemeProvider, useTheme } from "@chatbuddy-ai/ui/components/theme-provider";
import { ThemeToggle } from "@chatbuddy-ai/ui/components/theme-toggle"; <ThemeProvider defaultTheme="system" storageKey="chatbuddy-ai-theme" defaultThemeColor="indigo" themeColorStorageKey="chatbuddy-ai-theme-color"
> <App />
</ThemeProvider>;
useTheme返回:
theme/setTheme
themeColor/setThemeColor

CopyButton

import { CopyButton } from "@chatbuddy-ai/ui/components/copy-button"; <CopyButton value={code} timeout={1500} onCopy={ => toast.success("已复制")} onCopyError={(error) => toast.error(error.message)}
/>;

文件、图标、动效

组件用法
FileFormatIcon根据format渲染文件类型图标
LucideIcon用字符串图标名渲染 lucide 图标
IconPicker图标选择器,内置搜索和虚拟滚动
Loader品牌 loading
ReloadWindow选择刷新窗口
CountUp数字递增动画
SplitText文本拆分动画
<FileFormatIcon format="pdf" className="size-5" />
<LucideIcon name="settings" className="size-4" />
<IconPicker value={icon} onChange={setIcon} />

AI 展示组件

位置:packages/@chatbuddy-ai/web/ui/src/components/ai-elements
这组组件是 AI/Agent 场景的通用展示 primitives,不绑定主应用的会话接口。

常用组件分组

分组组件场景
对话容器ConversationConversationContentConversationScrollButtonConversationDownload聊天窗口、导出对话
消息MessageMessageContentMessageActions用户/助手消息布局
输入PromptInputPromptInputTextareaPromptInputSubmitPromptInputAttachmentsAI prompt 输入框
附件MessageAttachmentMessageAttachmentsPromptInputAttachment展示上传文件
推理ReasoningReasoningTriggerReasoningContentChainOfThought思考过程、推理折叠
工具调用ToolToolHeaderToolInputToolOutputConfirmation工具状态、入参、结果、审批
代码/终端CodeBlockCodeBlockCopyButtonTerminalTerminalCopyButton代码高亮、命令输出
引用InlineCitationSourcesSource知识库/网页引用
Agent 过程FileTreeQueueTaskTestResultsStackTrace文件变更、任务队列、测试结果、错误栈
媒体/预览ImageAudioPlayerWebPreviewJSXPreview图片、音频、网页/JSX 预览
选择器ModelSelectorVoiceSelectorMicSelector模型、声音、麦克风选择

基础消息

import { Message, MessageContent } from "@chatbuddy-ai/ui/components/ai-elements/message"; <Message from="assistant"> <MessageContent>你好,我可以帮你分析这个任务。</MessageContent>
</Message>;

工具调用

import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,
} from "@chatbuddy-ai/ui/components/ai-elements/tool"; <Tool defaultOpen> <ToolHeader type="tool-weather" state="output-available" toolName="weather" /> <ToolContent> <ToolInput input={{ city: "Shanghai" }} /> <ToolOutput output={{ temperature: 22 }} /> </ToolContent>
</Tool>;

代码块

import { CodeBlock, CodeBlockActions, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,
} from "@chatbuddy-ai/ui/components/ai-elements/code-block"; <CodeBlock code={source} language="tsx" showLineNumbers> <CodeBlockHeader> <CodeBlockTitle>example.tsx</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton /> </CodeBlockActions> </CodeBlockHeader>
</CodeBlock>;
迁移原则:
只是“展示一个 AI 产物”:放@chatbuddy-ai/ui/components/ai-elements
需调用主应用聊天接口、历史消息接口、反馈接口:放
packages/client/src/components/ask-assistant-ui

主应用业务组件

ask-assistant-ui

位置:packages/client/src/components/ask-assistant-ui
这是主应用完整 AI 对话模块,包含:
模型列表转换与选中模型。
MCP 服务选择。
文件上传与文件类型限制。
流式对话求。
历史消息分页和向上加载。
消息分支、重新生成、修改后继续对话。
点赞点踩反馈。
工具调用展示。
入口:
import { AssistantProvider, Chat, useAssistant } from "@/components/ask-assistant-ui";
import type { AiProvider } from "@chatbuddy-ai/services/web";
典型用法:
function AssistantChat({ providers }: { providers: AiProvider[] }) { const assistant = useAssistant({ providers, enableThinking: true, suggestions: [ { title: "帮我总结", prompt: "总结当前内容" }, { title: "生成计划", prompt: "给我一个执行计划" }, ], }); return ( <AssistantProvider {...assistant}> <Chat title="AI 助手" /> </AssistantProvider> );
}
内部模块怎么改:
需求修改位置
调整输入框、建议词、语音输入components/input/*
调整消息气泡、用量、分支、反馈components/message/*
新增工具展示components/tools/*
修改发送参数、停止、重新生成、审批hooks/use-chat-stream.ts
修改消息树、分支逻辑libs/message-repository.tshooks/use-message-repository.ts
修改历史消息分页hooks/use-messages-paging.ts
修改文件类型限制hooks/use-file-upload.ts

settings-dialog

位置:packages/client/src/components/settings-dialog
导入:
import { SettingsDialogProvider, useSettingsDialog } from "@/components/settings-dialog";
根部挂载:
function Root { return <SettingsDialogProvider>{children}</SettingsDialogProvider>;
}
业务打开:
function ProfileButton { const settings = useSettingsDialog; return <Button onClick={ => settings.open("profile")}>个人配置</Button>;
}
已有页面:
page内容
profile个人资料
general通用配置
wallet钱包/算力
redeemCDK兑换码
tools工具配置
subscribe订阅
personalized个性化
about关于

tags

位置:packages/client/src/components/tags
导入:
import { ManageTagsDialog, TagCreate, TagSelect } from "@/components/tags";
TagSelect
<TagSelect value={tagIds} onChange={setTagIds} type="app" tagsSource="console" showManage />
关键 props:
prop介绍
value已选标签 id 数组
onChange更新已选标签
type标签类型,如 app/dataset
tagsSourceconsole使用后台标签接口,web使用前台标签接口
showManage是否显示管理入口

其他主应用组件

组件位置作用
AgreementDialogpackages/client/src/components/agreement-dialog.tsx服务协议/隐私协议弹窗
AppLogopackages/client/src/components/app-logo.tsx根据站点配置渲染 Logo
ProviderAvatarpackages/client/src/components/provider-avatar.tsxAI provider 头像
ProviderIconpackages/client/src/components/provider-icons.tsxAI provider 图标
RightFloatingPanelpackages/client/src/components/right-floating-panel.tsx右侧浮动面板

前端 hooks

useAlertDialog

位置:packages/@chatbuddy-ai/web/ui/src/hooks/use-alert-dialog.tsx
根部挂载:
import { AlertDialogProvider } from "@chatbuddy-ai/ui/hooks/use-alert-dialog"; <AlertDialogProvider> <App />
</AlertDialogProvider>;
业务使用:
import { useAlertDialog } from "@chatbuddy-ai/ui/hooks/use-alert-dialog"; const { confirm } = useAlertDialog; async function handleDelete { await confirm({ title: "确认移除?", description: "移除后不可恢复", confirmText: "移除", confirmVariant: "destructive", }); await deleteMutation.mutateAsync(id);
}
注意:取消时confirm会 throw。可以不写 catch,让函数中断;需静默时自行try/catch

usePagination

import { usePagination } from "@chatbuddy-ai/ui/hooks/use-pagination"; const pagination = usePagination({ total: data?.total ?? 0, pageSize, page, siblingCount: 1, onPageChange: setPage,
}); return <pagination.PaginationComponent className="mt-4" />;
返回:
currentPage
totalPages
hasPrevious
hasNext
goToPage
nextPage
previousPage
reset
PaginationComponent

useUploadFile

简单上传 hook:
import { useUploadFile } from "@chatbuddy-ai/ui/hooks/use-upload-file"; const { uploadFile, isUploading, progress, uploadedFile } = useUploadFile({ onUploadComplete: (file) => console.log(file.url),
}); await uploadFile(file);

页面级 hooks

位置:packages/@chatbuddy-ai/web/hooks/src
hook介绍
useDocumentHead注册页面 head 配置
useHeadRenderer根部渲染 head 配置到 DOM
definePageMeta给页面模块定义标题、权限、隐藏等元信息
parsePageModules从 Vite glob 页面模块中解析页面路径和 meta
useCopy复制文本,内置 toast 与 fallback
useRefreshUser刷新用户信息
useRefreshUserConfig刷新用户配置
useRefreshWebsiteConfig刷新站点配置
useSettingsDialog配置弹窗命令式上下文
useInboxHeartbeat收件箱心跳刷新
import { useCopy, useDocumentHead } from "@chatbuddy-ai/hooks"; function Page { useDocumentHead({ title: "文章管理" }); const { copy, isCopying } = useCopy; return ( <Button disabled={isCopying} onClick={ => copy("hello")}> 复制 </Button> );
}

求层封装

HttpClient

位置:packages/@chatbuddy-ai/web/http/src
导入:
import { createHttpClient, createStandardApiParser } from "@chatbuddy-ai/http";
封装能力:
拼接baseURL + pathPrefix + url
每个求自动加x-request-id
支持query参数。
支持silent跳过全局错误提示。
支持get/post/put/patch/delete/download/upload
统一把 Axios 错误、业务错误转换为HttpError
401 触发onAuthError
业务无权限码40203触发onAccessError
const http = createHttpClient({ baseURL: import.meta.env.VITE_PRODUCTION_APP_BASE_URL, pathPrefix: "/api", parseResponse: createStandardApiParser, hooks: { getAccessToken: => token, onError: (error) => toast.error(error.message), },
}); const detail = await http.get<UserInfo>("/user/info");
const list = await http.get<PaginatedResponse<User>>("/user", { query: { page: 1, pageSize: 15 },
});

内置 client

位置:packages/@chatbuddy-ai/web/services/src/base.ts
import { apiHttpClient, consoleHttpClient, createPluginHttpClients } from "@chatbuddy-ai/services";
client用途前缀
apiHttpClient前台用户侧接口VITE_APP_WEB_API_PREFIX,默认/api
consoleHttpClient后台控制台接口VITE_APP_CONSOLE_API_PREFIX,默认/console
createPluginHttpClients(identifier?)扩展内接口/{identifier}/api/{identifier}/console
扩展内推荐写一个services/base.ts
import { createPluginHttpClients } from "@chatbuddy-ai/services"; const { apiHttpClient, consoleHttpClient } = createPluginHttpClients; export { apiHttpClient, consoleHttpClient };

API hooks 编写规范

位置:
packages/@chatbuddy-ai/web/services/src/web
packages/@chatbuddy-ai/web/services/src/console
扩展内src/web/services
约定:
查询:useXxxQueryuseXxxListQueryuseXxxInfiniteQuery
变更:useCreateXxxMutationuseUpdateXxxMutationuseDeleteXxxMutation
queryKey 使用数组,格式为[模块, 操作, 参数]
类型和 hook 放在同一模块里,方便业务导入
分页返回统一用PaginatedResponse<T>
import type { MutationOptionsUtil, PaginatedQueryOptionsUtil, PaginatedResponse, QueryOptionsUtil,
} from "@chatbuddy-ai/web-types";
import { useMutation, useQuery } from "@tanstack/react-query"; import { consoleHttpClient } from "../base"; export function useArticleListQuery( params?: QueryArticleParams, options?: PaginatedQueryOptionsUtil<Article>,
) { return useQuery({ queryKey: ["articles", "list", params], queryFn: => consoleHttpClient.get<PaginatedResponse<Article>>("/article", { params }), ...options, });
} export function useCreateArticleMutation( options?: MutationOptionsUtil<Partial<Article>, CreateArticleParams>,
) { return useMutation({ mutationFn: (data) => consoleHttpClient.post<Partial<Article>>("/article", data), ...options, });
}

全局状态封装

位置:packages/@chatbuddy-ai/web/stores/src

createStore

项目对 Zustand 做了统一创建封装,并接入持久化同步。
import { createStore } from "@chatbuddy-ai/stores"; export const useDemoStore = createStore(createDemoSlice, { persist: { name: "demo", partialize: (state) => ({ demo: state.demo }), },
});

已有 store

store作用
useAuthStoretoken、当前用户、登录状态、退出登录;同步扩展可读 cookie
useConfigStore站点配置、初始化状态
useUserConfigStore用户偏好配置,比如字号
useAssistantStoreAI 助手 UI 状态,比如选中模型
import { useAuthStore } from "@chatbuddy-ai/stores"; const token = useAuthStore((s) => s.auth.token);
const userInfo = useAuthStore((s) => s.auth.userInfo);
const logout = useAuthStore((s) => s.authActions.logout);
存储工具:
import { getLocalStorage, safeJsonParse, safeJsonStringify } from "@chatbuddy-ai/stores"; const storage = getLocalStorage;
const cached = safeJsonParse<string[]>(storage.getItem("selected_ids")) ?? [];
storage.setItem("selected_ids", safeJsonStringify(cached));

扩展前端封装

defineRouteOption

位置:packages/@chatbuddy-ai/web/core/src/defineRouteOption.tsx
封装内容:
统一扩展 basename。
接入AuthGuard
接入扩展控制台布局。
iframe 内 route 变化通过postMessage同步给父页面。
404 时通知父页面。
import { defineRouteOption } from "@chatbuddy-ai/web-core";
import packageJson from "./../../package.json"; export const routeOption = defineRouteOption({ base: `extension/${packageJson.name}`, identifier: packageJson.name, routes: [{ index: true, element: <IndexPage /> }], consoleMenus: [{ title: "文章管理", path: "/", icon: "file-text" }], consoleRoutes: [{ index: true, element: <ArticleListPage /> }],
});

defineExtensionViteConfig

位置:packages/@chatbuddy-ai/web/core/src/vite/defineExtensionViteConfig.ts
import { defineExtensionViteConfig } from "@chatbuddy-ai/web-core/vite/extension";
import packageJson from "./package.json"; export default defineExtensionViteConfig(packageJson, { server: { port: 5174 },
});
封装内容:
React 插件。
Tailwind Vite 插件。
React Compiler。
base: /extension/{packageName}
envDir: ./../../
默认输出.output/public

新增前端代码怎么选

1.
通用 UI:放packages/@chatbuddy-ai/web/ui/src/components
2.
shadcn/Radix 项目风格封装:放components/ui
3.
AI 展示 primitive:放components/ai-elements
4.
依赖主应用业务接口或状态:放packages/client/src/components
5.
业务接口 hooks:前台放services/src/web,后台放services/src/console
6.
扩展接口 hooks:放扩展自己的src/web/services
7.
文件上传:优先用已有上传组件和uploadFileAuto
8.
移除/危险操作:优先用useAlertDialog
9.
分页列表:优先用PaginatedResponse<T>usePagination

319 篇文档 · 内容同步自官方帮助中心