# Plus-One-UI **Repository Path**: lkShi/plus-one-ui ## Basic Information - **Project Name**: Plus-One-UI - **Description**: Plus One UI 是一个专注于企业级后台管理场景的 React + Semi Design 模板,基于 semi-design-pro 深度优化,提供开箱即用的实用组件与简洁架构,践行“+1”理念——在工程细节中持续精进。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2025-12-11 - **Last Updated**: 2026-07-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Plus One UI > **Plus One** —— 在现有基础上,多做一点,更好一点。 基于 [React 18](https://reactjs.org/) + [TypeScript](https://www.typescriptlang.org/) + [Semi Design](https://semi.design/zh-CN) 的现代化后台管理系统模板,fork 自 [semi-design-pro](https://github.com/xieyezi/semi-design-pro)。在保留原架构的基础上,**针对企业级后台做了针对性增强**:统一的列表/查询/上传/SSE 推送组件、字典化下拉、可视化主题、命令面板搜索等。 配合 [Plus-One 后端](https://gitee.com/lkShi/plus-one-system.git) 一起使用可获得完整功能(API 前缀 `/plus-one`,鉴权头 `satoken`)。 --- ## ✨ 核心特性 - **React 18 + TypeScript** —— 类型安全,工程健壮 - **Semi Design 2.99+** —— UI 美观、主题可定制、无障碍友好 - **Webpack 5** —— dev server / 多环境构建 - **Zustand** —— 轻量级状态管理(用户态 / 主题 / 字典 / 菜单) - **react-intl** —— 中英双语国际化 - **Axios + 拦截器** —— 统一鉴权、统一错误处理 - **15 个 One\* 业务组件** —— 开箱即用,覆盖 CRUD 90% 场景 - **SSE 实时推送** —— `useOneSSE` hook,自动重连 + 心跳 + Page Visibility - **6 套主题色 + 主题切换涟漪动画** —— `startViewTransition` + `clipPath` - **命令面板搜索(⌘K)** —— `fuse.js` 模糊匹配菜单 - **可配置水印 / 系统名 / SEO / 版权** —— SideSheet 主题设置 --- ## 📦 核心组件库 所有组件位于 `src/components/`,统一前缀 `One*`,全部支持 TypeScript 泛型 + ref 暴露命令式 API。 ### 表格 `OneTable` 基于 Semi `Table` 的封装。**约定 `refreshApi({ dto, pageNum, pageSize })` 返回 `{ data: { list, current, size, total } }`**。 内置:分页、loading、刷新、导出、批量操作、**列显隐 + 拖拽排序弹层(dnd-kit)**。 ```tsx import { useRef } from 'react' import { OneTable, OneTableRef } from '@components/OneTable' const ref = useRef(null) apiUserPage({ ...dto, pageNum, pageSize })} batchApi={async (keys, rows) => { /* 批量删除 */ }} exportApi={async (body) => (await apiExport(body)).data} topToolbar={} columns={[ { title: 'ID', dataIndex: 'id', key: 'id', width: 80 }, { title: '姓名', dataIndex: 'name', key: 'name', width: 120 }, { title: '操作', key: 'op', fixed: 'right', width: 200, render: (_, r) => } ]} /> ``` ref 暴露:`refresh(params?)` / `batchExec()` / `export()`。 --- ### 查询表单 `OneQueryForm` 声明式查询表单:按 `fieldsPerRow` 自动分行、超过 `defaultVisibleRowCount` 行的字段**可展开/收起**,过渡动画不卸载 DOM。 支持字段类型:`input` / `oneSelect` / `select` / `datePicker` / `dateRangePicker` / `custom`。 ```tsx import { OneQueryForm, OneQueryFormRef } from '@components/OneQueryForm' } ]} fieldsPerRow={4} defaultVisibleRowCount={1} onSearch={(values) => ref.current?.refresh(values)} onReset={() => ref.current?.refresh({})} /> ``` ref 暴露:`submit()` / `reset()` / `getValues()` / `setFieldValue()` / `getFormApi()`。 --- ### 字典下拉 `OneSelect` 按 `codeType` 自动从 `/common/code` 拉选项,**带 1s 防抖搜索**,内置 `i18n` 开关。也可传自定义 `api` 覆盖。 ```tsx import { OneSelect } from '@components/OneSelect' // 不用 codeType 也可以传自定义 api apiDeptList({ keyword: k })} label="部门" /> ``` --- ### 上传 `OneUpload` 封装 Semi `Form.Upload`,**支持两种后端**: - `minio` —— 先请求 `action` 拿预签名 URL,再 PUT 上传 - `form` —— POST multipart 到 `action` 通过后端返回的 `supportType` 字段自动判断,页面侧无需配置。 ```tsx import OneUpload from '@components/OneUpload' { /* 自动调 /oss/getObjUrl 拉 URL 后下载 */ }} /> ``` --- ### 实时推送 `useOneSSE`(hook) 封装浏览器原生 `EventSource`,项目内**统一的 SSE 订阅 hook**。 特性: - 自动重连 + 指数退避(1s → 2s → 4s → … → 30s 封顶) - 心跳超时检测 - Page Visibility:标签页隐藏时暂停,回前台再续 - `onMessage` 默认 `JSON.parse`,解析失败原样透传 - **同时监听 `onmessage` 默认通道和具名事件**(兼容后端 `SseEmitter.send` / `sendEvent` 两种写法) - 卸载 / 参数变化时彻底清理 > ⚠️ **鉴权 token 走 URL query**(浏览器原生 `EventSource` 不支持自定义请求头)。所有 SSE URL 统一通过 `buildSSEUrl(topic)` 拼接,**不要在页面里手写**。 ```tsx import { useOneSSE } from '@components/OneSSE' import { buildSSEUrl } from '@src/common' const { status, data, retryCount, disconnect } = useOneSSE<{ count: number }>({ url: buildSSEUrl('unreadCount'), eventName: 'unreadCount', onMessage: (msg) => setUnreadCount(msg.count), onError: (e) => console.warn('SSE 异常', e) }) ``` 返回:`{ status, data, error, retryCount, connect, disconnect }`。 --- ### 富文本编辑器 `OneEditor` / `OneEditorViewer` `OneEditor` 封装 toast-ui editor,支持预设(`presets.ts`)和类型守卫(`typeGuards.ts`);`OneEditorViewer` 是只读渲染版,给详情页用。 ```tsx import { OneEditor } from '@components/OneEditor' import { OneEditorViewer } from '@components/OneEditorViewer' ``` --- ### 头部消息 `HeaderMessage` Header 右侧的"信封"图标 + 角标,**通过 SSE 实时刷新未读数**(`unreadCount` 主题)。点击展开抽屉展示未读消息列表。 --- ### 命令面板 `GlobalSearch` `⌘K` / `Ctrl+K` 唤起全屏弹窗,用 `fuse.js` 在菜单 store 上做模糊匹配,支持最近搜索(localStorage 持久化)。 --- ### 水印 `GlobalWatermark` 根据 `global.watermarkEnabled` 叠加全局水印(背景 canvas 渲染,开销低)。 --- ### 主题设置 `ThemeSettings` SideSheet 抽屉,集中配置:主题色 / hue / 内容宽度 / 水印 / 系统名 / 版权 / SEO。所有配置持久化到 localStorage,**key 前缀 `plus_one_`**。 --- ### 倒计时按钮 `OneCountdownButton` 验证码倒计时按钮,内部维护倒计时状态,结束后自动恢复。 ### 错误边界 / 加载占位 / 空状态 - `ErrorBoundary` —— 顶层错误兜底 - `fallback-loading` —— 全局 loading 遮罩(与 `useLoading` 配套) - `empty` —— 统一空状态 --- ## 🪝 Hooks(`src/hooks/`) | Hook | 作用 | | --- | --- | | `usePagination(initialPage, initialSize)` | 表格分页状态,`updateFromRes` 接受 `current/size/total` 形式 | | `usePrimaryHueSync` | 把 store 的 `primaryHue` 同步成 `body.hue-*` 类名 | | `useContainerWidthSync` | 把 `containerWidth` 同步给布局 | | `useDocumentMeta` | 写 `document.title` 和 `` | | `useLoading` | 全局 loading 标志 | --- ## 🏪 状态管理(Zustand) 所有 store 在 `src/store/common/` 下,按域划分: | Store | 内容 | 持久化 | | --- | --- | --- | | `global.ts` | locale、theme、primaryHue、containerWidth、drawerOpen、watermark、systemName、copyright、SEO、siderWidth | localStorage(前缀 `plus_one_`) | | `user.ts` | userInfo + token | **sessionStorage**(key `userStorage`) | | `menu.ts` | `/sys/usermenu` 返回的菜单树 → `MenuItem[]` | — | | `codeState.ts` | 字典数据,按 `codeType` 分组 | localStorage | | `headerTag.ts` | Header 标签页状态 | — | 初始的 theme / hue / seo 等都用安全校验器(`getLocalStorageSafe` / `getLocalStorageBoolean`)读 localStorage,损坏值会回退到默认值。 --- ## 🎨 主题 `src/theme/palette.ts` 定义 6 套主题色: | Key | 名称 | | --- | --- | | `blue` | 经典蓝(默认) | | `violet` | 紫罗兰 | | `indigo` | 靛蓝 | | `teal` | 青绿 | | `amber` | 琥珀 | | `pink` | 樱粉 | 每套主题预编译在 `src/App.scss` 中,**切换 hue 只需要给 body 加/去掉 `hue-*` 类名**,无需运行时计算颜色。主题切换动画用 `document.startViewTransition` + `clipPath` 做涟漪展开。 --- ## 🌐 国际化 `src/locales/` 下分 `zh-CN` / `en-GB`,各包含 `index.ts` + `menu/` + `sys/` 子目录。 ```tsx import { useLocale } from '@src/locales' const { formatMessage } = useLocale() formatMessage({ id: 'app.system.common.button.query' }) ``` --- ## 🛰️ HTTP 层 - `src/utils/request/index.ts` —— Axios 实例 + 全局响应 / 错误拦截器 - `src/utils/fetcher/index.ts` —— 业务请求薄封装 - `src/api/**` —— 按域切分的接口 请求约定: - dev 模式 URL 前拼 `apiPrefix`(`/plus-one`),走 webpack-dev-server 代理 - 非 dev 用 `VITE_BASE_API` - 鉴权头 `satoken`(在 `src/config/index.ts` 的 `authKey` 中配置) - 响应结构 `{ code: 200, data, msg? }`,`code !== 200` 拦截器直接 reject;`code === 401` 自动清空 user store 并跳 `/login` - GET 请求用 `qs.stringify` 配 `arrayFormat: 'repeat'` --- ## 📁 项目结构 ``` src/ ├── api/ # 按域切分的接口 │ ├── common/ # code、menu、oss、download │ ├── org/ # dept/account/role/permission │ ├── sys/ # code、logs、menu、monitor、notice、schedule、task │ └── user/ # login、userinfo ├── common/ # 公共常量(buildSSEUrl、列宽常量、主题切换等) ├── components/ # One* 业务组件 + 布局类 │ ├── OneTable/ OneQueryForm/ OneSelect/ OneUpload/ │ ├── OneSSE/ OneEditor/ OneEditorViewer/ OneCountdownButton/ │ ├── HeaderMessage/ GlobalSearch/ GlobalWatermark/ ThemeSettings/ │ └── ErrorBoundary/ empty/ fallback-loading/ ├── hooks/ # usePagination、useDocumentMeta 等 ├── locales/ # zh-CN / en-GB ├── pages/ # 业务页面(路由处 React.lazy 加载) ├── router/ # 路由表 + WrapperRouteComponent / PrivateRoute ├── store/ # Zustand 状态 ├── theme/ # 调色板 ├── utils/ # 工具函数(request、fetcher、storage、date …) ├── App.tsx # Provider 嵌套 ├── App.scss # 全局样式 + 各 hue 变量 └── main.tsx # 入口 ``` --- ## 🚀 快速开始 ### 环境要求 - Node.js `>= 20.0.0` - yarn `>= 1.22.0`(package.json 有 `please-use-yarn` 提示,请优先使用 yarn) - 后端 [Plus-One-System](https://gitee.com/lkShi/plus-one-system.git)(如需完整功能体验) ### 安装依赖 ```bash yarn # 或 npm install ``` ### 开发环境 dev server 跑在 `:3000`,代理 `/plus-one` → `:8080`。 ```bash yarn dev # WEBPACK_MODE=dev,读 .env.dev(推荐) yarn start # WEBPACK_MODE=development,读 .env.development ``` ### 构建 ```bash npm run build:dev # 读 .env.dev npm run build:test # 读 .env.test npm run build:prod # 读 .env.prod ``` 构建产物在 `dist/`,可用 `yarn preview` 起本地静态服务预览。 ### 代码质量 ```bash npm run lint # eslint src --ext .ts,.tsx npm run lint:fix # tsc --noEmit && eslint --fix(最接近类型检查的命令) npm run format # prettier --write ``` > 项目使用 **Husky + lint-staged**,`pre-commit` 时对暂存区的 `.ts/.tsx` 自动跑 `npm run lint` + `npm run format`。 > > ⚠️ 项目**没有配置测试框架**(无 `vitest` / `jest`),不提供单测命令。 --- ## ⚙️ 环境变量 | 文件 | `VITE_BASE_API` | 用途 | | --- | --- | --- | | `.env.dev` | (空,走 `/plus-one` 代理) | `yarn dev`(webpack-dev-server) | | `.env.development` | 自定义 | `yarn start` | | `.env.test` | `/api` | `build:test` | | `.env.prod` | 自定义 | `build:prod` | --- ## 🛣️ 路径别名 `tsconfig.json` 和 `webpack.config.mjs` 一致: `@src` `@assets` `@components` `@pages` `@uitls` `@styles` `@config` `@mock` > ⚠️ `@uitls` 的拼写是历史遗留,**不要去"修正"** —— 很多文件从 `@uitls/...` 导入。 --- ## 📐 约定与规范 ### 时间展示 时间字段统一走 `src/utils/date`: ```ts import { fmtDateTime, fmtDate } from '@src/utils/date' render: fmtDateTime // yyyy-MM-dd HH:mm:ss(含秒,列表 / 详情) render: fmtDate // yyyy-MM-dd(纯日期字段) ``` 两个函数都做了**空值兜底**(`null` / `undefined` / `''` / `Invalid Date` 一律返回 `--`),**调用方不要再写三目兜底**。 ### 空值显示 后端返回 `null` 的字段统一显示 `--`: ```ts render: (text) => (text == null ? '--' : `${text}`) ``` ### 列宽常量 从 `@src/common` 引入,不要硬编码: ```ts import { columnDefaultWidth, columnLargeWidth } from '@src/common' // columnSmallWidth = 60 ID、状态码 // columnDefaultWidth = 120 默认(文本、时间、Tag、枚举) // columnLargeWidth = 200 操作列、标题 ``` ### 全局设置 localStorage key 统一前缀 `plus_one_`: ``` plus_one_theme / plus_one_locale / plus_one_primary_hue plus_one_container_width / plus_one_watermark_enabled plus_one_system_name / plus_one_copyright / plus_one_seo plus_one_sider_width ``` 用户 store 走 **sessionStorage**(key `userStorage`),不是 localStorage。 ### SSE 端点拼接 所有 SSE URL 走 `buildSSEUrl(topic)`,不要在页面里手拼: ```ts import { buildSSEUrl } from '@src/common' useOneSSE({ url: buildSSEUrl('unreadCount'), eventName: 'unreadCount', onMessage: ... }) ``` `topic` 跟 `eventName` 保持一致。 ### 新增页面 - 在 `src/pages//` 下新增,**默认导出** - 在 `src/router/index.tsx` 中加 `React.lazy(() => import('@pages/...'))` 路由项 - 用 `WrapperRouteComponent`(`src/router/config.tsx`)会自动加 `document.title` 和 `PrivateRoute` ### 缩进 `.prettierrc.json` 中 `useTabs: true` / `printWidth: 120`,所有 `.ts` / `.tsx` 用 **tab** 缩进。 `prettier` `semi: false`,**不要写防御性前导 `;`**(如 `;(expr).xxx`)。 --- ## 📸 截图 ### 登录页 ![登录页](./images/login.jpg) ### 部门列表 ![部门列表](./images/dept-list.jpg) ### 角色列表 ![角色列表](./images/role-back.jpg) ### 用户列表 ![用户列表](./images/account-back.jpg) --- ## 🌐 浏览器支持 项目支持现代浏览器,推荐使用 Chrome、Firefox 或 Edge 进行开发与运行。 --- ## 🤝 贡献 欢迎提交 issue 与 PR。如果你在使用过程中遇到问题,或者有改进意见,请在仓库中提交 issue。 --- ## 📄 许可证 本项目遵循原仓库 [semi-design-pro](https://github.com/xieyezi/semi-design-pro) 的许可协议。详见 [LICENSE](./LICENSE)。