diff --git a/.eslintrc.js b/.eslintrc.js index 7809e66..465774b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ -{ - "extends": ["taro/react"], - "rules": { - "react/jsx-uses-react": "off", - "react/react-in-jsx-scope": "off" - } -} +module.exports = { + extends: ['taro/react'], + rules: { + 'react/jsx-uses-react': 'off', + 'react/react-in-jsx-scope': 'off', + }, +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8e8864 --- /dev/null +++ b/README.md @@ -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`) diff --git a/package-lock.json b/package-lock.json index 0afb8c0..95fe96f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@babel/runtime": "^7.7.7", + "@reduxjs/toolkit": "^2.11.2", "@taro-hooks/plugin-react": "2", "@taro-hooks/shared": "2", "@tarojs/components": "4.1.11", @@ -27,6 +28,8 @@ "@tarojs/taro": "4.1.11", "react": "^18.0.0", "react-dom": "^18.0.0", + "react-redux": "^9.2.0", + "redux": "^5.0.1", "taro-hooks": "2" }, "devDependencies": { @@ -3184,6 +3187,32 @@ "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": { "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", @@ -3361,6 +3390,18 @@ "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": { "version": "2.22.3", "resolved": "https://registry.npmmirror.com/@stencil/core/-/core-2.22.3.tgz", @@ -5254,6 +5295,12 @@ "dev": true, "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": { "version": "6.21.0", "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -9572,6 +9619,16 @@ "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": { "version": "5.1.5", "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz", @@ -12355,6 +12412,29 @@ "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": { "version": "0.11.0", "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.11.0.tgz", @@ -12586,6 +12666,21 @@ "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": { "version": "1.0.10", "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12750,6 +12845,12 @@ "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": { "version": "1.22.11", "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", @@ -14662,6 +14763,15 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index f6409be..c799750 100644 --- a/package.json +++ b/package.json @@ -36,47 +36,50 @@ "author": "", "dependencies": { "@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/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-tt": "4.1.11", - "@tarojs/plugin-platform-swan": "4.1.11", + "@tarojs/plugin-platform-h5": "4.1.11", "@tarojs/plugin-platform-jd": "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/shared": "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-dom": "^18.0.0", + "react-redux": "^9.2.0", + "redux": "^5.0.1", "taro-hooks": "2" }, "devDependencies": { - "babel-plugin-import": "^1.13.3", + "@babel/core": "^7.8.0", "@babel/plugin-proposal-class-properties": "7.14.5", + "@babel/preset-react": "^7.24.1", "@tarojs/cli": "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", + "@typescript-eslint/eslint-plugin": "^6.2.0", + "@typescript-eslint/parser": "^6.2.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-react": "^7.8.2", "eslint-plugin-react-hooks": "^4.2.0", "react-refresh": "^0.11.0", - "@typescript-eslint/parser": "^6.2.0", - "@typescript-eslint/eslint-plugin": "^6.2.0", + "stylelint": "^14.4.0", + "terser": "^5.16.8", "typescript": "^5.1.0", - "@babel/core": "^7.8.0" + "vite": "^4.2.0" } } diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..9b4e94f --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,34 @@ +import { request } from '../utils/request'; +import type { UserInfo, LoginResponse, UpdateUserRequest } from '../types'; + +export const authApi = { + login: (code: string) => { + return request({ + url: '/auth/login', + method: 'POST', + data: { code }, + }); + }, + + refreshToken: (refreshToken: string) => { + return request<{ accessToken: string }>({ + url: '/auth/refresh', + method: 'POST', + data: { refreshToken }, + }); + }, + + getUserInfo: () => { + return request({ + url: '/auth/userinfo', + }); + }, + + updateUser: (data: UpdateUserRequest) => { + return request({ + url: '/auth/update', + method: 'POST', + data, + }); + }, +}; diff --git a/src/api/file.ts b/src/api/file.ts new file mode 100644 index 0000000..b689f06 --- /dev/null +++ b/src/api/file.ts @@ -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 => { + 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({ + url: `/file/${fileId}`, + }); + }, + + deleteFile: (fileId: number) => { + return request({ + url: `/file/${fileId}`, + method: 'DELETE', + }); + }, + + getFileList: (page = 1, pageSize = 20) => { + return request({ + url: '/file/list', + data: { page, pageSize }, + }); + }, + + addWatermark: (fileId: number, watermarkText: string) => { + return request<{ watermarkUrl: string }>({ + url: `/file/${fileId}/watermark`, + method: 'POST', + data: { watermark: watermarkText }, + }); + }, +}; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..e804d6d --- /dev/null +++ b/src/api/index.ts @@ -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'; diff --git a/src/api/order.ts b/src/api/order.ts new file mode 100644 index 0000000..34734ab --- /dev/null +++ b/src/api/order.ts @@ -0,0 +1,40 @@ +import { request } from '../utils/request'; +import type { Order, CreateOrderRequest, OrderListResponse } from '../types'; + +export const orderApi = { + createOrder: (data: CreateOrderRequest) => { + return request({ + url: '/order/create', + method: 'POST', + data, + }); + }, + + getOrder: (orderId: number) => { + return request({ + url: `/order/${orderId}`, + }); + }, + + getOrderList: (page = 1, pageSize = 20) => { + return request({ + url: '/order/list', + data: { page, pageSize }, + }); + }, + + updateOrderStatus: (orderId: number, status: number) => { + return request({ + url: `/order/${orderId}/status`, + method: 'PUT', + data: { status }, + }); + }, + + cancelOrder: (orderId: number) => { + return request({ + url: `/order/${orderId}/cancel`, + method: 'PUT', + }); + }, +}; diff --git a/src/api/pay.ts b/src/api/pay.ts new file mode 100644 index 0000000..d5e8a5f --- /dev/null +++ b/src/api/pay.ts @@ -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 }, + }); + }, +}; diff --git a/src/api/price.ts b/src/api/price.ts new file mode 100644 index 0000000..90a2e8e --- /dev/null +++ b/src/api/price.ts @@ -0,0 +1,26 @@ +import { request } from '../utils/request'; +import type { PriceItem } from '../types'; + +export const priceApi = { + getPriceList: () => { + return request({ + url: '/price/list', + }); + }, + + createPriceItem: (data: Omit) => { + return request({ + url: '/price/create', + method: 'POST', + data, + }); + }, + + updatePriceItem: (id: number, data: Partial) => { + return request({ + url: `/price/${id}`, + method: 'PUT', + data, + }); + }, +}; diff --git a/src/api/printer.ts b/src/api/printer.ts new file mode 100644 index 0000000..57846e9 --- /dev/null +++ b/src/api/printer.ts @@ -0,0 +1,39 @@ +import { request } from '../utils/request'; +import type { Printer } from '../types'; + +export const printerApi = { + getPrinters: () => { + return request({ + url: '/print/printers', + }); + }, + + getPrinterStatus: (printerId: number) => { + return request({ + url: `/print/printer/${printerId}/status`, + }); + }, + + registerPrinter: (name: string, location: string) => { + return request({ + 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({ + url: `/print/job/${jobId}/cancel`, + method: 'PUT', + }); + }, +}; diff --git a/src/app.config.ts b/src/app.config.ts index 3bfb426..5ea49b8 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,9 +1,19 @@ 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: { backgroundTextStyle: "light", - navigationBarBackgroundColor: "#fff", - navigationBarTitleText: "WeChat", - navigationBarTextStyle: "black", + navigationBarBackgroundColor: "#1890ff", + navigationBarTitleText: "萝卜打印", + navigationBarTextStyle: "white", }, }; diff --git a/src/app.css b/src/app.css index e69de29..d7495de 100644 --- a/src/app.css +++ b/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; + } +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index f71f47c..9dfdb7e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,4 @@ import { Component } from 'react' - import type { PropsWithChildren } from 'react' import './app.css' @@ -12,11 +11,9 @@ class App extends Component { componentDidHide() { } - // this.props.children 是将要会渲染的页面 render() { return this.props.children } } - export default App diff --git a/src/components/OrderCard/OrderCard.css b/src/components/OrderCard/OrderCard.css new file mode 100644 index 0000000..bda989c --- /dev/null +++ b/src/components/OrderCard/OrderCard.css @@ -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; +} diff --git a/src/components/OrderCard/index.ts b/src/components/OrderCard/index.ts new file mode 100644 index 0000000..12d7bd7 --- /dev/null +++ b/src/components/OrderCard/index.ts @@ -0,0 +1 @@ +export { OrderCard } from './index.tsx'; diff --git a/src/components/OrderCard/index.tsx b/src/components/OrderCard/index.tsx new file mode 100644 index 0000000..576ca4b --- /dev/null +++ b/src/components/OrderCard/index.tsx @@ -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 = ({ order, onClick }) => { + return ( + + + 订单号: {order.id} + {OrderStatusMap[order.orderStatus as keyof typeof OrderStatusMap]} + + + {order.fileName} + + {order.printType} {order.colorType} x {order.copies}份 + + 打印机: {order.printerName} + + + {formatPrice(order.totalPrice)} + {formatDate(order.createdAt)} + + + ); +}; diff --git a/src/components/PriceTable/PriceTable.css b/src/components/PriceTable/PriceTable.css new file mode 100644 index 0000000..f60caac --- /dev/null +++ b/src/components/PriceTable/PriceTable.css @@ -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; +} diff --git a/src/components/PriceTable/index.ts b/src/components/PriceTable/index.ts new file mode 100644 index 0000000..2366db9 --- /dev/null +++ b/src/components/PriceTable/index.ts @@ -0,0 +1 @@ +export { PriceTable } from './index'; diff --git a/src/components/PriceTable/index.tsx b/src/components/PriceTable/index.tsx new file mode 100644 index 0000000..2a9d997 --- /dev/null +++ b/src/components/PriceTable/index.tsx @@ -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 = ({ + prices = DefaultPriceList, + selectedType, + onSelect +}) => { + return ( + + 价目表 + + {prices.map(item => ( + onSelect?.(item)} + > + {item.name} + {formatPrice(item.price)}/{item.unit} + + ))} + + + ); +}; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..5aa8c9d --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,2 @@ +export { OrderCard } from './OrderCard'; +export { PriceTable } from './PriceTable'; diff --git a/src/config/api.ts b/src/config/api.ts new file mode 100644 index 0000000..efdb534 --- /dev/null +++ b/src/config/api.ts @@ -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; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..d9ae720 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useAuth'; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..efdff9b --- /dev/null +++ b/src/hooks/useAuth.ts @@ -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, + }; +}; diff --git a/src/pages/file/index.config.ts b/src/pages/file/index.config.ts new file mode 100644 index 0000000..615be7e --- /dev/null +++ b/src/pages/file/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '我的文件', +}; diff --git a/src/pages/file/index.css b/src/pages/file/index.css new file mode 100644 index 0000000..41532a6 --- /dev/null +++ b/src/pages/file/index.css @@ -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); + } +} diff --git a/src/pages/file/index.tsx b/src/pages/file/index.tsx new file mode 100644 index 0000000..078ad30 --- /dev/null +++ b/src/pages/file/index.tsx @@ -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([ + { 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 ( + + + 我的文件 + + + + {files.length > 0 ? ( + + {files.map(file => ( + + {getFileIcon(file.type)} + + {file.name} + {file.size} · {file.time} + + + handlePrint(file)}>打印 + handleDelete(file.id)}>删除 + + + ))} + + ) : ( + + 📂 + 暂无文件 + 点击上方按钮上传文件 + + )} + + ); +}; + +export default FileIndex; diff --git a/src/pages/file/upload/index.config.ts b/src/pages/file/upload/index.config.ts new file mode 100644 index 0000000..a90cd93 --- /dev/null +++ b/src/pages/file/upload/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '上传文件', +}; diff --git a/src/pages/file/upload/index.css b/src/pages/file/upload/index.css new file mode 100644 index 0000000..b147638 --- /dev/null +++ b/src/pages/file/upload/index.css @@ -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); + } +} diff --git a/src/pages/file/upload/index.tsx b/src/pages/file/upload/index.tsx new file mode 100644 index 0000000..3d037c3 --- /dev/null +++ b/src/pages/file/upload/index.tsx @@ -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 ( + + + 上传文件 + 支持 Word、PDF、图片等格式 + + + + {selectedFile ? ( + + 📄 + + {selectedFile.name} + {formatFileSize(selectedFile.size)} + + { e.stopPropagation(); handleSelectFile(); }}>更换 + + ) : ( + + 📤 + 点击选择文件 + 最大支持 50MB + + )} + + + {selectedFile && ( + + + + )} + + {uploading && ( + + + + + + )} + + + 上传须知 + + + 文件将在服务器保存 24 小时 + + + + 请确保文件内容不违反相关法规 + + + + 打印完成后请及时取走文件 + + + + ); +}; + +export default FileUpload; diff --git a/src/pages/index/index.config.ts b/src/pages/index/index.config.ts index 395416f..aa6db14 100644 --- a/src/pages/index/index.config.ts +++ b/src/pages/index/index.config.ts @@ -1,4 +1,4 @@ export default { - navigationBarTitleText: 'Taro-hooks', - enableShareAppMessage: true, + navigationBarTitleText: '萝卜打印', + navigationStyle: 'custom', }; diff --git a/src/pages/index/index.css b/src/pages/index/index.css index 8b87f58..daa145e 100644 --- a/src/pages/index/index.css +++ b/src/pages/index/index.css @@ -1,56 +1,405 @@ +/* ============================================ + 首页样式 - 爱印通 + ============================================ */ + +@import '../../styles/variables.css'; + page { - background-color: white; - padding : 14px; - box-sizing : border-box; - color : #333; + background: linear-gradient(180deg, #e8f0fe 0%, #f0f5ff 30%, #fafbfc 100%); + min-height: 100vh; } -.wrapper { - display : flex; +.index-page { + 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; } -.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; - width : 270px; - height : 270px; - margin : 0 auto; + margin-bottom: 16px; } -.title, -.desc { - text-align: center; - font-size : 32px; +.tips-item { + display: flex; + align-items: center; + margin-bottom: 12px; } -.desc { - margin : 20px 0; - font-size: 28px; - color : rgba(0, 0, 0, .85); +.tips-item:last-child { + margin-bottom: 0; } -.list { - padding : 24px 0; - font-size : 32px; - display : flex; - align-items : center; - border-color: rgba(51, 51, 51, .1); - border-style: solid; - border-width: 1px 0 1px 0; +.tips-number { + width: 28px; + height: 28px; + background: linear-gradient(135deg, #d4a853 0%, #c49a47 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: #fff; + font-weight: var(--font-weight-medium); + margin-right: 12px; + flex-shrink: 0; } -.list:not(:first-child) { - border-top-width: 0; +.tips-text { + 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; - width : 100%; - background-color: transparent; - border-radius : 2px; - margin-top : 20px; +/* 装饰光晕 */ +.index-page::before { + content: ''; + position: fixed; + top: -100px; + 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; } \ No newline at end of file diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 28c59f1..291d854 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -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 { useEnv, useNavigationBar, useModal, useToast } from "taro-hooks"; -import logo from "./hook.png"; +import Taro from '@tarojs/taro'; +import './index.css'; -import './index.css' +interface UserInfo { + nickname: string; + avatar: string; + balance: number; +} const Index = () => { - const env = useEnv(); - const { setTitle } = useNavigationBar({ title: "Taro Hooks" }); - const showModal = useModal({ - title: "Taro Hooks Canary!", - showCancel: false, - confirmColor: "#8c2de9", - confirmText: "支持一下" - }); - const { show } = useToast({ mask: true }); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [userInfo, setUserInfo] = useState(null); - const handleModal = useCallback(() => { - showModal({ content: "不如给一个star⭐️!" }).then(() => { - show({ title: "点击了支持!" }); + useEffect(() => { + checkLoginStatus(); + }, []); + + 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 ( - - - 为Taro而设计的Hooks Library - - 目前覆盖70%官方API. 抹平部分API在H5端短板. 提供近40+Hooks! - 并结合ahook适配Taro! 更多信息可以查看新版文档: https://next-version-taro-hooks.vercel.app/ - - - 运行环境 - {env} + + + + 萝卜打印 + 付费打印服务 + + + + {isLoggedIn && userInfo ? ( + + + + {userInfo.avatar ? ( + + ) : ( + + {userInitials} + + )} + + + {userInfo.nickname} + 余额: ¥{userInfo.balance.toFixed(2)} + + + + + ) : ( + + + + )} + + + + 📤 + 上传文件 + + + 📁 + 我的文件 + + + 📋 + 我的订单 + + + + + + 价目表 + + {priceList.map((item, index) => ( + + {item.name} + {item.price} + + ))} + + + + + + + 使用提示 + {tipsList.map((tip, index) => ( + + {index + 1} + {tip} + + ))} + - - ); }; diff --git a/src/pages/login/index.config.ts b/src/pages/login/index.config.ts new file mode 100644 index 0000000..b6a64fc --- /dev/null +++ b/src/pages/login/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '登录', +}; diff --git a/src/pages/login/index.css b/src/pages/login/index.css new file mode 100644 index 0000000..f7ac10c --- /dev/null +++ b/src/pages/login/index.css @@ -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; +} \ No newline at end of file diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..d3823e2 --- /dev/null +++ b/src/pages/login/index.tsx @@ -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 ( + + + 创建用户名 + 请设置一个方便识别的用户名 + setNickname(e.detail.value)} + /> + + + + ); + } + + return ( + + + 欢迎登录 + 萝卜打印付费打印服务 + + + + + 手机号 + + {getPhoneError ? ( + {getPhoneError} + ) : null} + + + + 验证码 + + setCode(e.detail.value.replace(/\D/g, ''))} + /> + + + + + + + + + 其他登录方式 + + + + + + + + 登录即表示同意 + 《用户服务协议》 + + 《隐私政策》 + + + ); +}; + +export default Login; diff --git a/src/pages/order/detail/index.config.ts b/src/pages/order/detail/index.config.ts new file mode 100644 index 0000000..3d5c2c6 --- /dev/null +++ b/src/pages/order/detail/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '订单详情', +}; diff --git a/src/pages/order/detail/index.css b/src/pages/order/detail/index.css new file mode 100644 index 0000000..cd08c8a --- /dev/null +++ b/src/pages/order/detail/index.css @@ -0,0 +1,10 @@ +page { + background-color: #f5f5f5; +} + +.detail-page { + padding: 30px; + text-align: center; + color: #666; + font-size: 28px; +} diff --git a/src/pages/order/detail/index.tsx b/src/pages/order/detail/index.tsx new file mode 100644 index 0000000..31a7373 --- /dev/null +++ b/src/pages/order/detail/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { View, Text } from "@tarojs/components"; +import './index.css'; + +const OrderDetail = () => { + return ( + + 订单详情 + + ); +}; + +export default OrderDetail; diff --git a/src/pages/order/index.config.ts b/src/pages/order/index.config.ts new file mode 100644 index 0000000..727080d --- /dev/null +++ b/src/pages/order/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '我的订单', +}; diff --git a/src/pages/order/index.css b/src/pages/order/index.css new file mode 100644 index 0000000..337e7e4 --- /dev/null +++ b/src/pages/order/index.css @@ -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); + } +} diff --git a/src/pages/order/index.tsx b/src/pages/order/index.tsx new file mode 100644 index 0000000..4822fa6 --- /dev/null +++ b/src/pages/order/index.tsx @@ -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([ + { + 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 ( + + + 我的订单 + + + {orders.length > 0 ? ( + + {orders.map(order => ( + handleViewDetail(order.id)}> + + 订单号: {order.id} + {order.status} + + + {order.fileName} + {order.printType} {order.colorType} × {order.copies}份 + 📍 {order.printerName} + + + ¥{order.totalPrice.toFixed(2)} + {order.time} + + + ))} + + ) : ( + + 📋 + 暂无订单 + 上传文件即可创建订单 + + )} + + ); +}; + +export default OrderIndex; diff --git a/src/pages/print/confirm/index.config.ts b/src/pages/print/confirm/index.config.ts new file mode 100644 index 0000000..54d3463 --- /dev/null +++ b/src/pages/print/confirm/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '确认订单', +}; diff --git a/src/pages/print/confirm/index.css b/src/pages/print/confirm/index.css new file mode 100644 index 0000000..fb9c34a --- /dev/null +++ b/src/pages/print/confirm/index.css @@ -0,0 +1,10 @@ +page { + background-color: #f5f5f5; +} + +.confirm-page { + padding: 30px; + text-align: center; + color: #666; + font-size: 28px; +} diff --git a/src/pages/print/confirm/index.tsx b/src/pages/print/confirm/index.tsx new file mode 100644 index 0000000..04e5aa3 --- /dev/null +++ b/src/pages/print/confirm/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { View, Text } from "@tarojs/components"; +import './index.css'; + +const PrintConfirm = () => { + return ( + + 确认订单 + + ); +}; + +export default PrintConfirm; diff --git a/src/pages/print/index.config.ts b/src/pages/print/index.config.ts new file mode 100644 index 0000000..0d08e7b --- /dev/null +++ b/src/pages/print/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '打印设置', +}; diff --git a/src/pages/print/index.css b/src/pages/print/index.css new file mode 100644 index 0000000..7448bfe --- /dev/null +++ b/src/pages/print/index.css @@ -0,0 +1,10 @@ +page { + background-color: #f5f5f5; +} + +.print-page { + padding: 30px; + text-align: center; + color: #666; + font-size: 28px; +} diff --git a/src/pages/print/index.tsx b/src/pages/print/index.tsx new file mode 100644 index 0000000..a78fffa --- /dev/null +++ b/src/pages/print/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { View, Text } from "@tarojs/components"; +import './index.css'; + +const PrintIndex = () => { + return ( + + 打印设置 + + ); +}; + +export default PrintIndex; diff --git a/src/pages/user/index.config.ts b/src/pages/user/index.config.ts new file mode 100644 index 0000000..b5ea809 --- /dev/null +++ b/src/pages/user/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '用户中心', +}; diff --git a/src/pages/user/index.css b/src/pages/user/index.css new file mode 100644 index 0000000..60e19dd --- /dev/null +++ b/src/pages/user/index.css @@ -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; +} diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx new file mode 100644 index 0000000..a5091ed --- /dev/null +++ b/src/pages/user/index.tsx @@ -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(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 ( + + + {isLoggedIn && userInfo ? ( + + + {userInfo.avatar ? ( + + {userInfo.avatar} + + ) : ( + + {userInfo.nickname[0]?.toUpperCase() || 'U'} + + )} + + {userInfo.nickname} + 余额: ¥{userInfo.balance.toFixed(2)} + + ) : ( + + + 👤 + + 点击登录 + + )} + + + + {menuItems.map((item, index) => ( + handleMenuClick(item.path)} + > + {item.icon} + {item.title} + {item.value && item.value !== '¥0.00' && {item.value}} + {!item.value && } + + ))} + + + {isLoggedIn && ( + + + + )} + + ); +}; + +export default UserIndex; diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 0000000..bd511ce --- /dev/null +++ b/src/store/hooks.ts @@ -0,0 +1,5 @@ +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; +import type { AppDispatch } from './index'; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook> = useSelector; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..72dca52 --- /dev/null +++ b/src/store/index.ts @@ -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; +export type AppDispatch = typeof store.dispatch; diff --git a/src/store/slices/fileSlice.ts b/src/store/slices/fileSlice.ts new file mode 100644 index 0000000..54e8851 --- /dev/null +++ b/src/store/slices/fileSlice.ts @@ -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) => { + state.fileList.unshift(action.payload); + state.total += 1; + }, + removeFile: (state, action: PayloadAction) => { + state.fileList = state.fileList.filter(f => f.id !== action.payload); + state.total -= 1; + }, + setUploading: (state, action: PayloadAction) => { + state.uploading = action.payload; + }, + setUploadProgress: (state, action: PayloadAction) => { + state.uploadProgress = action.payload; + }, + }, +}); + +export const { setFileList, addFile, removeFile, setUploading, setUploadProgress } = fileSlice.actions; +export const fileReducer = fileSlice.reducer; diff --git a/src/store/slices/orderSlice.ts b/src/store/slices/orderSlice.ts new file mode 100644 index 0000000..09a0e6a --- /dev/null +++ b/src/store/slices/orderSlice.ts @@ -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) => { + 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) => { + state.orderList.unshift(action.payload); + state.total += 1; + }, + updateOrder: (state, action: PayloadAction) => { + 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) => { + state.loading = action.payload; + }, + }, +}); + +export const { setCurrentOrder, setOrderList, addOrder, updateOrder, setLoading } = orderSlice.actions; +export const orderReducer = orderSlice.reducer; diff --git a/src/store/slices/printerSlice.ts b/src/store/slices/printerSlice.ts new file mode 100644 index 0000000..190d96e --- /dev/null +++ b/src/store/slices/printerSlice.ts @@ -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) => { + state.list = action.payload; + }, + setSelectedPrinter: (state, action: PayloadAction) => { + state.selected = action.payload; + }, + updatePrinter: (state, action: PayloadAction) => { + 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) => { + state.loading = action.payload; + }, + }, +}); + +export const { setPrinterList, setSelectedPrinter, updatePrinter, setLoading } = printerSlice.actions; +export const printerReducer = printerSlice.reducer; diff --git a/src/store/slices/userSlice.ts b/src/store/slices/userSlice.ts new file mode 100644 index 0000000..94cff90 --- /dev/null +++ b/src/store/slices/userSlice.ts @@ -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>) => { + if (state.info) { + state.info = { ...state.info, ...action.payload }; + } + }, + setToken: (state, action: PayloadAction) => { + state.token = action.payload; + }, + logout: (state) => { + state.info = null; + state.token = null; + state.refreshToken = null; + state.isLoggedIn = false; + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload; + }, + }, +}); + +export const { setUser, updateUserInfo, setToken, logout, setLoading } = userSlice.actions; +export const userReducer = userSlice.reducer; diff --git a/src/styles/animations.css b/src/styles/animations.css new file mode 100644 index 0000000..451550e --- /dev/null +++ b/src/styles/animations.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/common.css b/src/styles/common.css new file mode 100644 index 0000000..da696bb --- /dev/null +++ b/src/styles/common.css @@ -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); +} \ No newline at end of file diff --git a/src/styles/variables.css b/src/styles/variables.css new file mode 100644 index 0000000..e465682 --- /dev/null +++ b/src/styles/variables.css @@ -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; +} \ No newline at end of file diff --git a/src/types/file.ts b/src/types/file.ts new file mode 100644 index 0000000..917ac11 --- /dev/null +++ b/src/types/file.ts @@ -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; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..afdd9a1 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,16 @@ +export interface ApiResponse { + 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'; diff --git a/src/types/order.ts b/src/types/order.ts new file mode 100644 index 0000000..3a6d76e --- /dev/null +++ b/src/types/order.ts @@ -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 = { + 0: '待支付', + 1: '已支付', + 2: '打印中', + 3: '已完成', + 4: '已取消', +}; + +export const PayStatusMap: Record = { + 0: '待支付', + 1: '支付中', + 2: '成功', + 3: '失败', +}; diff --git a/src/types/price.ts b/src/types/price.ts new file mode 100644 index 0000000..8e307a1 --- /dev/null +++ b/src/types/price.ts @@ -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: '张' }, +]; diff --git a/src/types/printer.ts b/src/types/printer.ts new file mode 100644 index 0000000..2d2d664 --- /dev/null +++ b/src/types/printer.ts @@ -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 = { + 0: '空闲', + 1: '打印中', + 2: '离线', +}; diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..0e7be83 --- /dev/null +++ b/src/types/user.ts @@ -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; +} diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..5740eac --- /dev/null +++ b/src/utils/format.ts @@ -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}`; +}; diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..8ecb961 --- /dev/null +++ b/src/utils/request.ts @@ -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; +} + +export const request = async (options: RequestOptions): Promise => { + 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 = (filePath: string, onProgress?: (progress: number) => void): Promise => { + 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); + }); +}; diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..c618708 --- /dev/null +++ b/src/utils/storage.ts @@ -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(); + }, +};