初始化工程
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
7
.eslintrc
Normal file
7
.eslintrc
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
"extends": ["taro/react"],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
}
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
dist/
|
||||
deploy_versions/
|
||||
.temp/
|
||||
.rn_temp/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
.swc
|
||||
212
README.md
Normal file
212
README.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# PlayBallTogether - 约球应用
|
||||
|
||||
## 项目简介
|
||||
|
||||
这是一个基于Taro + React + TypeScript的约球应用,用户可以发布个人约球活动。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 核心功能特性
|
||||
|
||||
- ✅ **图片上传** - 支持上传多张活动封面图片
|
||||
- ✅ **活动信息填写** - 主题、时间、场地、价格等完整信息
|
||||
- ✅ **多玩法选择** - 单打、双打、娱乐拉球、到了再说
|
||||
- ✅ **人数控制** - 灵活设置最少到最多参与人数
|
||||
- ✅ **NTRP水平设置** - 滑动条选择技能水平区间
|
||||
- ✅ **性别偏好** - 支持性别限制或不限
|
||||
- ✅ **自动候补** - 开启自动候补逻辑
|
||||
- ✅ **多场发布** - 支持同时创建多场活动
|
||||
|
||||
### 技术架构特性 ⭐
|
||||
|
||||
- ✅ **动态表单配置** - 通过JSON配置动态生成表单
|
||||
- ✅ **组件化架构** - 可复用的字段渲染器
|
||||
- ✅ **表单验证** - 自动数据验证和错误提示
|
||||
- ✅ **API集中管理** - 统一services目录,按页面功能命名
|
||||
- ✅ **Token自动管理** - 自动携带token,处理过期刷新
|
||||
- ✅ **请求拦截处理** - 统一错误处理和加载状态
|
||||
- ✅ **多人协作友好** - 按页面隔离,避免代码冲突
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: Taro 4.1.5
|
||||
- **前端**: React 18 + TypeScript
|
||||
- **UI库**: NutUI React Taro
|
||||
- **样式**: Sass
|
||||
- **状态管理**: React Context + useReducer
|
||||
- **本地存储**: Taro Storage API
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
# 或
|
||||
yarn install
|
||||
|
||||
# 开发环境启动
|
||||
npm run dev:h5 # H5版本
|
||||
npm run dev:weapp # 微信小程序
|
||||
npm run dev:alipay # 支付宝小程序
|
||||
|
||||
# 构建生产版本
|
||||
npm run build:h5 # H5版本
|
||||
npm run build:weapp # 微信小程序
|
||||
npm run build:alipay # 支付宝小程序
|
||||
```
|
||||
|
||||
## 页面路由
|
||||
|
||||
- `/pages/index/index` - 首页
|
||||
- `/pages/publish/publish` - 原发布页面
|
||||
- `/pages/publishBall/publishBall` - 新的约球发布页面 ⭐
|
||||
- `/pages/dynamicFormDemo/dynamicFormDemo` - 动态表单演示页面 ⭐
|
||||
|
||||
## 项目结构 🏗️
|
||||
|
||||
本项目采用**按页面分层、组件共享**的架构设计,便于多人协作开发。
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # 🔥 公共组件(所有页面共享)
|
||||
│ └── DynamicForm/ # 动态表单组件 ⭐
|
||||
│ ├── DynamicForm.tsx # 动态表单容器
|
||||
│ ├── DynamicForm.scss # 动态表单样式
|
||||
│ ├── FieldRenderer.tsx # 字段渲染器
|
||||
│ ├── FieldRenderer.scss # 字段渲染器样式
|
||||
│ └── index.ts # 组件导出
|
||||
├── pages/ # 📱 页面目录(按功能模块划分)
|
||||
│ ├── index/ # 首页模块
|
||||
│ ├── publish/ # 原发布页面
|
||||
│ ├── publishBall/ # 约球发布模块 ⭐
|
||||
│ │ ├── publishBall.tsx
|
||||
│ │ ├── publishBall.scss
|
||||
│ │ └── publishBall.config.ts
|
||||
│ └── dynamicFormDemo/ # 动态表单演示模块 ⭐
|
||||
│ ├── dynamicFormDemo.tsx
|
||||
│ ├── dynamicFormDemo.scss
|
||||
│ └── dynamicFormDemo.config.ts
|
||||
├── services/ # 🌐 API服务(统一管理)
|
||||
│ ├── httpService.ts # HTTP基础服务 ⭐
|
||||
│ ├── authService.ts # 认证服务 ⭐
|
||||
│ ├── commonApi.ts # 通用API服务 ⭐
|
||||
│ ├── publishBallApi.ts # 🔥 约球发布API服务
|
||||
│ └── demoApi.ts # 🔥 演示页面API服务
|
||||
├── utils/ # 🛠️ 工具函数
|
||||
│ ├── tokenManager.ts # Token管理 ⭐
|
||||
│ └── storage.ts # 本地存储
|
||||
├── config/ # ⚙️ 配置文件
|
||||
│ ├── formConfig.ts # 表单配置定义 ⭐
|
||||
│ └── exampleConfigs.ts # 示例配置 ⭐
|
||||
├── store/ # 📦 全局状态管理
|
||||
│ ├── matchStore.ts # Zustand状态管理
|
||||
│ └── storage.ts # 本地存储工具
|
||||
├── app.ts # 应用入口
|
||||
├── app.config.ts # 应用配置
|
||||
└── app.scss # 全局样式
|
||||
```
|
||||
|
||||
详细架构说明请查看:[ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||
|
||||
## 设计特色
|
||||
|
||||
- 🎨 **严格按照设计图实现** - 像素级还原UI设计
|
||||
- 📱 **移动端优先** - 针对手机屏幕优化
|
||||
- ⚡ **流畅交互** - 高性能的用户体验
|
||||
- 💾 **数据持久化** - 自动保存用户输入
|
||||
- 🔄 **状态同步** - 全局状态实时更新
|
||||
|
||||
## 核心功能实现
|
||||
|
||||
### 动态表单配置 ⭐
|
||||
- **JSON驱动** - 通过配置文件定义表单结构
|
||||
- **字段类型丰富** - 支持10+种字段类型(图片上传、文本输入、多选、计数器、滑动条等)
|
||||
- **灵活配置** - 支持验证规则、默认值、选项配置
|
||||
- **自动渲染** - 根据配置自动生成对应的UI组件
|
||||
- **类型安全** - 完整的TypeScript类型定义
|
||||
|
||||
### 支持的字段类型
|
||||
- `image-upload` - 图片上传
|
||||
- `text-input` - 文本输入
|
||||
- `number-input` - 数字输入
|
||||
- `time-display` - 时间显示
|
||||
- `venue-input` - 场地输入(带箭头)
|
||||
- `multi-select` - 多选按钮
|
||||
- `counter` - 计数器(min/max)
|
||||
- `slider` - 滑动条
|
||||
- `radio-group` - 单选按钮组
|
||||
- `checkbox` - 复选框
|
||||
|
||||
### 图片上传
|
||||
- 使用Taro.chooseImage API
|
||||
- 支持相册和相机选择
|
||||
- 最多支持9张图片
|
||||
- 实时预览和删除功能
|
||||
|
||||
### 表单验证
|
||||
- 必填字段验证
|
||||
- 数字范围验证
|
||||
- 自定义验证规则
|
||||
- 友好的错误提示
|
||||
|
||||
### 响应式设计
|
||||
- 适配不同屏幕尺寸
|
||||
- 灵活的布局系统
|
||||
- 优雅的动画过渡
|
||||
|
||||
## 如何自定义表单配置
|
||||
|
||||
### 修改表单字段
|
||||
|
||||
编辑 `src/config/formConfig.ts` 文件:
|
||||
|
||||
```typescript
|
||||
// 添加新字段
|
||||
{
|
||||
key: 'newField', // 字段唯一标识
|
||||
type: 'text-input', // 字段类型
|
||||
title: '新字段', // 显示标题
|
||||
required: true, // 是否必填
|
||||
placeholder: '请输入内容', // 占位符
|
||||
defaultValue: '', // 默认值
|
||||
validation: { // 验证规则
|
||||
min: 1,
|
||||
max: 100,
|
||||
message: '请输入1-100字符'
|
||||
}
|
||||
}
|
||||
|
||||
// 修改现有字段
|
||||
{
|
||||
key: 'playStyle',
|
||||
type: 'multi-select',
|
||||
title: '活动玩法',
|
||||
options: [
|
||||
{ label: '新玩法', value: 'new-style' }, // 添加新选项
|
||||
{ label: '单打', value: '单打' },
|
||||
// ... 其他选项
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的配置选项
|
||||
|
||||
- **基础配置**: `key`, `type`, `title`, `required`, `placeholder`, `hint`
|
||||
- **默认值**: `defaultValue`
|
||||
- **验证规则**: `validation.min`, `validation.max`, `validation.message`
|
||||
- **选项配置**: `options` (用于选择类型字段)
|
||||
- **特殊配置**: `config.maxImages`, `config.minValue`, `config.showArrow` 等
|
||||
|
||||
## 开发者说明
|
||||
|
||||
本项目采用最简单有效、优雅的方式实现,避免代码冗余,遵循以下原则:
|
||||
|
||||
1. **组件化设计** - 每个功能模块独立封装
|
||||
2. **配置驱动** - 通过JSON配置实现灵活的表单生成 ⭐
|
||||
3. **类型安全** - 完整的TypeScript类型定义
|
||||
4. **用户体验** - 流畅的交互和反馈
|
||||
5. **代码质量** - 简洁、可维护的代码结构
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
24
babel.config.js
Normal file
24
babel.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// babel-preset-taro 更多选项和默认值:
|
||||
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
|
||||
module.exports = {
|
||||
presets: [
|
||||
['taro',
|
||||
{
|
||||
framework: 'react',
|
||||
ts: 'true',
|
||||
compiler: 'webpack5',
|
||||
}]
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
"import",
|
||||
{
|
||||
"libraryName": "@nutui/nutui-react-taro",
|
||||
"libraryDirectory": "dist/esm",
|
||||
"style": 'css',
|
||||
"camel2DashComponentName": false
|
||||
},
|
||||
'nutui-react-taro'
|
||||
]
|
||||
]
|
||||
}
|
||||
23
config/dev.ts
Normal file
23
config/dev.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { UserConfigExport } from "@tarojs/cli";
|
||||
export default {
|
||||
logger: {
|
||||
quiet: false,
|
||||
stats: true
|
||||
},
|
||||
mini: {},
|
||||
h5: {},
|
||||
// 添加这个配置来显示完整错误信息
|
||||
compiler: {
|
||||
type: 'webpack5',
|
||||
prebundle: {
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
enable: false // 禁用缓存确保重新编译
|
||||
},
|
||||
// 开发环境不压缩代码
|
||||
terser: {
|
||||
enable: false
|
||||
}
|
||||
} satisfies UserConfigExport<'webpack5'>
|
||||
104
config/index.ts
Normal file
104
config/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
|
||||
import devConfig from './dev'
|
||||
import prodConfig from './prod'
|
||||
import vitePluginImp from 'vite-plugin-imp'
|
||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||
export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
|
||||
const baseConfig: UserConfigExport<'webpack5'> = {
|
||||
projectName: 'playBallTogether',
|
||||
date: '2025-8-9',
|
||||
designWidth: 375,
|
||||
deviceRatio: {
|
||||
640: 2.34 / 2,
|
||||
750: 1,
|
||||
375: 2,
|
||||
828: 1.81 / 2
|
||||
},
|
||||
sourceRoot: 'src',
|
||||
outputRoot: 'dist',
|
||||
plugins: ['@tarojs/plugin-html'],
|
||||
defineConstants: {
|
||||
},
|
||||
copy: {
|
||||
patterns: [
|
||||
],
|
||||
options: {
|
||||
}
|
||||
},
|
||||
framework: 'react',
|
||||
compiler: {
|
||||
|
||||
type: 'webpack5',
|
||||
prebundle: {
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
|
||||
},
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: {
|
||||
enable: true,
|
||||
config: {
|
||||
selectorBlackList: ['nut-']
|
||||
}
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
|
||||
}
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
staticDirectory: 'static',
|
||||
output: {
|
||||
filename: 'js/[name].[hash:8].js',
|
||||
chunkFilename: 'js/[name].[chunkhash:8].js'
|
||||
},
|
||||
miniCssExtractPluginOption: {
|
||||
ignoreOrder: true,
|
||||
filename: 'css/[name].[hash].css',
|
||||
chunkFilename: 'css/[name].[chunkhash].css'
|
||||
},
|
||||
postcss: {
|
||||
autoprefixer: {
|
||||
enable: true,
|
||||
config: {}
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
|
||||
}
|
||||
},
|
||||
rn: {
|
||||
appName: 'taroDemo',
|
||||
postcss: {
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 本地开发构建配置(不混淆压缩)
|
||||
return merge({}, baseConfig, devConfig)
|
||||
}
|
||||
// 生产构建配置(默认开启压缩混淆等)
|
||||
return merge({}, baseConfig, prodConfig)
|
||||
})
|
||||
32
config/prod.ts
Normal file
32
config/prod.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { UserConfigExport } from "@tarojs/cli";
|
||||
export default {
|
||||
mini: {},
|
||||
h5: {
|
||||
/**
|
||||
* WebpackChain 插件配置
|
||||
* @docs https://github.com/neutrinojs/webpack-chain
|
||||
*/
|
||||
// webpackChain (chain) {
|
||||
// /**
|
||||
// * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
|
||||
// * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
|
||||
// */
|
||||
// chain.plugin('analyzer')
|
||||
// .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
|
||||
// /**
|
||||
// * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
|
||||
// * @docs https://github.com/chrisvfritz/prerender-spa-plugin
|
||||
// */
|
||||
// const path = require('path')
|
||||
// const Prerender = require('prerender-spa-plugin')
|
||||
// const staticDir = path.join(__dirname, '..', 'dist')
|
||||
// chain
|
||||
// .plugin('prerender')
|
||||
// .use(new Prerender({
|
||||
// staticDir,
|
||||
// routes: [ '/pages/index/index' ],
|
||||
// postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
|
||||
// }))
|
||||
// }
|
||||
}
|
||||
} satisfies UserConfigExport<'webpack5'>
|
||||
88
package.json
Normal file
88
package.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "playBallTogether",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "playBallTogether",
|
||||
"templateInfo": {
|
||||
"name": "react-NutUI",
|
||||
"typescript": true,
|
||||
"css": "Sass",
|
||||
"framework": "React"
|
||||
},
|
||||
"scripts": {
|
||||
"build:weapp": "taro build --type weapp",
|
||||
"build:swan": "taro build --type swan",
|
||||
"build:alipay": "taro build --type alipay",
|
||||
"build:tt": "taro build --type tt",
|
||||
"build:h5": "taro build --type h5",
|
||||
"build:rn": "taro build --type rn",
|
||||
"build:qq": "taro build --type qq",
|
||||
"build:jd": "taro build --type jd",
|
||||
"build:quickapp": "taro build --type quickapp",
|
||||
"dev:weapp": "npm run build:weapp -- --watch",
|
||||
"dev:swan": "npm run build:swan -- --watch",
|
||||
"dev:alipay": "npm run build:alipay -- --watch",
|
||||
"dev:tt": "npm run build:tt -- --watch",
|
||||
"dev:h5": "npm run build:h5 -- --watch",
|
||||
"dev:rn": "npm run build:rn -- --watch",
|
||||
"dev:qq": "npm run build:qq -- --watch",
|
||||
"dev:jd": "npm run build:jd -- --watch",
|
||||
"dev:quickapp": "npm run build:quickapp -- --watch"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 versions",
|
||||
"Android >= 4.1",
|
||||
"ios >= 8"
|
||||
],
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.5",
|
||||
"@nutui/nutui-react-taro": "^2.6.14",
|
||||
"@tarojs/components": "4.1.5",
|
||||
"@tarojs/helper": "4.1.5",
|
||||
"@tarojs/plugin-framework-react": "4.1.5",
|
||||
"@tarojs/plugin-html": "4.1.5",
|
||||
"@tarojs/plugin-platform-alipay": "4.1.5",
|
||||
"@tarojs/plugin-platform-h5": "4.1.5",
|
||||
"@tarojs/plugin-platform-jd": "4.1.5",
|
||||
"@tarojs/plugin-platform-qq": "4.1.5",
|
||||
"@tarojs/plugin-platform-swan": "4.1.5",
|
||||
"@tarojs/plugin-platform-tt": "4.1.5",
|
||||
"@tarojs/plugin-platform-weapp": "4.1.5",
|
||||
"@tarojs/react": "4.1.5",
|
||||
"@tarojs/runtime": "4.1.5",
|
||||
"@tarojs/shared": "4.1.5",
|
||||
"@tarojs/taro": "4.1.5",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.14.5",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
|
||||
"@tarojs/cli": "4.1.5",
|
||||
"@tarojs/taro-loader": "4.1.5",
|
||||
"@tarojs/webpack5-runner": "4.1.5",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/webpack-env": "^1.13.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"babel-plugin-import": "^1.13.8",
|
||||
"babel-preset-taro": "4.1.5",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-config-taro": "4.1.5",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"eslint-plugin-react": "^7.8.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"postcss": "^8.4.18",
|
||||
"react-refresh": "^0.11.0",
|
||||
"stylelint": "^14.4.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.1",
|
||||
"typescript": "^5.1.0",
|
||||
"webpack": "5.78.0"
|
||||
}
|
||||
}
|
||||
25
project.config.json
Normal file
25
project.config.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "playBallTogether",
|
||||
"description": "playBallTogether",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
"enhance": false,
|
||||
"compileHotReLoad": false,
|
||||
"postcss": false,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": false,
|
||||
"newFeature": true,
|
||||
"autoAudits": false,
|
||||
"coverView": true,
|
||||
"showShadowRootInWxmlPanel": false,
|
||||
"scopeDataCheck": false,
|
||||
"useCompilerModule": false
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorType": "wechat",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"condition": {}
|
||||
}
|
||||
13
project.tt.json
Normal file
13
project.tt.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "playBallTogether",
|
||||
"description": "playBallTogether",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
"postcss": false,
|
||||
"minified": false
|
||||
},
|
||||
"compileType": "miniprogram"
|
||||
}
|
||||
11
src/app.config.ts
Normal file
11
src/app.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index'
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: 'WeChat',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
})
|
||||
0
src/app.scss
Normal file
0
src/app.scss
Normal file
19
src/app.ts
Normal file
19
src/app.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useDidShow, useDidHide } from '@tarojs/taro'
|
||||
// 全局样式
|
||||
import './app.scss'
|
||||
|
||||
function App(props: any) {
|
||||
// 可以使用所有的 React Hooks
|
||||
useEffect(() => {})
|
||||
|
||||
// 对应 onShow
|
||||
useDidShow(() => {})
|
||||
|
||||
// 对应 onHide
|
||||
useDidHide(() => {})
|
||||
|
||||
return props.children
|
||||
}
|
||||
|
||||
export default App
|
||||
109
src/config/env.ts
Normal file
109
src/config/env.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
// 环境类型
|
||||
export type EnvType = 'development' | 'test' | 'production'
|
||||
|
||||
// 环境配置接口
|
||||
export interface EnvConfig {
|
||||
name: string
|
||||
apiBaseURL: string
|
||||
apiVersion: string
|
||||
timeout: number
|
||||
enableLog: boolean
|
||||
enableMock: boolean
|
||||
}
|
||||
|
||||
// 各环境配置
|
||||
const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
// 开发环境
|
||||
development: {
|
||||
name: '开发环境',
|
||||
apiBaseURL: 'https://dev-api.playballtogether.com',
|
||||
apiVersion: 'v1',
|
||||
timeout: 15000,
|
||||
enableLog: true,
|
||||
enableMock: true
|
||||
},
|
||||
|
||||
// 测试环境
|
||||
test: {
|
||||
name: '测试环境',
|
||||
apiBaseURL: 'https://test-api.playballtogether.com',
|
||||
apiVersion: 'v1',
|
||||
timeout: 12000,
|
||||
enableLog: true,
|
||||
enableMock: false
|
||||
},
|
||||
|
||||
// 生产环境
|
||||
production: {
|
||||
name: '生产环境',
|
||||
apiBaseURL: 'https://api.playballtogether.com',
|
||||
apiVersion: 'v1',
|
||||
timeout: 10000,
|
||||
enableLog: false,
|
||||
enableMock: false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境
|
||||
export const getCurrentEnv = (): EnvType => {
|
||||
// 在小程序环境中,使用默认逻辑判断环境
|
||||
// 可以根据实际需要配置不同的判断逻辑
|
||||
|
||||
// 可以根据实际部署情况添加更多判断逻辑
|
||||
// 比如通过 Taro.getEnv() 获取当前平台环境
|
||||
const currentEnv = Taro.getEnv()
|
||||
|
||||
// 在开发调试时,可以通过修改这里的逻辑来切换环境
|
||||
// 默认在小程序中使用生产环境配置
|
||||
if (currentEnv === Taro.ENV_TYPE.WEAPP) {
|
||||
// 微信小程序环境
|
||||
return 'production'
|
||||
}
|
||||
|
||||
// 默认返回开发环境(便于调试)
|
||||
return 'development'
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
export const getCurrentConfig = (): EnvConfig => {
|
||||
const env = getCurrentEnv()
|
||||
return envConfigs[env]
|
||||
}
|
||||
|
||||
// 获取指定环境配置
|
||||
export const getEnvConfig = (env: EnvType): EnvConfig => {
|
||||
return envConfigs[env]
|
||||
}
|
||||
|
||||
// 是否为开发环境
|
||||
export const isDevelopment = (): boolean => {
|
||||
return getCurrentEnv() === 'development'
|
||||
}
|
||||
|
||||
// 是否为生产环境
|
||||
export const isProduction = (): boolean => {
|
||||
return getCurrentEnv() === 'production'
|
||||
}
|
||||
|
||||
// 是否为测试环境
|
||||
export const isTest = (): boolean => {
|
||||
return getCurrentEnv() === 'test'
|
||||
}
|
||||
|
||||
// 环境配置调试信息
|
||||
export const getEnvInfo = () => {
|
||||
const config = getCurrentConfig()
|
||||
return {
|
||||
env: getCurrentEnv(),
|
||||
config,
|
||||
taroEnv: Taro.getEnv(),
|
||||
platform: Taro.getEnv() === Taro.ENV_TYPE.WEAPP ? '微信小程序' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.H5 ? 'H5' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.RN ? 'React Native' : '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出当前环境配置(方便直接使用)
|
||||
export default getCurrentConfig()
|
||||
17
src/index.html
Normal file
17
src/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-touch-fullscreen" content="yes">
|
||||
<meta name="format-detection" content="telephone=no,address=no">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="white">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
|
||||
<title>playBallTogether</title>
|
||||
<script><%= htmlWebpackPlugin.options.script %></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
3
src/pages/index/index.config.ts
Normal file
3
src/pages/index/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '首页'
|
||||
})
|
||||
261
src/pages/index/index.scss
Normal file
261
src/pages/index/index.scss
Normal file
@@ -0,0 +1,261 @@
|
||||
.index-page {
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
|
||||
.username {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.join-date {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global {
|
||||
.nut-cell {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 8px;
|
||||
border: none;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nut-cell__title {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.nut-cell__value {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.custom-button {
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
height: 48px;
|
||||
border: none;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
|
||||
&.primary-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
&.success-btn {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
&.warning-btn {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// 保留 NutUI 按钮样式(备用)
|
||||
:global {
|
||||
.nut-button {
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
height: 48px;
|
||||
border: none;
|
||||
|
||||
&--primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global {
|
||||
.nut-progress {
|
||||
.nut-progress-outer {
|
||||
background: #f0f0f0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.nut-progress-inner {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tips-section {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.tips-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tips-content {
|
||||
.tip-item {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.index-page {
|
||||
padding: 16px;
|
||||
|
||||
.page-header {
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card,
|
||||
.stats-section,
|
||||
.action-section,
|
||||
.loading-section,
|
||||
.tips-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
293
src/pages/index/index.tsx
Normal file
293
src/pages/index/index.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import { Cell, Avatar, Progress } from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
// 导入API服务
|
||||
import demoApi from '../../services/demoApi'
|
||||
import commonApi from '../../services/commonApi'
|
||||
import {
|
||||
useUserStats,
|
||||
useUserActions
|
||||
} from '../../store/userStore'
|
||||
import './index.scss'
|
||||
|
||||
function Index() {
|
||||
// 使用Zustand store
|
||||
const userStats = useUserStats()
|
||||
const { incrementRequestCount, resetUserStats } = useUserActions()
|
||||
|
||||
// 本地状态管理
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [userProfile, setUserProfile] = useState<any>(null)
|
||||
const [interests, setInterests] = useState<string[]>([])
|
||||
|
||||
// 页面加载时获取数据
|
||||
useEffect(() => {
|
||||
initializeData()
|
||||
}, [])
|
||||
|
||||
// 初始化数据
|
||||
const initializeData = async () => {
|
||||
try {
|
||||
// 获取推荐的兴趣爱好
|
||||
const interestsRes = await demoApi.getRecommendedInterests()
|
||||
if (interestsRes.success) {
|
||||
setInterests(interestsRes.data || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('获取初始数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取用户信息 API 请求
|
||||
const handleGetUserProfile = async () => {
|
||||
console.log('获取用户信息...');
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await demoApi.getUserProfile()
|
||||
|
||||
if (response.success) {
|
||||
setUserProfile(response.data)
|
||||
incrementRequestCount()
|
||||
|
||||
Taro.showToast({
|
||||
title: '获取用户信息成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
console.log('用户信息:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取失败,使用模拟数据',
|
||||
icon: 'none'
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
setUserProfile({
|
||||
id: '123',
|
||||
nickname: '网球爱好者',
|
||||
avatar: '',
|
||||
gender: 'male',
|
||||
interests: interests.slice(0, 3)
|
||||
})
|
||||
incrementRequestCount()
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 提交统计数据 API 请求
|
||||
const handleSubmitStats = async () => {
|
||||
console.log('提交统计数据...');
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await commonApi.submitForm('userStats', [
|
||||
{
|
||||
type: 'userStats',
|
||||
data: {
|
||||
requestCount: userStats.requestCount,
|
||||
matchesCreated: userStats.matchesCreated,
|
||||
matchesJoined: userStats.matchesJoined,
|
||||
lastActiveTime: userStats.lastActiveTime,
|
||||
userId: userProfile?.id || 'guest'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
if (response.success) {
|
||||
incrementRequestCount()
|
||||
|
||||
Taro.showToast({
|
||||
title: '统计数据提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
console.log('提交结果:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交统计数据失败:', error)
|
||||
incrementRequestCount() // 即使失败也计数,用于演示
|
||||
|
||||
Taro.showToast({
|
||||
title: '网络模拟提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 提交反馈 API 请求
|
||||
const handleSubmitFeedback = async () => {
|
||||
console.log('提交用户反馈...');
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await demoApi.submitFeedback({
|
||||
matchId: 'demo_match_' + Date.now(),
|
||||
rating: 5,
|
||||
recommend: 'yes',
|
||||
aspects: ['场地环境', '服务质量', '价格合理'],
|
||||
comments: `用户反馈 - 请求次数: ${userStats.requestCount + 1},体验良好!`
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
incrementRequestCount()
|
||||
|
||||
Taro.showToast({
|
||||
title: '反馈提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
console.log('反馈结果:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交反馈失败:', error)
|
||||
incrementRequestCount() // 即使失败也计数,用于演示
|
||||
|
||||
Taro.showToast({
|
||||
title: '网络模拟提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置所有数据
|
||||
const handleResetAllData = () => {
|
||||
console.log('重置所有数据...');
|
||||
resetUserStats()
|
||||
setUserProfile(null)
|
||||
|
||||
Taro.showToast({
|
||||
title: '数据已重置',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='index-page'>
|
||||
{/* 页面标题 */}
|
||||
<View className='page-header'>
|
||||
<Text className='page-title'>API 请求演示</Text>
|
||||
<Text className='page-subtitle'>真实网络请求功能测试</Text>
|
||||
</View>
|
||||
|
||||
{/* 用户信息卡片 */}
|
||||
<View className='user-card'>
|
||||
<View className='user-header'>
|
||||
<Avatar
|
||||
size="large"
|
||||
src={userProfile?.avatar || ''}
|
||||
style={{ backgroundColor: '#fa2c19' }}
|
||||
>
|
||||
{userProfile?.nickname?.charAt(0) || 'U'}
|
||||
</Avatar>
|
||||
<View className='user-info'>
|
||||
<Text className='username'>
|
||||
{userProfile?.nickname || '点击获取用户信息'}
|
||||
</Text>
|
||||
<Text className='user-level'>
|
||||
性别: {userProfile?.gender === 'male' ? '男' : userProfile?.gender === 'female' ? '女' : '未知'}
|
||||
</Text>
|
||||
<Text className='join-date'>
|
||||
兴趣: {userProfile?.interests?.join(', ') || '暂无'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<View className='stats-section'>
|
||||
<Text className='section-title'>📊 API 请求统计</Text>
|
||||
|
||||
<Cell title="API 请求次数" extra={userStats.requestCount} />
|
||||
<Cell title="创建的比赛" extra={userStats.matchesCreated} />
|
||||
<Cell title="参加的比赛" extra={userStats.matchesJoined} />
|
||||
<Cell
|
||||
title="最后活跃时间"
|
||||
extra={new Date(userStats.lastActiveTime).toLocaleTimeString()}
|
||||
/>
|
||||
{interests.length > 0 && (
|
||||
<Cell
|
||||
title="推荐兴趣"
|
||||
extra={interests.slice(0, 2).join(', ')}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* API 请求按钮区域 */}
|
||||
<View className='action-section'>
|
||||
<Text className='section-title'>🚀 API 请求功能</Text>
|
||||
|
||||
<View className='button-group'>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleGetUserProfile}
|
||||
disabled={loading}
|
||||
className="custom-button primary-btn"
|
||||
>
|
||||
{loading ? '请求中...' : '获取用户信息'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
loading={loading}
|
||||
onClick={handleSubmitStats}
|
||||
disabled={loading}
|
||||
className="custom-button success-btn"
|
||||
>
|
||||
{loading ? '提交中...' : '提交统计数据'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
loading={loading}
|
||||
onClick={handleSubmitFeedback}
|
||||
disabled={loading}
|
||||
className="custom-button warning-btn"
|
||||
>
|
||||
{loading ? '提交中...' : '提交用户反馈'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="warn"
|
||||
onClick={handleResetAllData}
|
||||
disabled={loading}
|
||||
className="custom-button warning-btn"
|
||||
>
|
||||
重置所有数据
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 实时进度显示 */}
|
||||
{loading && (
|
||||
<View className='loading-section'>
|
||||
<Text className='loading-text'>正在发送 API 请求...</Text>
|
||||
<Progress percent={100} animated />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 提示信息 */}
|
||||
<View className='tips-section'>
|
||||
<Text className='tips-title'>💡 API 功能说明</Text>
|
||||
<View className='tips-content'>
|
||||
<Text className='tip-item'>• "获取用户信息" - 调用用户资料 API</Text>
|
||||
<Text className='tip-item'>• "提交统计数据" - 发送统计到服务器</Text>
|
||||
<Text className='tip-item'>• "提交用户反馈" - 发送用户评价数据</Text>
|
||||
<Text className='tip-item'>• 所有请求都会增加 API 计数统计</Text>
|
||||
<Text className='tip-item'>• 请求失败时会自动使用模拟数据</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
||||
176
src/services/authService.ts
Normal file
176
src/services/authService.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import httpService from './httpService'
|
||||
import tokenManager from '../utils/tokenManager'
|
||||
import type { ApiResponse } from './httpService'
|
||||
|
||||
// 用户接口
|
||||
export interface User {
|
||||
id: string
|
||||
nickname: string
|
||||
avatar?: string
|
||||
phone?: string
|
||||
email?: string
|
||||
status: 'active' | 'inactive' | 'banned'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 登录响应接口
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
refreshToken: string
|
||||
user: User
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
// 认证服务类
|
||||
class AuthService {
|
||||
// 用户登录
|
||||
async login(data: { phone: string; code: string }): Promise<ApiResponse<LoginResponse>> {
|
||||
const response = await httpService.post('/auth/login', data, {
|
||||
needAuth: false,
|
||||
showLoading: true,
|
||||
loadingText: '登录中...'
|
||||
})
|
||||
|
||||
// 登录成功后保存token
|
||||
if (response.success && response.data) {
|
||||
tokenManager.setToken({
|
||||
accessToken: response.data.token,
|
||||
refreshToken: response.data.refreshToken,
|
||||
expiresAt: response.data.expiresAt
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
async sendSmsCode(phone: string): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return httpService.post('/auth/sms-code', { phone }, {
|
||||
needAuth: false,
|
||||
showLoading: true,
|
||||
loadingText: '发送中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新token
|
||||
async refreshToken(): Promise<ApiResponse<{ token: string; expiresAt: number }>> {
|
||||
const refreshToken = tokenManager.getRefreshToken()
|
||||
if (!refreshToken) {
|
||||
throw new Error('没有刷新令牌')
|
||||
}
|
||||
|
||||
const response = await httpService.post('/auth/refresh', {
|
||||
refreshToken
|
||||
}, {
|
||||
needAuth: false
|
||||
})
|
||||
|
||||
// 更新token
|
||||
if (response.success && response.data) {
|
||||
tokenManager.setToken({
|
||||
accessToken: response.data.token,
|
||||
expiresAt: response.data.expiresAt
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// 用户登出
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
// 调用登出接口
|
||||
await httpService.post('/auth/logout', {}, {
|
||||
showLoading: true,
|
||||
loadingText: '退出中...'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('登出接口调用失败:', error)
|
||||
} finally {
|
||||
// 清除本地token
|
||||
tokenManager.clearTokens()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
async getCurrentUser(): Promise<ApiResponse<User>> {
|
||||
return httpService.get('/auth/me')
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
async updateProfile(data: Partial<User>): Promise<ApiResponse<User>> {
|
||||
return httpService.put('/auth/profile', data, {
|
||||
showLoading: true,
|
||||
loadingText: '更新中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
async changePassword(data: {
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
}): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return httpService.put('/auth/password', data, {
|
||||
showLoading: true,
|
||||
loadingText: '修改中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 绑定手机号
|
||||
async bindPhone(data: {
|
||||
phone: string
|
||||
code: string
|
||||
}): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return httpService.post('/auth/bind-phone', data, {
|
||||
showLoading: true,
|
||||
loadingText: '绑定中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 解绑手机号
|
||||
async unbindPhone(data: {
|
||||
code: string
|
||||
}): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return httpService.post('/auth/unbind-phone', data, {
|
||||
showLoading: true,
|
||||
loadingText: '解绑中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
isLoggedIn(): boolean {
|
||||
return tokenManager.hasValidToken()
|
||||
}
|
||||
|
||||
// 获取当前用户token
|
||||
getToken(): string | null {
|
||||
return tokenManager.getAccessToken()
|
||||
}
|
||||
|
||||
// 清除登录状态
|
||||
clearAuth(): void {
|
||||
tokenManager.clearTokens()
|
||||
}
|
||||
|
||||
// 账号注销
|
||||
async deleteAccount(data: {
|
||||
password: string
|
||||
code: string
|
||||
}): Promise<ApiResponse<{ success: boolean }>> {
|
||||
const response = await httpService.delete('/auth/account', data, {
|
||||
showLoading: true,
|
||||
loadingText: '注销中...'
|
||||
})
|
||||
|
||||
// 注销成功后清除本地数据
|
||||
if (response.success) {
|
||||
this.clearAuth()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
// 导出认证服务实例
|
||||
export default new AuthService()
|
||||
74
src/services/commonApi.ts
Normal file
74
src/services/commonApi.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import httpService from './httpService'
|
||||
import type { ApiResponse } from './httpService'
|
||||
|
||||
// 图片上传响应
|
||||
export interface UploadResponse {
|
||||
url: string
|
||||
filename: string
|
||||
size: number
|
||||
}
|
||||
|
||||
// 通用API服务类
|
||||
class CommonApiService {
|
||||
// ==================== 文件上传接口 ====================
|
||||
|
||||
// 上传单个图片
|
||||
async uploadImage(filePath: string): Promise<ApiResponse<UploadResponse>> {
|
||||
const uploadTask = Taro.uploadFile({
|
||||
url: `${httpService['baseURL']}/upload/image`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
...httpService['buildHeaders']({ url: '', needAuth: true })
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uploadTask.then(response => {
|
||||
try {
|
||||
const data = JSON.parse(response.data)
|
||||
resolve(data)
|
||||
} catch (error) {
|
||||
reject(new Error('上传响应解析失败'))
|
||||
}
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
// 批量上传图片
|
||||
async uploadImages(filePaths: string[]): Promise<ApiResponse<UploadResponse[]>> {
|
||||
try {
|
||||
Taro.showLoading({ title: '上传图片中...', mask: true })
|
||||
|
||||
const uploadPromises = filePaths.map(filePath => this.uploadImage(filePath))
|
||||
const results = await Promise.all(uploadPromises)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: '上传成功',
|
||||
data: results.map(result => result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==================== 用户信息接口(基础版本) ====================
|
||||
|
||||
// 更新用户信息
|
||||
async updateUserProfile(data: any): Promise<ApiResponse<any>> {
|
||||
return httpService.put('/user/profile', data, {
|
||||
showLoading: true,
|
||||
loadingText: '保存中...'
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 导出通用API服务实例
|
||||
export default new CommonApiService()
|
||||
134
src/services/demoApi.ts
Normal file
134
src/services/demoApi.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import httpService from './httpService'
|
||||
import type { ApiResponse } from './httpService'
|
||||
|
||||
// 用户信息接口
|
||||
export interface UserProfile {
|
||||
id: string
|
||||
nickname: string
|
||||
avatar?: string
|
||||
age?: number
|
||||
gender: 'male' | 'female'
|
||||
interests: string[]
|
||||
acceptNotification: boolean
|
||||
}
|
||||
|
||||
// 反馈评价接口
|
||||
export interface Feedback {
|
||||
id: string
|
||||
matchId: string
|
||||
userId: string
|
||||
photos: string[]
|
||||
rating: number
|
||||
recommend: 'yes' | 'no' | 'neutral'
|
||||
aspects: string[]
|
||||
comments: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// DynamicFormDemo页面API服务类
|
||||
class DemoApiService {
|
||||
// ==================== 用户信息相关接口 ====================
|
||||
|
||||
// 获取用户信息
|
||||
async getUserProfile(): Promise<ApiResponse<UserProfile>> {
|
||||
return httpService.get('/user/profile')
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
async updateUserProfile(data: Partial<UserProfile>): Promise<ApiResponse<UserProfile>> {
|
||||
return httpService.put('/user/profile', data, {
|
||||
showLoading: true,
|
||||
loadingText: '保存中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 上传头像
|
||||
async uploadAvatar(filePath: string): Promise<ApiResponse<{ url: string }>> {
|
||||
return httpService.post('/user/avatar', { filePath }, {
|
||||
showLoading: true,
|
||||
loadingText: '上传头像中...'
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 反馈评价相关接口 ====================
|
||||
|
||||
// 提交活动评价
|
||||
async submitFeedback(data: {
|
||||
matchId: string
|
||||
photos?: string[]
|
||||
rating: number
|
||||
recommend: 'yes' | 'no' | 'neutral'
|
||||
aspects: string[]
|
||||
comments: string
|
||||
}): Promise<ApiResponse<Feedback>> {
|
||||
return httpService.post('/feedback', data, {
|
||||
showLoading: true,
|
||||
loadingText: '提交评价中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取我的评价列表
|
||||
async getMyFeedbacks(params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
}): Promise<ApiResponse<{ list: Feedback[]; total: number }>> {
|
||||
return httpService.get('/feedback/my', params)
|
||||
}
|
||||
|
||||
// 获取活动的所有评价
|
||||
async getMatchFeedbacks(matchId: string, params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
}): Promise<ApiResponse<{ list: Feedback[]; total: number }>> {
|
||||
return httpService.get(`/feedback/match/${matchId}`, params)
|
||||
}
|
||||
|
||||
// ==================== 通用表单提交接口 ====================
|
||||
|
||||
// 提交表单数据(通用接口)
|
||||
async submitForm(formType: string, formData: any[]): Promise<ApiResponse<any>> {
|
||||
return httpService.post('/forms/submit', {
|
||||
type: formType,
|
||||
data: formData
|
||||
}, {
|
||||
showLoading: true,
|
||||
loadingText: '提交中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存表单草稿
|
||||
async saveFormDraft(formType: string, formData: any[]): Promise<ApiResponse<{ id: string }>> {
|
||||
return httpService.post('/forms/draft', {
|
||||
type: formType,
|
||||
data: formData
|
||||
}, {
|
||||
showLoading: true,
|
||||
loadingText: '保存中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取表单草稿
|
||||
async getFormDrafts(formType: string): Promise<ApiResponse<{ id: string; data: any[]; createdAt: string }[]>> {
|
||||
return httpService.get('/forms/drafts', { type: formType })
|
||||
}
|
||||
|
||||
// 删除表单草稿
|
||||
async deleteFormDraft(id: string): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return httpService.delete(`/forms/draft/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 兴趣爱好相关接口 ====================
|
||||
|
||||
// 获取兴趣爱好选项
|
||||
async getInterestOptions(): Promise<ApiResponse<{ label: string; value: string }[]>> {
|
||||
return httpService.get('/interests')
|
||||
}
|
||||
|
||||
// 获取推荐的兴趣爱好
|
||||
async getRecommendedInterests(): Promise<ApiResponse<string[]>> {
|
||||
return httpService.get('/interests/recommended')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出API服务实例
|
||||
export default new DemoApiService()
|
||||
326
src/services/httpService.ts
Normal file
326
src/services/httpService.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import tokenManager from '../utils/tokenManager'
|
||||
import envConfig, { isDevelopment, getEnvInfo } from '../config/env'
|
||||
|
||||
// 请求方法类型
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||
|
||||
// 请求配置接口
|
||||
export interface RequestConfig {
|
||||
url: string
|
||||
method?: HttpMethod
|
||||
data?: any
|
||||
params?: Record<string, any>
|
||||
headers?: Record<string, string>
|
||||
needAuth?: boolean // 是否需要token认证
|
||||
showLoading?: boolean // 是否显示加载提示
|
||||
loadingText?: string // 加载提示文本
|
||||
}
|
||||
|
||||
// 响应数据接口
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
data: T
|
||||
message: string
|
||||
success: boolean
|
||||
}
|
||||
|
||||
// HTTP状态码常量
|
||||
export const HTTP_STATUS = {
|
||||
SUCCESS: 200,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
SERVER_ERROR: 500
|
||||
}
|
||||
|
||||
class HttpService {
|
||||
private baseURL: string
|
||||
private timeout: number
|
||||
private enableLog: boolean
|
||||
|
||||
constructor() {
|
||||
// 使用环境配置
|
||||
this.baseURL = `${envConfig.apiBaseURL}/api/${envConfig.apiVersion}`
|
||||
this.timeout = envConfig.timeout
|
||||
this.enableLog = envConfig.enableLog
|
||||
|
||||
// 在开发环境下输出配置信息
|
||||
if (isDevelopment()) {
|
||||
console.log('🌍 HTTP服务初始化:', {
|
||||
baseURL: this.baseURL,
|
||||
timeout: this.timeout,
|
||||
envInfo: getEnvInfo()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 构建完整URL
|
||||
private buildUrl(url: string, params?: Record<string, any>): string {
|
||||
const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`
|
||||
|
||||
if (params) {
|
||||
const searchParams = new URLSearchParams()
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, String(value))
|
||||
}
|
||||
})
|
||||
const queryString = searchParams.toString()
|
||||
return queryString ? `${fullUrl}?${queryString}` : fullUrl
|
||||
}
|
||||
|
||||
return fullUrl
|
||||
}
|
||||
|
||||
// 构建请求头
|
||||
private buildHeaders(config: RequestConfig): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Environment': envConfig.name, // 添加环境标识
|
||||
...config.headers
|
||||
}
|
||||
|
||||
// 如果需要认证,添加token
|
||||
if (config.needAuth !== false) {
|
||||
const authHeader = tokenManager.getAuthHeader()
|
||||
Object.assign(headers, authHeader)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// 日志输出
|
||||
private log(level: 'info' | 'warn' | 'error', message: string, data?: any) {
|
||||
if (!this.enableLog) return
|
||||
|
||||
const logMethod = console[level] || console.log
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
|
||||
if (data) {
|
||||
logMethod(`[${timestamp}] HTTP ${level.toUpperCase()}: ${message}`, data)
|
||||
} else {
|
||||
logMethod(`[${timestamp}] HTTP ${level.toUpperCase()}: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
private handleResponse<T>(response: any): Promise<ApiResponse<T>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { statusCode, data } = response
|
||||
|
||||
this.log('info', `响应状态码: ${statusCode}`, { data })
|
||||
|
||||
// HTTP状态码检查
|
||||
if (statusCode !== HTTP_STATUS.SUCCESS) {
|
||||
this.handleHttpError(statusCode)
|
||||
reject(new Error(`HTTP Error: ${statusCode}`))
|
||||
return
|
||||
}
|
||||
|
||||
// 业务状态码检查
|
||||
if (data && typeof data === 'object') {
|
||||
if (data.success === false || (data.code && data.code !== 0 && data.code !== 200)) {
|
||||
this.handleBusinessError(data)
|
||||
reject(new Error(data.message || '请求失败'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resolve(data)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理HTTP错误
|
||||
private handleHttpError(statusCode: number): void {
|
||||
let message = '网络请求失败'
|
||||
|
||||
switch (statusCode) {
|
||||
case HTTP_STATUS.UNAUTHORIZED:
|
||||
message = '登录已过期,请重新登录'
|
||||
tokenManager.clearTokens()
|
||||
// 可以在这里跳转到登录页面
|
||||
break
|
||||
case HTTP_STATUS.FORBIDDEN:
|
||||
message = '没有权限访问该资源'
|
||||
break
|
||||
case HTTP_STATUS.NOT_FOUND:
|
||||
message = '请求的资源不存在'
|
||||
break
|
||||
case HTTP_STATUS.SERVER_ERROR:
|
||||
message = '服务器内部错误'
|
||||
break
|
||||
default:
|
||||
message = `请求失败 (${statusCode})`
|
||||
}
|
||||
|
||||
this.log('error', `HTTP错误 ${statusCode}: ${message}`)
|
||||
|
||||
Taro.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
// 处理业务错误
|
||||
private handleBusinessError(data: any): void {
|
||||
const message = data.message || '操作失败'
|
||||
|
||||
this.log('error', `业务错误: ${message}`, data)
|
||||
|
||||
Taro.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
// 统一请求方法
|
||||
async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
data,
|
||||
params,
|
||||
showLoading = false,
|
||||
loadingText = '请求中...'
|
||||
} = config
|
||||
|
||||
const fullUrl = this.buildUrl(url, method === 'GET' ? params : undefined)
|
||||
|
||||
this.log('info', `发起请求: ${method} ${fullUrl}`, {
|
||||
data: method !== 'GET' ? data : undefined,
|
||||
params: method === 'GET' ? params : undefined
|
||||
})
|
||||
// 检查token(如果需要认证)
|
||||
if (config.needAuth === true && !tokenManager.hasValidToken()) {
|
||||
Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
throw new Error('Token无效或已过期')
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
if (showLoading) {
|
||||
Taro.showLoading({
|
||||
title: loadingText,
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const requestConfig = {
|
||||
url: fullUrl,
|
||||
method: method,
|
||||
data: method !== 'GET' ? data : undefined,
|
||||
header: this.buildHeaders(config),
|
||||
timeout: this.timeout
|
||||
}
|
||||
|
||||
const response = await Taro.request(requestConfig)
|
||||
return this.handleResponse<T>(response)
|
||||
} catch (error) {
|
||||
this.log('error', '请求失败', error)
|
||||
|
||||
// 在模拟模式下返回模拟数据
|
||||
if (envConfig.enableMock && isDevelopment()) {
|
||||
this.log('info', '使用模拟数据')
|
||||
return this.getMockResponse<T>(url, method)
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '网络连接失败',
|
||||
icon: 'none'
|
||||
})
|
||||
|
||||
throw error
|
||||
} finally {
|
||||
// 隐藏加载提示
|
||||
if (showLoading) {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模拟数据
|
||||
private getMockResponse<T>(url: string, method: string): ApiResponse<T> {
|
||||
this.log('info', `返回模拟数据: ${method} ${url}`)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: '模拟请求成功',
|
||||
data: {
|
||||
mockData: true,
|
||||
url,
|
||||
method,
|
||||
timestamp: new Date().toISOString()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
|
||||
// GET请求
|
||||
get<T = any>(url: string, params?: Record<string, any>, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// POST请求
|
||||
post<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
put<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
delete<T = any>(url: string, params?: Record<string, any>, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// PATCH请求
|
||||
patch<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'PATCH',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前环境信息
|
||||
getEnvInfo() {
|
||||
return {
|
||||
baseURL: this.baseURL,
|
||||
timeout: this.timeout,
|
||||
enableLog: this.enableLog,
|
||||
...getEnvInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出HTTP服务实例
|
||||
export default new HttpService()
|
||||
199
src/store/README.md
Normal file
199
src/store/README.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 用户状态管理使用说明
|
||||
|
||||
本项目使用 Zustand 进行用户状态管理,提供简洁、高效的用户信息管理解决方案。
|
||||
|
||||
## 主要特性
|
||||
|
||||
- 🚀 简单易用:无需 Provider 包装,直接使用 hooks
|
||||
- 🔄 自动持久化:状态变化自动保存到本地存储
|
||||
- 🎯 TypeScript 友好:完整的类型支持
|
||||
- 👥 团队协作:清晰的状态结构,易于维护
|
||||
- 📊 实时统计:自动记录用户活动和 API 请求
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 1. 获取用户信息
|
||||
|
||||
```tsx
|
||||
import { useUserInfo, useUserStats, useIsLoggedIn } from '../store/userStore'
|
||||
|
||||
function MyComponent() {
|
||||
// 获取用户信息
|
||||
const userInfo = useUserInfo()
|
||||
|
||||
// 获取用户统计数据
|
||||
const userStats = useUserStats()
|
||||
|
||||
// 获取登录状态
|
||||
const isLoggedIn = useIsLoggedIn()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>欢迎 {userInfo?.username}!</h1>
|
||||
<p>API 请求次数: {userStats.requestCount}</p>
|
||||
<p>登录状态: {isLoggedIn ? '已登录' : '未登录'}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 修改用户信息
|
||||
|
||||
```tsx
|
||||
import { useUserActions } from '../store/userStore'
|
||||
|
||||
function MyComponent() {
|
||||
const {
|
||||
updateUserInfo,
|
||||
incrementRequestCount,
|
||||
incrementMatchesCreated
|
||||
} = useUserActions()
|
||||
|
||||
const handleUpdateUsername = () => {
|
||||
updateUserInfo('username', '新用户名')
|
||||
}
|
||||
|
||||
const handleApiRequest = () => {
|
||||
incrementRequestCount() // API 请求计数 +1
|
||||
}
|
||||
|
||||
const handleCreateMatch = () => {
|
||||
incrementMatchesCreated() // 创建比赛计数 +1
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleUpdateUsername}>更新用户名</button>
|
||||
<button onClick={handleApiRequest}>发送 API 请求</button>
|
||||
<button onClick={handleCreateMatch}>创建比赛</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 模拟 API 请求示例
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react'
|
||||
import { useUserActions } from '../store/userStore'
|
||||
|
||||
function MyComponent() {
|
||||
const { incrementRequestCount } = useUserActions()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSendRequest = async () => {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 更新请求计数
|
||||
incrementRequestCount()
|
||||
|
||||
console.log('请求成功!')
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleSendRequest} disabled={loading}>
|
||||
{loading ? '发送中...' : '发送请求'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 数据结构
|
||||
|
||||
### UserInfo 接口
|
||||
|
||||
```typescript
|
||||
interface UserInfo {
|
||||
id: string // 用户唯一标识
|
||||
username: string // 用户名
|
||||
avatar: string // 头像 URL
|
||||
phone: string // 手机号
|
||||
email: string // 邮箱
|
||||
location: string // 位置
|
||||
ntrpLevel: number // NTRP 等级
|
||||
joinDate: string // 加入日期
|
||||
totalMatches: number // 总比赛数
|
||||
winRate: number // 胜率
|
||||
}
|
||||
```
|
||||
|
||||
### UserStats 接口
|
||||
|
||||
```typescript
|
||||
interface UserStats {
|
||||
requestCount: number // API 请求次数
|
||||
matchesCreated: number // 创建的比赛数量
|
||||
matchesJoined: number // 参加的比赛数量
|
||||
lastActiveTime: string // 最后活跃时间
|
||||
}
|
||||
```
|
||||
|
||||
## 可用的 Actions
|
||||
|
||||
| Action | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| `setUserInfo(userInfo)` | userInfo: UserInfo | 设置用户信息 |
|
||||
| `updateUserInfo(field, value)` | field: keyof UserInfo, value: any | 更新用户信息字段 |
|
||||
| `clearUserInfo()` | 无 | 清除用户信息(登出) |
|
||||
| `incrementRequestCount()` | 无 | API 请求计数 +1 |
|
||||
| `incrementMatchesCreated()` | 无 | 创建比赛计数 +1 |
|
||||
| `incrementMatchesJoined()` | 无 | 参加比赛计数 +1 |
|
||||
| `updateLastActiveTime()` | 无 | 更新最后活跃时间 |
|
||||
| `resetUserStats()` | 无 | 重置用户统计数据 |
|
||||
| `setLoading(loading)` | loading: boolean | 设置加载状态 |
|
||||
|
||||
## 本地存储
|
||||
|
||||
- 用户信息保存键值:`user_info`
|
||||
- 用户统计保存键值:`user_stats`
|
||||
- 所有状态变化会自动保存到本地存储
|
||||
- 应用启动时自动加载保存的数据
|
||||
|
||||
## 便捷 Hooks
|
||||
|
||||
| Hook | 返回值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `useUserInfo()` | UserInfo \| null | 获取用户信息 |
|
||||
| `useUserStats()` | UserStats | 获取用户统计 |
|
||||
| `useIsLoggedIn()` | boolean | 获取登录状态 |
|
||||
| `useUserActions()` | Actions | 获取所有操作函数 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **优先使用专用 hooks**:使用 `useUserInfo()` 和 `useUserActions()` 而不是完整的 `useUserStore()`
|
||||
2. **按需获取**:只获取组件需要的状态,避免不必要的重渲染
|
||||
3. **类型安全**:充分利用 TypeScript 类型检查,避免字段名错误
|
||||
4. **数据纯净**:状态管理只处理数据,不包含业务逻辑
|
||||
5. **及时更新**:用户操作后及时更新相关统计数据
|
||||
|
||||
## 使用示例
|
||||
|
||||
查看 `src/pages/index/index.tsx` 获取完整的使用示例,包括:
|
||||
|
||||
- 用户信息展示
|
||||
- 统计数据实时更新
|
||||
- API 请求模拟
|
||||
- 加载状态管理
|
||||
- 数据持久化演示
|
||||
|
||||
## 扩展建议
|
||||
|
||||
如需添加新的用户状态或功能:
|
||||
|
||||
1. 在 `UserInfo` 或 `UserStats` 接口中添加新字段
|
||||
2. 在 store 的初始状态中设置默认值
|
||||
3. 添加相应的 action 函数
|
||||
4. 导出便捷的 hooks(如果需要)
|
||||
|
||||
这种设计让用户状态管理既简单又强大,非常适合团队协作开发。
|
||||
39
src/store/storage.ts
Normal file
39
src/store/storage.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
// 通用存储工具函数
|
||||
export const setStorage = (key: string, data: any) => {
|
||||
try {
|
||||
Taro.setStorageSync(key, JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.error('保存数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const getStorage = <T>(key: string): T | null => {
|
||||
try {
|
||||
const data = Taro.getStorageSync(key)
|
||||
if (data) {
|
||||
return JSON.parse(data)
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('读取数据失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const removeStorage = (key: string) => {
|
||||
try {
|
||||
Taro.removeStorageSync(key)
|
||||
} catch (error) {
|
||||
console.error('清除数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export const clearAllStorage = () => {
|
||||
try {
|
||||
Taro.clearStorageSync()
|
||||
} catch (error) {
|
||||
console.error('清除所有数据失败:', error)
|
||||
}
|
||||
}
|
||||
58
src/store/userStore.ts
Normal file
58
src/store/userStore.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
// 用户统计信息
|
||||
export interface UserStats {
|
||||
requestCount: number
|
||||
matchesCreated: number
|
||||
matchesJoined: number
|
||||
lastActiveTime: string
|
||||
}
|
||||
|
||||
// Store 状态接口
|
||||
interface UserState {
|
||||
userStats: UserStats
|
||||
incrementRequestCount: () => void
|
||||
resetUserStats: () => void
|
||||
}
|
||||
|
||||
// 创建适配 Zustand 4.x 的 store
|
||||
export const useUserStore = create<UserState>()((set) => ({
|
||||
// 初始状态
|
||||
userStats: {
|
||||
requestCount: 0,
|
||||
matchesCreated: 0,
|
||||
matchesJoined: 0,
|
||||
lastActiveTime: new Date().toISOString()
|
||||
},
|
||||
|
||||
// Actions
|
||||
incrementRequestCount: () => {
|
||||
console.log('store: incrementRequestCount 被调用')
|
||||
set((state) => ({
|
||||
userStats: {
|
||||
...state.userStats,
|
||||
requestCount: state.userStats.requestCount + 1,
|
||||
lastActiveTime: new Date().toISOString()
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
resetUserStats: () => {
|
||||
console.log('store: resetUserStats 被调用')
|
||||
set({
|
||||
userStats: {
|
||||
requestCount: 0,
|
||||
matchesCreated: 0,
|
||||
matchesJoined: 0,
|
||||
lastActiveTime: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// 简单的 hooks
|
||||
export const useUserStats = () => useUserStore((state) => state.userStats)
|
||||
export const useUserActions = () => useUserStore((state) => ({
|
||||
incrementRequestCount: state.incrementRequestCount,
|
||||
resetUserStats: state.resetUserStats
|
||||
}))
|
||||
102
src/utils/tokenManager.ts
Normal file
102
src/utils/tokenManager.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const TOKEN_KEY = 'access_token'
|
||||
const REFRESH_TOKEN_KEY = 'refresh_token'
|
||||
const TOKEN_EXPIRES_KEY = 'token_expires'
|
||||
|
||||
export interface TokenInfo {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
expiresAt?: number
|
||||
}
|
||||
|
||||
class TokenManager {
|
||||
// 获取访问令牌
|
||||
getAccessToken(): string | null {
|
||||
try {
|
||||
return Taro.getStorageSync(TOKEN_KEY)
|
||||
} catch (error) {
|
||||
console.error('获取访问令牌失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取刷新令牌
|
||||
getRefreshToken(): string | null {
|
||||
try {
|
||||
return Taro.getStorageSync(REFRESH_TOKEN_KEY)
|
||||
} catch (error) {
|
||||
console.error('获取刷新令牌失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取令牌过期时间
|
||||
getTokenExpires(): number | null {
|
||||
try {
|
||||
return Taro.getStorageSync(TOKEN_EXPIRES_KEY)
|
||||
} catch (error) {
|
||||
console.error('获取令牌过期时间失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 设置令牌信息
|
||||
setToken(tokenInfo: TokenInfo): void {
|
||||
try {
|
||||
Taro.setStorageSync(TOKEN_KEY, tokenInfo.accessToken)
|
||||
|
||||
if (tokenInfo.refreshToken) {
|
||||
Taro.setStorageSync(REFRESH_TOKEN_KEY, tokenInfo.refreshToken)
|
||||
}
|
||||
|
||||
if (tokenInfo.expiresAt) {
|
||||
Taro.setStorageSync(TOKEN_EXPIRES_KEY, tokenInfo.expiresAt)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置令牌失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除所有令牌
|
||||
clearTokens(): void {
|
||||
try {
|
||||
Taro.removeStorageSync(TOKEN_KEY)
|
||||
Taro.removeStorageSync(REFRESH_TOKEN_KEY)
|
||||
Taro.removeStorageSync(TOKEN_EXPIRES_KEY)
|
||||
} catch (error) {
|
||||
console.error('清除令牌失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查令牌是否过期
|
||||
isTokenExpired(): boolean {
|
||||
const expiresAt = this.getTokenExpires()
|
||||
if (!expiresAt) {
|
||||
return false // 如果没有过期时间,假设未过期
|
||||
}
|
||||
|
||||
return Date.now() >= expiresAt
|
||||
}
|
||||
|
||||
// 检查是否有有效令牌
|
||||
hasValidToken(): boolean {
|
||||
const token = this.getAccessToken()
|
||||
return !!token && !this.isTokenExpired()
|
||||
}
|
||||
|
||||
// 获取Authorization头
|
||||
getAuthHeader(): Record<string, string> {
|
||||
const token = this.getAccessToken()
|
||||
if (!token) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export default new TokenManager()
|
||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "lib",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": ["./src", "./types"],
|
||||
"compileOnSave": false
|
||||
}
|
||||
27
types/global.d.ts
vendored
Normal file
27
types/global.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types="@tarojs/taro" />
|
||||
|
||||
declare module '*.png';
|
||||
declare module '*.gif';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.svg';
|
||||
declare module '*.css';
|
||||
declare module '*.less';
|
||||
declare module '*.scss';
|
||||
declare module '*.sass';
|
||||
declare module '*.styl';
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/** NODE 内置环境变量, 会影响到最终构建生成产物 */
|
||||
NODE_ENV: 'development' | 'production',
|
||||
/** 当前构建的平台 */
|
||||
TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'
|
||||
/**
|
||||
* 当前构建的小程序 appid
|
||||
* @description 若不同环境有不同的小程序,可通过在 env 文件中配置环境变量`TARO_APP_ID`来方便快速切换 appid, 而不必手动去修改 dist/project.config.json 文件
|
||||
* @see https://taro-docs.jd.com/docs/next/env-mode-config#特殊环境变量-taro_app_id
|
||||
*/
|
||||
TARO_APP_ID: string
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user