|
| 1 | +# Zustand 状态迁移与示例集成 |
| 2 | + |
| 3 | +## 概要 |
| 4 | +本文件记录将项目中 Redux 逻辑替换为 Zustand(模块化 slice + 组合式 API)的完整变更与使用说明,包含:代码位置、改动说明、使用示例、验证步骤和扩展指南,作为后续功能扩展的参考示例。 |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## 本次变更一览(文件) |
| 9 | +- 新增 / 修改: |
| 10 | + - `src/store/types.ts` —— 新增状态类型声明 |
| 11 | + - `src/store/modules/counterSlice.ts` —— 新增 Counter slice |
| 12 | + - `src/store/modules/appSlice.ts` —— 新增 App slice(包含 `isSidebarOpen`) |
| 13 | + - `src/store/index.ts` —— 组合 store(`useStore`)并添加 `persist`/`devtools` 中间件 |
| 14 | + - `src/pages/zustand/index.tsx` —— 新增演示页面(ZustandDemo) |
| 15 | + - `src/pages/layout/proSider/index.jsx` —— 改造:侧边栏切换由全局 `isSidebarOpen` 控制 |
| 16 | + - `src/pages/layout/index.jsx` —— 移除传入 `collapsed`/`onCollapse` 的调用点 |
| 17 | + - `src/routers/config/lazyLoad.config.jsx` —— 注册 `ZustandDemo` 懒加载组件 |
| 18 | + - `src/routers/modules/business.routes.jsx` —— 添加 `/zustand` 路由并配置 `meta.permission` |
| 19 | + - `src/config/menu.config.jsx` —— 在主菜单中新增 `Zustand演示` 条目 |
| 20 | + - `docs/Zustand-Migration.md` —— (本文件)记录说明 |
| 21 | +- 删除: |
| 22 | + - 项目中原始 Redux demo / action / reducer 文件(已删除冗余 `src/actions` / `src/reducers` 中的示例文件) |
| 23 | + |
| 24 | + |
| 25 | +## 主要改动说明 |
| 26 | +- 状态组织 |
| 27 | + - 采用 Slice Pattern:每个功能模块(例如 counter、app)在 `src/store/modules` 下维护自己的 state + actions。所有 slice 在 `src/store/index.ts` 中组合为单个 `useStore` Hook。 |
| 28 | + - 持久化:通过 `zustand/middleware` 的 `persist` 对 `isSidebarOpen` 做了示例性持久化(可在 `partialize` 中调整持久字段)。 |
| 29 | + |
| 30 | +- 侧边栏(ProSider)改造 |
| 31 | + - 原来由 `Layout` 组件通过 props `collapsed/onCollapse` 控制的逻辑,已统一改为 `useStore` 中的 `isSidebarOpen` / `toggleSidebar()`。优点:中央化、任何组件可以直接控制侧边栏、保持状态一致。 |
| 32 | + - 修改位置:`src/pages/layout/proSider/index.jsx`、`src/pages/layout/index.jsx` |
| 33 | + |
| 34 | +- 路由与页面 |
| 35 | + - 新增演示页面 `src/pages/zustand/index.tsx`,作为 Zustand 使用示例。通过 `lazyLoad.config.jsx` 注册后在 `business.routes.jsx` 中加入路由并配置权限(`meta.permission`),在 `menu.config.jsx` 新增菜单项。 |
| 36 | + |
| 37 | +- 清理 Redux 代码 |
| 38 | + - 项目中原有用于示例的 Redux action/reducer 已删除,且已从依赖中卸载 `redux react-redux @reduxjs/toolkit redux-logger`。若项目还有其它 Redux 依赖点,会在构建/运行时暴露引用错误,请按需删除或替换。 |
| 39 | + |
| 40 | + |
| 41 | +## 如何在组件中使用(示例) |
| 42 | +- 仅使用需要的字段以避免不必要渲染: |
| 43 | + |
| 44 | +```tsx |
| 45 | +// 仅订阅 count |
| 46 | +const count = useStore((s) => s.count) |
| 47 | +// 仅订阅方法(不会触发渲染) |
| 48 | +const increment = useStore((s) => s.increment) |
| 49 | +``` |
| 50 | + |
| 51 | +- 示例:控制侧边栏(任意组件) |
| 52 | +```tsx |
| 53 | +const toggle = useStore((s) => s.toggleSidebar) |
| 54 | +<button onClick={toggle}>切换侧边栏</button> |
| 55 | +``` |
| 56 | + |
| 57 | + |
| 58 | +## 新增 Slice(扩展步骤) |
| 59 | +1. 在 `src/store/modules` 新建 `yourSlice.ts`:导出 `createYourSlice: StateCreator<StoreState, [], [], YourSlice>`。 |
| 60 | +2. 在 `src/store/types.ts` 中添加 `YourSlice` 的类型,并将 `StoreState` 组合上新的类型。 |
| 61 | +3. 在 `src/store/index.ts` 中引入并展开该 slice: |
| 62 | +```ts |
| 63 | +...createCounterSlice(...a), |
| 64 | +...createAppSlice(...a), |
| 65 | +...createYourSlice(...a), |
| 66 | +``` |
| 67 | +4. 在页面中直接使用 `useStore((s) => s.yourField)`。 |
| 68 | + |
| 69 | + |
| 70 | +## 验证与运行 |
| 71 | +- 本地开发预览: |
| 72 | +```bash |
| 73 | +npm run dev |
| 74 | +# 或(项目有多个脚本) |
| 75 | +npm run dev:vite |
| 76 | +``` |
| 77 | +- 打包测试(库构建): |
| 78 | +```bash |
| 79 | +npm run build:lib |
| 80 | +``` |
| 81 | +- 推荐检查点: |
| 82 | + - 访问 `/zustand` 页面,验证计数器、异步动作、侧边栏切换是否生效。 |
| 83 | + - 在多个页面切换,确认 `keepAlive`(路由 meta)与组件行为一致。 |
| 84 | + |
| 85 | + |
| 86 | +## 回滚与注意事项 |
| 87 | +- 如果需回滚到 Redux,保留已删除文件的历史(Git 提交),通过 `git checkout` 恢复。当前改动假设项目不再使用 Redux。 |
| 88 | +- 注意第三方依赖:某些库或页面可能仍旧通过 `react-redux` 提供的 context 注入数据,迁移需逐步替换对应调用点(`useSelector`、`useDispatch`)。 |
| 89 | + |
| 90 | + |
| 91 | +## 结语 |
| 92 | +本次迁移以最小侵入(模块化 slice)方式将示例功能从 Redux 替换为 Zustand,并演示了如何将全局 UI 状态(如侧边栏)统一管理。该结构便于横向扩展(继续加入更多 slice),且组件可以按需订阅状态以获得高性能渲染。 |
| 93 | + |
| 94 | +如需我: |
| 95 | +- 把 README / contrib 文档中添加迁移说明; |
| 96 | +- 运行一次端到端检查(`npm run test:e2e`)并修复发现的问题; |
| 97 | +- 或把更多业务模块按同样模式迁移 —— 我可以继续逐步替换并提交 PR。 |
| 98 | + |
| 99 | +--- |
| 100 | +文档位置:`docs/Zustand-Migration.md`。 |
| 101 | + |
| 102 | +## 权限、角色与 i18n 更新说明 |
| 103 | + |
| 104 | +为确保 `Zustand` 示例页面在真实项目中按照角色与本地化规则正确展示,以下是本次针对权限与国际化(i18n)的具体修改与操作说明: |
| 105 | + |
| 106 | +### 代码层面已做的修改 |
| 107 | +- 路由:`src/routers/modules/business.routes.jsx` 中新增路由条目 `/zustand`,并在 `meta` 中声明 `permission`(示例为 `['admin','manager','dev','user']`)与 `keepAlive: true`。 |
| 108 | +- 懒加载:在 `src/routers/config/lazyLoad.config.jsx` 注册 `ZustandDemo`(`ZustandDemo: lazyLoad(() => import('@pages/zustand'), { preload: true })`)。 |
| 109 | +- 菜单:在 `src/config/menu.config.jsx` 中新增菜单项并设置 `i18nKey: 'menu.zustand'`。 |
| 110 | +- 路由权限映射:`src/mock/permission.ts` 中已包含 `/zustand` 对应的 `routePermissionMap`(`'/zustand': 'zustand:read'`),并在 `mockRoles` 中为部分角色分配了 `zustand:read` 权限(例如 `admin`)。 |
| 111 | + |
| 112 | +### 如何为账号分配角色(开发/测试) |
| 113 | +项目提供了三种常见的测试/开发方式来给账号分配角色: |
| 114 | + |
| 115 | +1. 使用内置测试账号(推荐快速验证) |
| 116 | + - `src/mock/permission.ts` 中定义了 `testAccounts`: |
| 117 | + - `[email protected]` → `super_admin` |
| 118 | + |
| 119 | + - `[email protected]` → `business_user` |
| 120 | + |
| 121 | + - 在本地使用这些账号模拟登录(token 为邮箱字符串或 mock 流程),系统会根据 `testAccounts` 返回对应权限集。 |
| 122 | + |
| 123 | +2. 临时覆盖(便于调试) |
| 124 | + - 在浏览器 console 或代码中设置 `localStorage.user_role`: |
| 125 | + |
| 126 | +```js |
| 127 | +// 设置为角色 code,示例:'admin' 或 'business_user' 或 'super_admin' |
| 128 | +localStorage.setItem('user_role', 'admin') |
| 129 | +// 然后刷新,permissionService 会优先使用该覆盖 |
| 130 | +``` |
| 131 | + |
| 132 | + - 这种方式仅用于开发或 demo,不会影响真实后端数据。 |
| 133 | + |
| 134 | +3. 真实后端授权(生产环境) |
| 135 | + - 若项目对接真实权限 API,请在后端给对应用户分配 `roles` / `permissions`,后端 `permission` 接口会返回完整的 `UserPermission`(包含 `roles`、`permissions`、`routes`),`permissionService` 会从该接口读取并缓存。 |
| 136 | + |
| 137 | +### 修改 i18n key 的建议与示例 |
| 138 | +- 菜单项已设置 `i18nKey: 'menu.zustand'`(`src/config/menu.config.jsx`)。请在项目的 i18n 字典中添加对应条目,例如: |
| 139 | + |
| 140 | +```json |
| 141 | +{ |
| 142 | + "menu": { |
| 143 | + "zustand": "Zustand 示例" |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +- 若需要页面标题国际化,可在路由配置添加 `i18nKey: 'routes.zustand.title'`,并在 i18n 词典中添加 `routes.zustand.title`。 |
| 149 | + |
| 150 | +### 路由 & 权限生效说明 |
| 151 | +- 应用启动或登录时,`permissionService.getPermissions()` 会根据: |
| 152 | + 1. `localStorage.user_role`(手动覆盖) |
| 153 | + 2. GitHub / token 标识(若启用) |
| 154 | + 3. 后端权限接口 |
| 155 | + 依次获取并构建 `UserPermission` 对象,包含 `roles`、`permissions` 与 `routes`。路由访问控制通过 `permissionService.canAccessRoute(path)` 实现。 |
| 156 | + |
| 157 | +### 在代码中判断权限的示例 |
| 158 | +在组件或路由守卫中检查角色/权限: |
| 159 | + |
| 160 | +```ts |
| 161 | +import { permissionService } from '@src/service/permissionService' |
| 162 | + |
| 163 | +// 检查角色 |
| 164 | +const ok = await permissionService.hasRole('admin') |
| 165 | + |
| 166 | +// 检查路由访问 |
| 167 | +const canAccess = await permissionService.canAccessRoute('/zustand') |
| 168 | +``` |
| 169 | + |
| 170 | +### 文档记录位置与变更说明 |
| 171 | +- 本节记录了如何为 `Zustand` 页面配置权限(路由 `meta.permission`)、如何给账号分配角色(mock、localStorage 覆盖、后端)以及 i18n key 的使用建议。 |
| 172 | +- 已在代码中完成的改动请参考上方“本次变更一览(文件)”。 |
| 173 | + |
| 174 | +## 操作小结(快速步骤) |
| 175 | +1. 添加页面并注册路由:`src/pages/zustand/index.tsx` → `lazyLoad.config.jsx` → `business.routes.jsx`(加 `meta.permission`) |
| 176 | +2. 在 `src/config/menu.config.jsx` 中添加 `i18nKey` 并在本地化文件中添加对应翻译。 |
| 177 | +3. 本地测试权限:使用 `localStorage.setItem('user_role', '<role_code>')` 或使用测试账号登录。 |
| 178 | +4. 验证:登录后访问 `/zustand`,或在控制台调用 `permissionService.canAccessRoute('/zustand')`。 |
| 179 | + |
| 180 | + |
| 181 | +## 已添加的翻译条目 |
| 182 | + |
| 183 | +为方便直接展示菜单项,已在项目的中英文 i18n 词典中添加或确认了 `menu.zustand` 条目: |
| 184 | + |
| 185 | +- 中文(已添加):[src/locales/zh/translation.js](src/locales/zh/translation.js#L1) 中的 `menu.zustand: 'Zustand'`(也可改为 `Zustand 示例` 或其它更友好的文案)。 |
| 186 | +- 英文(已添加):[src/locales/en/translation.js](src/locales/en/translation.js#L1) 中的 `menu.zustand: 'Zustand'`。 |
| 187 | + |
| 188 | +如果你希望使用不同的显示文案(例如中文显示为 “Zustand 示例”),请直接在上述文件中修改对应值并重启应用以加载新的语言资源。 |
| 189 | + |
| 190 | + |
0 commit comments