npm package: @kordar-lib/reducers
Description: 预置基于 Redux Toolkit 的通用状态管理包，默认挂载 user / app / layout 三个 slice，内置类型化的 React Hooks，以及常用的业务工具方法。支持与 redux-persist 组合和动态注入业务 reducer。
Latest version: 2.0.1

# @kordar-lib/reducers

预置基于 Redux Toolkit 的通用状态管理包，默认挂载 user / app / layout 三个 slice，内置类型化的 React Hooks，以及常用的业务工具方法。支持与 redux-persist 组合和动态注入业务 reducer。

## 特性
- 内置 store，默认挂载 user、app、layout
- 提供 useAppDispatch、useAppSelector 强类型 Hooks
- 暴露 RootState、AppDispatch 类型
- 提供 Admin、App 工具方法，简化常见调用
- 支持动态替换 reducer（loadNewReducer）

## 安装
```bash
npm install @kordar-lib/reducers @reduxjs/toolkit react-redux redux-persist
```

## 快速开始
```ts
// store.ts(x)（应用侧）
import {persistReducer, persistStore} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import {combineReducers} from 'redux'
import {
  store,
  loadNewReducer,
  adminReducer,
  appReducer,
  layoutReducer,
  type RootState,
  type AppDispatch,
  useAppDispatch,
  useAppSelector
} from '@kordar-lib/reducers'

const userPersist = persistReducer({ key: 'user', storage, whitelist: ['token', 'info'] }, adminReducer)
const appPersist = persistReducer({ key: 'app', storage, whitelist: ['sidebarCollapsed', 'locale', 'localeOptions', 'historyLocation', 'theme'] }, appReducer)
const layoutPersist = persistReducer({ key: 'layout', storage, whitelist: ['choices', 'tabs', 'choiceId'] }, layoutReducer)

loadNewReducer(combineReducers({
  user: userPersist,
  app: appPersist,
  layout: layoutPersist
}))

export const persistor = persistStore(store)
export { store }
```

## 在组件中使用
```tsx
import React from 'react'
import {Provider} from 'react-redux'
import {store, useAppDispatch, useAppSelector, setLocale} from '@kordar-lib/reducers'

function LanguageSwitcher() {
  const dispatch = useAppDispatch()
  const locale = useAppSelector(s => s.app.locale)
  return (
    <button onClick={() => dispatch(setLocale({locale: 'en'}))}>
      {locale}
    </button>
  )
}

export default function App() {
  return (
    <Provider store={store}>
      <LanguageSwitcher />
    </Provider>
  )
}
```

## API 概览

### Store 与类型
- store：预置的 Redux store 实例（已挂载 user/app/layout）
- loadNewReducer(reducer)：动态替换根 reducer（常与 combineReducers + persistReducer 配合）
- RootState：返回值类型，含默认 slice 与扩展键
- AppDispatch：dispatch 类型
- useAppDispatch、useAppSelector：类型化的 React Hooks

### Slice：user（./src/admin.ts）
- state: { token: string; info: Record<string, any>; permissions: Record<string, any>; version: number }
- actions: login(payload: {token?, info?, success?, error?}), logout(callback?), loadPermissions(data), incrementVersion()

### Slice：app（./src/app.ts）
- state: { sidebarCollapsed: boolean; locale: string; localeOptions: Record<string,string>; theme?: string; historyLocation: {hash,search,pathname} }
- actions: setSidebarCollapsed(boolean), setLocale({locale,callback?}), setAppTheme(string), setLocaleOptions(record), saveHistoryLocation({hash,search,pathname})

### Slice：layout（./src/layout.ts）
- state: { choices: (number|string)[]; tabs: (number|string)[]; choiceId: number|string }
- actions: sidebarChoice({id,choices|null}), justSaveChoice({choiceId}), saveTabs({newTabs?}), saveChoice({choices}), removeTab({id}), clearLayoutData()

### 工具方法（./src/util.ts）
- Admin
  - isGuest(): boolean
  - isSuper(): boolean
  - token(): string
  - info(): Record<string, any>
  - logout(callback?: () => void): void
  - permissions(): Record<string, any>
  - loadPermissions(data: any[] | Record<string, any>): void
  - incrementVersion(): void
  - can(...names: string[]): boolean
  - orCan(...names: string[]): boolean
  - clear(): void  // 清理 layout 并重置历史位置
- App
  - locale(): string
  - loadLanguageOptions(record: Record<string,string>): void
  - saveHistoryLocation(location: Location): void

