Commit a503cdbd authored by Andy-bubu's avatar Andy-bubu

feat: migrate UI to Xinyuan HeroUI design system

parent ba702ba8
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
/.umi-test /.umi-test
/dist /dist
/.mfsu /.mfsu
/.playwright-cli/
.swc .swc
.idea/* .idea/*
.git/* .git/*
......
module.exports = { module.exports = {
extends: require.resolve('@umijs/max/stylelint'), extends: require.resolve('@umijs/max/stylelint'),
overrides: [
{
files: ['src/design-system/shell.css'],
rules: {
'no-duplicate-selectors': null,
'selector-class-pattern': null,
'value-keyword-case': null,
},
},
{
files: ['src/tailwind.css'],
rules: {
'at-rule-no-unknown': [true, { ignoreAtRules: ['theme'] }],
'no-duplicate-selectors': null,
'property-no-vendor-prefix': null,
'selector-class-pattern': null,
},
},
],
}; };
import { defineConfig } from '@umijs/max'; import { defineConfig } from '@umijs/max';
import path from 'path';
function resolvePackageRoot(packageName: string) {
return path.dirname(require.resolve(`${packageName}/package.json`));
}
const APP_BASE_PATH = '/ai_chat/';
export default defineConfig({ export default defineConfig({
base: '/ai_chat/', base: APP_BASE_PATH,
publicPath: '/ai_chat/', publicPath: APP_BASE_PATH,
antd: {}, links: [
{
rel: 'stylesheet',
href: `${APP_BASE_PATH}fonts/inter.css`,
},
],
alias: {
react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'),
},
access: {}, access: {},
model: {}, model: {},
initialState: {}, initialState: {},
request: {}, request: {},
mfsu: false,
esbuildMinifyIIFE: true, esbuildMinifyIIFE: true,
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
reactQuery: { reactQuery: {
devtool: false, devtool: false,
}, },
......
# AI 应用 UI 架构
## 分层
```text
Umi route /ai_chat/
-> root theme and CSS imports
-> HeroUI Pro AppShell
-> Sidebar: identity, new session, conversations, app switcher, user menu
-> Navbar: Sidebar.Trigger, route title, page actions, theme action
-> page canvas: AI workspace
-> conversation/content surface
-> fixed PromptInput composer
-> PromptInput and attachment workflow
-> root-level HeroUI overlays
-> login
-> application switcher
-> conversation confirmations
-> audit knowledge and prompt dialogs
```
## 所有权规则
- 页面/controller 保留业务 state、API 参数、回调、格式转换、请求时机和权限判断。
- HeroUI/HeroUI Pro 组件拥有可见控件、ARIA、焦点、键盘交互、Portal 和视觉状态。
- 标准弹窗通过共享 `rc-dialog-panel--{size}` 尺寸梯度约束宽度;内容型配置/导入弹窗使用 `4xl = 1088px`,桌面保留 40px 安全边距,手机保留 16px 安全边距。
- 页面级短命令通过 AppShell 的 Navbar actions 插槽渲染在主题按钮左侧;窄屏使用 HeroUI Dropdown,页面正文不重复同一状态摘要。
- AI 工作区由固定视口高度承载;`ChatConversation` 是唯一纵向滚动区域,PromptInput 作为 flex 尾部固定在底部。PromptInput 使用 HeroUI Pro 官方 `stacked`/`lg` 组合,保持附件、停止生成、发送和自动扩展行为。
- 共享组件只能是业务无关的 HeroUI-native composition,不得复刻 Ant/旧组件 props、事件或 DOM。
- Ant Design Icons 可作为纯图标目录保留;不得通过它包装 UI primitive。
- 服务层、审计引擎、Excel 解析/导出和 tracking 不因 UI 迁移而重写。
## 依赖来源
- HeroUI Pro 由目标仓库内官方授权 setup 流程独立安装。
- `package.json` 使用正式版本号,不使用 `file:`、绝对路径、相邻项目 `node_modules`、软链接或 vendor 包。
- 项目只保留 `pnpm-lock.yaml`。
- 最终交付前必须验证一个兼容的 React、HeroUI 与 React Aria runtime graph。
实际安装解析为 `@heroui-pro/react@1.0.0-beta.8`、`@heroui/react@3.2.4`、`@heroui/styles@3.2.4`、React/ReactDOM `19.2.8`。官方安装壳的锁文件元数据不包含完整 Pro 包的 peer 声明,因此 `pnpm-workspace.yaml` 使用 `packageExtensions` 补充包内实际声明的 React/ReactDOM `>=19.0.0`,使 Pro 运行链固定到根 React 19。Umi 工具链仍保留自身的 React 18 传递副本;生产构建成功,运行产物与浏览器未出现重复 React、React Aria Context、slot 或 provider 警告。
## 字体加载边界
- `src/tailwind.css` 只负责 Tailwind、HeroUI、HeroUI Pro、鑫元主题与共享样式,不得导入 `@fontsource/inter/*` 或声明 `@font-face`。
- Inter 400/500/600 的字体声明与 WOFF2 由 `public/fonts/` 持有,并通过 `.umirc.ts` 的稳定 HTML stylesheet link 加载。
- 该 link 不属于 Umi 的 `umi.css` HMR 生命周期;更新页面 CSS 时允许替换 `umi.css`,不得替换或移除 `fonts/inter.css`。
- `--font-inter`、`--font-sans` 与页面字体栈仍由鑫元主题/Tokens 消费,不改变组件字体契约。
## 迁移映射
| 旧边界 | 新边界 |
| --- | --- |
| Ant ConfigProvider/antd-style 全局主题 | HeroUI CSS + 权威 theme.css + tokens.css |
| Ant Modal | HeroUI Modal/AlertDialog compound API |
| Ant Button/Input/Form/Dropdown/Tooltip/Drawer | 对应 HeroUI primitive/compound API |
| Ant X Conversations/Bubble/Sender/Attachments/Prompts | HeroUI Pro AI 与导航组件 |
| Ant Upload | HeroUI Pro DropZone |
| Ant Table | HeroUI Pro DataGrid |
| Ant Empty/Spin/Skeleton/Tag | HeroUI/Pro EmptyState、Spinner、Skeleton、Chip |
## 业务保护检查点
- 登录仍调用现有 `getCaptcha` 与 `login`,保留 token、错误和刷新逻辑。
- 登录成功仍写入原 `current_user` 结构并触发 Home 的现有刷新路径。
- 退出仍删除 `current_user`,不更改路由 base。
- 会话 CRUD、置顶顺序、当前会话选择和请求参数不变。
- AI 请求 messages/context、知识库检索、MCP、附件和导出协议不变。
- 应用列表仍来自现有接口并沿用当前应用合并与跳转规则。
## 不适用范围
本应用当前没有真实 KPI、图表、分页列表、日期筛选或页面级 Tabs。架构不添加模拟业务数据;未来出现真实需求时,必须按 HeroUI Pro KPI/Chart/DataGrid/Pagination 和 HeroUI Tabs/DatePicker 契约直接实现。
## 验收边界
业务保护优先于制造测试状态。当前账号没有真实会话,且没有提供审计知识库 Excel,因此不会创建测试会话或伪造风险点数据。会话行操作和解析后 DataGrid 仍需在真实数据可用时完成最终操作验收;这两个项目在检查清单中保持进行中,不作为“迁移完成”宣称。
# AI 应用 UI 迁移检查清单
状态:`[x]` 已完成,`[~]` 进行中,`[ ]` 未完成,`[-]` 当前业务不存在。
## 接入与审计
- [x] 拉取最新 `main` 并创建专用迁移分支
- [x] 在目标根目录完成官方 HeroUI Pro 授权安装
- [x] 验证 Pro 版本、解析路径、CSS 与指定子路径
- [x] 启动未修改目标应用并记录 1440x900 登录基线
- [x] 同时启动“问题整改”并记录同视口登录基线
- [x] 审计唯一活动路由、登录、退出、会话、应用切换、Portal 与业务服务
- [x] 建立四份强制文档
## 主题与架构
- [x] 引入权威 `theme.css` 原始副本
- [x] 引入批准的 `tokens.css` 与 Shell 布局 Tokens
- [x] 建立 Tailwind -> HeroUI -> Pro -> theme -> tokens 顺序
- [x] 根级浅/深色与 Portal 主题继承
- [x] 活动页面 Ant Design/Ant Design X/antd-style 视觉引用归零
- [x] 生产解析固定为 React 19;浏览器无重复 Context/slot/provider 警告
- [x] Inter 从 Umi 热更新 CSS 移出,改由稳定 HTML link 加载本地 400/500/600 WOFF2
## 登录与 Shell
- [x] 登录页居中 Card、Logo、标题、宽度和 24px 字段节奏
- [x] 账号、密码、验证码 40px 等高与 filled/autofill 状态
- [x] HeroUI Pro Sidebar 240px 与真 offcanvas
- [x] HeroUI Pro Navbar、Sidebar.Trigger 与 48/32/16px 对称边距
- [x] 主题切换及持久化
- [x] Sidebar 用户 Dropdown 与真实退出登录
- [x] 应用切换 HeroUI Modal、两列应用 Card 和 identity well
- [x] 移动端 Sidebar.Mobile
## AI 与业务模块
- [~] 会话列表、新建、切换、重命名、置顶和删除确认:实现已迁移;当前账号无真实会话,行菜单待真实记录复验
- [x] 主对话列表、PromptInput、附件和发送/停止状态
- [x] 消息区独立滚动、PromptInput 底部固定,并提升为 HeroUI Pro stacked/lg 高度布局
- [x] 空会话使用 HeroUI Pro PromptSuggestion 原生分组与 Card 网格;单项满行、双项两列、移动单列,PromptInput 使用 compact 布局
- [x] 审计知识库操作迁入 Navbar 主题按钮左侧,移动端使用 HeroUI Dropdown;删除 PromptInput 上方重复的文件与数量摘要
- [~] 审计知识库状态、配置、导入、结果与导出:DropZone/Modal 已验;无授权 Excel,DataGrid 真实行待复验
- [x] 发票助手上传、步骤、确认和下载的组件边界迁移
- [x] 所有 Modal/AlertDialog/Dropdown/Tooltip/DropZone 使用 HeroUI 根
- [x] 标准 Modal 使用共享尺寸梯度;提示词与 Excel 配置弹窗限制为 `4xl = 1088px`,手机宽度为视口减 32px,移动 Footer 操作纵向满宽排列
- [-] 页面级 Tabs:当前活动路由不存在
- [-] KPI:当前无真实 KPI 数据
- [-] PieChart/BarChart/ComposedChart:当前无真实图表数据
- [-] 业务分页:当前无分页列表
## 浏览器验收
- [x] 1440x900 浅色与深色
- [x] 1280x720 浅色与深色
- [x] 768x1024 浅色与深色
- [x] 390x844 浅色与深色
- [x] 登录、filled/autofill、失败刷新验证码
- [x] Sidebar 开关、移动 offcanvas、用户菜单、退出
- [x] 应用切换打开态、图片/失败/兜底状态
- [~] 会话菜单、删除确认、业务配置弹窗和上传打开态:配置弹窗/上传通过;空会话导致行菜单待真实记录复验
- [x] Tooltip、焦点、Escape、Console、页面溢出
- [x] 长消息滚动时输入框坐标稳定;390×844 移动端无页面横向溢出
- [x] 连续 3 次 CSS HMR 中字体 link 不变、字体资源无二次请求、35 次拉丁字符宽度采样无变化
## 最终门槛
- [x] `npm run build`
- [x] `git diff --check`
- [x] 鑫元 `audit_project.sh` 已执行;记录脚本的 PromptInput/CommonJS 子路径误报
- [x] 旧组件引用扫描为零
- [x] 页面与共享 JSX 私有视觉 utility 扫描为零
- [x] `file:`、绝对本机路径、相邻 `node_modules` 依赖扫描为零
- [x] HeroUI/React Aria 生产解析检查通过
- [x] HeroUI Pro 指定子路径 ESM 导入检查通过
- [x] 授权值扫描为零
- [x] 未提交、未推送、未合并
# AI 应用鑫元设计系统
## 权威来源
本项目以已确认的“问题整改”应用为唯一可执行视觉与交互基准,并使用鑫元 Skill 的 HeroUI 权威主题和共享 Tokens。页面不得自行近似组件外观。
全局 CSS 顺序固定为:
```css
@import 'tailwindcss';
@import '@heroui/styles';
@import '@heroui-pro/react/css';
@import './design-system/theme.css';
@import './design-system/tokens.css';
```
`src/design-system/theme.css` 必须与权威资产逐字节一致。`tokens.css` 仅新增或映射 `--rc-*`,不得覆盖 HeroUI 核心变量。
Inter 字体文件独立存放于 `public/fonts/`,并由 `.umirc.ts` 注入稳定的 `/ai_chat/fonts/inter.css` link。禁止在 `src/tailwind.css` 或其它参与 Umi HMR 的样式入口中导入 `@fontsource/inter/*`,避免热替换 `umi.css` 时撤销字体注册并触发拉丁字符回退。
## 组件边界
- 基础组件直接从 `@heroui/react` 导入。
- Sidebar、Navbar、AI、DataGrid、DropZone、ActionBar、EmptyState 等从 `@heroui-pro/react` 或正式子路径直接导入。
- 使用 compound API 和 `onPress`;不建立兼容旧 props 的包装层。
- Ant Design Icons 是唯一图标来源;Ant Design、Ant Design Pro、Ant Design X 和 antd-style 不再拥有活动页面视觉组件。
- `className` 只承担 flex、grid、宽度、间距、对齐、滚动和响应式布局。
- Button、Input、Tabs、Card、Modal、DataGrid、Pagination、Tooltip、Dropdown 的背景、颜色、边框、圆角、阴影和状态由 HeroUI 与权威主题拥有。
## Shell 与认证
- 应用统一展示名:`AI 应用`。
- 登录为居中认证 Card,结构、宽度、字段高度、验证码网格和 24px 节奏与“问题整改”一致。
- Sidebar 宽 240px,使用 HeroUI Pro `collapsible="offcanvas"`;收起后占用宽度为零,不保留 icon rail。
- Navbar 高 64px,使用 HeroUI Pro `Sidebar.Trigger` 默认图形。
- Navbar 与页面内容共享对称 gutter:桌面 48px、平板 32px、手机 16px。
- Sidebar 用户整行通过 `Dropdown.Trigger` 打开菜单,`退出登录` 调用现有清理流程。
- 应用切换为 HeroUI Modal,46rem/42rem 上限,一列手机/两列 `sm` 以上,应用 identity well 为 36px `Surface tertiary + Avatar soft`。
## Surface 与主题
- 页面画布 L0:`background`。
- 第一内容层 L1:`surface/default`。
- 嵌套层 L2:`surface-secondary`,Surface/Card/Modal/Overlay 内控件使用 `secondary`。
- 仅真实第三层静态内容使用 L3 `surface-tertiary`。
- Modal、Dropdown、Popover、Tooltip 等浮层使用独立 `overlay`,不当作第四层 Surface。
- 根节点拥有 `.light/.dark` 与 `data-theme`,Portal 必须继承;主题持久化键为 `xinyuan_color_theme`。
## AI 工作区
- 会话导航位于标准 Sidebar 内容区,不作为独立浮动 Card。
- 新建会话是清晰命令;会话项和用户菜单使用 HeroUI 原生交互,不自制 Hover/Selected 外观。
- 对话内容使用 HeroUI Pro AI 组件;附件使用 HeroUI Pro DropZone/ChatAttachment;输入使用 PromptInput。
- 审计知识库配置、提示词、导入、结果和发票流程沿用真实业务数据,只迁移视觉组件边界。
- 不存在真实 KPI、图表、Tabs 或分页业务时不制造示例数据。
## 响应式与验收
- 必测 1440x900、1280x720、768x1024、390x844。
- 每个视口覆盖浅色和深色、Sidebar、Navbar、登录、退出、应用切换、所有打开态菜单/弹窗/上传/Tooltip。
- 不允许页面级横向溢出、文本遮挡、Portal 白色孤岛、控制台错误或 React Aria slot/provider 警告。
## 本次落地结果
- 权威 `theme.css` SHA-256 为 `a0bf6f243aaf2c94cc8b577145d33d765f8ecab863b3a3b4a358bad4d1a05901`,与 Skill 资产逐字节一致。
- Umi 的未使用 `antd` 插件已移除,构建产物不再注入 Ant reset;暗色标题计算为 HeroUI 前景色。
- 页面、业务模块和共享 JSX 的私有视觉 utility 扫描为零;共享外观仅位于权威主题、批准 Tokens 和共享 Shell 设计系统层。
- Ant Design、Ant Design Pro、Ant Design X、Ant X Markdown、antd-style、Headless UI 和 Catalyst UI 活动引用为零。
- 当前业务没有真实 Tabs、筛选、KPI、图表或分页;不得为补齐目录而新增非业务模块。
This diff is collapsed.
...@@ -11,20 +11,48 @@ ...@@ -11,20 +11,48 @@
"start": "npm run dev" "start": "npm run dev"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.0.1", "@ant-design/icons": "^6.3.2",
"@ant-design/pro-components": "^2.4.4", "@fontsource/inter": "^5.3.0",
"@ant-design/x": "^1.6.0", "@heroui-pro/react": "1.0.0-beta.8",
"@ant-design/x-markdown": "2.0.0-alpha.5", "@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"@modelcontextprotocol/sdk": "^1.17.5", "@modelcontextprotocol/sdk": "^1.17.5",
"@number-flow/react": "^0.6.2",
"@react-aria/utils": "^3.34.1",
"@tailwindcss/postcss": "^4.3.3",
"@tiptap/core": "^3.30.2",
"@tiptap/extension-link": "^3.30.2",
"@tiptap/extension-underline": "^3.30.2",
"@tiptap/extensions": "^3.30.2",
"@tiptap/pm": "^3.30.2",
"@tiptap/react": "^3.30.2",
"@tiptap/starter-kit": "^3.30.2",
"@tiptap/suggestion": "^3.30.2",
"@umijs/max": "^4.4.12", "@umijs/max": "^4.4.12",
"antd": "^5.4.0", "clsx": "^2.1.1",
"antd-style": "^3.7.1",
"dayjs": "^1.11.18", "dayjs": "^1.11.18",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"katex": "^0.16.22", "katex": "^0.16.22",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maplibre-gl": "^6.4.1",
"marked": "^16.2.1", "marked": "^16.2.1",
"marked-katex-extension": "^5.1.5", "marked-katex-extension": "^5.1.5",
"motion": "^13.1.0",
"react": "^19.2.8",
"react-aria-components": "^1.20.0",
"react-dom": "^19.2.8",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.12.3",
"recharts": "^3.10.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^4.4.3",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.3",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
......
This diff is collapsed.
...@@ -3,3 +3,11 @@ allowBuilds: ...@@ -3,3 +3,11 @@ allowBuilds:
core-js-pure: false core-js-pure: false
es5-ext: false es5-ext: false
esbuild: false esbuild: false
'@heroui-pro/react': true
'@zowe/secrets-for-zowe-sdk': true
packageExtensions:
'@heroui-pro/react@1.0.0-beta.8':
peerDependencies:
react: '>=19.0.0'
react-dom: '>=19.0.0'
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};
@font-face {
font-family: Inter;
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('./inter-latin-400-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: Inter;
font-style: normal;
font-display: swap;
font-weight: 500;
src: url('./inter-latin-500-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: Inter;
font-style: normal;
font-display: swap;
font-weight: 600;
src: url('./inter-latin-600-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
import '@/tailwind.css';
import { defineApp, matchRoutes } from 'umi'; import { defineApp, matchRoutes } from 'umi';
import { THEME_STORAGE_KEY } from './config/application';
import { createTracking } from './services/DataService'; import { createTracking } from './services/DataService';
import { import {
collectAppInfo, collectAppInfo,
...@@ -6,6 +8,17 @@ import { ...@@ -6,6 +8,17 @@ import {
collectUserInfo, collectUserInfo,
} from './utils/trackingHelper'; } from './utils/trackingHelper';
if (typeof document !== 'undefined') {
const storedTheme = window.localStorage?.getItem?.(THEME_STORAGE_KEY);
const activeTheme = storedTheme === 'dark' ? 'dark' : 'light';
const root = document.documentElement;
root.classList.toggle('dark', activeTheme === 'dark');
root.classList.toggle('light', activeTheme === 'light');
root.dataset.theme = activeTheme;
root.style.colorScheme = activeTheme;
}
export const request = { export const request = {
prefix: '/api', prefix: '/api',
timeout: 30000, timeout: 30000,
......
import AppstoreOutlined from '@ant-design/icons/AppstoreOutlined';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import CommentOutlined from '@ant-design/icons/CommentOutlined';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined';
import LogoutOutlined from '@ant-design/icons/LogoutOutlined';
import MoonOutlined from '@ant-design/icons/MoonOutlined';
import MoreOutlined from '@ant-design/icons/MoreOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import PushpinOutlined from '@ant-design/icons/PushpinOutlined';
import SunOutlined from '@ant-design/icons/SunOutlined';
import WarningOutlined from '@ant-design/icons/WarningOutlined';
function withSize(Icon) {
return function AppIcon({ size, style, ...props }) {
return (
<Icon
aria-hidden="true"
{...props}
style={size === undefined ? style : { fontSize: size, ...style }}
/>
);
};
}
export const AppGrid = withSize(AppstoreOutlined);
export const Close = withSize(CloseOutlined);
export const Comment = withSize(CommentOutlined);
export const Delete = withSize(DeleteOutlined);
export const Edit = withSize(EditOutlined);
export const ExclamationTriangleIcon = withSize(WarningOutlined);
export const LogOut = withSize(LogoutOutlined);
export const Moon = withSize(MoonOutlined);
export const More = withSize(MoreOutlined);
export const Plus = withSize(PlusOutlined);
export const Pin = withSize(PushpinOutlined);
export const Sun = withSize(SunOutlined);
export function SidebarPanel({ size = 16, style, ...props }) {
return (
<span
aria-hidden="true"
role="img"
{...props}
className={['anticon', props.className].filter(Boolean).join(' ')}
style={{ fontSize: size, ...style }}
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
viewBox="0 0 16 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clipRule="evenodd"
d="M6 3.5h6A1.5 1.5 0 0 1 13.5 5v6a1.5 1.5 0 0 1-1.5 1.5H6zm-1.5 0H4A1.5 1.5 0 0 0 2.5 5v6A1.5 1.5 0 0 0 4 12.5h.5zM1 5a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
</span>
);
}
import { THEME_STORAGE_KEY } from '@/config/application';
import { Sidebar } from '@heroui-pro/react/sidebar';
import { Toast } from '@heroui/react';
import { createContext, useContext, useLayoutEffect, useState } from 'react';
import DashboardNavbar from './DashboardNavbar';
import DashboardSidebar from './DashboardSidebar';
const NavbarActionsContext = createContext(null);
export function useNavbarActions(actions) {
const setNavbarActions = useContext(NavbarActionsContext);
useLayoutEffect(() => {
if (!setNavbarActions) return undefined;
setNavbarActions(actions || null);
return () => {
setNavbarActions((current) => (current === actions ? null : current));
};
}, [actions, setNavbarActions]);
}
function getInitialTheme() {
if (typeof window === 'undefined') return 'light';
try {
return window.localStorage.getItem(THEME_STORAGE_KEY) === 'dark'
? 'dark'
: 'light';
} catch {
return 'light';
}
}
export default function AppShell({
children,
open,
onOpenChange,
title,
...sidebarProps
}) {
const [theme, setTheme] = useState(getInitialTheme);
const [navbarActions, setNavbarActions] = useState(null);
useLayoutEffect(() => {
const root = document.documentElement;
root.classList.toggle('dark', theme === 'dark');
root.classList.toggle('light', theme === 'light');
root.dataset.theme = theme;
root.style.colorScheme = theme;
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// The theme still applies for this session when storage is unavailable.
}
}, [theme]);
return (
<NavbarActionsContext.Provider value={setNavbarActions}>
<Sidebar.Provider
className={`template-dashboard-shell rc-app-shell ${theme}`}
collapsible="offcanvas"
open={open}
onOpenChange={onOpenChange}
>
<DashboardSidebar theme={theme} {...sidebarProps} />
<Sidebar.Main className="app-main-glass rc-app-main">
<DashboardNavbar
actions={navbarActions}
theme={theme}
title={title}
onThemeChange={setTheme}
/>
<div className="app-shell-content-scroll min-h-0 flex-1 overflow-y-auto">
<div className="rc-app-page">{children}</div>
</div>
</Sidebar.Main>
<Toast.Provider maxVisibleToasts={3} placement="top" />
</Sidebar.Provider>
</NavbarActionsContext.Provider>
);
}
This diff is collapsed.
import { Navbar } from '@heroui-pro/react/navbar';
import { Sidebar, useSidebar } from '@heroui-pro/react/sidebar';
import { Button, Tooltip } from '@heroui/react';
import { Moon, SidebarPanel, Sun } from './AppIcons';
export default function DashboardNavbar({
actions,
onThemeChange,
theme,
title,
}) {
const { isMobile, isOpen } = useSidebar();
const isDark = theme === 'dark';
const themeLabel = isDark ? '切换至浅色模式' : '切换至深色模式';
return (
<Navbar className="app-navbar rc-navbar" maxWidth="full">
<Navbar.Header>
<Sidebar.Trigger
aria-label={isMobile ? '打开导航' : isOpen ? '收起侧栏' : '展开侧栏'}
className="app-navbar-trigger rc-navbar-trigger"
>
<SidebarPanel />
</Sidebar.Trigger>
<h1 className="app-navbar-page-title rc-navbar-title">{title}</h1>
<Navbar.Spacer />
<div className="rc-navbar-actions">
{actions}
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={themeLabel}
aria-pressed={isDark}
className="app-navbar-icon-button rc-navbar-icon-button"
size="sm"
variant="ghost"
onPress={() => onThemeChange(isDark ? 'light' : 'dark')}
>
{isDark ? <Sun size={16} /> : <Moon size={16} />}
</Button>
<Tooltip.Content>{themeLabel}</Tooltip.Content>
</Tooltip>
</div>
</Navbar.Header>
</Navbar>
);
}
import xyLogoRed from '@/assets/xy-logo-red.png';
import xyLogoWhite from '@/assets/xy-logo-white.png';
import { APPLICATION_DISPLAY_NAME } from '@/config/application';
import { Sidebar } from '@heroui-pro/react/sidebar';
import { Avatar, Button, Dropdown, Label, Tooltip } from '@heroui/react';
import {
AppGrid,
Comment,
Delete,
Edit,
LogOut,
More,
Pin,
Plus,
} from './AppIcons';
function getUserDisplay(userInfo) {
const displayName =
userInfo?.display_name || userInfo?.username || '当前用户';
const username = userInfo?.username || userInfo?.email || '已登录';
return {
displayName,
username,
initials: String(displayName).trim().slice(0, 1).toUpperCase() || 'U',
};
}
function SidebarUserMenu({ userInfo, onLogout }) {
const user = getUserDisplay(userInfo);
return (
<Dropdown>
<Dropdown.Trigger
aria-label="打开用户菜单"
className="app-sidebar-user-trigger rc-sidebar-user-trigger"
>
<Avatar className="app-sidebar-user-avatar rc-sidebar-avatar">
{userInfo?.avatar ? (
<Avatar.Image alt={user.displayName} src={userInfo.avatar} />
) : null}
<Avatar.Fallback>{user.initials}</Avatar.Fallback>
</Avatar>
<span
className="app-sidebar-user-copy rc-sidebar-user-copy"
data-sidebar="label"
>
<span className="app-sidebar-user-name">{user.displayName}</span>
<span className="app-sidebar-user-description">{user.username}</span>
</span>
</Dropdown.Trigger>
<Dropdown.Popover placement="top start">
<Dropdown.Menu
aria-label="用户菜单"
onAction={(key) => {
if (key === 'logout') onLogout?.();
}}
>
<Dropdown.Item id="logout" textValue="退出登录" variant="danger">
<div className="rc-sidebar-user-menu-item">
<Label>退出登录</Label>
<LogOut size={14} />
</div>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
);
}
function ConversationActions({ conversation, onDelete, onPin, onRename }) {
return (
<Dropdown>
<Dropdown.Trigger aria-label={`${conversation.label}会话操作`}>
<More size={16} />
</Dropdown.Trigger>
<Dropdown.Popover placement="bottom end">
<Dropdown.Menu
aria-label="会话操作"
onAction={(key) => {
if (key === 'pin') onPin(conversation);
if (key === 'rename') onRename(conversation);
if (key === 'delete') onDelete(conversation);
}}
>
<Dropdown.Item
id="pin"
textValue={conversation.pinned ? '取消置顶' : '置顶'}
>
<div className="rc-sidebar-user-menu-item">
<Label>{conversation.pinned ? '取消置顶' : '置顶'}</Label>
<Pin size={14} />
</div>
</Dropdown.Item>
<Dropdown.Item id="rename" textValue="重命名">
<div className="rc-sidebar-user-menu-item">
<Label>重命名</Label>
<Edit size={14} />
</div>
</Dropdown.Item>
<Dropdown.Item id="delete" textValue="删除" variant="danger">
<div className="rc-sidebar-user-menu-item">
<Label>删除</Label>
<Delete size={14} />
</div>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
);
}
function SidebarContents({
activeKey,
conversations,
onDeleteConversation,
onLogout,
onNewConversation,
onOpenApplicationSwitcher,
onOpenConversation,
onPinConversation,
onRenameConversation,
userInfo,
}) {
return (
<>
<Sidebar.Header>
<div className="app-sidebar-brand rc-sidebar-brand">
<div className="app-sidebar-brand-mark rc-sidebar-brand-mark">
<img
alt="鑫元"
className="rc-sidebar-logo rc-sidebar-logo--light"
src={xyLogoRed}
/>
<img
alt="鑫元"
className="rc-sidebar-logo rc-sidebar-logo--dark"
src={xyLogoWhite}
/>
</div>
<div
className="app-sidebar-brand-main rc-sidebar-brand-main"
data-sidebar="label"
>
<span className="app-sidebar-title rc-sidebar-title">
{APPLICATION_DISPLAY_NAME}
</span>
</div>
<div
className="app-sidebar-app-switcher rc-sidebar-app-switcher"
data-sidebar="label"
>
<Tooltip delay={0}>
<Button
isIconOnly
aria-label="切换应用"
className="app-sidebar-app-trigger rc-sidebar-app-trigger"
size="sm"
variant="ghost"
onPress={onOpenApplicationSwitcher}
>
<AppGrid size={16} />
</Button>
<Tooltip.Content>切换应用</Tooltip.Content>
</Tooltip>
</div>
</div>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.Menu aria-label="会话操作">
<Sidebar.MenuItem
id="new-conversation"
textValue="新建会话"
onAction={onNewConversation}
>
<Sidebar.MenuIcon>
<Plus size={16} />
</Sidebar.MenuIcon>
<Sidebar.MenuLabel>新建会话</Sidebar.MenuLabel>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group closeMobileOnAction>
<Sidebar.GroupLabel>历史会话</Sidebar.GroupLabel>
{conversations.length > 0 ? (
<Sidebar.Menu aria-label="历史会话">
{conversations.map((conversation) => (
<Sidebar.MenuItem
id={conversation.key}
isCurrent={conversation.key === activeKey}
key={conversation.key}
textValue={conversation.label}
tooltip={conversation.label}
onAction={() => onOpenConversation(conversation.key)}
>
<Sidebar.MenuIcon>
<Comment size={16} />
</Sidebar.MenuIcon>
<Sidebar.MenuLabel>{conversation.label}</Sidebar.MenuLabel>
{conversation.pinned ? (
<Sidebar.MenuChip aria-label="已置顶">
置顶
</Sidebar.MenuChip>
) : null}
<Sidebar.MenuActions>
<ConversationActions
conversation={conversation}
onDelete={onDeleteConversation}
onPin={onPinConversation}
onRename={onRenameConversation}
/>
</Sidebar.MenuActions>
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
) : (
<p className="px-2 py-3">还没有历史会话</p>
)}
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<SidebarUserMenu userInfo={userInfo} onLogout={onLogout} />
</Sidebar.Footer>
</>
);
}
export default function DashboardSidebar(props) {
return (
<>
<Sidebar className="app-sidebar-glass rc-sidebar">
<SidebarContents {...props} />
</Sidebar>
<Sidebar.Mobile className="app-sidebar-glass rc-mobile-sidebar">
<h2 className="sr-only" slot="title">
{APPLICATION_DISPLAY_NAME}导航
</h2>
<SidebarContents {...props} />
</Sidebar.Mobile>
</>
);
}
This diff is collapsed.
export const APPLICATION_DISPLAY_NAME = 'AI 应用';
export const APPLICATION_BASE_PATH = '/ai_chat/';
export const THEME_STORAGE_KEY = 'xinyuan_color_theme';
This diff is collapsed.
/*
* HeroUI Theme Customization
* Add this to your global.css after importing @heroui/styles
* Only includes variables users need to customize
* @see https://heroui.com/docs/react/getting-started/theming
*/
:root,
.light,
.default,
[data-theme='light'],
[data-theme='default'] {
/* Theme Colors (Light Mode) */
--accent: oklch(45.32% 0.1661 27.03);
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(97.02% 0.0045 27.03);
--border: oklch(90% 0.0045 27.03);
--danger: oklch(45.32% 0.1661 27.03);
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(55.17% 0.009 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(21.03% 0.0045 27.03);
--muted: oklch(55.17% 0.009 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(87.1% 0.0045 27.03);
--segment: oklch(100% 0.0045 27.03);
--segment-foreground: oklch(21.03% 0.0045 27.03);
--separator: oklch(92% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(100% 0.0022 27.03);
--surface-foreground: oklch(21.03% 0.0045 27.03);
--surface-secondary: oklch(95.24% 0.0036 27.03);
--surface-secondary-foreground: oklch(21.03% 0.0045 27.03);
--surface-tertiary: oklch(93.73% 0.0036 27.03);
--surface-tertiary-foreground: oklch(21.03% 0.0045 27.03);
--warning: oklch(78.19% 0.1599 45.11);
--warning-foreground: oklch(21.03% 0.0059 45.11);
/* Border Radius */
--radius: 0.5rem;
--field-radius: 0.75rem;
/* Font Family */
/* Make sure to load Inter font in your app */
--font-sans: var(--font-inter);
}
.dark,
[data-theme='dark'] {
color-scheme: dark;
/* Theme Colors (Dark Mode) */
--accent: oklch(45.32% 0.1661 27.03);
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(12% 0.0045 27.03);
--border: oklch(28% 0.0045 27.03);
--danger: oklch(62% 0.16 27.03);
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(70.5% 0.009 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(99.11% 0.0045 27.03);
--muted: oklch(70.5% 0.009 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(70.5% 0.0045 27.03);
--segment: oklch(39.64% 0.0045 27.03);
--segment-foreground: oklch(99.11% 0.0045 27.03);
--separator: oklch(25% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(21.03% 0.009 27.03);
--surface-foreground: oklch(99.11% 0.0045 27.03);
--surface-secondary: oklch(25.7% 0.0067 27.03);
--surface-secondary-foreground: oklch(99.11% 0.0045 27.03);
--surface-tertiary: oklch(27.21% 0.0067 27.03);
--surface-tertiary-foreground: oklch(99.11% 0.0045 27.03);
--warning: oklch(82.03% 0.14 49.12);
--warning-foreground: oklch(21.03% 0.0059 49.12);
}
:root,
.light,
.default,
[data-theme='light'],
[data-theme='default'],
.dark,
[data-theme='dark'] {
/* Xinyuan aliases and extension tokens. HeroUI core tokens are owned by theme.css. */
--rc-brand-primary: var(--accent);
--rc-brand-light: color-mix(in oklab, var(--accent) 84%, white);
--rc-brand-dark: color-mix(in oklab, var(--accent) 82%, black);
--rc-bg-canvas: var(--background);
--rc-bg-surface: var(--surface);
--rc-border-default: var(--border);
--rc-field-background: var(--field-background);
--rc-field-border-focus: var(--focus);
--rc-button-secondary-border: color-mix(
in oklab,
var(--foreground) 12%,
transparent
);
--rc-text-primary: var(--foreground);
--rc-text-secondary: color-mix(
in oklab,
var(--foreground) 72%,
var(--background)
);
--rc-text-muted: var(--muted);
--rc-font-sans: var(--font-sans);
--rc-font-medium: 500;
--rc-font-semibold: 600;
--rc-line-normal: 1.5;
--rc-text-xs: 0.75rem;
--rc-text-sm: 0.875rem;
--rc-space-1: 0.25rem;
--rc-space-2: 0.5rem;
--rc-space-3: 0.75rem;
--rc-space-4: 1rem;
--rc-space-5: 1.25rem;
--rc-space-6: 1.5rem;
--rc-space-8: 2rem;
/* Page geometry only. Component appearance remains owned by HeroUI. */
--rc-shell-inline-gutter: 3rem;
--rc-navbar-inline-padding: var(--rc-shell-inline-gutter);
--rc-page-inline-start: var(--rc-shell-inline-gutter);
--rc-page-inline-end: var(--rc-shell-inline-gutter);
--rc-page-padding-block-start: 1.5rem;
--rc-page-padding-block-end: 2rem;
--rc-page-section-gap: 2rem;
--rc-page-module-gap: 1.5rem;
--rc-related-item-gap: 1rem;
--rc-page-toolbar-gap: 1rem;
--rc-list-stack-gap: 1rem;
--rc-radius-control: 0.375rem;
--rc-radius-container: 0.5rem;
--rc-radius-overlay: 0.75rem;
--rc-motion-fast: 120ms;
--rc-motion-normal: 180ms;
--rc-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--rc-button-radius: 1.5rem;
--rc-navbar-trigger-size: 2rem;
--rc-navbar-icon-button-size: 2.25rem;
--rc-navbar-title-size: 1.25rem;
--rc-navbar-title-weight: 600;
--rc-navbar-title-line-height: 1.75rem;
--rc-sidebar-app-trigger-size: 2.25rem;
--rc-sidebar-app-trigger-icon-size: 1rem;
--rc-login-panel-width: 28rem;
--rc-login-brand-size: 1.5rem;
--rc-login-brand-weight: var(--rc-font-semibold);
--rc-login-brand-gap: 1.5rem;
/* Shared modal width ladder from the Xinyuan overlay contract. */
--rc-dialog-width-xs: 20rem;
--rc-dialog-width-sm: 24rem;
--rc-dialog-width-md: 30rem;
--rc-dialog-width-lg: 40rem;
--rc-dialog-width-xl: 46rem;
--rc-dialog-width-2xl: 52.5rem;
--rc-dialog-width-3xl: 60rem;
--rc-dialog-width-4xl: 68rem;
--rc-dialog-width-5xl: 76rem;
--rc-shadow-raised: 0 1px 2px rgb(15 23 42 / 6%),
0 8px 24px rgb(15 23 42 / 8%);
--rc-shadow-overlay: 0 16px 40px rgb(15 23 42 / 14%);
--rc-shadow-card: 0 1px 2px rgb(15 23 42 / 2%), 0 4px 12px rgb(15 23 42 / 2%);
/* KPI icon surfaces. Card bodies remain neutral; only the icon well carries tone. */
--rc-metric-tone-info: oklch(55% 0.14 255);
--rc-metric-tone-success: oklch(54% 0.13 145);
--rc-metric-tone-teal: oklch(55% 0.1 205);
--rc-metric-tone-danger: oklch(52% 0.17 25);
--rc-metric-tone-warning: oklch(62% 0.12 75);
--rc-metric-tone-violet: oklch(56% 0.12 300);
--rc-metric-tone-indigo: oklch(53% 0.13 275);
--rc-metric-tone-coral: oklch(60% 0.12 48);
--rc-metric-tone-neutral: var(--muted);
--rc-metric-tone-info-soft: color-mix(
in oklab,
var(--rc-metric-tone-info) 9%,
var(--surface)
);
--rc-metric-tone-success-soft: color-mix(
in oklab,
var(--rc-metric-tone-success) 9%,
var(--surface)
);
--rc-metric-tone-teal-soft: color-mix(
in oklab,
var(--rc-metric-tone-teal) 9%,
var(--surface)
);
--rc-metric-tone-danger-soft: color-mix(
in oklab,
var(--rc-metric-tone-danger) 9%,
var(--surface)
);
--rc-metric-tone-warning-soft: color-mix(
in oklab,
var(--rc-metric-tone-warning) 9%,
var(--surface)
);
--rc-metric-tone-violet-soft: color-mix(
in oklab,
var(--rc-metric-tone-violet) 9%,
var(--surface)
);
--rc-metric-tone-indigo-soft: color-mix(
in oklab,
var(--rc-metric-tone-indigo) 9%,
var(--surface)
);
--rc-metric-tone-coral-soft: color-mix(
in oklab,
var(--rc-metric-tone-coral) 9%,
var(--surface)
);
--rc-metric-tone-neutral-soft: color-mix(
in oklab,
var(--rc-metric-tone-neutral) 8%,
var(--surface)
);
/* Data visualization only. Do not reuse UI status colors as a series palette. */
--rc-chart-1: oklch(45.32% 0.1661 27.03);
--rc-chart-2: oklch(56% 0.135 255);
--rc-chart-3: oklch(64% 0.11 220);
--rc-chart-4: oklch(56% 0.115 280);
--rc-chart-5: oklch(60% 0.095 200);
--rc-chart-6: oklch(70% 0.12 80);
--rc-chart-7: oklch(60% 0.1 295);
--rc-chart-8: oklch(57% 0.035 250);
--rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: oklch(47% 0.145 27.03);
--rc-chart-warning: var(--rc-chart-6);
--rc-chart-positive: var(--rc-chart-2);
--rc-chart-comparison: var(--rc-chart-4);
--rc-chart-neutral: var(--rc-chart-8);
--rc-chart-legend-font-size: 0.75rem;
--rc-chart-legend-line-height: 1rem;
}
.dark,
[data-theme='dark'] {
color-scheme: dark;
--rc-text-secondary: color-mix(
in oklab,
var(--foreground) 76%,
var(--background)
);
--rc-button-secondary-border: color-mix(
in oklab,
var(--foreground) 18%,
transparent
);
--rc-shadow-raised: 0 1px 2px rgb(0 0 0 / 24%), 0 8px 24px rgb(0 0 0 / 24%);
--rc-shadow-overlay: 0 16px 40px rgb(0 0 0 / 38%);
--rc-shadow-card: 0 1px 2px rgb(0 0 0 / 20%), 0 4px 12px rgb(0 0 0 / 18%);
}
This diff is collapsed.
import { Outlet } from '@umijs/max'; import { Outlet } from '@umijs/max';
import { App, ConfigProvider, theme } from 'antd';
import { createGlobalStyle } from 'antd-style';
const { defaultSeed, defaultAlgorithm } = theme; export default function RootLayout() {
return <Outlet />;
const APP_FONT_FAMILY = }
"'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif";
const GlobalStyle = createGlobalStyle`
:root {
--app-shell-bg:
radial-gradient(
circle at 18% 8%,
rgba(34, 211, 238, 0.16),
transparent 28%
),
radial-gradient(
circle at 84% 16%,
rgba(99, 102, 241, 0.2),
transparent 26%
),
radial-gradient(
circle at 74% 86%,
rgba(14, 165, 233, 0.13),
transparent 30%
),
linear-gradient(135deg, #050816 0%, #08111f 46%, #111827 100%);
--app-surface: rgba(15, 23, 42, 0.74);
--app-surface-strong: rgba(15, 23, 42, 0.94);
--app-brand: #22d3ee;
--app-brand-strong: #38bdf8;
--app-sidebar: rgba(2, 6, 23, 0.82);
--app-sidebar-soft: rgba(15, 23, 42, 0.84);
--app-border: rgba(125, 211, 252, 0.16);
--app-shadow: 0 28px 80px rgba(0, 0, 0, 0.34);
--app-shadow-soft: 0 18px 48px rgba(0, 0, 0, 0.28);
--app-radius-xl: 32px;
--app-radius-lg: 24px;
--app-radius-md: 18px;
--app-radius-sm: 14px;
--app-content-width: 960px;
}
html,
body,
#root {
width: 100%;
height: 100%;
min-height: 100%;
overflow: hidden;
}
body {
margin: 0;
background: var(--app-shell-bg);
color: #e5eefb;
font-family: ${APP_FONT_FAMILY};
}
* {
box-sizing: border-box;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
background: rgba(125, 211, 252, 0.36);
border: 2px solid transparent;
border-radius: 999px;
background-clip: content-box;
}
*::-webkit-scrollbar-track {
background: transparent;
}
a {
color: inherit;
text-decoration: none;
}
.app-glass-card {
background: var(--app-surface);
border: 1px solid var(--app-border);
box-shadow: var(--app-shadow-soft);
backdrop-filter: blur(18px);
}
`;
export default () => {
const themeToken = defaultAlgorithm({
...defaultSeed,
colorPrimary: '#22d3ee',
colorInfo: '#38bdf8',
colorSuccess: '#1d9b62',
colorWarning: '#d97706',
colorError: '#dc2626',
colorBgBase: '#050816',
colorBgContainer: '#0f172a',
colorTextBase: '#e5eefb',
borderRadius: 18,
borderRadiusLG: 24,
borderRadiusSM: 14,
boxShadow:
'0 20px 50px rgba(15, 23, 42, 0.12), 0 6px 18px rgba(15, 23, 42, 0.06)',
boxShadowSecondary:
'0 12px 30px rgba(15, 23, 42, 0.08), 0 4px 12px rgba(15, 23, 42, 0.05)',
controlHeight: 44,
fontSize: 15,
});
return (
<>
<GlobalStyle />
<ConfigProvider
theme={{
token: themeToken,
components: {
Layout: {
bodyBg: 'transparent',
siderBg: 'transparent',
},
Button: {
borderRadius: 14,
controlHeight: 42,
primaryShadow: 'none',
},
Input: {
borderRadius: 14,
activeShadow: '0 0 0 3px rgba(200, 30, 58, 0.14)',
},
Modal: {
borderRadiusLG: 24,
},
Collapse: {
headerBg: 'transparent',
contentBg: 'transparent',
},
Drawer: {
colorBgElevated: '#0f172a',
},
Steps: {
colorPrimary: '#c81e3a',
},
},
}}
>
<App>
<Outlet />
</App>
</ConfigProvider>
</>
);
};
import { PageContainer } from '@ant-design/pro-components'; import { Button } from '@heroui/react';
import { Access, useAccess } from '@umijs/max'; import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';
const AccessPage = () => { export default function AccessPage() {
const access = useAccess(); const access = useAccess();
return ( return (
<PageContainer <main className="rc-app-page">
ghost <h1 className="sr-only">权限示例</h1>
header={{
title: '权限示例',
}}
>
<Access accessible={access.canSeeAdmin}> <Access accessible={access.canSeeAdmin}>
<Button>只有 Admin 可以看到这个按钮</Button> <Button variant="primary">只有 Admin 可以看到这个按钮</Button>
</Access> </Access>
</PageContainer> </main>
); );
}; }
export default AccessPage;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
.container {
padding-top: 80px;
}
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment