| 类型 | 位置 | 导入方式 | 说明 |
|---|---|---|---|
| 跨项目 UI 组件 | packages/@buildingai/web/ui/src/components | @buildingai/ui/components/... | 可被主应用和扩展共同使用 |
| 跨项目 UI hooks | packages/@buildingai/web/ui/src/hooks | @buildingai/ui/hooks/... | 偏 UI 行为,如确认弹窗、分页、上传 |
| 页面级 hooks | packages/@buildingai/web/hooks/src | @buildingai/hooks | 页面 head、复制、刷新用户配置、设置弹窗上下文 |
| 请求封装 | packages/@buildingai/web/http/src、packages/@buildingai/web/services/src | @buildingai/http、@buildingai/services/... | HTTP client、业务 API hooks、上传服务 |
| 全局状态 | packages/@buildingai/web/stores/src | @buildingai/stores | Zustand store 与持久化 |
| 主应用业务组件 | packages/client/src/components | 主应用内相对路径或 @/components/... | 依赖主应用业务接口和状态,不默认给扩展使用 |
| 扩展前端封装 | packages/@buildingai/web/core/src | @buildingai/web-core | 扩展路由、Vite 配置 |
@buildingai/ui/components/ui/* 基础组件。ImageUpload、Upload、useUploadFile,不要在页面里手写 FormData。useXxxQuery / useXxxMutation,不要在组件里直接调用 http.get。@buildingai/ui/components/ai-elements;依赖主应用会话、反馈、历史消息的组件留在packages/client/src/components/ask-assistant-ui。| 组件 | 导入 | 适用场景 | 项目封装点 |
|---|---|---|---|
Button | @buildingai/ui/components/ui/button | 所有按钮 | 增加 loading、统一 variant/size、支持 asChild |
Spinner | @buildingai/ui/components/ui/spinner | 局部加载状态 | 统一 loading SVG |
Empty 系列 | @buildingai/ui/components/ui/empty | 空列表、空结果、缺省态 | 组合式空状态结构 |
Field 系列 | @buildingai/ui/components/ui/field | 表单字段布局 | 统一 label、description、error、横纵向布局 |
StatusBadge | @buildingai/ui/components/ui/status-badge | 启用/禁用、状态显示 | 内置图标、文案、颜色变体 |
TimeText | @buildingai/ui/components/ui/time-text | 时间展示 | 支持时间戳、字符串、Date、相对时间和 i18n locale |
InputGroup 系列 | @buildingai/ui/components/ui/input-group | 带前后缀、按钮、图标的输入框 | 统一 addon/button/input/textarea 组合 |
Combobox 系列 | @buildingai/ui/components/ui/combobox | 搜索选择、多选 chips | 基于 Base UI 的项目样式封装 |
DataTableFacetedFilter | @buildingai/ui/components/ui/data-table-faceted-filter | 表格单选筛选 | Popover + Command + Badge 的固定交互 |
| prop | 说明 |
|---|---|
variant | default、outline、secondary、ghost、destructive、link |
size | default、xs、sm、lg、icon、icon-xs、icon-sm、icon-lg |
loading | 为 true 时显示 Spinner,并自动 disabled |
asChild | 使用 Radix Slot,让按钮样式应用到子元素 |
import { Button } from "@buildingai/ui/components/ui/button";
<Button type="submit" variant="outline" size="sm" loading={isSubmitting}>
保存
</Button>;loading,不要额外 手写禁用和 spinner。size="icon" 或 icon-sm,并补 aria-label。asChild 包一层 Link。import {
Empty,
EmptyContent,
EmptyDescription,
EmptyMedia,
EmptyTitle,
} from "@buildingai/ui/components/ui/empty";
<Empty>
<EmptyMedia variant="icon">
<Search className="size-5" />
</EmptyMedia>
<EmptyContent>
<EmptyTitle>暂无数据</EmptyTitle>
<EmptyDescription>调整筛选条件后再试。</EmptyDescription>
</EmptyContent>
</Empty>;Field 是表单布局封装,不负责校验。校验仍由 react-hook-form、zod 或页面逻辑处理。import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@buildingai/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>;import { StatusBadge } from "@buildingai/ui/components/ui/status-badge";
<StatusBadge active={record.isEnabled} />;
<StatusBadge active={false} inactiveText="已下架" inactiveVariant="muted" />;import { TimeText } from "@buildingai/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 | 输出 |
|---|---|
datetime | YYYY-MM-DD HH:mm:ss |
date | YYYY-MM-DD |
time | HH:mm:ss |
relative | 根据当前语言显示“几分钟前/几小时前” |
import { IconPicker } from "@buildingai/ui/components/icon-picker";
import { LucideIcon } from "@buildingai/ui/components/lucide-icon";
<IconPicker value={icon} onChange={setIcon} placeholder="选择菜单图标" />;
<LucideIcon name="settings" className="size-4" />;IconPicker 内部做了虚拟滚动和搜索,适合后台菜单、功能入口、插件配置选择图标。packages/@buildingai/web/ui/src/components/ui/image-upload.tsximport { ImageUpload } from "@buildingai/ui/components/ui/image-upload";| prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | undefined | - | 受控 URL |
defaultValue | string | - | 非受控默认 URL |
onChange | (url, result) => void | - | 上传成功或清除时触发 |
variant | default | outline | default | 外观 |
size | sm | default | lg | xl | default | 尺寸 |
shape | circle | rounded | rounded | 圆形或圆角 |
accept | string | image/* | 文件类型 |
maxSize | number | 5 * 1024 * 1024 | 文件大小限制 |
forceMobile | boolean | false | 桌面也使用移动端删除按钮 |
import { ImageUpload } from "@buildingai/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。packages/@buildingai/web/ui/src/components/upload.tsximport {
Upload,
UploadDropzone,
UploadFileList,
UploadRoot,
UploadTrigger,
useUpload,
} from "@buildingai/ui/components/upload";type UploadStatus = "idle" | "uploading" | "success" | "error";
interface UploadFile {
id: string;
file: File;
status: UploadStatus;
progress: number;
result?: UploadFileResult;
error?: string;
}<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)}
/><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/@buildingai/web/services/src/shared/upload.tspackages/@buildingai/web/services/src/shared/upload-oss.tsimport {
invalidateStorageConfigCache,
uploadFile,
uploadFileAuto,
uploadFiles,
uploadFilesAuto,
uploadInitFile,
} from "@buildingai/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);
},
},
);ImageUpload、Upload、useUploadFile。invalidateStorageConfigCache(),否则前端可能继续使用旧的 activeimport { InfiniteScroll } from "@buildingai/ui/components/infinite-scroll";
<InfiniteScroll
loading={query.isFetchingNextPage}
hasMore={hasNextPage}
threshold={120}
onLoadMore={() => fetchNextPage()}
>
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</InfiniteScroll>;import { InfiniteScrollTop } from "@buildingai/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/@buildingai/web/ui/src/components/editorimport {
Editor,
EditorContainer,
EditorKit,
markdownToValue,
Plate,
serializeEditorToMarkdown,
usePlateEditor,
} from "@buildingai/ui/components/editor";EditorKit:项目默认 Plate 插件组合。BaseEditorKit:较基础的插件组合。markdownToValue:Markdown 转编辑器 value。serializeEditorToMarkdown:编辑器内容转 Markdown。EditorContainer、Editor:项目统一编辑器 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]),不要每次输入都重新构造。import AuthGuard from "@buildingai/ui/components/auth/auth-guard";{
element: <AuthGuard />,
children: [{ path: "/console", element: <ConsoleLayout /> }],
}import { PermissionGuard } from "@buildingai/ui/components/auth/permission-guard";| 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>import { RootOnly } from "@buildingai/ui/components/auth/root-only";
<RootOnly fallback={null}>
<Button variant="destructive">系统维护</Button>
</RootOnly>
<RootOnly reverse>
<span>普通用户可见</span>
</RootOnly>import { ThemeProvider, useTheme } from "@buildingai/ui/components/theme-provider";
import { ThemeToggle } from "@buildingai/ui/components/theme-toggle";
<ThemeProvider
defaultTheme="system"
storageKey="buildingai-theme"
defaultThemeColor="indigo"
themeColorStorageKey="buildingai-theme-color"
>
<App />
</ThemeProvider>;useTheme() 返回:theme / setThemethemeColor / setThemeColorimport { CopyButton } from "@buildingai/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} />packages/@buildingai/web/ui/src/components/ai-elements| 分组 | 组件 | 场景 |
|---|---|---|
| 对话容器 | Conversation、ConversationContent、ConversationScrollButton、ConversationDownload | 聊天窗口、导出对话 |
| 消息 | Message、MessageContent、MessageActions | 用户/助手消息布局 |
| 输入 | PromptInput、PromptInputTextarea、PromptInputSubmit、PromptInputAttachments | AI prompt 输入框 |
| 附件 | MessageAttachment、MessageAttachments、PromptInputAttachment | 展示上传文件 |
| 推理 | Reasoning、ReasoningTrigger、ReasoningContent、ChainOfThought | 思考过程、推理折叠 |
| 工具调用 | Tool、ToolHeader、ToolInput、ToolOutput、Confirmation | 工具状态、入参、结果、审批 |
| 代码/终端 | CodeBlock、CodeBlockCopyButton、Terminal、TerminalCopyButton | 代码高亮、命令输出 |
| 引用 | InlineCitation、Sources、Source | 知识库/网页引用 |
| Agent 过程 | FileTree、Queue、Task、TestResults、StackTrace | 文件变更、任务队列、测试结果、错误栈 |
| 媒体/预览 | Image、AudioPlayer、WebPreview、JSXPreview | 图片、音频、网页/JSX 预览 |
| 选择器 | ModelSelector、VoiceSelector、MicSelector | 模型、声音、麦克风选择 |
import { Message, MessageContent } from "@buildingai/ui/components/ai-elements/message";
<Message from="assistant">
<MessageContent>你好,我可以帮你分析这个任务。</MessageContent>
</Message>;import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from "@buildingai/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 "@buildingai/ui/components/ai-elements/code-block";
<CodeBlock code={source} language="tsx" showLineNumbers>
<CodeBlockHeader>
<CodeBlockTitle>example.tsx</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton />
</CodeBlockActions>
</CodeBlockHeader>
</CodeBlock>;@buildingai/ui/components/ai-elements。packages/client/src/components/ask-assistant-ui。packages/client/src/components/ask-assistant-uiimport { AssistantProvider, Chat, useAssistant } from "@/components/ask-assistant-ui";
import type { AiProvider } from "@buildingai/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.ts、hooks/use-message-repository.ts |
| 修改历史消息分页 | hooks/use-messages-paging.ts |
| 修改文件类型限制 | hooks/use-file-upload.ts |
packages/client/src/components/settings-dialogimport { 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 | 关于 |
packages/client/src/components/tagsimport { ManageTagsDialog, TagCreate, TagSelect } from "@/components/tags";TagSelect:<TagSelect value={tagIds} onChange={setTagIds} type="app" tagsSource="console" showManage />| prop | 说明 |
|---|---|
value | 已选标签 id 数组 |
onChange | 更新已选标签 |
type | 标签类型,如 app/dataset |
tagsSource | console 使用后台标签接口,web 使用前台标签接口 |
showManage | 是否显示管理入口 |
| 组件 | 位置 | 作用 |
|---|---|---|
AgreementDialog | packages/client/src/components/agreement-dialog.tsx | 服务协议/隐私协议弹窗 |
AppLogo | packages/client/src/components/app-logo.tsx | 根据站点配置渲染 Logo |
ProviderAvatar | packages/client/src/components/provider-avatar.tsx | AI provider 头像 |
ProviderIcon | packages/client/src/components/provider-icons.tsx | AI provider 图标 |
RightFloatingPanel | packages/client/src/components/right-floating-panel.tsx | 右侧浮动面板 |
packages/@buildingai/web/ui/src/hooks/use-alert-dialog.tsximport { AlertDialogProvider } from "@buildingai/ui/hooks/use-alert-dialog";
<AlertDialogProvider>
<App />
</AlertDialogProvider>;import { useAlertDialog } from "@buildingai/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。import { usePagination } from "@buildingai/ui/hooks/use-pagination";
const pagination = usePagination({
total: data?.total ?? 0,
pageSize,
page,
siblingCount: 1,
onPageChange: setPage,
});
return <pagination.PaginationComponent className="mt-4" />;currentPagetotalPageshasPrevioushasNextgoToPagenextPagepreviousPageresetPaginationComponentimport { useUploadFile } from "@buildingai/ui/hooks/use-upload-file";
const { uploadFile, isUploading, progress, uploadedFile } = useUploadFile({
onUploadComplete: (file) => console.log(file.url),
});
await uploadFile(file);packages/@buildingai/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 "@buildingai/hooks";
function Page() {
useDocumentHead({ title: "文章管理" });
const { copy, isCopying } = useCopy();
return (
<Button disabled={isCopying} onClick={() => copy("hello")}>
复制
</Button>
);
}packages/@buildingai/web/http/srcimport { createHttpClient, createStandardApiParser } from "@buildingai/http";baseURL + pathPrefix + url。x-request-id。query 参数。silent 跳过全局错误提示。get/post/put/patch/delete/download/upload。HttpError。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 },
});packages/@buildingai/web/services/src/base.tsimport { apiHttpClient, consoleHttpClient, createPluginHttpClients } from "@buildingai/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 "@buildingai/services";
const { apiHttpClient, consoleHttpClient } = createPluginHttpClients();
export { apiHttpClient, consoleHttpClient };packages/@buildingai/web/services/src/webpackages/@buildingai/web/services/src/consolesrc/web/servicesuseXxxQuery、useXxxListQuery、useXxxInfiniteQueryuseCreateXxxMutation、useUpdateXxxMutation、useDeleteXxxMutation[模块, 操作, 参数]PaginatedResponse<T>import type {
MutationOptionsUtil,
PaginatedQueryOptionsUtil,
PaginatedResponse,
QueryOptionsUtil,
} from "@buildingai/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/@buildingai/web/stores/srcimport { createStore } from "@buildingai/stores";
export const useDemoStore = createStore(createDemoSlice, {
persist: {
name: "demo",
partialize: (state) => ({ demo: state.demo }),
},
});| store | 作用 |
|---|---|
useAuthStore | token、当前用户、登录状态、退出登录;同步扩展可读 cookie |
useConfigStore | 站点配置、初始化状态 |
useUserConfigStore | 用户偏好配置,例如字号 |
useAssistantStore | AI 助手 UI 状态,例如选中模型 |
import { useAuthStore } from "@buildingai/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 "@buildingai/stores";
const storage = getLocalStorage();
const cached = safeJsonParse<string[]>(storage.getItem("selected_ids")) ?? [];
storage.setItem("selected_ids", safeJsonStringify(cached));packages/@buildingai/web/core/src/defineRouteOption.tsxAuthGuard。postMessage 同步给父页面。import { defineRouteOption } from "@buildingai/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 /> }],
});packages/@buildingai/web/core/src/vite/defineExtensionViteConfig.tsimport { defineExtensionViteConfig } from "@buildingai/web-core/vite/extension";
import packageJson from "./package.json";
export default defineExtensionViteConfig(packageJson, {
server: { port: 5174 },
});base: /extension/{packageName}。envDir: ./../../。.output/public。packages/@buildingai/web/ui/src/components。components/ui。components/ai-elements。packages/client/src/components。services/src/web,后台放 services/src/console。src/web/services。uploadFileAuto。useAlertDialog。PaginatedResponse<T> 和 usePagination。