feat: 配置 TailwindCSS 并迁移到 Redux
- 配置 TailwindCSS 支持小程序开发
- 安装 tailwindcss, postcss, autoprefixer
- 安装 weapp-tailwindcss 插件支持小程序
- 配置 tailwind.config.js 和 postcss.config.js
- 更新 Taro 配置支持 TailwindCSS
- 从 Zustand 迁移到 Redux Toolkit
- 移除 zustand 依赖
- 安装 redux, react-redux, redux-thunk, @reduxjs/toolkit
- 重构状态管理架构:
- src/constants/ - Action 类型常量
- src/actions/ - Action creators 和异步 actions
- src/reducers/ - Reducers
- src/selectors/ - 状态选择器
- src/hooks/redux.ts - 类型化 hooks
- 更新组件使用新的 Redux API
- 保持数据持久化功能
- 更新应用配置
- 将 app.ts 重命名为 app.tsx 支持 JSX
- 添加 Redux Provider 到应用根组件
- 更新 TODO.md 标记完成状态
- 构建验证通过,所有功能正常
This commit is contained in:
13
TODO.md
13
TODO.md
@@ -53,11 +53,14 @@ interface HistoryRecord {
|
||||
```
|
||||
|
||||
#### 2.2 状态管理
|
||||
- [ ] 创建 `src/store/` 目录
|
||||
- [ ] 实现 Zustand 状态管理
|
||||
- [ ] `src/store/historyStore.ts` - 历史记录管理
|
||||
- [ ] `src/store/templateStore.ts` - 模板配置管理
|
||||
- [ ] 添加数据持久化功能
|
||||
- [x] 创建 `src/store/` 目录
|
||||
- [x] 实现 Redux 状态管理(已从 Zustand 迁移到 Redux Toolkit)
|
||||
- [x] `src/reducers/history.ts` - 历史记录管理
|
||||
- [x] `src/reducers/template.ts` - 模板配置管理
|
||||
- [x] `src/actions/` - Action creators
|
||||
- [x] `src/selectors/` - 选择器
|
||||
- [x] `src/hooks/redux.ts` - 类型化 hooks
|
||||
- [x] 添加数据持久化功能(通过 Taro Storage API)
|
||||
|
||||
### 阶段三:模板卡片首页开发 (优先级:高)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
import { UnifiedWebpackPluginV5 } from 'weapp-tailwindcss/webpack'
|
||||
|
||||
import devConfig from './dev'
|
||||
import prodConfig from './prod'
|
||||
@@ -56,6 +57,20 @@ export default defineConfig<'vite'>(async (merge) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.merge({
|
||||
plugin: {
|
||||
install: {
|
||||
plugin: UnifiedWebpackPluginV5,
|
||||
args: [{
|
||||
appType: 'taro',
|
||||
// 下面个配置,会开启 rem -> rpx 的转化
|
||||
rem2rpx: true
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
// 小程序平台特定配置
|
||||
weapp: {
|
||||
|
||||
11
package.json
11
package.json
@@ -12,6 +12,7 @@
|
||||
"scripts": {
|
||||
"claude": "claude --dangerously-skip-permissions",
|
||||
"prepare": "husky",
|
||||
"postinstall": "npx weapp-tw patch",
|
||||
"new": "taro new",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -51,6 +52,7 @@
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.4",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@tarojs/components": "4.1.6",
|
||||
"@tarojs/helper": "4.1.6",
|
||||
"@tarojs/plugin-framework-react": "4.1.6",
|
||||
@@ -68,7 +70,9 @@
|
||||
"@tarojs/taro": "4.1.6",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"zustand": "^5.0.8"
|
||||
"react-redux": "^9.2.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
@@ -87,6 +91,7 @@
|
||||
"@types/minimatch": "^5",
|
||||
"@types/react": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-jest": "^30.1.2",
|
||||
"babel-preset-taro": "4.1.6",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -102,9 +107,11 @@
|
||||
"react-refresh": "^0.14.0",
|
||||
"stylelint": "^16.4.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"terser": "^5.30.4",
|
||||
"ts-jest": "^29.4.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^4.2.0"
|
||||
"vite": "^4.2.0",
|
||||
"weapp-tailwindcss": "^4.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
2001
pnpm-lock.yaml
generated
2001
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
postcss.config.js
Normal file
7
postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// postcss 插件以 object 方式注册的话,是按照由上到下的顺序执行的
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
200
src/actions/history.ts
Normal file
200
src/actions/history.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Dispatch } from 'redux'
|
||||
import { HistoryRecord } from '../store/types'
|
||||
import * as types from '../constants/history'
|
||||
|
||||
const STORAGE_KEY = 'historyRecords'
|
||||
|
||||
// Action creators
|
||||
export const loadRecordsRequest = () => ({
|
||||
type: types.LOAD_RECORDS_REQUEST
|
||||
})
|
||||
|
||||
export const loadRecordsSuccess = (records: HistoryRecord[]) => ({
|
||||
type: types.LOAD_RECORDS_SUCCESS,
|
||||
payload: records
|
||||
})
|
||||
|
||||
export const loadRecordsFailure = (error: string) => ({
|
||||
type: types.LOAD_RECORDS_FAILURE,
|
||||
payload: error
|
||||
})
|
||||
|
||||
export const addRecordRequest = () => ({
|
||||
type: types.ADD_RECORD_REQUEST
|
||||
})
|
||||
|
||||
export const addRecordSuccess = (record: HistoryRecord) => ({
|
||||
type: types.ADD_RECORD_SUCCESS,
|
||||
payload: record
|
||||
})
|
||||
|
||||
export const addRecordFailure = (error: string) => ({
|
||||
type: types.ADD_RECORD_FAILURE,
|
||||
payload: error
|
||||
})
|
||||
|
||||
export const updateRecordRequest = () => ({
|
||||
type: types.UPDATE_RECORD_REQUEST
|
||||
})
|
||||
|
||||
export const updateRecordSuccess = (record: HistoryRecord) => ({
|
||||
type: types.UPDATE_RECORD_SUCCESS,
|
||||
payload: record
|
||||
})
|
||||
|
||||
export const updateRecordFailure = (error: string) => ({
|
||||
type: types.UPDATE_RECORD_FAILURE,
|
||||
payload: error
|
||||
})
|
||||
|
||||
export const deleteRecordRequest = () => ({
|
||||
type: types.DELETE_RECORD_REQUEST
|
||||
})
|
||||
|
||||
export const deleteRecordSuccess = (id: string) => ({
|
||||
type: types.DELETE_RECORD_SUCCESS,
|
||||
payload: id
|
||||
})
|
||||
|
||||
export const deleteRecordFailure = (error: string) => ({
|
||||
type: types.DELETE_RECORD_FAILURE,
|
||||
payload: error
|
||||
})
|
||||
|
||||
export const clearRecordsRequest = () => ({
|
||||
type: types.CLEAR_RECORDS_REQUEST
|
||||
})
|
||||
|
||||
export const clearRecordsSuccess = () => ({
|
||||
type: types.CLEAR_RECORDS_SUCCESS
|
||||
})
|
||||
|
||||
export const clearRecordsFailure = (error: string) => ({
|
||||
type: types.CLEAR_RECORDS_FAILURE,
|
||||
payload: error
|
||||
})
|
||||
|
||||
export const clearError = () => ({
|
||||
type: types.CLEAR_ERROR
|
||||
})
|
||||
|
||||
// Async actions
|
||||
export const loadRecords = () => {
|
||||
return async (dispatch: Dispatch) => {
|
||||
dispatch(loadRecordsRequest())
|
||||
try {
|
||||
const savedRecords = Taro.getStorageSync(STORAGE_KEY) || []
|
||||
dispatch(loadRecordsSuccess(savedRecords))
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '加载历史记录失败'
|
||||
dispatch(loadRecordsFailure(errorMsg))
|
||||
console.error('加载历史记录失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const addRecord = (recordData: Omit<HistoryRecord, 'id' | 'createTime' | 'updateTime'>) => {
|
||||
return async (dispatch: Dispatch, getState: () => any) => {
|
||||
dispatch(addRecordRequest())
|
||||
try {
|
||||
const id = `record_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const newRecord: HistoryRecord = {
|
||||
...recordData,
|
||||
id,
|
||||
createTime: now,
|
||||
updateTime: now
|
||||
}
|
||||
|
||||
const currentRecords = getState().history.records
|
||||
const updatedRecords = [newRecord, ...currentRecords]
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
dispatch(addRecordSuccess(newRecord))
|
||||
return id
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '添加记录失败'
|
||||
dispatch(addRecordFailure(errorMsg))
|
||||
console.error('添加记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const updateRecord = (id: string, updates: Partial<HistoryRecord>) => {
|
||||
return async (dispatch: Dispatch, getState: () => any) => {
|
||||
dispatch(updateRecordRequest())
|
||||
try {
|
||||
const currentRecords = getState().history.records
|
||||
const recordIndex = currentRecords.findIndex((record: HistoryRecord) => record.id === id)
|
||||
|
||||
if (recordIndex === -1) {
|
||||
throw new Error('记录不存在')
|
||||
}
|
||||
|
||||
const updatedRecord = {
|
||||
...currentRecords[recordIndex],
|
||||
...updates,
|
||||
updateTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
const updatedRecords = [...currentRecords]
|
||||
updatedRecords[recordIndex] = updatedRecord
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
dispatch(updateRecordSuccess(updatedRecord))
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '更新记录失败'
|
||||
dispatch(updateRecordFailure(errorMsg))
|
||||
console.error('更新记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteRecord = (id: string) => {
|
||||
return async (dispatch: Dispatch, getState: () => any) => {
|
||||
dispatch(deleteRecordRequest())
|
||||
try {
|
||||
const currentRecords = getState().history.records
|
||||
const updatedRecords = currentRecords.filter((record: HistoryRecord) => record.id !== id)
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
dispatch(deleteRecordSuccess(id))
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '删除记录失败'
|
||||
dispatch(deleteRecordFailure(errorMsg))
|
||||
console.error('删除记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const clearRecords = () => {
|
||||
return async (dispatch: Dispatch) => {
|
||||
dispatch(clearRecordsRequest())
|
||||
try {
|
||||
await Taro.removeStorage({ key: STORAGE_KEY })
|
||||
dispatch(clearRecordsSuccess())
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '清空记录失败'
|
||||
dispatch(clearRecordsFailure(errorMsg))
|
||||
console.error('清空记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/actions/template.ts
Normal file
12
src/actions/template.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Template } from '../store/types'
|
||||
import * as types from '../constants/template'
|
||||
|
||||
export const initTemplates = (templates: Template[]) => ({
|
||||
type: types.INIT_TEMPLATES,
|
||||
payload: templates
|
||||
})
|
||||
|
||||
export const setSelectedTemplate = (template: Template | null) => ({
|
||||
type: types.SET_SELECTED_TEMPLATE,
|
||||
payload: template
|
||||
})
|
||||
@@ -1,4 +1,8 @@
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
#app{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { useLaunch } from '@tarojs/taro'
|
||||
import { Provider } from 'react-redux'
|
||||
import configStore from './store'
|
||||
|
||||
import './app.css'
|
||||
|
||||
const store = configStore()
|
||||
|
||||
function App({ children }: PropsWithChildren<any>) {
|
||||
useLaunch(() => {
|
||||
console.log('App launched.')
|
||||
|
||||
})
|
||||
|
||||
// children 是将要会渲染的页面
|
||||
return children
|
||||
return (
|
||||
<Provider store={store}>
|
||||
{children}
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
22
src/constants/history.ts
Normal file
22
src/constants/history.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// History action types
|
||||
export const LOAD_RECORDS_REQUEST = 'LOAD_RECORDS_REQUEST'
|
||||
export const LOAD_RECORDS_SUCCESS = 'LOAD_RECORDS_SUCCESS'
|
||||
export const LOAD_RECORDS_FAILURE = 'LOAD_RECORDS_FAILURE'
|
||||
|
||||
export const ADD_RECORD_REQUEST = 'ADD_RECORD_REQUEST'
|
||||
export const ADD_RECORD_SUCCESS = 'ADD_RECORD_SUCCESS'
|
||||
export const ADD_RECORD_FAILURE = 'ADD_RECORD_FAILURE'
|
||||
|
||||
export const UPDATE_RECORD_REQUEST = 'UPDATE_RECORD_REQUEST'
|
||||
export const UPDATE_RECORD_SUCCESS = 'UPDATE_RECORD_SUCCESS'
|
||||
export const UPDATE_RECORD_FAILURE = 'UPDATE_RECORD_FAILURE'
|
||||
|
||||
export const DELETE_RECORD_REQUEST = 'DELETE_RECORD_REQUEST'
|
||||
export const DELETE_RECORD_SUCCESS = 'DELETE_RECORD_SUCCESS'
|
||||
export const DELETE_RECORD_FAILURE = 'DELETE_RECORD_FAILURE'
|
||||
|
||||
export const CLEAR_RECORDS_REQUEST = 'CLEAR_RECORDS_REQUEST'
|
||||
export const CLEAR_RECORDS_SUCCESS = 'CLEAR_RECORDS_SUCCESS'
|
||||
export const CLEAR_RECORDS_FAILURE = 'CLEAR_RECORDS_FAILURE'
|
||||
|
||||
export const CLEAR_ERROR = 'CLEAR_ERROR'
|
||||
3
src/constants/template.ts
Normal file
3
src/constants/template.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Template action types
|
||||
export const INIT_TEMPLATES = 'INIT_TEMPLATES'
|
||||
export const SET_SELECTED_TEMPLATE = 'SET_SELECTED_TEMPLATE'
|
||||
6
src/hooks/redux.ts
Normal file
6
src/hooks/redux.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'
|
||||
import type { RootState, AppDispatch } from '../store'
|
||||
|
||||
// 使用类型化的 hooks
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>()
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
@@ -1,19 +1,18 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useHistoryStore } from '../../store'
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks/redux'
|
||||
import { selectHistoryRecords } from '../../selectors/history'
|
||||
import { loadRecords, clearRecords } from '../../actions/history'
|
||||
import './index.css'
|
||||
|
||||
export default function History() {
|
||||
const {
|
||||
records,
|
||||
loadRecords,
|
||||
clearRecords
|
||||
} = useHistoryStore()
|
||||
const dispatch = useAppDispatch()
|
||||
const records = useAppSelector(selectHistoryRecords)
|
||||
|
||||
useEffect(() => {
|
||||
loadRecords()
|
||||
}, [loadRecords])
|
||||
dispatch(loadRecords())
|
||||
}, [dispatch])
|
||||
|
||||
const clearHistory = () => {
|
||||
Taro.showModal({
|
||||
@@ -22,7 +21,7 @@ export default function History() {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await clearRecords()
|
||||
await dispatch(clearRecords())
|
||||
Taro.showToast({
|
||||
title: '清空成功',
|
||||
icon: 'success'
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { useEffect } from 'react'
|
||||
import { navigateTo } from '@tarojs/taro'
|
||||
import { useTemplateStore, Template } from '../../store/index'
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks/redux'
|
||||
import { selectTemplates } from '../../selectors/template'
|
||||
import { initTemplates, setSelectedTemplate } from '../../actions/template'
|
||||
import { Template } from '../../store/types'
|
||||
import TemplateCard from '../../components/TemplateCard'
|
||||
import './index.css'
|
||||
|
||||
export default function Home() {
|
||||
const { templates, initTemplates, setSelectedTemplate } = useTemplateStore()
|
||||
const dispatch = useAppDispatch()
|
||||
const templates = useAppSelector(selectTemplates)
|
||||
|
||||
useEffect(() => {
|
||||
initTemplates()
|
||||
}, [initTemplates])
|
||||
// 需要先检查是否有模板配置文件
|
||||
const { TEMPLATE_CONFIG } = require('../../config/templates')
|
||||
dispatch(initTemplates(TEMPLATE_CONFIG))
|
||||
}, [dispatch])
|
||||
|
||||
const handleTemplateClick = (template: Template) => {
|
||||
// 设置选中的模板
|
||||
setSelectedTemplate(template)
|
||||
|
||||
dispatch(setSelectedTemplate(template))
|
||||
|
||||
// 跳转到原有的功能页面,保持老功能可用
|
||||
navigateTo({
|
||||
url: `/pages/index/index?templateId=${template.id}&templateName=${encodeURIComponent(template.name)}&templateType=${template.type}`
|
||||
|
||||
91
src/reducers/history.ts
Normal file
91
src/reducers/history.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { HistoryRecord } from '../store/types'
|
||||
import * as types from '../constants/history'
|
||||
|
||||
export interface HistoryState {
|
||||
records: HistoryRecord[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const initialState: HistoryState = {
|
||||
records: [],
|
||||
loading: false,
|
||||
error: null
|
||||
}
|
||||
|
||||
export default function historyReducer(state = initialState, action: any): HistoryState {
|
||||
switch (action.type) {
|
||||
case types.LOAD_RECORDS_REQUEST:
|
||||
case types.ADD_RECORD_REQUEST:
|
||||
case types.UPDATE_RECORD_REQUEST:
|
||||
case types.DELETE_RECORD_REQUEST:
|
||||
case types.CLEAR_RECORDS_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
error: null
|
||||
}
|
||||
|
||||
case types.LOAD_RECORDS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
records: action.payload,
|
||||
error: null
|
||||
}
|
||||
|
||||
case types.ADD_RECORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
records: [action.payload, ...state.records],
|
||||
error: null
|
||||
}
|
||||
|
||||
case types.UPDATE_RECORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
records: state.records.map(record =>
|
||||
record.id === action.payload.id ? action.payload : record
|
||||
),
|
||||
error: null
|
||||
}
|
||||
|
||||
case types.DELETE_RECORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
records: state.records.filter(record => record.id !== action.payload),
|
||||
error: null
|
||||
}
|
||||
|
||||
case types.CLEAR_RECORDS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
records: [],
|
||||
error: null
|
||||
}
|
||||
|
||||
case types.LOAD_RECORDS_FAILURE:
|
||||
case types.ADD_RECORD_FAILURE:
|
||||
case types.UPDATE_RECORD_FAILURE:
|
||||
case types.DELETE_RECORD_FAILURE:
|
||||
case types.CLEAR_RECORDS_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: action.payload
|
||||
}
|
||||
|
||||
case types.CLEAR_ERROR:
|
||||
return {
|
||||
...state,
|
||||
error: null
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
11
src/reducers/index.ts
Normal file
11
src/reducers/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import history from './history'
|
||||
import template from './template'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
history,
|
||||
template
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>
|
||||
export default rootReducer
|
||||
31
src/reducers/template.ts
Normal file
31
src/reducers/template.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Template } from '../store/types'
|
||||
import * as types from '../constants/template'
|
||||
|
||||
export interface TemplateState {
|
||||
templates: Template[]
|
||||
selectedTemplate: Template | null
|
||||
}
|
||||
|
||||
const initialState: TemplateState = {
|
||||
templates: [],
|
||||
selectedTemplate: null
|
||||
}
|
||||
|
||||
export default function templateReducer(state = initialState, action: any): TemplateState {
|
||||
switch (action.type) {
|
||||
case types.INIT_TEMPLATES:
|
||||
return {
|
||||
...state,
|
||||
templates: action.payload
|
||||
}
|
||||
|
||||
case types.SET_SELECTED_TEMPLATE:
|
||||
return {
|
||||
...state,
|
||||
selectedTemplate: action.payload
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
18
src/selectors/history.ts
Normal file
18
src/selectors/history.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { RootState } from '../store'
|
||||
import { HistoryRecord } from '../store/types'
|
||||
|
||||
// History selectors
|
||||
export const selectHistoryRecords = (state: RootState): HistoryRecord[] =>
|
||||
state.history.records
|
||||
|
||||
export const selectHistoryLoading = (state: RootState): boolean =>
|
||||
state.history.loading
|
||||
|
||||
export const selectHistoryError = (state: RootState): string | null =>
|
||||
state.history.error
|
||||
|
||||
export const selectRecordById = (state: RootState, id: string): HistoryRecord | undefined =>
|
||||
state.history.records.find(record => record.id === id)
|
||||
|
||||
export const selectRecordsByStatus = (state: RootState, status: HistoryRecord['status']): HistoryRecord[] =>
|
||||
state.history.records.filter(record => record.status === status)
|
||||
15
src/selectors/template.ts
Normal file
15
src/selectors/template.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { RootState } from '../store'
|
||||
import { Template } from '../store/types'
|
||||
|
||||
// Template selectors
|
||||
export const selectTemplates = (state: RootState): Template[] =>
|
||||
state.template.templates
|
||||
|
||||
export const selectSelectedTemplate = (state: RootState): Template | null =>
|
||||
state.template.selectedTemplate
|
||||
|
||||
export const selectTemplateById = (state: RootState, id: string): Template | undefined =>
|
||||
state.template.templates.find(template => template.id === id)
|
||||
|
||||
export const selectTemplatesByType = (state: RootState, type: 'image-to-image' | 'image-to-video'): Template[] =>
|
||||
state.template.templates.filter(template => template.type === type)
|
||||
167
src/store/README.md
Normal file
167
src/store/README.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Redux Store 使用指南
|
||||
|
||||
本项目已从 Zustand 迁移到 Redux Toolkit,以下是使用指南。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── store/
|
||||
│ ├── index.ts # Store 配置
|
||||
│ └── types.ts # 类型定义
|
||||
├── constants/ # Action 类型常量
|
||||
│ ├── history.ts
|
||||
│ └── template.ts
|
||||
├── actions/ # Action creators
|
||||
│ ├── history.ts
|
||||
│ └── template.ts
|
||||
├── reducers/ # Reducers
|
||||
│ ├── index.ts
|
||||
│ ├── history.ts
|
||||
│ └── template.ts
|
||||
├── selectors/ # 选择器
|
||||
│ ├── history.ts
|
||||
│ └── template.ts
|
||||
└── hooks/
|
||||
└── redux.ts # 类型化的 hooks
|
||||
```
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 1. 在组件中使用 Redux
|
||||
|
||||
```tsx
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks/redux'
|
||||
import { selectHistoryRecords, selectHistoryLoading } from '../../selectors/history'
|
||||
import { loadRecords, addRecord } from '../../actions/history'
|
||||
|
||||
export default function MyComponent() {
|
||||
const dispatch = useAppDispatch()
|
||||
const records = useAppSelector(selectHistoryRecords)
|
||||
const loading = useAppSelector(selectHistoryLoading)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(loadRecords())
|
||||
}, [dispatch])
|
||||
|
||||
const handleAddRecord = async () => {
|
||||
try {
|
||||
await dispatch(addRecord({
|
||||
type: 'image-to-image',
|
||||
templateId: '1',
|
||||
templateName: '艺术风格',
|
||||
inputImage: 'path/to/image.jpg',
|
||||
status: 'generating'
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('添加记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{loading && <Text>加载中...</Text>}
|
||||
{records.map(record => (
|
||||
<View key={record.id}>
|
||||
<Text>{record.templateName}</Text>
|
||||
</View>
|
||||
))}
|
||||
<Button onClick={handleAddRecord}>添加记录</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 历史记录管理
|
||||
|
||||
```tsx
|
||||
// 加载历史记录
|
||||
dispatch(loadRecords())
|
||||
|
||||
// 添加新记录
|
||||
dispatch(addRecord({
|
||||
type: 'image-to-video',
|
||||
templateId: '2',
|
||||
templateName: '视频生成',
|
||||
inputImage: 'path/to/image.jpg',
|
||||
status: 'generating'
|
||||
}))
|
||||
|
||||
// 更新记录
|
||||
dispatch(updateRecord('record_id', {
|
||||
status: 'completed',
|
||||
outputResult: 'path/to/result.mp4'
|
||||
}))
|
||||
|
||||
// 删除记录
|
||||
dispatch(deleteRecord('record_id'))
|
||||
|
||||
// 清空所有记录
|
||||
dispatch(clearRecords())
|
||||
```
|
||||
|
||||
### 3. 模板管理
|
||||
|
||||
```tsx
|
||||
// 初始化模板
|
||||
import { TEMPLATE_CONFIG } from '../../config/templates'
|
||||
dispatch(initTemplates(TEMPLATE_CONFIG))
|
||||
|
||||
// 设置选中的模板
|
||||
dispatch(setSelectedTemplate(template))
|
||||
|
||||
// 获取模板列表
|
||||
const templates = useAppSelector(selectTemplates)
|
||||
|
||||
// 获取选中的模板
|
||||
const selectedTemplate = useAppSelector(selectSelectedTemplate)
|
||||
|
||||
// 根据类型筛选模板
|
||||
const videoTemplates = useAppSelector(state =>
|
||||
selectTemplatesByType(state, 'image-to-video')
|
||||
)
|
||||
```
|
||||
|
||||
### 4. 选择器使用
|
||||
|
||||
```tsx
|
||||
// 基本选择器
|
||||
const records = useAppSelector(selectHistoryRecords)
|
||||
const loading = useAppSelector(selectHistoryLoading)
|
||||
const error = useAppSelector(selectHistoryError)
|
||||
|
||||
// 带参数的选择器
|
||||
const record = useAppSelector(state => selectRecordById(state, 'record_id'))
|
||||
const generatingRecords = useAppSelector(state =>
|
||||
selectRecordsByStatus(state, 'generating')
|
||||
)
|
||||
```
|
||||
|
||||
## 迁移说明
|
||||
|
||||
### 从 Zustand 迁移到 Redux
|
||||
|
||||
原来的 Zustand 用法:
|
||||
```tsx
|
||||
// 旧的 Zustand 用法
|
||||
const { records, loadRecords, addRecord } = useHistoryStore()
|
||||
```
|
||||
|
||||
新的 Redux 用法:
|
||||
```tsx
|
||||
// 新的 Redux 用法
|
||||
const dispatch = useAppDispatch()
|
||||
const records = useAppSelector(selectHistoryRecords)
|
||||
|
||||
// 调用 actions
|
||||
dispatch(loadRecords())
|
||||
dispatch(addRecord(recordData))
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有异步操作都通过 Redux Thunk 处理
|
||||
2. 使用类型化的 hooks (`useAppDispatch`, `useAppSelector`)
|
||||
3. 选择器函数提供了类型安全的状态访问
|
||||
4. 所有状态更新都是不可变的
|
||||
5. 错误处理通过 Redux 状态管理
|
||||
@@ -1,137 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { HistoryRecord } from './types'
|
||||
|
||||
interface HistoryStore {
|
||||
records: HistoryRecord[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
loadRecords: () => Promise<void>
|
||||
addRecord: (record: Omit<HistoryRecord, 'id' | 'createTime' | 'updateTime'>) => Promise<string>
|
||||
updateRecord: (id: string, updates: Partial<HistoryRecord>) => Promise<void>
|
||||
deleteRecord: (id: string) => Promise<void>
|
||||
clearRecords: () => Promise<void>
|
||||
getRecordById: (id: string) => HistoryRecord | undefined
|
||||
getRecordsByStatus: (status: HistoryRecord['status']) => HistoryRecord[]
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'historyRecords'
|
||||
|
||||
export const useHistoryStore = create<HistoryStore>((set, get) => ({
|
||||
records: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// 加载历史记录
|
||||
loadRecords: async () => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const savedRecords = Taro.getStorageSync(STORAGE_KEY) || []
|
||||
set({ records: savedRecords, loading: false })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '加载历史记录失败'
|
||||
set({ error: errorMsg, loading: false })
|
||||
console.error('加载历史记录失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 添加新记录
|
||||
addRecord: async (recordData) => {
|
||||
const id = `record_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const newRecord: HistoryRecord = {
|
||||
...recordData,
|
||||
id,
|
||||
createTime: now,
|
||||
updateTime: now
|
||||
}
|
||||
|
||||
try {
|
||||
const currentRecords = get().records
|
||||
const updatedRecords = [newRecord, ...currentRecords]
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
set({ records: updatedRecords })
|
||||
return id
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '添加记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('添加记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 更新记录
|
||||
updateRecord: async (id, updates) => {
|
||||
try {
|
||||
const currentRecords = get().records
|
||||
const updatedRecords = currentRecords.map(record =>
|
||||
record.id === id
|
||||
? { ...record, ...updates, updateTime: new Date().toISOString() }
|
||||
: record
|
||||
)
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
set({ records: updatedRecords })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '更新记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('更新记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 删除记录
|
||||
deleteRecord: async (id) => {
|
||||
try {
|
||||
const currentRecords = get().records
|
||||
const updatedRecords = currentRecords.filter(record => record.id !== id)
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
set({ records: updatedRecords })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '删除记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('删除记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 清空所有记录
|
||||
clearRecords: async () => {
|
||||
try {
|
||||
await Taro.removeStorage({ key: STORAGE_KEY })
|
||||
set({ records: [] })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '清空记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('清空记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个记录
|
||||
getRecordById: (id) => {
|
||||
return get().records.find(record => record.id === id)
|
||||
},
|
||||
|
||||
// 按状态筛选记录
|
||||
getRecordsByStatus: (status) => {
|
||||
return get().records.filter(record => record.status === status)
|
||||
}
|
||||
}))
|
||||
@@ -1,4 +1,25 @@
|
||||
// 统一导出所有store
|
||||
export * from './types'
|
||||
export * from './historyStore'
|
||||
export * from './templateStore'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import historyReducer from '../reducers/history'
|
||||
import templateReducer from '../reducers/template'
|
||||
|
||||
export default function configStore() {
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
history: historyReducer,
|
||||
template: templateReducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: ['persist/PERSIST']
|
||||
}
|
||||
})
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
export type RootState = ReturnType<ReturnType<typeof configStore>['getState']>
|
||||
export type AppDispatch = ReturnType<typeof configStore>['dispatch']
|
||||
|
||||
// 导出类型
|
||||
export * from './types'
|
||||
@@ -1,39 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import { Template } from './types'
|
||||
import { TEMPLATE_CONFIG } from '../config/templates'
|
||||
|
||||
interface TemplateStore {
|
||||
templates: Template[]
|
||||
selectedTemplate: Template | null
|
||||
|
||||
// Actions
|
||||
initTemplates: () => void
|
||||
getTemplateById: (id: string) => Template | undefined
|
||||
setSelectedTemplate: (template: Template | null) => void
|
||||
getTemplatesByType: (type: 'image-to-image' | 'image-to-video') => Template[]
|
||||
}
|
||||
|
||||
export const useTemplateStore = create<TemplateStore>((set, get) => ({
|
||||
templates: [],
|
||||
selectedTemplate: null,
|
||||
|
||||
// 初始化模板数据
|
||||
initTemplates: () => {
|
||||
set({ templates: TEMPLATE_CONFIG })
|
||||
},
|
||||
|
||||
// 根据ID获取模板
|
||||
getTemplateById: (id) => {
|
||||
return get().templates.find(template => template.id === id)
|
||||
},
|
||||
|
||||
// 设置选中的模板
|
||||
setSelectedTemplate: (template) => {
|
||||
set({ selectedTemplate: template })
|
||||
},
|
||||
|
||||
// 根据类型筛选模板
|
||||
getTemplatesByType: (type) => {
|
||||
return get().templates.filter(template => template.type === type)
|
||||
}
|
||||
}))
|
||||
15
tailwind.config.js
Normal file
15
tailwind.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
// 这里给出了一份 taro 通用示例,具体要根据你自己项目的目录结构进行配置
|
||||
// 比如你使用 vue3 项目,你就需要把 vue 这个格式也包括进来
|
||||
// 不在 content glob 表达式中包括的文件,在里面编写 tailwindcss class,是不会生成对应的 css 工具类的
|
||||
content: ['./public/index.html', './src/**/*.{html,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
// 小程序不需要 preflight,因为这主要是给 h5 的,如果你要同时开发多端,你应该使用 process.env.TARO_ENV 环境变量来控制它
|
||||
preflight: false,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user