## 动态注入业务 reducer

loadNewReducer 会整体替换根 reducer。注入业务 slice 时，务必把 user/app/layout 也一起合并进新的根 reducer（否则对应状态会丢失）。

```ts
import {combineReducers} from 'redux'
import {persistReducer} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import {loadNewReducer, adminReducer, appReducer, layoutReducer} from '@kordar-lib/reducers'
import someFeature from './someFeature'

const userPersist = persistReducer({ key: 'user', storage, whitelist: ['token', 'info'] }, adminReducer)
const appPersist = persistReducer({ key: 'app', storage, whitelist: ['sidebarCollapsed', 'locale', 'localeOptions', 'historyLocation', 'theme'] }, appReducer)
const layoutPersist = persistReducer({ key: 'layout', storage, whitelist: ['choices', 'tabs', 'choiceId'] }, layoutReducer)

const nextRoot = combineReducers({
  user: userPersist,
  app: appPersist,
  layout: layoutPersist,
  someFeature
})

loadNewReducer(nextRoot)
```

## 自定义 Slice 教程（创建并挂载）

下面以“计数器”功能为例，演示如何新建业务 slice，并在应用侧挂载到根 reducer。

步骤一：创建业务 slice（TypeScript）

```ts
// src/store/myCounter.ts
import {createSlice, PayloadAction} from '@reduxjs/toolkit'

export interface MyCounterState {
  value: number
}

const initialState: MyCounterState = { value: 0 }

const myCounterSlice = createSlice({
  name: 'myCounter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    add: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    }
  }
})

export const { increment, add } = myCounterSlice.actions
export default myCounterSlice.reducer
```

步骤二（可选）：配置持久化

```ts
// src/store/index.ts(x)
import {persistReducer, persistStore} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import myCounterReducer from './myCounter'

const myCounterPersist = persistReducer(
  { key: 'myCounter', storage, whitelist: ['value'] },
  myCounterReducer
)
```

步骤三：与既有切片合并并注入

```ts
import {combineReducers} from 'redux'
import {persistReducer} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import {
  loadNewReducer,
  adminReducer, appReducer, layoutReducer
} from '@kordar-lib/reducers'
import myCounterReducer from './myCounter'

const userPersist = persistReducer({ key: 'user', storage, whitelist: ['token','info'] }, adminReducer)
const appPersist = persistReducer({ key: 'app', storage, whitelist: ['sidebarCollapsed','locale','localeOptions','historyLocation','theme'] }, appReducer)
const layoutPersist = persistReducer({ key: 'layout', storage, whitelist: ['choices','tabs','choiceId'] }, layoutReducer)

const root = combineReducers({
  user: userPersist,
  app: appPersist,
  layout: layoutPersist,
  myCounter: myCounterReducer        // 新增挂载
})

loadNewReducer(root)
```

步骤四：组件中使用

```tsx
import React from 'react'
import {useAppDispatch, useAppSelector} from '@kordar-lib/reducers'
import {increment, add} from '@/store/myCounter'

export default function Counter() {
  const dispatch = useAppDispatch()
  const value = useAppSelector(state => state.myCounter.value)
  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch(increment())}>+1</button>
      <button onClick={() => dispatch(add(5))}>+5</button>
    </div>
  )
}
```

提示
- 如果根 reducer 由 `loadNewReducer` 动态替换，确保每次替换时都包含 user/app/layout 以及你的自定义切片。
- 需要持久化的切片用 `persistReducer` 包裹后再参与 `combineReducers`。
- 在 TS 项目中，选择器可通过 `useAppSelector((s) => s.myCounter.value as number)` 或为 `RootState` 编写模块扩展来获得更精准类型。

## 持久化集成（redux-persist）
- 推荐在应用侧以 persistReducer 包裹各切片
- 再通过 combineReducers + loadNewReducer 注入
- 参考示例：
  - 模板 TS 示例：../../templates/ant-design-tpl/examples/store.ts
  - 模板 JS 示例：../../projects/template/src/store.js

## 构建与类型（Monorepo：npm）

在仓库根目录执行：
```bash
npm install
npm run -w @kordar-lib/reducers build
```

需要单独做 TypeScript 类型检查时：
```bash
npm exec -w @kordar-lib/reducers -- tsc -p tsconfig.json --noEmit
```

## 文件索引
- 入口导出：./src/index.ts
- 基础 store：./src/store.ts
- 切片：
  - user：./src/admin.ts
  - app：./src/app.ts
  - layout：./src/layout.ts