feat: implement Redux store with slices for user, order, file, and printer management
- Added Redux store configuration in `src/store/index.ts`. - Created slices for user, order, file, and printer with respective reducers and actions. - Implemented custom hooks `useAppDispatch` and `useAppSelector` for typed Redux usage. - Defined types for file, order, printer, and user in `src/types`. - Added utility functions for formatting prices, file sizes, and dates in `src/utils`. - Created common styles and animations in `src/styles` for consistent UI design. - Integrated API request handling with token management in `src/utils/request.ts`. - Implemented local storage management for user token and info in `src/utils/storage.ts`.
This commit is contained in:
14
.eslintrc.js
14
.eslintrc.js
@@ -1,7 +1,7 @@
|
|||||||
{
|
module.exports = {
|
||||||
"extends": ["taro/react"],
|
extends: ['taro/react'],
|
||||||
"rules": {
|
rules: {
|
||||||
"react/jsx-uses-react": "off",
|
'react/jsx-uses-react': 'off',
|
||||||
"react/react-in-jsx-scope": "off"
|
'react/react-in-jsx-scope': 'off',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# 萝卜打印 - 微信小程序
|
||||||
|
|
||||||
|
付费打印服务微信小程序前端项目,基于 Taro + React + TypeScript。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 技术 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 框架 | Taro 4.x + React |
|
||||||
|
| 语言 | TypeScript |
|
||||||
|
| 状态管理 | Redux Toolkit |
|
||||||
|
| 编译工具 | Vite |
|
||||||
|
| UI | 自定义组件 |
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/ # API 接口层
|
||||||
|
│ ├── auth.ts # 认证服务
|
||||||
|
│ ├── file.ts # 文件服务
|
||||||
|
│ ├── order.ts # 订单服务
|
||||||
|
│ ├── printer.ts # 打印机服务
|
||||||
|
│ ├── price.ts # 价目服务
|
||||||
|
│ └── pay.ts # 支付服务
|
||||||
|
├── config/ # 配置文件
|
||||||
|
│ └── api.ts # API 地址配置
|
||||||
|
├── store/ # Redux 状态管理
|
||||||
|
│ ├── index.ts # Store 入口
|
||||||
|
│ ├── hooks.ts # Hooks
|
||||||
|
│ └── slices/ # Slice 模块
|
||||||
|
│ ├── userSlice.ts
|
||||||
|
│ ├── orderSlice.ts
|
||||||
|
│ ├── fileSlice.ts
|
||||||
|
│ └── printerSlice.ts
|
||||||
|
├── pages/ # 页面
|
||||||
|
│ ├── index/ # 首页
|
||||||
|
│ ├── login/ # 登录页
|
||||||
|
│ ├── user/ # 用户中心
|
||||||
|
│ ├── file/ # 文件管理
|
||||||
|
│ └── order/ # 订单管理
|
||||||
|
├── components/ # 公共组件
|
||||||
|
│ ├── OrderCard/ # 订单卡片
|
||||||
|
│ └── PriceTable/ # 价目表
|
||||||
|
├── types/ # TypeScript 类型
|
||||||
|
│ ├── user.ts
|
||||||
|
│ ├── order.ts
|
||||||
|
│ ├── file.ts
|
||||||
|
│ ├── printer.ts
|
||||||
|
│ └── price.ts
|
||||||
|
├── utils/ # 工具函数
|
||||||
|
│ ├── format.ts # 格式化工具
|
||||||
|
│ └── storage.ts # 本地存储
|
||||||
|
└── hooks/ # 自定义 Hooks
|
||||||
|
└── useAuth.ts # 认证 Hook
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
API 地址配置:`src/config/api.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const DEV_API_URL = 'http://localhost:8080';
|
||||||
|
const PROD_API_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
|
export const API_BASE_URL = process.env.NODE_ENV === 'development' ? DEV_API_URL : PROD_API_URL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发模式(微信小程序)
|
||||||
|
npm run dev:weapp
|
||||||
|
|
||||||
|
# 生产构建
|
||||||
|
npm run build:weapp
|
||||||
|
```
|
||||||
|
|
||||||
|
## 微信开发者工具配置
|
||||||
|
|
||||||
|
1. 导入项目目录:`dist/miniprogram`
|
||||||
|
2. AppID:使用 `touristappid` 或自己申请的
|
||||||
|
3. 启动开发服务器后点击刷新
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
### 用户模块
|
||||||
|
- 手机号 + 验证码登录
|
||||||
|
- 微信一键登录
|
||||||
|
- 用户信息管理
|
||||||
|
- 退出登录
|
||||||
|
|
||||||
|
### 文件模块
|
||||||
|
- 文件上传
|
||||||
|
- 文件列表
|
||||||
|
- 文件删除
|
||||||
|
- 打印功能
|
||||||
|
|
||||||
|
### 订单模块
|
||||||
|
- 创建订单
|
||||||
|
- 订单列表
|
||||||
|
- 订单详情
|
||||||
|
- 取消订单
|
||||||
|
|
||||||
|
### 打印模块
|
||||||
|
- 选择打印规格(A4/A3/照片)
|
||||||
|
- 选择打印颜色(黑白/彩色)
|
||||||
|
- 选择份数
|
||||||
|
- 选择打印机
|
||||||
|
|
||||||
|
## 页面说明
|
||||||
|
|
||||||
|
| 页面 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 首页 | `/pages/index/index` | 主入口,显示快捷操作和价目表 |
|
||||||
|
| 登录 | `/pages/login/index` | 手机号登录/微信登录 |
|
||||||
|
| 用户中心 | `/pages/user/index` | 用户信息、余额、菜单 |
|
||||||
|
| 我的文件 | `/pages/file/index` | 文件列表管理 |
|
||||||
|
| 上传文件 | `/pages/file/upload/index` | 文件上传 |
|
||||||
|
| 我的订单 | `/pages/order/index` | 订单列表 |
|
||||||
|
| 订单详情 | `/pages/order/detail/index` | 订单详情 |
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
|
||||||
|
### 导入规范
|
||||||
|
- Redux 类型从 `@reduxjs/toolkit` 导入
|
||||||
|
- API_BASE_URL 从 `../config/api` 导入
|
||||||
|
|
||||||
|
### 函数缓存
|
||||||
|
- 使用 `useCallback` 缓存事件处理函数
|
||||||
|
- 使用 `useMemo` 缓存计算结果
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
- 组件使用 PascalCase(如 `OrderCard`)
|
||||||
|
- 样式类使用小写 + 连字符(如 `.order-card`)
|
||||||
|
- 常量使用全大写(如 `BASE_URL`)
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### 2026-03-30
|
||||||
|
- 项目初始化
|
||||||
|
- 完成首页、登录页、用户中心、文件管理、订单管理页面开发
|
||||||
|
- 实现 Redux Toolkit 状态管理
|
||||||
|
- 实现 API 接口层统一管理
|
||||||
|
- 优化手机号输入验证
|
||||||
|
- 美化界面样式(淡雅配色)
|
||||||
|
|
||||||
|
### 技术优化
|
||||||
|
- 使用 `useCallback` 缓存函数减少重复创建
|
||||||
|
- 使用 `useMemo` 缓存计算结果
|
||||||
|
- API URL 统一配置文件管理
|
||||||
|
- Redux 导入路径修正为 `@reduxjs/toolkit`
|
||||||
|
- 提取共享 request 函数消除代码重复(`src/utils/request.ts`)
|
||||||
110
package-lock.json
generated
110
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.7.7",
|
"@babel/runtime": "^7.7.7",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@taro-hooks/plugin-react": "2",
|
"@taro-hooks/plugin-react": "2",
|
||||||
"@taro-hooks/shared": "2",
|
"@taro-hooks/shared": "2",
|
||||||
"@tarojs/components": "4.1.11",
|
"@tarojs/components": "4.1.11",
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
"@tarojs/taro": "4.1.11",
|
"@tarojs/taro": "4.1.11",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
"taro-hooks": "2"
|
"taro-hooks": "2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3184,6 +3187,32 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rnx-kit/babel-preset-metro-react-native": {
|
"node_modules/@rnx-kit/babel-preset-metro-react-native": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmmirror.com/@rnx-kit/babel-preset-metro-react-native/-/babel-preset-metro-react-native-1.1.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@rnx-kit/babel-preset-metro-react-native/-/babel-preset-metro-react-native-1.1.8.tgz",
|
||||||
@@ -3361,6 +3390,18 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@stencil/core": {
|
"node_modules/@stencil/core": {
|
||||||
"version": "2.22.3",
|
"version": "2.22.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@stencil/core/-/core-2.22.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@stencil/core/-/core-2.22.3.tgz",
|
||||||
@@ -5254,6 +5295,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||||
@@ -9572,6 +9619,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "11.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immer/-/immer-11.1.4.tgz",
|
||||||
|
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.5",
|
"version": "5.1.5",
|
||||||
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz",
|
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz",
|
||||||
@@ -12355,6 +12412,29 @@
|
|||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
@@ -12586,6 +12666,21 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -12750,6 +12845,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -14662,6 +14763,15 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
45
package.json
45
package.json
@@ -36,47 +36,50 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.7.7",
|
"@babel/runtime": "^7.7.7",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
|
"@taro-hooks/plugin-react": "2",
|
||||||
|
"@taro-hooks/shared": "2",
|
||||||
"@tarojs/components": "4.1.11",
|
"@tarojs/components": "4.1.11",
|
||||||
"@tarojs/helper": "4.1.11",
|
"@tarojs/helper": "4.1.11",
|
||||||
"@tarojs/plugin-platform-weapp": "4.1.11",
|
"@tarojs/plugin-framework-react": "4.1.11",
|
||||||
"@tarojs/plugin-platform-alipay": "4.1.11",
|
"@tarojs/plugin-platform-alipay": "4.1.11",
|
||||||
"@tarojs/plugin-platform-tt": "4.1.11",
|
"@tarojs/plugin-platform-h5": "4.1.11",
|
||||||
"@tarojs/plugin-platform-swan": "4.1.11",
|
|
||||||
"@tarojs/plugin-platform-jd": "4.1.11",
|
"@tarojs/plugin-platform-jd": "4.1.11",
|
||||||
"@tarojs/plugin-platform-qq": "4.1.11",
|
"@tarojs/plugin-platform-qq": "4.1.11",
|
||||||
"@tarojs/plugin-platform-h5": "4.1.11",
|
"@tarojs/plugin-platform-swan": "4.1.11",
|
||||||
|
"@tarojs/plugin-platform-tt": "4.1.11",
|
||||||
|
"@tarojs/plugin-platform-weapp": "4.1.11",
|
||||||
|
"@tarojs/react": "4.1.11",
|
||||||
"@tarojs/runtime": "4.1.11",
|
"@tarojs/runtime": "4.1.11",
|
||||||
"@tarojs/shared": "4.1.11",
|
"@tarojs/shared": "4.1.11",
|
||||||
"@tarojs/taro": "4.1.11",
|
"@tarojs/taro": "4.1.11",
|
||||||
"@taro-hooks/shared": "2",
|
|
||||||
"@tarojs/plugin-framework-react": "4.1.11",
|
|
||||||
"@tarojs/react": "4.1.11",
|
|
||||||
"@taro-hooks/plugin-react": "2",
|
|
||||||
"react-dom": "^18.0.0",
|
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
"taro-hooks": "2"
|
"taro-hooks": "2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-plugin-import": "^1.13.3",
|
"@babel/core": "^7.8.0",
|
||||||
"@babel/plugin-proposal-class-properties": "7.14.5",
|
"@babel/plugin-proposal-class-properties": "7.14.5",
|
||||||
|
"@babel/preset-react": "^7.24.1",
|
||||||
"@tarojs/cli": "4.1.11",
|
"@tarojs/cli": "4.1.11",
|
||||||
"@tarojs/vite-runner": "4.1.11",
|
"@tarojs/vite-runner": "4.1.11",
|
||||||
"babel-preset-taro": "4.1.11",
|
|
||||||
"eslint-config-taro": "4.1.11",
|
|
||||||
"eslint": "^8.12.0",
|
|
||||||
"stylelint": "^14.4.0",
|
|
||||||
"terser": "^5.16.8",
|
|
||||||
"vite": "^4.2.0",
|
|
||||||
"@babel/preset-react": "^7.24.1",
|
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||||
|
"@typescript-eslint/parser": "^6.2.0",
|
||||||
"@vitejs/plugin-react": "^4.1.0",
|
"@vitejs/plugin-react": "^4.1.0",
|
||||||
"eslint-plugin-react": "^7.8.2",
|
"babel-plugin-import": "^1.13.3",
|
||||||
|
"babel-preset-taro": "4.1.11",
|
||||||
|
"eslint": "^8.12.0",
|
||||||
|
"eslint-config-taro": "4.1.11",
|
||||||
"eslint-plugin-import": "^2.12.0",
|
"eslint-plugin-import": "^2.12.0",
|
||||||
|
"eslint-plugin-react": "^7.8.2",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"react-refresh": "^0.11.0",
|
"react-refresh": "^0.11.0",
|
||||||
"@typescript-eslint/parser": "^6.2.0",
|
"stylelint": "^14.4.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
"terser": "^5.16.8",
|
||||||
"typescript": "^5.1.0",
|
"typescript": "^5.1.0",
|
||||||
"@babel/core": "^7.8.0"
|
"vite": "^4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/api/auth.ts
Normal file
34
src/api/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { request } from '../utils/request';
|
||||||
|
import type { UserInfo, LoginResponse, UpdateUserRequest } from '../types';
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
login: (code: string) => {
|
||||||
|
return request<LoginResponse>({
|
||||||
|
url: '/auth/login',
|
||||||
|
method: 'POST',
|
||||||
|
data: { code },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshToken: (refreshToken: string) => {
|
||||||
|
return request<{ accessToken: string }>({
|
||||||
|
url: '/auth/refresh',
|
||||||
|
method: 'POST',
|
||||||
|
data: { refreshToken },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserInfo: () => {
|
||||||
|
return request<UserInfo>({
|
||||||
|
url: '/auth/userinfo',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUser: (data: UpdateUserRequest) => {
|
||||||
|
return request<UserInfo>({
|
||||||
|
url: '/auth/update',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
60
src/api/file.ts
Normal file
60
src/api/file.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { store } from '../store';
|
||||||
|
import type { FileInfo, UploadFileResponse, FileListResponse } from '../types';
|
||||||
|
import { API_BASE_URL } from '../config/api';
|
||||||
|
import { request } from '../utils/request';
|
||||||
|
|
||||||
|
export const uploadFile = (filePath: string): Promise<UploadFileResponse> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const token = store.getState().user.token;
|
||||||
|
|
||||||
|
Taro.uploadFile({
|
||||||
|
url: `${API_BASE_URL}/file/upload`,
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
header: {
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve(JSON.parse(res.data));
|
||||||
|
} else {
|
||||||
|
reject(new Error('Upload failed'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: reject,
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileApi = {
|
||||||
|
upload: uploadFile,
|
||||||
|
|
||||||
|
getFile: (fileId: number) => {
|
||||||
|
return request<FileInfo>({
|
||||||
|
url: `/file/${fileId}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteFile: (fileId: number) => {
|
||||||
|
return request<void>({
|
||||||
|
url: `/file/${fileId}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileList: (page = 1, pageSize = 20) => {
|
||||||
|
return request<FileListResponse>({
|
||||||
|
url: '/file/list',
|
||||||
|
data: { page, pageSize },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addWatermark: (fileId: number, watermarkText: string) => {
|
||||||
|
return request<{ watermarkUrl: string }>({
|
||||||
|
url: `/file/${fileId}/watermark`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { watermark: watermarkText },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
6
src/api/index.ts
Normal file
6
src/api/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { authApi } from './auth';
|
||||||
|
export { fileApi } from './file';
|
||||||
|
export { orderApi } from './order';
|
||||||
|
export { printerApi } from './printer';
|
||||||
|
export { priceApi } from './price';
|
||||||
|
export { payApi } from './pay';
|
||||||
40
src/api/order.ts
Normal file
40
src/api/order.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { request } from '../utils/request';
|
||||||
|
import type { Order, CreateOrderRequest, OrderListResponse } from '../types';
|
||||||
|
|
||||||
|
export const orderApi = {
|
||||||
|
createOrder: (data: CreateOrderRequest) => {
|
||||||
|
return request<Order>({
|
||||||
|
url: '/order/create',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrder: (orderId: number) => {
|
||||||
|
return request<Order>({
|
||||||
|
url: `/order/${orderId}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrderList: (page = 1, pageSize = 20) => {
|
||||||
|
return request<OrderListResponse>({
|
||||||
|
url: '/order/list',
|
||||||
|
data: { page, pageSize },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateOrderStatus: (orderId: number, status: number) => {
|
||||||
|
return request<Order>({
|
||||||
|
url: `/order/${orderId}/status`,
|
||||||
|
method: 'PUT',
|
||||||
|
data: { status },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelOrder: (orderId: number) => {
|
||||||
|
return request<Order>({
|
||||||
|
url: `/order/${orderId}/cancel`,
|
||||||
|
method: 'PUT',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
31
src/api/pay.ts
Normal file
31
src/api/pay.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { request } from '../utils/request';
|
||||||
|
|
||||||
|
export const payApi = {
|
||||||
|
createWxPayOrder: (orderId: number) => {
|
||||||
|
return request<{
|
||||||
|
prepayId: string;
|
||||||
|
paySign: string;
|
||||||
|
timeStamp: string;
|
||||||
|
nonceStr: string;
|
||||||
|
signType: string;
|
||||||
|
}>({
|
||||||
|
url: '/pay/create',
|
||||||
|
method: 'POST',
|
||||||
|
data: { orderId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
queryPayStatus: (orderId: number) => {
|
||||||
|
return request<{ status: number }>({
|
||||||
|
url: `/pay/query/${orderId}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
payByBalance: (orderId: number) => {
|
||||||
|
return request<{ success: boolean }>({
|
||||||
|
url: '/pay/balance',
|
||||||
|
method: 'POST',
|
||||||
|
data: { orderId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
26
src/api/price.ts
Normal file
26
src/api/price.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { request } from '../utils/request';
|
||||||
|
import type { PriceItem } from '../types';
|
||||||
|
|
||||||
|
export const priceApi = {
|
||||||
|
getPriceList: () => {
|
||||||
|
return request<PriceItem[]>({
|
||||||
|
url: '/price/list',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createPriceItem: (data: Omit<PriceItem, 'id'>) => {
|
||||||
|
return request<PriceItem>({
|
||||||
|
url: '/price/create',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePriceItem: (id: number, data: Partial<PriceItem>) => {
|
||||||
|
return request<PriceItem>({
|
||||||
|
url: `/price/${id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
39
src/api/printer.ts
Normal file
39
src/api/printer.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { request } from '../utils/request';
|
||||||
|
import type { Printer } from '../types';
|
||||||
|
|
||||||
|
export const printerApi = {
|
||||||
|
getPrinters: () => {
|
||||||
|
return request<Printer[]>({
|
||||||
|
url: '/print/printers',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getPrinterStatus: (printerId: number) => {
|
||||||
|
return request<Printer>({
|
||||||
|
url: `/print/printer/${printerId}/status`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
registerPrinter: (name: string, location: string) => {
|
||||||
|
return request<Printer>({
|
||||||
|
url: '/print/printer/register',
|
||||||
|
method: 'POST',
|
||||||
|
data: { name, location },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
submitPrintJob: (orderId: number, printerId: number) => {
|
||||||
|
return request<{ jobId: number }>({
|
||||||
|
url: '/print/job/submit',
|
||||||
|
method: 'POST',
|
||||||
|
data: { orderId, printerId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelPrintJob: (jobId: number) => {
|
||||||
|
return request<void>({
|
||||||
|
url: `/print/job/${jobId}/cancel`,
|
||||||
|
method: 'PUT',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
export default {
|
export default {
|
||||||
pages: ["pages/index/index"],
|
pages: [
|
||||||
|
"pages/index/index",
|
||||||
|
"pages/login/index",
|
||||||
|
"pages/user/index",
|
||||||
|
"pages/file/index",
|
||||||
|
"pages/file/upload/index",
|
||||||
|
"pages/order/index",
|
||||||
|
"pages/order/detail/index",
|
||||||
|
"pages/print/index",
|
||||||
|
"pages/print/confirm/index",
|
||||||
|
],
|
||||||
window: {
|
window: {
|
||||||
backgroundTextStyle: "light",
|
backgroundTextStyle: "light",
|
||||||
navigationBarBackgroundColor: "#fff",
|
navigationBarBackgroundColor: "#1890ff",
|
||||||
navigationBarTitleText: "WeChat",
|
navigationBarTitleText: "萝卜打印",
|
||||||
navigationBarTextStyle: "black",
|
navigationBarTextStyle: "white",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
165
src/app.css
165
src/app.css
@@ -0,0 +1,165 @@
|
|||||||
|
/* ============================================
|
||||||
|
全局样式入口 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 引入设计系统和动画 */
|
||||||
|
@import './styles/variables.css';
|
||||||
|
@import './styles/common.css';
|
||||||
|
@import './styles/animations.css';
|
||||||
|
|
||||||
|
/* 全局页面样式 */
|
||||||
|
page {
|
||||||
|
background: var(--bg-page);
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
|
||||||
|
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-primary);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中文本样式 */
|
||||||
|
::selection {
|
||||||
|
background: rgba(90, 147, 223, 0.2);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 焦点样式 */
|
||||||
|
:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接样式 */
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片样式 */
|
||||||
|
image {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本选择禁用 */
|
||||||
|
.no-select {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 安全区域 */
|
||||||
|
.safe-area-top {
|
||||||
|
padding-top: constant(safe-area-inset-top);
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-bottom {
|
||||||
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-left {
|
||||||
|
padding-left: constant(safe-area-inset-left);
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-right {
|
||||||
|
padding-right: constant(safe-area-inset-right);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏滚动条但保持可滚动 */
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本截断 */
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncate-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncate-3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点击高亮移除 */
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 打印样式 */
|
||||||
|
@media print {
|
||||||
|
page {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式支持 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
/* 未来可扩展深色模式 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 减少动画偏好 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Component } from 'react'
|
import { Component } from 'react'
|
||||||
|
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
import './app.css'
|
import './app.css'
|
||||||
@@ -12,11 +11,9 @@ class App extends Component<PropsWithChildren> {
|
|||||||
|
|
||||||
componentDidHide() { }
|
componentDidHide() { }
|
||||||
|
|
||||||
// this.props.children 是将要会渲染的页面
|
|
||||||
render() {
|
render() {
|
||||||
return this.props.children
|
return this.props.children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
164
src/components/OrderCard/OrderCard.css
Normal file
164
src/components/OrderCard/OrderCard.css
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/* ============================================
|
||||||
|
订单卡片组件样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../styles/variables.css';
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单头部 */
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(24, 144, 255, 0.1);
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.completed {
|
||||||
|
background: rgba(82, 196, 26, 0.1);
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.cancelled {
|
||||||
|
background: rgba(255, 77, 79, 0.1);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.pending {
|
||||||
|
background: rgba(250, 173, 20, 0.1);
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单内容 */
|
||||||
|
.order-body {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-name {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单底部 */
|
||||||
|
.order-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-price {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-time {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单操作区 */
|
||||||
|
.order-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
border: none;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn.primary {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(90, 147, 223, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn.secondary {
|
||||||
|
background: #f5f5f7;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片变体 */
|
||||||
|
.order-card.highlighted {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(90, 147, 223, 0.1), var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card.compact {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card.compact .order-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card.compact .order-body {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
1
src/components/OrderCard/index.ts
Normal file
1
src/components/OrderCard/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { OrderCard } from './index.tsx';
|
||||||
43
src/components/OrderCard/index.tsx
Normal file
43
src/components/OrderCard/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text } from '@tarojs/components';
|
||||||
|
import { OrderStatusMap } from '../../types';
|
||||||
|
import { formatPrice, formatDate } from '../../utils/format';
|
||||||
|
import './OrderCard.css';
|
||||||
|
|
||||||
|
interface OrderCardProps {
|
||||||
|
order: {
|
||||||
|
id: number;
|
||||||
|
fileName: string;
|
||||||
|
printerName: string;
|
||||||
|
printType: string;
|
||||||
|
colorType: string;
|
||||||
|
copies: number;
|
||||||
|
totalPrice: number;
|
||||||
|
orderStatus: number;
|
||||||
|
payStatus: number;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderCard: React.FC<OrderCardProps> = ({ order, onClick }) => {
|
||||||
|
return (
|
||||||
|
<View className='order-card' onClick={onClick}>
|
||||||
|
<View className='order-header'>
|
||||||
|
<Text className='order-id'>订单号: {order.id}</Text>
|
||||||
|
<Text className='order-status'>{OrderStatusMap[order.orderStatus as keyof typeof OrderStatusMap]}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='order-body'>
|
||||||
|
<Text className='file-name'>{order.fileName}</Text>
|
||||||
|
<Text className='order-detail'>
|
||||||
|
{order.printType} {order.colorType} x {order.copies}份
|
||||||
|
</Text>
|
||||||
|
<Text className='printer-name'>打印机: {order.printerName}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='order-footer'>
|
||||||
|
<Text className='order-price'>{formatPrice(order.totalPrice)}</Text>
|
||||||
|
<Text className='order-time'>{formatDate(order.createdAt)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
140
src/components/PriceTable/PriceTable.css
Normal file
140
src/components/PriceTable/PriceTable.css
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/* ============================================
|
||||||
|
价目表组件样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../styles/variables.css';
|
||||||
|
|
||||||
|
.price-table {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-title {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item:nth-child(odd) {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item:nth-child(even) {
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item.selected {
|
||||||
|
background: rgba(90, 147, 223, 0.1);
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item.selected .price-name {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-name {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 价目表变体 */
|
||||||
|
.price-table.compact {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-table.compact .price-title {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-table.compact .price-item {
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-table.compact .price-name {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-table.compact .price-value {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 带图标的价目项 */
|
||||||
|
.price-item-with-icon {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
background: linear-gradient(135deg, #e8f0fe 0%, #d0dcfa 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 促销标签 */
|
||||||
|
.price-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-left: 8px;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 原价显示 */
|
||||||
|
.price-original {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
1
src/components/PriceTable/index.ts
Normal file
1
src/components/PriceTable/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { PriceTable } from './index';
|
||||||
35
src/components/PriceTable/index.tsx
Normal file
35
src/components/PriceTable/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text } from '@tarojs/components';
|
||||||
|
import { DefaultPriceList, PriceItem } from '../../types';
|
||||||
|
import { formatPrice } from '../../utils/format';
|
||||||
|
import './PriceTable.css';
|
||||||
|
|
||||||
|
interface PriceTableProps {
|
||||||
|
prices?: PriceItem[];
|
||||||
|
selectedType?: string;
|
||||||
|
onSelect?: (item: PriceItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PriceTable: React.FC<PriceTableProps> = ({
|
||||||
|
prices = DefaultPriceList,
|
||||||
|
selectedType,
|
||||||
|
onSelect
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<View className='price-table'>
|
||||||
|
<Text className='price-title'>价目表</Text>
|
||||||
|
<View className='price-list'>
|
||||||
|
{prices.map(item => (
|
||||||
|
<View
|
||||||
|
key={item.id}
|
||||||
|
className={`price-item ${selectedType === item.name ? 'selected' : ''}`}
|
||||||
|
onClick={() => onSelect?.(item)}
|
||||||
|
>
|
||||||
|
<Text className='price-name'>{item.name}</Text>
|
||||||
|
<Text className='price-value'>{formatPrice(item.price)}/{item.unit}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
src/components/index.ts
Normal file
2
src/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { OrderCard } from './OrderCard';
|
||||||
|
export { PriceTable } from './PriceTable';
|
||||||
4
src/config/api.ts
Normal file
4
src/config/api.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const DEV_API_URL = 'http://localhost:8080';
|
||||||
|
const PROD_API_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
|
export const API_BASE_URL = process.env.NODE_ENV === 'development' ? DEV_API_URL : PROD_API_URL;
|
||||||
1
src/hooks/index.ts
Normal file
1
src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './useAuth';
|
||||||
70
src/hooks/useAuth.ts
Normal file
70
src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||||
|
import { setUser, logout as logoutAction, updateUserInfo, setLoading } from '../store/slices/userSlice';
|
||||||
|
import { authApi } from '../api/auth';
|
||||||
|
import { storage } from '../utils/storage';
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { info, token, refreshToken, isLoggedIn, loading } = useAppSelector(state => state.user);
|
||||||
|
|
||||||
|
const login = useCallback(async (code: string) => {
|
||||||
|
dispatch(setLoading(true));
|
||||||
|
try {
|
||||||
|
const res = await authApi.login(code);
|
||||||
|
dispatch(setUser({
|
||||||
|
user: res.userInfo,
|
||||||
|
token: res.accessToken,
|
||||||
|
refreshToken: res.refreshToken,
|
||||||
|
}));
|
||||||
|
storage.setToken(res.accessToken);
|
||||||
|
storage.setUserInfo(res.userInfo);
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
dispatch(setLoading(false));
|
||||||
|
}
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
dispatch(logoutAction());
|
||||||
|
storage.removeToken();
|
||||||
|
storage.removeUserInfo();
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const refreshAccessToken = useCallback(async () => {
|
||||||
|
if (!refreshToken) return;
|
||||||
|
try {
|
||||||
|
const res = await authApi.refreshToken(refreshToken);
|
||||||
|
dispatch(setUser({ user: info!, token: res.accessToken, refreshToken }));
|
||||||
|
storage.setToken(res.accessToken);
|
||||||
|
} catch (error) {
|
||||||
|
logout();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [dispatch, info, logout, refreshToken]);
|
||||||
|
|
||||||
|
const updateInfo = useCallback(async (data: { nickname?: string; avatar?: string }) => {
|
||||||
|
try {
|
||||||
|
const res = await authApi.updateUser(data);
|
||||||
|
dispatch(updateUserInfo(res));
|
||||||
|
storage.setUserInfo(res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update user info failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: info,
|
||||||
|
token,
|
||||||
|
isLoggedIn,
|
||||||
|
loading,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
refreshToken: refreshAccessToken,
|
||||||
|
updateInfo,
|
||||||
|
};
|
||||||
|
};
|
||||||
3
src/pages/file/index.config.ts
Normal file
3
src/pages/file/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '我的文件',
|
||||||
|
};
|
||||||
277
src/pages/file/index.css
Normal file
277
src/pages/file/index.css
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/* ============================================
|
||||||
|
文件列表页面样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../styles/variables.css';
|
||||||
|
|
||||||
|
page {
|
||||||
|
background: linear-gradient(180deg, #e8f0fe 0%, #f0f5ff 30%, #fafbfc 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 32px 24px 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 */
|
||||||
|
.file-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
animation: fadeInDown 0.4s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-radius: 36px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(90, 147, 223, 0.2);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件列表 */
|
||||||
|
.file-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
animation: slideUp 0.4s ease forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
.file-item:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.file-item:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
|
||||||
|
.file-item:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 44px;
|
||||||
|
margin-right: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #e8f0fe 0%, #d0dcfa 100%);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮 */
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: none;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.print {
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: rgba(90, 147, 223, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.print:active {
|
||||||
|
background: rgba(90, 147, 223, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete {
|
||||||
|
color: var(--error-color);
|
||||||
|
background: rgba(255, 77, 79, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete:active {
|
||||||
|
background: rgba(255, 77, 79, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100px 0;
|
||||||
|
animation: fadeIn 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 100px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选栏 */
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip.active {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 4px 12px rgba(90, 147, 223, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计信息 */
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--primary-color);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/pages/file/index.tsx
Normal file
82
src/pages/file/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { View, Text, Button } from "@tarojs/components";
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
interface FileItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
size: string;
|
||||||
|
time: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileIndex = () => {
|
||||||
|
const [files] = useState<FileItem[]>([
|
||||||
|
{ id: 1, name: '期末考试复习资料.docx', size: '2.5 MB', time: '2026-03-30 10:30', type: 'word' },
|
||||||
|
{ id: 2, name: '毕业设计.pdf', size: '15.8 MB', time: '2026-03-29 15:20', type: 'pdf' },
|
||||||
|
{ id: 3, name: '证件照.jpg', size: '1.2 MB', time: '2026-03-28 09:15', type: 'image' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handlePrint = (file: FileItem) => {
|
||||||
|
Taro.navigateTo({
|
||||||
|
url: `/pages/print/index?fileId=${file.id}&fileName=${file.name}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (fileId: number) => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除这个文件吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
Taro.showToast({ title: '删除成功', icon: 'success' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'pdf': return '📕';
|
||||||
|
case 'word': return '📄';
|
||||||
|
case 'image': return '🖼️';
|
||||||
|
default: return '📁';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='file-page'>
|
||||||
|
<View className='file-header'>
|
||||||
|
<Text className='header-title'>我的文件</Text>
|
||||||
|
<Button className="upload-btn">上传新文件</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{files.length > 0 ? (
|
||||||
|
<View className='file-list'>
|
||||||
|
{files.map(file => (
|
||||||
|
<View key={file.id} className='file-item'>
|
||||||
|
<View className='file-icon'>{getFileIcon(file.type)}</View>
|
||||||
|
<View className='file-info'>
|
||||||
|
<Text className='file-name'>{file.name}</Text>
|
||||||
|
<Text className='file-meta'>{file.size} · {file.time}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='file-actions'>
|
||||||
|
<Text className='action-btn print' onClick={() => handlePrint(file)}>打印</Text>
|
||||||
|
<Text className='action-btn delete' onClick={() => handleDelete(file.id)}>删除</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className='empty-state'>
|
||||||
|
<Text className='empty-icon'>📂</Text>
|
||||||
|
<Text className='empty-text'>暂无文件</Text>
|
||||||
|
<Text className='empty-hint'>点击上方按钮上传文件</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileIndex;
|
||||||
3
src/pages/file/upload/index.config.ts
Normal file
3
src/pages/file/upload/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '上传文件',
|
||||||
|
};
|
||||||
375
src/pages/file/upload/index.css
Normal file
375
src/pages/file/upload/index.css
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
/* ============================================
|
||||||
|
文件上传页面样式 - 萝卜打印
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../../styles/variables.css';
|
||||||
|
|
||||||
|
page {
|
||||||
|
background: linear-gradient(180deg, #e8f0fe 0%, #f0f5ff 30%, #fafbfc 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 32px 24px 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 */
|
||||||
|
.upload-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
animation: fadeInDown 0.4s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-subtitle {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-area {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: 60px 32px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
min-height: 280px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px dashed var(--border-primary);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area.dragging {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: rgba(90, 147, 223, 0.05);
|
||||||
|
box-shadow: 0 0 0 4px rgba(90, 147, 223, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
animation: pulse 2s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 100px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-hint {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件预览 */
|
||||||
|
.file-preview {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fb 0%, #f0f2f7 100%);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
animation: slideIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-right: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-file {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(90, 147, 223, 0.1);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-file:active {
|
||||||
|
background: rgba(90, 147, 223, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮 */
|
||||||
|
.file-actions {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 96px;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
border-radius: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8px 24px rgba(90, 147, 223, 0.3);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn[disabled] {
|
||||||
|
background: linear-gradient(135deg, #c5cede 0%, #b0bccf 100%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条 */
|
||||||
|
.progress-area {
|
||||||
|
margin-top: 24px;
|
||||||
|
animation: fadeIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percent {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: var(--border-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(255, 255, 255, 0.3) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提示区域 */
|
||||||
|
.tips-section {
|
||||||
|
background: linear-gradient(135deg, #fefcf3 0%, #fdf9f0 100%);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
margin-top: 32px;
|
||||||
|
border: 1px solid #f5e8c7;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: #b08945;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-dot {
|
||||||
|
color: #d4a853;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-text {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: #8a7340;
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 打印选项 */
|
||||||
|
.print-options {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-title {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
height: 72px;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn.selected {
|
||||||
|
background: rgba(90, 147, 223, 0.1);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/pages/file/upload/index.tsx
Normal file
138
src/pages/file/upload/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { View, Text, Button, Image } from "@tarojs/components";
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const FileUpload = () => {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<{ path: string; name: string; size: number } | null>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
|
||||||
|
const handleSelectFile = () => {
|
||||||
|
Taro.chooseMessageFile({
|
||||||
|
count: 1,
|
||||||
|
type: 'file',
|
||||||
|
success: (res) => {
|
||||||
|
const file = res.tempFiles[0];
|
||||||
|
setSelectedFile({
|
||||||
|
path: file.path,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
Taro.showToast({ title: '选择文件失败', icon: 'none' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
Taro.showToast({ title: '请先选择文件', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setUploadProgress(prev => {
|
||||||
|
if (prev >= 90) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return prev + 10;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
clearInterval(interval);
|
||||||
|
setUploadProgress(100);
|
||||||
|
Taro.showToast({ title: '上传成功', icon: 'success' });
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
clearInterval(interval);
|
||||||
|
Taro.showToast({ title: '上传失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='upload-page'>
|
||||||
|
<View className='upload-header'>
|
||||||
|
<Text className='upload-title'>上传文件</Text>
|
||||||
|
<Text className='upload-subtitle'>支持 Word、PDF、图片等格式</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='upload-area' onClick={handleSelectFile}>
|
||||||
|
{selectedFile ? (
|
||||||
|
<View className='file-preview'>
|
||||||
|
<View className='file-icon'>📄</View>
|
||||||
|
<View className='file-info'>
|
||||||
|
<Text className='file-name'>{selectedFile.name}</Text>
|
||||||
|
<Text className='file-size'>{formatFileSize(selectedFile.size)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='change-file' onClick={(e) => { e.stopPropagation(); handleSelectFile(); }}>更换</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className='upload-placeholder'>
|
||||||
|
<Text className='upload-icon'>📤</Text>
|
||||||
|
<Text className='upload-text'>点击选择文件</Text>
|
||||||
|
<Text className='upload-hint'>最大支持 50MB</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{selectedFile && (
|
||||||
|
<View className='file-actions'>
|
||||||
|
<Button
|
||||||
|
className='upload-btn'
|
||||||
|
onClick={handleUpload}
|
||||||
|
loading={uploading}
|
||||||
|
disabled={uploading}
|
||||||
|
>
|
||||||
|
{uploading ? `上传中 ${uploadProgress}%` : '开始上传'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploading && (
|
||||||
|
<View className='progress-area'>
|
||||||
|
<View className='progress-bar'>
|
||||||
|
<View className='progress-fill' style={{ width: `${uploadProgress}%` }} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className='tips-section'>
|
||||||
|
<Text className='tips-title'>上传须知</Text>
|
||||||
|
<View className='tips-item'>
|
||||||
|
<Text className='tips-dot'>•</Text>
|
||||||
|
<Text className='tips-text'>文件将在服务器保存 24 小时</Text>
|
||||||
|
</View>
|
||||||
|
<View className='tips-item'>
|
||||||
|
<Text className='tips-dot'>•</Text>
|
||||||
|
<Text className='tips-text'>请确保文件内容不违反相关法规</Text>
|
||||||
|
</View>
|
||||||
|
<View className='tips-item'>
|
||||||
|
<Text className='tips-dot'>•</Text>
|
||||||
|
<Text className='tips-text'>打印完成后请及时取走文件</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileUpload;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
navigationBarTitleText: 'Taro-hooks',
|
navigationBarTitleText: '萝卜打印',
|
||||||
enableShareAppMessage: true,
|
navigationStyle: 'custom',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,56 +1,405 @@
|
|||||||
|
/* ============================================
|
||||||
|
首页样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../styles/variables.css';
|
||||||
|
|
||||||
page {
|
page {
|
||||||
background-color: white;
|
background: linear-gradient(180deg, #e8f0fe 0%, #f0f5ff 30%, #fafbfc 100%);
|
||||||
padding : 14px;
|
min-height: 100vh;
|
||||||
box-sizing : border-box;
|
|
||||||
color : #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.index-page {
|
||||||
display : flex;
|
min-height: 100vh;
|
||||||
|
padding-bottom: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部区域 */
|
||||||
|
.header {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
padding: 100px 32px 80px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0 0 40px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -30%;
|
||||||
|
right: -15%;
|
||||||
|
width: 350px;
|
||||||
|
height: 350px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20%;
|
||||||
|
left: -10%;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: float 8s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-subtitle {
|
||||||
|
font-size: 24px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 8px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮区域 */
|
||||||
|
.login-section {
|
||||||
|
padding: 0 32px;
|
||||||
|
margin-top: -30px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
animation: slideUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 1px solid rgba(90, 147, 223, 0.2);
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
padding: 24px 0;
|
||||||
|
border-radius: 50px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户卡片 */
|
||||||
|
.user-section {
|
||||||
|
padding: 0 32px;
|
||||||
|
margin-top: -30px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
animation: slideUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
margin-right: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(90, 147, 223, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-balance {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn-small {
|
||||||
|
background: #f5f5f7;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: none;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn-small:active {
|
||||||
|
background: #e8e8ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷操作 */
|
||||||
|
.quick-actions {
|
||||||
|
display: flex;
|
||||||
|
padding: 32px 24px 0;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 28px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item:active::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item.primary {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
box-shadow: 0 8px 24px rgba(90, 147, 223, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item.primary .action-text {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item.primary .action-icon {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: linear-gradient(135deg, #f0f5ff 0%, #e8eefa 100%);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-text {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 价目表 */
|
||||||
|
.price-section {
|
||||||
|
padding: 28px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-table {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-title {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-list {
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.price-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item:nth-child(odd) {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item:nth-child(even) {
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-name {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用提示 */
|
||||||
|
.tips-section {
|
||||||
|
padding: 28px 24px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-card {
|
||||||
|
background: linear-gradient(135deg, #fefcf3 0%, #fdf9f0 100%);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid #f5e8c7;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: #b08945;
|
||||||
display: block;
|
display: block;
|
||||||
width : 270px;
|
margin-bottom: 16px;
|
||||||
height : 270px;
|
|
||||||
margin : 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title,
|
.tips-item {
|
||||||
.desc {
|
display: flex;
|
||||||
text-align: center;
|
align-items: center;
|
||||||
font-size : 32px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc {
|
.tips-item:last-child {
|
||||||
margin : 20px 0;
|
margin-bottom: 0;
|
||||||
font-size: 28px;
|
|
||||||
color : rgba(0, 0, 0, .85);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.tips-number {
|
||||||
padding : 24px 0;
|
width: 28px;
|
||||||
font-size : 32px;
|
height: 28px;
|
||||||
display : flex;
|
background: linear-gradient(135deg, #d4a853 0%, #c49a47 100%);
|
||||||
align-items : center;
|
border-radius: 50%;
|
||||||
border-color: rgba(51, 51, 51, .1);
|
display: flex;
|
||||||
border-style: solid;
|
align-items: center;
|
||||||
border-width: 1px 0 1px 0;
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list:not(:first-child) {
|
.tips-text {
|
||||||
border-top-width: 0;
|
font-size: var(--font-size-sm);
|
||||||
|
color: #8a7340;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
/* 动画 */
|
||||||
flex: 0.4;
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
/* 装饰光晕 */
|
||||||
display : block;
|
.index-page::before {
|
||||||
width : 100%;
|
content: '';
|
||||||
background-color: transparent;
|
position: fixed;
|
||||||
border-radius : 2px;
|
top: -100px;
|
||||||
margin-top : 20px;
|
right: -100px;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
background: radial-gradient(circle, rgba(125, 179, 245, 0.1) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,170 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { View, Text, Button, Image } from "@tarojs/components";
|
import { View, Text, Button, Image } from "@tarojs/components";
|
||||||
import { useEnv, useNavigationBar, useModal, useToast } from "taro-hooks";
|
import Taro from '@tarojs/taro';
|
||||||
import logo from "./hook.png";
|
import './index.css';
|
||||||
|
|
||||||
import './index.css'
|
interface UserInfo {
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const env = useEnv();
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const { setTitle } = useNavigationBar({ title: "Taro Hooks" });
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||||
const showModal = useModal({
|
|
||||||
title: "Taro Hooks Canary!",
|
|
||||||
showCancel: false,
|
|
||||||
confirmColor: "#8c2de9",
|
|
||||||
confirmText: "支持一下"
|
|
||||||
});
|
|
||||||
const { show } = useToast({ mask: true });
|
|
||||||
|
|
||||||
const handleModal = useCallback(() => {
|
useEffect(() => {
|
||||||
showModal({ content: "不如给一个star⭐️!" }).then(() => {
|
checkLoginStatus();
|
||||||
show({ title: "点击了支持!" });
|
}, []);
|
||||||
|
|
||||||
|
const checkLoginStatus = useCallback(() => {
|
||||||
|
const storedUser = Taro.getStorageSync('userInfo');
|
||||||
|
if (storedUser) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
setUserInfo(storedUser);
|
||||||
|
} else {
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
setUserInfo(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = useCallback(() => {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpload = useCallback(() => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Taro.navigateTo({ url: '/pages/file/upload/index' });
|
||||||
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
|
const handleMyFiles = useCallback(() => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Taro.navigateTo({ url: '/pages/file/index' });
|
||||||
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
|
const handleMyOrders = useCallback(() => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Taro.navigateTo({ url: '/pages/order/index' });
|
||||||
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
|
const handleLogout = useCallback(() => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '确认退出',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
Taro.removeStorageSync('userInfo');
|
||||||
|
Taro.removeStorageSync('token');
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
setUserInfo(null);
|
||||||
|
Taro.showToast({ title: '已退出登录', icon: 'success' });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [show, showModal]);
|
}, []);
|
||||||
|
|
||||||
|
const priceList = useMemo(() => [
|
||||||
|
{ name: 'A4黑白打印', price: '¥0.10/页' },
|
||||||
|
{ name: 'A4彩色打印', price: '¥0.30/页' },
|
||||||
|
{ name: 'A3黑白打印', price: '¥0.20/页' },
|
||||||
|
{ name: 'A3彩色打印', price: '¥0.60/页' },
|
||||||
|
{ name: '照片打印', price: '¥2.00/张' },
|
||||||
|
], []);
|
||||||
|
|
||||||
|
const tipsList = useMemo(() => [
|
||||||
|
'支持 Word、PDF、图片等格式',
|
||||||
|
'请选择合适的打印规格',
|
||||||
|
'打印完成后请及时取走文件',
|
||||||
|
], []);
|
||||||
|
|
||||||
|
const userInitials = useMemo(() => {
|
||||||
|
if (!userInfo?.nickname) return 'U';
|
||||||
|
return userInfo.nickname[0]?.toUpperCase() || 'U';
|
||||||
|
}, [userInfo?.nickname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="wrapper">
|
<View className='index-page'>
|
||||||
<Image className="logo" src={logo} />
|
<View className='header'>
|
||||||
<Text className="title">为Taro而设计的Hooks Library</Text>
|
<View className='logo-area'>
|
||||||
<Text className="desc">
|
<Text className='app-title'>萝卜打印</Text>
|
||||||
目前覆盖70%官方API. 抹平部分API在H5端短板. 提供近40+Hooks!
|
<Text className='app-subtitle'>付费打印服务</Text>
|
||||||
并结合ahook适配Taro! 更多信息可以查看新版文档: https://next-version-taro-hooks.vercel.app/
|
</View>
|
||||||
</Text>
|
</View>
|
||||||
<View className="list">
|
|
||||||
<Text className="label">运行环境</Text>
|
{isLoggedIn && userInfo ? (
|
||||||
<Text className="note">{env}</Text>
|
<View className='user-section'>
|
||||||
|
<View className='user-card'>
|
||||||
|
<View className='avatar-wrapper'>
|
||||||
|
{userInfo.avatar ? (
|
||||||
|
<Image className='avatar-img' src={userInfo.avatar} mode='aspectFill' />
|
||||||
|
) : (
|
||||||
|
<View className='avatar-placeholder'>
|
||||||
|
<Text className='avatar-text'>{userInitials}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View className='user-detail'>
|
||||||
|
<Text className='user-name'>{userInfo.nickname}</Text>
|
||||||
|
<Text className='user-balance'>余额: ¥{userInfo.balance.toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
<Button className='logout-btn-small' onClick={handleLogout}>退出</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className='login-section'>
|
||||||
|
<Button className='login-btn' onClick={handleLogin}>登录 / 注册</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className='quick-actions'>
|
||||||
|
<View className='action-item primary' onClick={handleUpload}>
|
||||||
|
<Text className='action-icon'>📤</Text>
|
||||||
|
<Text className='action-text'>上传文件</Text>
|
||||||
|
</View>
|
||||||
|
<View className='action-item' onClick={handleMyFiles}>
|
||||||
|
<Text className='action-icon'>📁</Text>
|
||||||
|
<Text className='action-text'>我的文件</Text>
|
||||||
|
</View>
|
||||||
|
<View className='action-item' onClick={handleMyOrders}>
|
||||||
|
<Text className='action-icon'>📋</Text>
|
||||||
|
<Text className='action-text'>我的订单</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='price-section'>
|
||||||
|
<View className='price-table'>
|
||||||
|
<Text className='price-title'>价目表</Text>
|
||||||
|
<View className='price-list'>
|
||||||
|
{priceList.map((item, index) => (
|
||||||
|
<View key={index} className='price-item'>
|
||||||
|
<Text className='price-name'>{item.name}</Text>
|
||||||
|
<Text className='price-value'>{item.price}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='tips-section'>
|
||||||
|
<View className='tips-card'>
|
||||||
|
<Text className='tips-title'>使用提示</Text>
|
||||||
|
{tipsList.map((tip, index) => (
|
||||||
|
<View key={index} className='tips-item'>
|
||||||
|
<Text className='tips-number'>{index + 1}</Text>
|
||||||
|
<Text className='tips-text'>{tip}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Button className="button" onClick={() => setTitle("Taro Hooks Nice!")}>
|
|
||||||
设置标题
|
|
||||||
</Button>
|
|
||||||
<Button className="button" onClick={handleModal}>
|
|
||||||
使用Modal
|
|
||||||
</Button>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
3
src/pages/login/index.config.ts
Normal file
3
src/pages/login/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '登录',
|
||||||
|
};
|
||||||
367
src/pages/login/index.css
Normal file
367
src/pages/login/index.css
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
/* ============================================
|
||||||
|
登录页面样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../styles/variables.css';
|
||||||
|
|
||||||
|
page {
|
||||||
|
background: linear-gradient(180deg, #e8f0fe 0%, #f0f5ff 30%, #fafbfc 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 100px 32px 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部区域 */
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
animation: fadeInDown 0.6s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
border-radius: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
box-shadow: 0 8px 24px rgba(90, 147, 223, 0.3);
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单卡片 */
|
||||||
|
.login-form {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: 36px 28px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
animation: slideUp 0.6s ease 0.2s forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 88px;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0 24px;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 4px rgba(90, 147, 223, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--error-color);
|
||||||
|
margin-top: 8px;
|
||||||
|
display: block;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 验证码输入区域 */
|
||||||
|
.code-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
width: 140px;
|
||||||
|
height: 88px;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn[disabled],
|
||||||
|
.code-btn.disabled {
|
||||||
|
background: linear-gradient(135deg, #c5cede 0%, #b0bccf 100%);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 96px;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
border-radius: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
box-shadow: 0 8px 24px rgba(90, 147, 223, 0.3);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
box-shadow: 0 4px 12px rgba(90, 147, 223, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn[disabled],
|
||||||
|
.login-btn.disabled {
|
||||||
|
background: linear-gradient(135deg, #c5cede 0%, #b0bccf 100%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割线 */
|
||||||
|
.divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 36px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-text {
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 微信登录按钮 */
|
||||||
|
.wechat-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 96px;
|
||||||
|
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
border-radius: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8px 24px rgba(7, 193, 96, 0.3);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部链接 */
|
||||||
|
.login-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 昵称弹窗 */
|
||||||
|
.nickname-modal {
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: 48px 32px;
|
||||||
|
margin-top: 100px;
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
animation: modalIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subtitle {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 88px;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0 24px;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 4px rgba(90, 147, 223, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-nickname-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 96px;
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
border-radius: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8px 24px rgba(90, 147, 223, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-nickname-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 装饰元素 */
|
||||||
|
.login-page::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: -100px;
|
||||||
|
right: -100px;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
background: radial-gradient(circle, rgba(125, 179, 245, 0.15) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
bottom: -50px;
|
||||||
|
left: -50px;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
background: radial-gradient(circle, rgba(125, 179, 245, 0.1) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
232
src/pages/login/index.tsx
Normal file
232
src/pages/login/index.tsx
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import React, { useState, useCallback, useMemo } from "react";
|
||||||
|
import { View, Text, Button, Input } from "@tarojs/components";
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [phone, setPhone] = useState('');
|
||||||
|
const [code, setCode] = useState('');
|
||||||
|
const [countdown, setCountdown] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showNicknameModal, setShowNicknameModal] = useState(false);
|
||||||
|
const [nickname, setNickname] = useState('');
|
||||||
|
|
||||||
|
const handlePhoneInput = useCallback((e: any) => {
|
||||||
|
const value = e.detail.value;
|
||||||
|
const numericValue = value.replace(/\D/g, '');
|
||||||
|
const truncatedValue = numericValue.slice(0, 11);
|
||||||
|
setPhone(truncatedValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isPhoneValid = useMemo(() => {
|
||||||
|
return /^1[3-9]\d{9}$/.test(phone);
|
||||||
|
}, [phone]);
|
||||||
|
|
||||||
|
const getPhoneError = useMemo(() => {
|
||||||
|
if (phone.length === 0) return '';
|
||||||
|
if (phone.length < 11) return '请输入完整的11位手机号';
|
||||||
|
if (!/^1[3-9]/.test(phone)) return '手机号格式不正确';
|
||||||
|
return '';
|
||||||
|
}, [phone]);
|
||||||
|
|
||||||
|
const handleGetCode = useCallback(() => {
|
||||||
|
if (!phone) {
|
||||||
|
Taro.showToast({ title: '请输入手机号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isPhoneValid) {
|
||||||
|
Taro.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCountdown(60);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCountdown(prev => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
Taro.showToast({ title: '验证码已发送', icon: 'success' });
|
||||||
|
}, [phone, isPhoneValid]);
|
||||||
|
|
||||||
|
const handlePhoneLogin = useCallback(async () => {
|
||||||
|
if (!phone) {
|
||||||
|
Taro.showToast({ title: '请输入手机号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isPhoneValid) {
|
||||||
|
Taro.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!code) {
|
||||||
|
Taro.showToast({ title: '请输入验证码', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setShowNicknameModal(true);
|
||||||
|
} catch (error) {
|
||||||
|
Taro.showToast({ title: '登录失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [phone, isPhoneValid, code]);
|
||||||
|
|
||||||
|
const handleCreateNickname = useCallback(() => {
|
||||||
|
if (!nickname.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入用户名', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const userInfo = {
|
||||||
|
nickname: nickname.trim(),
|
||||||
|
avatar: '',
|
||||||
|
balance: 0,
|
||||||
|
};
|
||||||
|
Taro.setStorageSync('userInfo', userInfo);
|
||||||
|
Taro.showToast({ title: '登录成功', icon: 'success' });
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
}, [nickname]);
|
||||||
|
|
||||||
|
const handleWechatLogin = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const loginResult = await Taro.login();
|
||||||
|
console.log('微信登录 code:', loginResult.code);
|
||||||
|
|
||||||
|
let avatarUrl = '';
|
||||||
|
let nickName = '微信用户';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userInfoRes = await Taro.getUserProfile({
|
||||||
|
desc: '用于完善用户资料',
|
||||||
|
});
|
||||||
|
if (userInfoRes.userInfo) {
|
||||||
|
avatarUrl = userInfoRes.userInfo.avatarUrl;
|
||||||
|
nickName = userInfoRes.userInfo.nickName;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('游客模式获取用户信息受限,使用默认信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = {
|
||||||
|
nickname: nickName,
|
||||||
|
avatar: avatarUrl,
|
||||||
|
balance: 0,
|
||||||
|
};
|
||||||
|
Taro.setStorageSync('userInfo', userInfo);
|
||||||
|
Taro.showToast({ title: '登录成功', icon: 'success' });
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('微信登录失败:', error);
|
||||||
|
Taro.showToast({ title: '登录失败,请重试', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (showNicknameModal) {
|
||||||
|
return (
|
||||||
|
<View className='login-page'>
|
||||||
|
<View className='nickname-modal'>
|
||||||
|
<Text className='modal-title'>创建用户名</Text>
|
||||||
|
<Text className='modal-subtitle'>请设置一个方便识别的用户名</Text>
|
||||||
|
<Input
|
||||||
|
className='nickname-input'
|
||||||
|
placeholder='请输入用户名'
|
||||||
|
maxlength={20}
|
||||||
|
value={nickname}
|
||||||
|
onInput={(e) => setNickname(e.detail.value)}
|
||||||
|
/>
|
||||||
|
<Button className='confirm-btn' onClick={handleCreateNickname}>
|
||||||
|
完成
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='login-page'>
|
||||||
|
<View className='login-header'>
|
||||||
|
<Text className='login-title'>欢迎登录</Text>
|
||||||
|
<Text className='login-subtitle'>萝卜打印付费打印服务</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='login-form'>
|
||||||
|
<View className='form-item'>
|
||||||
|
<Text className='form-label'>手机号</Text>
|
||||||
|
<Input
|
||||||
|
className='form-input'
|
||||||
|
type='number'
|
||||||
|
placeholder='请输入手机号'
|
||||||
|
maxlength={11}
|
||||||
|
value={phone}
|
||||||
|
onInput={handlePhoneInput}
|
||||||
|
/>
|
||||||
|
{getPhoneError ? (
|
||||||
|
<Text className='form-error'>{getPhoneError}</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='form-item'>
|
||||||
|
<Text className='form-label'>验证码</Text>
|
||||||
|
<View className='code-input-wrapper'>
|
||||||
|
<Input
|
||||||
|
className='form-input code-input'
|
||||||
|
type='number'
|
||||||
|
placeholder='请输入验证码'
|
||||||
|
maxlength={6}
|
||||||
|
value={code}
|
||||||
|
onInput={(e) => setCode(e.detail.value.replace(/\D/g, ''))}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className={`code-btn ${!isPhoneValid ? 'disabled' : ''}`}
|
||||||
|
onClick={handleGetCode}
|
||||||
|
disabled={countdown > 0 || !isPhoneValid}
|
||||||
|
>
|
||||||
|
{countdown > 0 ? `${countdown}s` : '获取验证码'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='login-btn'
|
||||||
|
onClick={handlePhoneLogin}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<View className='divider'>
|
||||||
|
<View className='divider-line' />
|
||||||
|
<Text className='divider-text'>其他登录方式</Text>
|
||||||
|
<View className='divider-line' />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='wechat-btn'
|
||||||
|
onClick={handleWechatLogin}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
微信一键登录
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='login-footer'>
|
||||||
|
<Text className='footer-text'>登录即表示同意</Text>
|
||||||
|
<Text className='link-text'>《用户服务协议》</Text>
|
||||||
|
<Text className='footer-text'>和</Text>
|
||||||
|
<Text className='link-text'>《隐私政策》</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
3
src/pages/order/detail/index.config.ts
Normal file
3
src/pages/order/detail/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '订单详情',
|
||||||
|
};
|
||||||
10
src/pages/order/detail/index.css
Normal file
10
src/pages/order/detail/index.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
page {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-page {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
13
src/pages/order/detail/index.tsx
Normal file
13
src/pages/order/detail/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text } from "@tarojs/components";
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const OrderDetail = () => {
|
||||||
|
return (
|
||||||
|
<View className='detail-page'>
|
||||||
|
<Text>订单详情</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderDetail;
|
||||||
3
src/pages/order/index.config.ts
Normal file
3
src/pages/order/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '我的订单',
|
||||||
|
};
|
||||||
354
src/pages/order/index.css
Normal file
354
src/pages/order/index.css
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
/* ============================================
|
||||||
|
订单列表页面样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../styles/variables.css';
|
||||||
|
|
||||||
|
page {
|
||||||
|
background: linear-gradient(180deg, #e8f0fe 0%, #f0f5ff 30%, #fafbfc 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 32px 24px 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 */
|
||||||
|
.order-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
animation: fadeInDown 0.4s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单状态筛选 */
|
||||||
|
.status-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tab:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tab.active {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 4px 12px rgba(90, 147, 223, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单列表 */
|
||||||
|
.order-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
animation: slideUp 0.4s ease forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
.order-item:nth-child(2) { animation-delay: 0.15s; }
|
||||||
|
.order-item:nth-child(3) { animation-delay: 0.2s; }
|
||||||
|
|
||||||
|
.order-item:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单顶部 */
|
||||||
|
.order-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.pending {
|
||||||
|
background: rgba(250, 173, 20, 0.1);
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.processing {
|
||||||
|
background: rgba(24, 144, 255, 0.1);
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.completed {
|
||||||
|
background: rgba(82, 196, 26, 0.1);
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.cancelled {
|
||||||
|
background: rgba(255, 77, 79, 0.1);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单中部 */
|
||||||
|
.order-middle {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-name {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单底部 */
|
||||||
|
.order-bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-price {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-time {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100px 0;
|
||||||
|
animation: fadeIn 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 100px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单操作按钮 */
|
||||||
|
.order-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
border: none;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn.primary {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(90, 147, 223, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn.secondary {
|
||||||
|
background: #f5f5f7;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度指示器 */
|
||||||
|
.order-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step:last-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed::before {
|
||||||
|
background: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dot {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed .progress-dot {
|
||||||
|
background: var(--success-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.current .progress-dot {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
animation: pulse 2s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed .progress-label,
|
||||||
|
.progress-step.current .progress-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/pages/order/index.tsx
Normal file
90
src/pages/order/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { View, Text, Button } from "@tarojs/components";
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
interface OrderItem {
|
||||||
|
id: number;
|
||||||
|
fileName: string;
|
||||||
|
printerName: string;
|
||||||
|
printType: string;
|
||||||
|
colorType: string;
|
||||||
|
copies: number;
|
||||||
|
totalPrice: number;
|
||||||
|
status: string;
|
||||||
|
statusColor: string;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderIndex = () => {
|
||||||
|
const [orders] = useState<OrderItem[]>([
|
||||||
|
{
|
||||||
|
id: 10001,
|
||||||
|
fileName: '期末考试复习资料.docx',
|
||||||
|
printerName: '图书馆A打印机',
|
||||||
|
printType: 'A4',
|
||||||
|
colorType: '黑白',
|
||||||
|
copies: 1,
|
||||||
|
totalPrice: 0.10,
|
||||||
|
status: '已完成',
|
||||||
|
statusColor: '#52c41a',
|
||||||
|
time: '2026-03-30 10:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10002,
|
||||||
|
fileName: '毕业设计.pdf',
|
||||||
|
printerName: '图书馆B打印机',
|
||||||
|
printType: 'A4',
|
||||||
|
colorType: '彩色',
|
||||||
|
copies: 2,
|
||||||
|
totalPrice: 1.20,
|
||||||
|
status: '打印中',
|
||||||
|
statusColor: '#1890ff',
|
||||||
|
time: '2026-03-29 15:20'
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleViewDetail = (orderId: number) => {
|
||||||
|
Taro.navigateTo({
|
||||||
|
url: `/pages/order/detail/index?orderId=${orderId}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='order-page'>
|
||||||
|
<View className='order-header'>
|
||||||
|
<Text className='header-title'>我的订单</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{orders.length > 0 ? (
|
||||||
|
<View className='order-list'>
|
||||||
|
{orders.map(order => (
|
||||||
|
<View key={order.id} className='order-item' onClick={() => handleViewDetail(order.id)}>
|
||||||
|
<View className='order-top'>
|
||||||
|
<Text className='order-id'>订单号: {order.id}</Text>
|
||||||
|
<Text className='order-status' style={{ color: order.statusColor }}>{order.status}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='order-middle'>
|
||||||
|
<Text className='file-name'>{order.fileName}</Text>
|
||||||
|
<Text className='order-detail'>{order.printType} {order.colorType} × {order.copies}份</Text>
|
||||||
|
<Text className='printer-name'>📍 {order.printerName}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='order-bottom'>
|
||||||
|
<Text className='order-price'>¥{order.totalPrice.toFixed(2)}</Text>
|
||||||
|
<Text className='order-time'>{order.time}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className='empty-state'>
|
||||||
|
<Text className='empty-icon'>📋</Text>
|
||||||
|
<Text className='empty-text'>暂无订单</Text>
|
||||||
|
<Text className='empty-hint'>上传文件即可创建订单</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderIndex;
|
||||||
3
src/pages/print/confirm/index.config.ts
Normal file
3
src/pages/print/confirm/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '确认订单',
|
||||||
|
};
|
||||||
10
src/pages/print/confirm/index.css
Normal file
10
src/pages/print/confirm/index.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
page {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-page {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
13
src/pages/print/confirm/index.tsx
Normal file
13
src/pages/print/confirm/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text } from "@tarojs/components";
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const PrintConfirm = () => {
|
||||||
|
return (
|
||||||
|
<View className='confirm-page'>
|
||||||
|
<Text>确认订单</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrintConfirm;
|
||||||
3
src/pages/print/index.config.ts
Normal file
3
src/pages/print/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '打印设置',
|
||||||
|
};
|
||||||
10
src/pages/print/index.css
Normal file
10
src/pages/print/index.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
page {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-page {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
13
src/pages/print/index.tsx
Normal file
13
src/pages/print/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { View, Text } from "@tarojs/components";
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const PrintIndex = () => {
|
||||||
|
return (
|
||||||
|
<View className='print-page'>
|
||||||
|
<Text>打印设置</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrintIndex;
|
||||||
3
src/pages/user/index.config.ts
Normal file
3
src/pages/user/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '用户中心',
|
||||||
|
};
|
||||||
250
src/pages/user/index.css
Normal file
250
src/pages/user/index.css
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
/* ============================================
|
||||||
|
用户中心页面样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import '../../styles/variables.css';
|
||||||
|
|
||||||
|
page {
|
||||||
|
background: linear-gradient(180deg, #e8f0fe 0%, #f0f5ff 30%, #fafbfc 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户头部 */
|
||||||
|
.user-header {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
padding: 80px 32px 60px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0 0 40px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -30%;
|
||||||
|
right: -15%;
|
||||||
|
width: 350px;
|
||||||
|
height: 350px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20%;
|
||||||
|
left: -10%;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: float 8s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-15px) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info,
|
||||||
|
.login-tip {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像 */
|
||||||
|
.avatar {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.4);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-default {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息 */
|
||||||
|
.nickname {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录提示 */
|
||||||
|
.login-text {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tip:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单区域 */
|
||||||
|
.menu-section {
|
||||||
|
margin: 24px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 22px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-right: 16px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-value {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出登录 */
|
||||||
|
.logout-section {
|
||||||
|
padding: 0 24px;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--error-color);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
border-radius: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(255, 77, 79, 0.15);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单项图标容器 */
|
||||||
|
.menu-icon-wrapper {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon-wrapper.primary {
|
||||||
|
background: linear-gradient(135deg, #e8f0fe 0%, #d0dcfa 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon-wrapper.success {
|
||||||
|
background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon-wrapper.warning {
|
||||||
|
background: linear-gradient(135deg, #fffbe6 0%, #ffe58f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon-wrapper.error {
|
||||||
|
background: linear-gradient(135deg, #fff1f0 0%, #ffccc7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 余额高亮 */
|
||||||
|
.balance-highlight {
|
||||||
|
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
116
src/pages/user/index.tsx
Normal file
116
src/pages/user/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { View, Text, Button } from "@tarojs/components";
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserIndex = () => {
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkLoginStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
const storedUser = Taro.getStorageSync('userInfo');
|
||||||
|
if (storedUser) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
setUserInfo(storedUser);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ icon: '💰', title: '我的余额', value: userInfo ? `¥${userInfo.balance.toFixed(2)}` : '¥0.00', path: '' },
|
||||||
|
{ icon: '📁', title: '我的文件', value: '', path: '/pages/file/index' },
|
||||||
|
{ icon: '📋', title: '我的订单', value: '', path: '/pages/order/index' },
|
||||||
|
{ icon: '📖', title: '使用指南', value: '', path: '' },
|
||||||
|
{ icon: '❓', title: '帮助中心', value: '', path: '' },
|
||||||
|
{ icon: '⚙️', title: '设置', value: '', path: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleMenuClick = (path: string) => {
|
||||||
|
if (path) {
|
||||||
|
Taro.navigateTo({ url: path });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '确认退出',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
Taro.removeStorageSync('userInfo');
|
||||||
|
Taro.removeStorageSync('token');
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
setUserInfo(null);
|
||||||
|
Taro.showToast({ title: '已退出登录', icon: 'success' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='user-page'>
|
||||||
|
<View className='user-header'>
|
||||||
|
{isLoggedIn && userInfo ? (
|
||||||
|
<View className='user-info'>
|
||||||
|
<View className='avatar'>
|
||||||
|
{userInfo.avatar ? (
|
||||||
|
<View className='avatar-img-wrapper'>
|
||||||
|
<Text className='avatar-img-text' style={{fontSize: '0'}}>{userInfo.avatar}</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className='avatar-default'>
|
||||||
|
<Text className='avatar-text'>{userInfo.nickname[0]?.toUpperCase() || 'U'}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text className='nickname'>{userInfo.nickname}</Text>
|
||||||
|
<Text className='balance'>余额: ¥{userInfo.balance.toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className='login-tip' onClick={handleLogin}>
|
||||||
|
<View className='avatar-placeholder'>
|
||||||
|
<Text className='avatar-icon'>👤</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='login-text'>点击登录</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='menu-section'>
|
||||||
|
{menuItems.map((item, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
className='menu-item'
|
||||||
|
onClick={() => handleMenuClick(item.path)}
|
||||||
|
>
|
||||||
|
<Text className='menu-icon'>{item.icon}</Text>
|
||||||
|
<Text className='menu-title'>{item.title}</Text>
|
||||||
|
{item.value && item.value !== '¥0.00' && <Text className='menu-value'>{item.value}</Text>}
|
||||||
|
{!item.value && <Text className='menu-arrow'>›</Text>}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isLoggedIn && (
|
||||||
|
<View className='logout-section'>
|
||||||
|
<Button className='logout-btn' onClick={handleLogout}>退出登录</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserIndex;
|
||||||
5
src/store/hooks.ts
Normal file
5
src/store/hooks.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||||
|
import type { AppDispatch } from './index';
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<ReturnType<typeof import('./index').store.getState>> = useSelector;
|
||||||
17
src/store/index.ts
Normal file
17
src/store/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { userReducer } from './slices/userSlice';
|
||||||
|
import { orderReducer } from './slices/orderSlice';
|
||||||
|
import { fileReducer } from './slices/fileSlice';
|
||||||
|
import { printerReducer } from './slices/printerSlice';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
user: userReducer,
|
||||||
|
order: orderReducer,
|
||||||
|
file: fileReducer,
|
||||||
|
printer: printerReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
44
src/store/slices/fileSlice.ts
Normal file
44
src/store/slices/fileSlice.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { FileInfo } from '../../types';
|
||||||
|
|
||||||
|
interface FileState {
|
||||||
|
fileList: FileInfo[];
|
||||||
|
uploading: boolean;
|
||||||
|
uploadProgress: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: FileState = {
|
||||||
|
fileList: [],
|
||||||
|
uploading: false,
|
||||||
|
uploadProgress: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileSlice = createSlice({
|
||||||
|
name: 'file',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setFileList: (state, action: PayloadAction<{ files: FileInfo[]; total: number }>) => {
|
||||||
|
state.fileList = action.payload.files;
|
||||||
|
state.total = action.payload.total;
|
||||||
|
},
|
||||||
|
addFile: (state, action: PayloadAction<FileInfo>) => {
|
||||||
|
state.fileList.unshift(action.payload);
|
||||||
|
state.total += 1;
|
||||||
|
},
|
||||||
|
removeFile: (state, action: PayloadAction<number>) => {
|
||||||
|
state.fileList = state.fileList.filter(f => f.id !== action.payload);
|
||||||
|
state.total -= 1;
|
||||||
|
},
|
||||||
|
setUploading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.uploading = action.payload;
|
||||||
|
},
|
||||||
|
setUploadProgress: (state, action: PayloadAction<number>) => {
|
||||||
|
state.uploadProgress = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setFileList, addFile, removeFile, setUploading, setUploadProgress } = fileSlice.actions;
|
||||||
|
export const fileReducer = fileSlice.reducer;
|
||||||
49
src/store/slices/orderSlice.ts
Normal file
49
src/store/slices/orderSlice.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { Order } from '../../types';
|
||||||
|
|
||||||
|
interface OrderState {
|
||||||
|
currentOrder: Order | null;
|
||||||
|
orderList: Order[];
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: OrderState = {
|
||||||
|
currentOrder: null,
|
||||||
|
orderList: [],
|
||||||
|
total: 0,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const orderSlice = createSlice({
|
||||||
|
name: 'order',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setCurrentOrder: (state, action: PayloadAction<Order | null>) => {
|
||||||
|
state.currentOrder = action.payload;
|
||||||
|
},
|
||||||
|
setOrderList: (state, action: PayloadAction<{ orders: Order[]; total: number }>) => {
|
||||||
|
state.orderList = action.payload.orders;
|
||||||
|
state.total = action.payload.total;
|
||||||
|
},
|
||||||
|
addOrder: (state, action: PayloadAction<Order>) => {
|
||||||
|
state.orderList.unshift(action.payload);
|
||||||
|
state.total += 1;
|
||||||
|
},
|
||||||
|
updateOrder: (state, action: PayloadAction<Order>) => {
|
||||||
|
const index = state.orderList.findIndex(o => o.id === action.payload.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.orderList[index] = action.payload;
|
||||||
|
}
|
||||||
|
if (state.currentOrder?.id === action.payload.id) {
|
||||||
|
state.currentOrder = action.payload;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setCurrentOrder, setOrderList, addOrder, updateOrder, setLoading } = orderSlice.actions;
|
||||||
|
export const orderReducer = orderSlice.reducer;
|
||||||
42
src/store/slices/printerSlice.ts
Normal file
42
src/store/slices/printerSlice.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { Printer } from '../../types';
|
||||||
|
|
||||||
|
interface PrinterState {
|
||||||
|
list: Printer[];
|
||||||
|
selected: Printer | null;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: PrinterState = {
|
||||||
|
list: [],
|
||||||
|
selected: null,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printerSlice = createSlice({
|
||||||
|
name: 'printer',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setPrinterList: (state, action: PayloadAction<Printer[]>) => {
|
||||||
|
state.list = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedPrinter: (state, action: PayloadAction<Printer | null>) => {
|
||||||
|
state.selected = action.payload;
|
||||||
|
},
|
||||||
|
updatePrinter: (state, action: PayloadAction<Printer>) => {
|
||||||
|
const index = state.list.findIndex(p => p.id === action.payload.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.list[index] = action.payload;
|
||||||
|
}
|
||||||
|
if (state.selected?.id === action.payload.id) {
|
||||||
|
state.selected = action.payload;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setPrinterList, setSelectedPrinter, updatePrinter, setLoading } = printerSlice.actions;
|
||||||
|
export const printerReducer = printerSlice.reducer;
|
||||||
51
src/store/slices/userSlice.ts
Normal file
51
src/store/slices/userSlice.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { UserInfo } from '../../types';
|
||||||
|
|
||||||
|
interface UserState {
|
||||||
|
info: UserInfo | null;
|
||||||
|
token: string | null;
|
||||||
|
refreshToken: string | null;
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: UserState = {
|
||||||
|
info: null,
|
||||||
|
token: null,
|
||||||
|
refreshToken: null,
|
||||||
|
isLoggedIn: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userSlice = createSlice({
|
||||||
|
name: 'user',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setUser: (state, action: PayloadAction<{ user: UserInfo; token: string; refreshToken: string }>) => {
|
||||||
|
state.info = action.payload.user;
|
||||||
|
state.token = action.payload.token;
|
||||||
|
state.refreshToken = action.payload.refreshToken;
|
||||||
|
state.isLoggedIn = true;
|
||||||
|
},
|
||||||
|
updateUserInfo: (state, action: PayloadAction<Partial<UserInfo>>) => {
|
||||||
|
if (state.info) {
|
||||||
|
state.info = { ...state.info, ...action.payload };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setToken: (state, action: PayloadAction<string>) => {
|
||||||
|
state.token = action.payload;
|
||||||
|
},
|
||||||
|
logout: (state) => {
|
||||||
|
state.info = null;
|
||||||
|
state.token = null;
|
||||||
|
state.refreshToken = null;
|
||||||
|
state.isLoggedIn = false;
|
||||||
|
},
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setUser, updateUserInfo, setToken, logout, setLoading } = userSlice.actions;
|
||||||
|
export const userReducer = userSlice.reducer;
|
||||||
470
src/styles/animations.css
Normal file
470
src/styles/animations.css
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
/* ============================================
|
||||||
|
微交互和动画效果 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import './variables.css';
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
进入动画
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 淡入 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入上浮 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入下沉 */
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 从左淡入 */
|
||||||
|
@keyframes fadeInLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 从右淡入 */
|
||||||
|
@keyframes fadeInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缩放淡入 */
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗动画 */
|
||||||
|
@keyframes modalIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗关闭 */
|
||||||
|
@keyframes modalOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
持续动画
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 脉冲 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 轻微脉冲 */
|
||||||
|
@keyframes pulseSoft {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 闪烁 */
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转 */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 摇摆 */
|
||||||
|
@keyframes swing {
|
||||||
|
0%, 100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(3deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(-3deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮动 */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹跳 */
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 光泽扫过 */
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条动画 */
|
||||||
|
@keyframes progressStripes {
|
||||||
|
from {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-position: 40px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
交互动画类
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 点击反馈 */
|
||||||
|
.tap-feedback {
|
||||||
|
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tap-feedback:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停反馈(用于支持hover的设备) */
|
||||||
|
@media (hover: hover) {
|
||||||
|
.hover-elevate {
|
||||||
|
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-elevate:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片进入动画 */
|
||||||
|
.animate-card-enter {
|
||||||
|
animation: fadeInUp 0.4s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表项动画延迟 */
|
||||||
|
.animate-stagger-1 { animation-delay: 0.05s; }
|
||||||
|
.animate-stagger-2 { animation-delay: 0.1s; }
|
||||||
|
.animate-stagger-3 { animation-delay: 0.15s; }
|
||||||
|
.animate-stagger-4 { animation-delay: 0.2s; }
|
||||||
|
.animate-stagger-5 { animation-delay: 0.25s; }
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
加载状态动画
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 加载旋转器 */
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid var(--border-light);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner.small {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner.large {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏动画 */
|
||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#f0f0f0 25%,
|
||||||
|
#e8e8e8 50%,
|
||||||
|
#f0f0f0 75%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
height: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.short {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-title {
|
||||||
|
height: 24px;
|
||||||
|
width: 40%;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-button {
|
||||||
|
height: 44px;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
状态指示器动画
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 成功动画 */
|
||||||
|
@keyframes successCheck {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 100;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误抖动 */
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
10%, 30%, 50%, 70%, 90% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
20%, 40%, 60%, 80% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通知弹跳 */
|
||||||
|
@keyframes notificationBounce {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
实用动画类
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fadeInUp 0.4s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-down {
|
||||||
|
animation: fadeInDown 0.4s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scale-in {
|
||||||
|
animation: scaleIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-soft {
|
||||||
|
animation: pulseSoft 3s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-blink {
|
||||||
|
animation: blink 1s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-swing {
|
||||||
|
animation: swing 1s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-bounce {
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shake {
|
||||||
|
animation: shake 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(255, 255, 255, 0.4) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
过渡效果
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.transition-all {
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-fast {
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-slow {
|
||||||
|
transition: all var(--transition-slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-opacity {
|
||||||
|
transition: opacity var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-transform {
|
||||||
|
transition: transform var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-colors {
|
||||||
|
transition: color var(--transition-base), background-color var(--transition-base), border-color var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
页面切换动画
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.page-enter {
|
||||||
|
animation: fadeInRight 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-leave {
|
||||||
|
animation: fadeInLeft 0.3s ease reverse forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框动画 */
|
||||||
|
.modal-overlay-enter {
|
||||||
|
animation: fadeIn 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-enter {
|
||||||
|
animation: modalIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay-leave {
|
||||||
|
animation: fadeIn 0.2s ease reverse forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-leave {
|
||||||
|
animation: modalOut 0.2s ease forwards;
|
||||||
|
}
|
||||||
325
src/styles/common.css
Normal file
325
src/styles/common.css
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
/* ============================================
|
||||||
|
全局通用样式 - 爱印通
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@import './variables.css';
|
||||||
|
|
||||||
|
/* 重置样式 */
|
||||||
|
page {
|
||||||
|
background: var(--bg-page);
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
|
||||||
|
'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用容器 */
|
||||||
|
.container {
|
||||||
|
padding: 0 var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用卡片 */
|
||||||
|
.common-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用按钮 */
|
||||||
|
.common-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-btn.primary {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-btn.outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-btn.ghost {
|
||||||
|
background: rgba(90, 147, 223, 0.1);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-btn.danger {
|
||||||
|
background: linear-gradient(135deg, #ff7875 0%, #ff4d4f 100%);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-btn.disabled,
|
||||||
|
.common-btn[disabled] {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用输入框 */
|
||||||
|
.common-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 88px;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0 var(--spacing-md);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(90, 147, 223, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-input::placeholder {
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用标签 */
|
||||||
|
.common-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-tag.primary {
|
||||||
|
background: rgba(90, 147, 223, 0.1);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-tag.success {
|
||||||
|
background: rgba(82, 196, 26, 0.1);
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-tag.warning {
|
||||||
|
background: rgba(250, 173, 20, 0.1);
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-tag.error {
|
||||||
|
background: rgba(255, 77, 79, 0.1);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-2xl) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-icon {
|
||||||
|
font-size: 120px;
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-text {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-hint {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-2xl) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--border-light);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割线 */
|
||||||
|
.divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: var(--spacing-lg) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-text {
|
||||||
|
padding: 0 var(--spacing-md);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本样式 */
|
||||||
|
.text-primary {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-tertiary {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-placeholder {
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-brand {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 字体大小 */
|
||||||
|
.text-xs {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-base {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-md {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-lg {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xl {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 字体粗细 */
|
||||||
|
.font-normal {
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-medium {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-bold {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 间距工具类 */
|
||||||
|
.mt-xs { margin-top: var(--spacing-xs); }
|
||||||
|
.mt-sm { margin-top: var(--spacing-sm); }
|
||||||
|
.mt-md { margin-top: var(--spacing-md); }
|
||||||
|
.mt-lg { margin-top: var(--spacing-lg); }
|
||||||
|
.mt-xl { margin-top: var(--spacing-xl); }
|
||||||
|
|
||||||
|
.mb-xs { margin-bottom: var(--spacing-xs); }
|
||||||
|
.mb-sm { margin-bottom: var(--spacing-sm); }
|
||||||
|
.mb-md { margin-bottom: var(--spacing-md); }
|
||||||
|
.mb-lg { margin-bottom: var(--spacing-lg); }
|
||||||
|
.mb-xl { margin-bottom: var(--spacing-xl); }
|
||||||
|
|
||||||
|
.p-xs { padding: var(--spacing-xs); }
|
||||||
|
.p-sm { padding: var(--spacing-sm); }
|
||||||
|
.p-md { padding: var(--spacing-md); }
|
||||||
|
.p-lg { padding: var(--spacing-lg); }
|
||||||
|
.p-xl { padding: var(--spacing-xl); }
|
||||||
|
|
||||||
|
/* Flex布局 */
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 截断文本 */
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 安全区域 */
|
||||||
|
.safe-area-bottom {
|
||||||
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
196
src/styles/variables.css
Normal file
196
src/styles/variables.css
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/* ============================================
|
||||||
|
全局主题变量 - 爱印通设计系统
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* 主色调 - 科技蓝 */
|
||||||
|
--primary-color: #5a93df;
|
||||||
|
--primary-light: #7db3f5;
|
||||||
|
--primary-dark: #4a7bc0;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #7db3f5 0%, #5a93df 50%, #4a7bc0 100%);
|
||||||
|
--primary-gradient-light: linear-gradient(135deg, #8ec5f8 0%, #7db3f5 100%);
|
||||||
|
|
||||||
|
/* 辅助色 */
|
||||||
|
--success-color: #52c41a;
|
||||||
|
--success-light: #73d13d;
|
||||||
|
--warning-color: #faad14;
|
||||||
|
--warning-light: #ffc53d;
|
||||||
|
--error-color: #ff4d4f;
|
||||||
|
--error-light: #ff7875;
|
||||||
|
--info-color: #1890ff;
|
||||||
|
|
||||||
|
/* 中性色 */
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #555566;
|
||||||
|
--text-tertiary: #888899;
|
||||||
|
--text-placeholder: #b0b0c0;
|
||||||
|
--text-inverse: #ffffff;
|
||||||
|
|
||||||
|
/* 背景色 */
|
||||||
|
--bg-page: #f5f7fa;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--bg-hover: #f0f4f8;
|
||||||
|
--bg-overlay: rgba(0, 0, 0, 0.45);
|
||||||
|
|
||||||
|
/* 边框色 */
|
||||||
|
--border-primary: #e5e8f0;
|
||||||
|
--border-light: #f0f2f7;
|
||||||
|
--border-focus: #7db3f5;
|
||||||
|
|
||||||
|
/* 阴影 */
|
||||||
|
--shadow-sm: 0 2px 8px rgba(90, 147, 223, 0.06);
|
||||||
|
--shadow-md: 0 4px 16px rgba(90, 147, 223, 0.1);
|
||||||
|
--shadow-lg: 0 8px 32px rgba(90, 147, 223, 0.15);
|
||||||
|
--shadow-xl: 0 12px 48px rgba(90, 147, 223, 0.2);
|
||||||
|
|
||||||
|
/* 圆角 */
|
||||||
|
--radius-xs: 8px;
|
||||||
|
--radius-sm: 12px;
|
||||||
|
--radius-md: 16px;
|
||||||
|
--radius-lg: 20px;
|
||||||
|
--radius-xl: 24px;
|
||||||
|
--radius-full: 50%;
|
||||||
|
|
||||||
|
/* 间距 */
|
||||||
|
--spacing-xs: 8px;
|
||||||
|
--spacing-sm: 12px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
--spacing-2xl: 48px;
|
||||||
|
|
||||||
|
/* 字体 */
|
||||||
|
--font-size-xs: 20px;
|
||||||
|
--font-size-sm: 22px;
|
||||||
|
--font-size-base: 24px;
|
||||||
|
--font-size-md: 26px;
|
||||||
|
--font-size-lg: 28px;
|
||||||
|
--font-size-xl: 30px;
|
||||||
|
--font-size-2xl: 34px;
|
||||||
|
--font-size-3xl: 40px;
|
||||||
|
--font-size-4xl: 48px;
|
||||||
|
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
--transition-fast: 0.15s ease;
|
||||||
|
--transition-base: 0.25s ease;
|
||||||
|
--transition-slow: 0.4s ease;
|
||||||
|
|
||||||
|
/* 模糊 */
|
||||||
|
--blur-sm: 8px;
|
||||||
|
--blur-md: 16px;
|
||||||
|
--blur-lg: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面背景渐变 */
|
||||||
|
.bg-gradient-page {
|
||||||
|
background: linear-gradient(180deg, #eef5ff 0%, #f5f7fa 50%, #fafbfc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主色渐变背景 */
|
||||||
|
.bg-gradient-primary {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 玻璃态效果 */
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片基础样式 */
|
||||||
|
.card-base {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮基础样式 */
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框基础样式 */
|
||||||
|
.input-base {
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-base:focus {
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(90, 147, 223, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画关键帧 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画类 */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.4s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slideUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s ease infinite;
|
||||||
|
}
|
||||||
20
src/types/file.ts
Normal file
20
src/types/file.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export interface FileInfo {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
fileName: string;
|
||||||
|
fileSize: number;
|
||||||
|
fileType: string;
|
||||||
|
fileUrl: string;
|
||||||
|
watermarkUrl?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadFileResponse {
|
||||||
|
fileId: number;
|
||||||
|
fileUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileListResponse {
|
||||||
|
files: FileInfo[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
16
src/types/index.ts
Normal file
16
src/types/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageParams {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './user';
|
||||||
|
export * from './order';
|
||||||
|
export * from './file';
|
||||||
|
export * from './printer';
|
||||||
|
export * from './price';
|
||||||
48
src/types/order.ts
Normal file
48
src/types/order.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export type OrderStatus = 0 | 1 | 2 | 3 | 4;
|
||||||
|
export type PayStatus = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
fileId: number;
|
||||||
|
fileName: string;
|
||||||
|
printerId: number;
|
||||||
|
printerName: string;
|
||||||
|
printType: 'A4' | 'A3' | '照片';
|
||||||
|
colorType: '黑白' | '彩色';
|
||||||
|
copies: number;
|
||||||
|
pages: number;
|
||||||
|
totalPrice: number;
|
||||||
|
orderStatus: OrderStatus;
|
||||||
|
payStatus: PayStatus;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOrderRequest {
|
||||||
|
fileId: number;
|
||||||
|
printerId: number;
|
||||||
|
printType: 'A4' | 'A3' | '照片';
|
||||||
|
colorType: '黑白' | '彩色';
|
||||||
|
copies: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderListResponse {
|
||||||
|
orders: Order[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderStatusMap: Record<OrderStatus, string> = {
|
||||||
|
0: '待支付',
|
||||||
|
1: '已支付',
|
||||||
|
2: '打印中',
|
||||||
|
3: '已完成',
|
||||||
|
4: '已取消',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PayStatusMap: Record<PayStatus, string> = {
|
||||||
|
0: '待支付',
|
||||||
|
1: '支付中',
|
||||||
|
2: '成功',
|
||||||
|
3: '失败',
|
||||||
|
};
|
||||||
17
src/types/price.ts
Normal file
17
src/types/price.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export type PriceType = 'A4' | 'A3' | '照片';
|
||||||
|
|
||||||
|
export interface PriceItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: PriceType;
|
||||||
|
price: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultPriceList: PriceItem[] = [
|
||||||
|
{ id: 1, name: 'A4黑白打印', type: 'A4', price: 0.10, unit: '页' },
|
||||||
|
{ id: 2, name: 'A4彩色打印', type: 'A4', price: 0.30, unit: '页' },
|
||||||
|
{ id: 3, name: 'A3黑白打印', type: 'A3', price: 0.20, unit: '页' },
|
||||||
|
{ id: 4, name: 'A3彩色打印', type: 'A3', price: 0.60, unit: '页' },
|
||||||
|
{ id: 5, name: '照片打印', type: '照片', price: 2.00, unit: '张' },
|
||||||
|
];
|
||||||
15
src/types/printer.ts
Normal file
15
src/types/printer.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export type PrinterStatus = 0 | 1 | 2;
|
||||||
|
|
||||||
|
export interface Printer {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
location: string;
|
||||||
|
status: PrinterStatus;
|
||||||
|
isAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrinterStatusMap: Record<PrinterStatus, string> = {
|
||||||
|
0: '空闲',
|
||||||
|
1: '打印中',
|
||||||
|
2: '离线',
|
||||||
|
};
|
||||||
19
src/types/user.ts
Normal file
19
src/types/user.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export interface UserInfo {
|
||||||
|
id: number;
|
||||||
|
openid: string;
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
balance: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
userInfo: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserRequest {
|
||||||
|
nickname?: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
28
src/utils/format.ts
Normal file
28
src/utils/format.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export const formatPrice = (price: number): string => {
|
||||||
|
return `¥${price.toFixed(2)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (dateString: string): string => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDateShort = (dateString: string): string => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${month}-${day}`;
|
||||||
|
};
|
||||||
67
src/utils/request.ts
Normal file
67
src/utils/request.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { store } from '../store';
|
||||||
|
import { logout } from '../store/slices/userSlice';
|
||||||
|
import { API_BASE_URL } from '../config/api';
|
||||||
|
|
||||||
|
interface RequestOptions {
|
||||||
|
url: string;
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
data?: any;
|
||||||
|
header?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const request = async <T>(options: RequestOptions): Promise<T> => {
|
||||||
|
const { url, method = 'GET', data, header = {} } = options;
|
||||||
|
|
||||||
|
const token = store.getState().user.token;
|
||||||
|
if (token) {
|
||||||
|
header['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await Taro.request({
|
||||||
|
url: `${API_BASE_URL}${url}`,
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...header,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
return response.data as T;
|
||||||
|
} else if (response.statusCode === 401) {
|
||||||
|
store.dispatch(logout());
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data?.message || 'Request failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadFile = <T>(filePath: string, onProgress?: (progress: number) => void): Promise<T> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const token = store.getState().user.token;
|
||||||
|
|
||||||
|
Taro.uploadFile({
|
||||||
|
url: `${API_BASE_URL}/file/upload`,
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
header: {
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve(JSON.parse(res.data) as T);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Upload failed'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: reject,
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
};
|
||||||
34
src/utils/storage.ts
Normal file
34
src/utils/storage.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
|
||||||
|
const USER_TOKEN_KEY = 'user_token';
|
||||||
|
const USER_INFO_KEY = 'user_info';
|
||||||
|
|
||||||
|
export const storage = {
|
||||||
|
setToken: (token: string) => {
|
||||||
|
Taro.setStorageSync(USER_TOKEN_KEY, token);
|
||||||
|
},
|
||||||
|
|
||||||
|
getToken: (): string => {
|
||||||
|
return Taro.getStorageSync(USER_TOKEN_KEY) || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
removeToken: () => {
|
||||||
|
Taro.removeStorageSync(USER_TOKEN_KEY);
|
||||||
|
},
|
||||||
|
|
||||||
|
setUserInfo: (info: any) => {
|
||||||
|
Taro.setStorageSync(USER_INFO_KEY, info);
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserInfo: () => {
|
||||||
|
return Taro.getStorageSync(USER_INFO_KEY);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeUserInfo: () => {
|
||||||
|
Taro.removeStorageSync(USER_INFO_KEY);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll: () => {
|
||||||
|
Taro.clearStorageSync();
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user