Generating commit message...
This commit is contained in:
44471
.gitignore
vendored
Normal file
44471
.gitignore
vendored
Normal file
File diff suppressed because it is too large
Load Diff
11
admin-system/.env
Normal file
11
admin-system/.env
Normal file
@@ -0,0 +1,11 @@
|
||||
# 环境配置
|
||||
VITE_APP_NAME=结伴客后台管理系统
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_API_TIMEOUT=10000
|
||||
|
||||
# 功能开关
|
||||
VITE_FEATURE_ANALYTICS=false
|
||||
VITE_FEATURE_DEBUG=false
|
||||
13
admin-system/.env.development
Normal file
13
admin-system/.env.development
Normal file
@@ -0,0 +1,13 @@
|
||||
# 开发环境配置
|
||||
NODE_ENV=development
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=http://localhost:3000/api/v1
|
||||
VITE_API_TIMEOUT=30000
|
||||
|
||||
# 功能开关
|
||||
VITE_FEATURE_ANALYTICS=true
|
||||
VITE_FEATURE_DEBUG=true
|
||||
|
||||
# 开发工具
|
||||
VITE_DEV_TOOLS=true
|
||||
13
admin-system/.env.production
Normal file
13
admin-system/.env.production
Normal file
@@ -0,0 +1,13 @@
|
||||
# 生产环境配置
|
||||
NODE_ENV=production
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=https://api.jiebanke.com/api/v1
|
||||
VITE_API_TIMEOUT=15000
|
||||
|
||||
# 功能开关
|
||||
VITE_FEATURE_ANALYTICS=true
|
||||
VITE_FEATURE_DEBUG=false
|
||||
|
||||
# 性能优化
|
||||
VITE_COMPRESSION=true
|
||||
245
admin-system/DEPLOYMENT.md
Normal file
245
admin-system/DEPLOYMENT.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# 结伴客后台管理系统部署指南
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 1. 本地开发环境部署
|
||||
|
||||
#### 使用 npm 脚本
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 预览生产版本
|
||||
npm run preview
|
||||
```
|
||||
|
||||
#### 使用部署脚本
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
chmod +x deploy.sh
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
**Windows PowerShell:**
|
||||
```powershell
|
||||
.\deploy.ps1
|
||||
```
|
||||
|
||||
### 2. Docker 容器化部署
|
||||
|
||||
#### 开发环境
|
||||
```bash
|
||||
# 启动开发环境(包含前端、后端、数据库等所有服务)
|
||||
docker-compose up frontend-dev backend mysql redis rabbitmq
|
||||
|
||||
# 或启动所有服务
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
#### 生产环境
|
||||
```bash
|
||||
# 构建并启动生产环境
|
||||
docker-compose up -d frontend backend mysql redis rabbitmq
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs frontend
|
||||
```
|
||||
|
||||
### 3. 单独服务部署
|
||||
|
||||
#### 前端服务
|
||||
```bash
|
||||
# 开发模式
|
||||
docker-compose up frontend-dev
|
||||
|
||||
# 生产模式
|
||||
docker-compose up frontend
|
||||
```
|
||||
|
||||
#### 后端服务
|
||||
```bash
|
||||
docker-compose up backend
|
||||
```
|
||||
|
||||
#### 数据库服务
|
||||
```bash
|
||||
docker-compose up mysql redis rabbitmq
|
||||
```
|
||||
|
||||
## 环境配置
|
||||
|
||||
### 环境变量文件
|
||||
|
||||
创建以下环境变量文件:
|
||||
|
||||
**.env.development** (开发环境):
|
||||
```
|
||||
VITE_APP_TITLE=结伴客后台管理系统(开发)
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
VITE_APP_VERSION=dev
|
||||
```
|
||||
|
||||
**.env.production** (生产环境):
|
||||
```
|
||||
VITE_APP_TITLE=结伴客后台管理系统
|
||||
VITE_API_BASE_URL=https://api.jiebanke.com
|
||||
VITE_APP_VERSION=v1.0.0
|
||||
```
|
||||
|
||||
### Docker 环境变量
|
||||
|
||||
在 `docker-compose.yml` 中配置:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_NAME=jiebanke
|
||||
- DB_USER=root
|
||||
- DB_PASSWORD=password
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- RABBITMQ_HOST=rabbitmq
|
||||
- RABBITMQ_PORT=5672
|
||||
```
|
||||
|
||||
## 端口映射
|
||||
|
||||
| 服务 | 容器端口 | 主机端口 | 说明 |
|
||||
|------|----------|----------|------|
|
||||
| 前端 | 80 | 80 | 生产环境前端 |
|
||||
| 前端开发 | 5173 | 5173 | 开发环境前端 |
|
||||
| 后端API | 3000 | 3000 | Node.js后端服务 |
|
||||
| MySQL | 3306 | 3306 | 数据库服务 |
|
||||
| Redis | 6379 | 6379 | 缓存服务 |
|
||||
| RabbitMQ | 5672 | 5672 | 消息队列服务 |
|
||||
| RabbitMQ管理 | 15672 | 15672 | 管理界面 |
|
||||
|
||||
## 健康检查
|
||||
|
||||
系统提供健康检查端点:
|
||||
- 前端: `http://localhost:80/health`
|
||||
- 后端: `http://localhost:3000/health`
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker-compose logs
|
||||
|
||||
# 查看特定服务日志
|
||||
docker-compose logs frontend
|
||||
docker-compose logs backend
|
||||
```
|
||||
|
||||
### 服务状态监控
|
||||
```bash
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看资源使用情况
|
||||
docker stats
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **端口冲突**
|
||||
- 检查端口是否被占用
|
||||
- 修改 `docker-compose.yml` 中的端口映射
|
||||
|
||||
2. **依赖安装失败**
|
||||
- 清除缓存: `npm cache clean --force`
|
||||
- 删除 node_modules 重新安装
|
||||
|
||||
3. **数据库连接失败**
|
||||
- 检查数据库服务是否启动
|
||||
- 验证环境变量配置
|
||||
|
||||
4. **构建失败**
|
||||
- 检查 Node.js 版本 (要求 >= 16)
|
||||
- 检查系统依赖
|
||||
|
||||
### 调试模式
|
||||
|
||||
启动服务时添加调试标志:
|
||||
```bash
|
||||
# 前端开发调试
|
||||
npm run dev -- --debug
|
||||
|
||||
# Docker 调试模式
|
||||
docker-compose up --build --force-recreate
|
||||
```
|
||||
|
||||
## 备份和恢复
|
||||
|
||||
### 数据库备份
|
||||
```bash
|
||||
# 备份MySQL数据
|
||||
docker exec -i mysql mysqldump -uroot -ppassword jiebanke > backup.sql
|
||||
|
||||
# 恢复MySQL数据
|
||||
docker exec -i mysql mysql -uroot -ppassword jiebanke < backup.sql
|
||||
```
|
||||
|
||||
### 卷备份
|
||||
```bash
|
||||
# 备份数据库卷
|
||||
docker run --rm -v mysql_data:/source -v $(pwd):/backup alpine tar czf /backup/mysql_backup.tar.gz -C /source .
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **修改默认密码**
|
||||
- MySQL root 密码
|
||||
- RabbitMQ 默认用户密码
|
||||
- Redis 密码(如果需要)
|
||||
|
||||
2. **启用HTTPS**
|
||||
- 配置SSL证书
|
||||
- 使用反向代理
|
||||
|
||||
3. **防火墙配置**
|
||||
- 只开放必要的端口
|
||||
- 限制外部访问
|
||||
|
||||
4. **定期更新**
|
||||
- 更新Docker镜像
|
||||
- 更新依赖包
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **启用缓存**
|
||||
- 配置Redis缓存
|
||||
- 使用CDN加速静态资源
|
||||
|
||||
2. **负载均衡**
|
||||
- 使用nginx负载均衡
|
||||
- 配置多实例部署
|
||||
|
||||
3. **数据库优化**
|
||||
- 添加索引
|
||||
- 查询优化
|
||||
- 连接池配置
|
||||
|
||||
## 支持
|
||||
|
||||
如遇部署问题,请检查:
|
||||
1. Docker 和 Docker Compose 版本
|
||||
2. 系统资源(内存、磁盘空间)
|
||||
3. 网络连接
|
||||
4. 查看详细错误日志
|
||||
53
admin-system/Dockerfile
Normal file
53
admin-system/Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
# 结伴客后台管理系统 Dockerfile
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制package.json和package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --only=production
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN npm run build
|
||||
|
||||
# 生产环境
|
||||
FROM nginx:alpine AS production
|
||||
|
||||
# 复制构建好的文件到nginx目录
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制nginx配置
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
# 启动nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
# 开发环境
|
||||
FROM node:18-alpine AS development
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制package.json和package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖(包括开发依赖)
|
||||
RUN npm ci
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5173
|
||||
|
||||
# 启动开发服务器
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
286
admin-system/README.md
Normal file
286
admin-system/README.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# 结伴客后台管理系统
|
||||
|
||||
基于 Vue 3 + TypeScript + Ant Design Vue 构建的后台管理系统,用于管理结伴客平台的用户、商家、旅行计划、动物认领等业务。
|
||||
|
||||
## 🚀 特性
|
||||
|
||||
- **现代化技术栈**: Vue 3 + TypeScript + Vite
|
||||
- **UI框架**: Ant Design Vue 4.x
|
||||
- **状态管理**: Pinia
|
||||
- **路由管理**: Vue Router 4
|
||||
- **HTTP客户端**: Axios
|
||||
- **构建工具**: Vite
|
||||
- **代码规范**: ESLint + Prettier
|
||||
|
||||
## 📦 安装依赖
|
||||
|
||||
```bash
|
||||
cd admin-system
|
||||
npm install
|
||||
```
|
||||
|
||||
## 🛠️ 开发
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 类型检查
|
||||
npm run type-check
|
||||
|
||||
# 代码格式化
|
||||
npm run lint
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 预览生产版本
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 🌐 环境配置
|
||||
|
||||
系统支持多环境配置:
|
||||
|
||||
### 开发环境 (.env.development)
|
||||
```env
|
||||
NODE_ENV=development
|
||||
VITE_API_BASE_URL=http://localhost:3000/api/v1
|
||||
VITE_FEATURE_DEBUG=true
|
||||
```
|
||||
|
||||
### 生产环境 (.env.production)
|
||||
```env
|
||||
NODE_ENV=production
|
||||
VITE_API_BASE_URL=https://api.jiebanke.com/api/v1
|
||||
VITE_FEATURE_DEBUG=false
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
admin-system/
|
||||
├── src/
|
||||
│ ├── api/ # API接口
|
||||
│ ├── assets/ # 静态资源
|
||||
│ ├── components/ # 组件
|
||||
│ │ ├── common/ # 通用组件
|
||||
│ │ ├── layout/ # 布局组件
|
||||
│ │ ├── user/ # 用户相关组件
|
||||
│ │ ├── merchant/ # 商家相关组件
|
||||
│ │ ├── travel/ # 旅行相关组件
|
||||
│ │ ├── animal/ # 动物相关组件
|
||||
│ │ ├── order/ # 订单相关组件
|
||||
│ │ ├── promotion/ # 推广相关组件
|
||||
│ │ └── dashboard/ # 仪表板组件
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ ├── layouts/ # 布局文件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── dashboard/ # 仪表板
|
||||
│ │ ├── user/ # 用户管理
|
||||
│ │ ├── merchant/ # 商家管理
|
||||
│ │ ├── travel/ # 旅行管理
|
||||
│ │ ├── animal/ # 动物管理
|
||||
│ │ ├── order/ # 订单管理
|
||||
│ │ ├── promotion/ # 推广管理
|
||||
│ │ ├── system/ # 系统设置
|
||||
│ │ └── Login.vue # 登录页面
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── stores/ # 状态管理
|
||||
│ ├── styles/ # 样式文件
|
||||
│ ├── types/ # TypeScript类型定义
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.ts # 入口文件
|
||||
├── public/ # 公共资源
|
||||
├── tests/ # 测试文件
|
||||
├── index.html # HTML模板
|
||||
├── vite.config.ts # Vite配置
|
||||
├── tsconfig.json # TypeScript配置
|
||||
├── package.json # 项目配置
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 🔐 权限管理
|
||||
|
||||
系统采用基于角色的权限控制(RBAC):
|
||||
|
||||
### 用户角色
|
||||
- **超级管理员**: 所有权限
|
||||
- **运营管理员**: 用户管理、内容审核
|
||||
- **财务管理员**: 订单管理、财务管理
|
||||
- **客服管理员**: 用户支持、投诉处理
|
||||
|
||||
### 权限标识
|
||||
- `user:read` - 查看用户
|
||||
- `user:write` - 管理用户
|
||||
- `merchant:read` - 查看商家
|
||||
- `merchant:write` - 管理商家
|
||||
- `travel:read` - 查看旅行计划
|
||||
- `travel:write` - 管理旅行计划
|
||||
- `animal:read` - 查看动物信息
|
||||
- `animal:write` - 管理动物信息
|
||||
- `order:read` - 查看订单
|
||||
- `order:write` - 管理订单
|
||||
- `system:read` - 查看系统设置
|
||||
- `system:write` - 管理系统设置
|
||||
|
||||
## 📊 功能模块
|
||||
|
||||
### 1. 仪表板
|
||||
- 系统概览统计
|
||||
- 实时数据监控
|
||||
- 快捷操作入口
|
||||
|
||||
### 2. 用户管理
|
||||
- 用户列表查看
|
||||
- 用户信息编辑
|
||||
- 用户状态管理
|
||||
- 用户行为分析
|
||||
|
||||
### 3. 商家管理
|
||||
- 商家入驻审核
|
||||
- 商家信息管理
|
||||
- 商家商品管理
|
||||
- 商家数据统计
|
||||
|
||||
### 4. 旅行管理
|
||||
- 旅行计划审核
|
||||
- 旅行计划管理
|
||||
- 旅行匹配监控
|
||||
- 旅行数据统计
|
||||
|
||||
### 5. 动物管理
|
||||
- 动物信息管理
|
||||
- 认领申请审核
|
||||
- 动物状态跟踪
|
||||
- 认领数据分析
|
||||
|
||||
### 6. 订单管理
|
||||
- 订单列表查看
|
||||
- 订单状态管理
|
||||
- 订单数据分析
|
||||
- 财务报表生成
|
||||
|
||||
### 7. 推广管理
|
||||
- 推广数据统计
|
||||
- 提现申请审核
|
||||
- 推广效果分析
|
||||
- 奖励发放管理
|
||||
|
||||
### 8. 系统设置
|
||||
- 系统参数配置
|
||||
- 操作日志查看
|
||||
- 缓存管理
|
||||
- 系统监控
|
||||
|
||||
## 🎨 主题定制
|
||||
|
||||
系统支持主题定制:
|
||||
|
||||
```less
|
||||
// 主要主题变量
|
||||
--primary-color: #1890ff; // 主题色
|
||||
--success-color: #52c41a; // 成功色
|
||||
--warning-color: #faad14; // 警告色
|
||||
--error-color: #ff4d4f; // 错误色
|
||||
```
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
系统支持多种屏幕尺寸:
|
||||
|
||||
- **桌面端** (≥1200px): 完整功能展示
|
||||
- **平板端** (768px-1199px): 适配布局
|
||||
- **移动端** (<768px): 简化界面
|
||||
|
||||
## 🔧 开发规范
|
||||
|
||||
### 代码风格
|
||||
- 使用TypeScript严格模式
|
||||
- 遵循Vue 3组合式API规范
|
||||
- 使用ESLint + Prettier统一代码风格
|
||||
- 组件使用PascalCase命名
|
||||
- 文件使用kebab-case命名
|
||||
|
||||
### 组件开发
|
||||
```vue
|
||||
<template>
|
||||
<!-- 模板代码 -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 组合式API
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 组件样式 */
|
||||
</style>
|
||||
```
|
||||
|
||||
### API调用
|
||||
```typescript
|
||||
// 使用封装的request方法
|
||||
import { request } from '@/api'
|
||||
|
||||
const getUsers = async () => {
|
||||
try {
|
||||
const response = await request.get('/users')
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取用户失败:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 部署指南
|
||||
|
||||
### 开发环境部署
|
||||
1. 安装依赖: `npm install`
|
||||
2. 启动开发服务器: `npm run dev`
|
||||
3. 访问: http://localhost:3001
|
||||
|
||||
### 生产环境部署
|
||||
1. 构建项目: `npm run build`
|
||||
2. 部署到静态文件服务器
|
||||
3. 配置Nginx反向代理
|
||||
|
||||
### Docker部署
|
||||
```dockerfile
|
||||
FROM node:18-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
- 文档: [查看API文档](./docs/api-documentation.md)
|
||||
- 问题反馈: [创建Issue](https://github.com/jiebanke/admin-system/issues)
|
||||
- 邮箱: support@jiebanke.com
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License - 详见 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 🎯 版本历史
|
||||
|
||||
### v1.0.0 (2025-01-01)
|
||||
- 初始版本发布
|
||||
- 基础框架搭建
|
||||
- 核心功能模块
|
||||
- 权限管理系统
|
||||
- 响应式设计
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-01-01
|
||||
**当前版本**: v1.0.0
|
||||
46
admin-system/deploy.ps1
Normal file
46
admin-system/deploy.ps1
Normal file
@@ -0,0 +1,46 @@
|
||||
# 结伴客后台管理系统部署脚本 (PowerShell)
|
||||
Write-Host "开始部署结伴客后台管理系统..." -ForegroundColor Green
|
||||
|
||||
# 检查Node.js是否安装
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "错误: Node.js 未安装,请先安装 Node.js" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 检查npm是否安装
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "错误: npm 未安装,请先安装 npm" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 安装依赖
|
||||
Write-Host "安装项目依赖..." -ForegroundColor Yellow
|
||||
npm install
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "依赖安装失败,请检查错误信息" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 构建项目
|
||||
Write-Host "构建项目..." -ForegroundColor Yellow
|
||||
npm run build
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "构建失败,请检查错误信息" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "构建成功!" -ForegroundColor Green
|
||||
|
||||
# 创建生产环境配置文件
|
||||
Write-Host "创建生产环境配置..." -ForegroundColor Yellow
|
||||
@"
|
||||
VITE_APP_TITLE=结伴客后台管理系统
|
||||
VITE_API_BASE_URL=https://api.jiebanke.com
|
||||
VITE_APP_VERSION=v1.0.0
|
||||
"@ | Out-File -FilePath .env.production -Encoding UTF8
|
||||
|
||||
Write-Host "部署完成!" -ForegroundColor Green
|
||||
Write-Host "运行 'npm run preview' 预览生产版本" -ForegroundColor Cyan
|
||||
Write-Host "或运行 'npm run dev' 启动开发服务器" -ForegroundColor Cyan
|
||||
44
admin-system/deploy.sh
Normal file
44
admin-system/deploy.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 结伴客后台管理系统部署脚本
|
||||
echo "开始部署结伴客后台管理系统..."
|
||||
|
||||
# 检查Node.js是否安装
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "错误: Node.js 未安装,请先安装 Node.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查npm是否安装
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "错误: npm 未安装,请先安装 npm"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo "安装项目依赖..."
|
||||
npm install
|
||||
|
||||
# 构建项目
|
||||
echo "构建项目..."
|
||||
npm run build
|
||||
|
||||
# 检查构建是否成功
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "构建成功!"
|
||||
else
|
||||
echo "构建失败,请检查错误信息"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建生产环境配置文件
|
||||
echo "创建生产环境配置..."
|
||||
cat > .env.production << EOF
|
||||
VITE_APP_TITLE=结伴客后台管理系统
|
||||
VITE_API_BASE_URL=https://api.jiebanke.com
|
||||
VITE_APP_VERSION=v1.0.0
|
||||
EOF
|
||||
|
||||
echo "部署完成!"
|
||||
echo "运行 'npm run preview' 预览生产版本"
|
||||
echo "或运行 'npm run dev' 启动开发服务器"
|
||||
109
admin-system/docker-compose.yml
Normal file
109
admin-system/docker-compose.yml
Normal file
@@ -0,0 +1,109 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 前端应用
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
target: production
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jiebanke-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# 开发环境前端
|
||||
frontend-dev:
|
||||
build:
|
||||
context: .
|
||||
target: development
|
||||
ports:
|
||||
- "5173:5173"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jiebanke-network
|
||||
command: npm run dev -- --host
|
||||
|
||||
# 后端API服务
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_NAME=jiebanke
|
||||
- DB_USER=root
|
||||
- DB_PASSWORD=password
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- RABBITMQ_HOST=rabbitmq
|
||||
- RABBITMQ_PORT=5672
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
- rabbitmq
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jiebanke-network
|
||||
|
||||
# MySQL数据库
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=password
|
||||
- MYSQL_DATABASE=jiebanke
|
||||
- MYSQL_USER=jiebanke
|
||||
- MYSQL_PASSWORD=password
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ../backend/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jiebanke-network
|
||||
|
||||
# Redis缓存
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jiebanke-network
|
||||
|
||||
# RabbitMQ消息队列
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
environment:
|
||||
- RABBITMQ_DEFAULT_USER=admin
|
||||
- RABBITMQ_DEFAULT_PASS=password
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jiebanke-network
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
|
||||
networks:
|
||||
jiebanke-network:
|
||||
driver: bridge
|
||||
86
admin-system/env.d.ts
vendored
Normal file
86
admin-system/env.d.ts
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_NAME: string
|
||||
readonly VITE_APP_VERSION: string
|
||||
readonly VITE_API_BASE_URL: string
|
||||
readonly VITE_API_TIMEOUT: string
|
||||
readonly VITE_FEATURE_ANALYTICS: string
|
||||
readonly VITE_FEATURE_DEBUG: string
|
||||
readonly VITE_DEV_TOOLS?: string
|
||||
readonly VITE_COMPRESSION?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
// Vue文件模块声明
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
// 图片资源声明
|
||||
declare module '*.png' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.jpeg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.gif' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.ico' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*.webp' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
// 样式文件声明
|
||||
declare module '*.css' {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module '*.scss' {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module '*.less' {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module '*.styl' {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
// JSON文件声明
|
||||
declare module '*.json' {
|
||||
const value: any
|
||||
export default value
|
||||
}
|
||||
36
admin-system/index.html
Normal file
36
admin-system/index.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%- VITE_APP_NAME %></title>
|
||||
<meta name="description" content="结伴客后台管理系统 - 专业的旅行结伴与动物认领平台管理后台" />
|
||||
<meta name="keywords" content="结伴客,后台管理,旅行,动物认领,商家管理" />
|
||||
|
||||
<!-- 预加载关键资源 -->
|
||||
<link rel="preconnect" href="<%- VITE_API_BASE_URL %>" crossorigin>
|
||||
|
||||
<!-- 主题颜色 -->
|
||||
<meta name="theme-color" content="#1890ff">
|
||||
|
||||
<!-- 移动端优化 -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="<%- VITE_APP_NAME %>">
|
||||
|
||||
<!-- 避免被搜索引擎索引 -->
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
<!-- 环境变量提示 -->
|
||||
<script>
|
||||
console.log('环境:', '<%- NODE_ENV %>');
|
||||
console.log('版本:', '<%- VITE_APP_VERSION %>');
|
||||
console.log('API地址:', '<%- VITE_API_BASE_URL %>');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
38
admin-system/nginx.conf
Normal file
38
admin-system/nginx.conf
Normal file
@@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 静态文件服务
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API代理配置
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
}
|
||||
|
||||
# 错误页面
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
3257
admin-system/package-lock.json
generated
Normal file
3257
admin-system/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
admin-system/package.json
Normal file
41
admin-system/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "jiebanke-admin",
|
||||
"version": "1.0.0",
|
||||
"description": "结伴客后台管理系统",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"pinia": "^2.1.0",
|
||||
"ant-design-vue": "^4.0.0",
|
||||
"axios": "^1.4.0",
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.0",
|
||||
"@types/node": "^18.0.0",
|
||||
"@vitejs/plugin-vue": "^4.2.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"typescript": "~5.0.0",
|
||||
"vite": "^4.3.0",
|
||||
"vue-tsc": "^1.4.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"prettier": "^2.8.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
30
admin-system/src/App.vue
Normal file
30
admin-system/src/App.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<a-config-provider :locale="zhCN">
|
||||
<router-view />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { onMounted } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化应用
|
||||
appStore.initializeApp()
|
||||
|
||||
// 开发环境调试信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🎯 应用组件挂载完成')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
339
admin-system/src/api/index.ts
Normal file
339
admin-system/src/api/index.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import axios from 'axios'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
|
||||
// API基础配置
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||
const timeout = parseInt(import.meta.env.VITE_API_TIMEOUT || '10000')
|
||||
|
||||
// 创建axios实例
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL,
|
||||
timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config: AxiosRequestConfig) => {
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('admin_token')
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 记录请求日志(开发环境)
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🚀 API请求:', config.method?.toUpperCase(), config.url)
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ 请求拦截器错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 处理成功响应
|
||||
const { data } = response
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ API响应:', response.config.url, data)
|
||||
}
|
||||
|
||||
// 检查业务逻辑成功
|
||||
if (data && data.success) {
|
||||
return data
|
||||
} else {
|
||||
// 业务逻辑错误
|
||||
const errorMsg = data?.message || '请求失败'
|
||||
message.error(errorMsg)
|
||||
return Promise.reject(new Error(errorMsg))
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// 处理错误响应
|
||||
console.error('❌ API错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 未授权,跳转到登录页
|
||||
message.error('登录已过期,请重新登录')
|
||||
localStorage.removeItem('admin_token')
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
message.error('权限不足,无法访问')
|
||||
break
|
||||
case 404:
|
||||
message.error('请求的资源不存在')
|
||||
break
|
||||
case 429:
|
||||
message.error('请求过于频繁,请稍后再试')
|
||||
break
|
||||
case 500:
|
||||
message.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
message.error(data?.message || '请求失败')
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 网络错误
|
||||
message.error('网络连接失败,请检查网络设置')
|
||||
} else {
|
||||
// 其他错误
|
||||
message.error('请求配置错误')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 通用请求方法
|
||||
export const request = {
|
||||
get: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
|
||||
api.get(url, config).then(res => res.data),
|
||||
|
||||
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
|
||||
api.post(url, data, config).then(res => res.data),
|
||||
|
||||
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
|
||||
api.put(url, data, config).then(res => res.data),
|
||||
|
||||
delete: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
|
||||
api.delete(url, config).then(res => res.data),
|
||||
|
||||
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
|
||||
api.patch(url, data, config).then(res => res.data)
|
||||
}
|
||||
|
||||
// 认证相关API
|
||||
export const authAPI = {
|
||||
// 管理员登录
|
||||
login: (credentials: { username: string; password: string }) =>
|
||||
request.post<{
|
||||
success: boolean
|
||||
data: {
|
||||
token: string
|
||||
user: any
|
||||
}
|
||||
}>('/auth/admin/login', credentials),
|
||||
|
||||
// 获取当前用户信息
|
||||
getCurrentUser: () =>
|
||||
request.get<{
|
||||
success: boolean
|
||||
data: {
|
||||
user: any
|
||||
}
|
||||
}>('/auth/me'),
|
||||
|
||||
// 刷新token
|
||||
refreshToken: () =>
|
||||
request.post<{
|
||||
success: boolean
|
||||
data: {
|
||||
token: string
|
||||
}
|
||||
}>('/auth/refresh'),
|
||||
|
||||
// 退出登录
|
||||
logout: () =>
|
||||
request.post('/auth/logout')
|
||||
}
|
||||
|
||||
// 用户管理API
|
||||
export const userAPI = {
|
||||
// 获取用户列表
|
||||
getUsers: (params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
status?: string
|
||||
}) => request.get('/users', { params }),
|
||||
|
||||
// 获取用户详情
|
||||
getUser: (id: number) => request.get(`/users/${id}`),
|
||||
|
||||
// 创建用户
|
||||
createUser: (data: any) => request.post('/users', data),
|
||||
|
||||
// 更新用户
|
||||
updateUser: (id: number, data: any) => request.put(`/users/${id}`, data),
|
||||
|
||||
// 删除用户
|
||||
deleteUser: (id: number) => request.delete(`/users/${id}`),
|
||||
|
||||
// 批量操作
|
||||
batchUsers: (ids: number[], action: string) =>
|
||||
request.post('/users/batch', { ids, action })
|
||||
}
|
||||
|
||||
// 商家管理API
|
||||
export const merchantAPI = {
|
||||
// 获取商家列表
|
||||
getMerchants: (params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
status?: string
|
||||
type?: string
|
||||
}) => request.get('/merchants', { params }),
|
||||
|
||||
// 获取商家详情
|
||||
getMerchant: (id: number) => request.get(`/merchants/${id}`),
|
||||
|
||||
// 审核商家
|
||||
approveMerchant: (id: number, data: any) => request.post(`/merchants/${id}/approve`, data),
|
||||
|
||||
// 拒绝商家
|
||||
rejectMerchant: (id: number, reason: string) => request.post(`/merchants/${id}/reject`, { reason }),
|
||||
|
||||
// 禁用商家
|
||||
disableMerchant: (id: number) => request.post(`/merchants/${id}/disable`),
|
||||
|
||||
// 启用商家
|
||||
enableMerchant: (id: number) => request.post(`/merchants/${id}/enable`)
|
||||
}
|
||||
|
||||
// 旅行管理API
|
||||
export const travelAPI = {
|
||||
// 获取旅行计划列表
|
||||
getTravelPlans: (params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
status?: string
|
||||
destination?: string
|
||||
}) => request.get('/travel/plans', { params }),
|
||||
|
||||
// 获取旅行计划详情
|
||||
getTravelPlan: (id: number) => request.get(`/travel/plans/${id}`),
|
||||
|
||||
// 审核旅行计划
|
||||
approveTravelPlan: (id: number) => request.post(`/travel/plans/${id}/approve`),
|
||||
|
||||
// 拒绝旅行计划
|
||||
rejectTravelPlan: (id: number, reason: string) => request.post(`/travel/plans/${id}/reject`, { reason }),
|
||||
|
||||
// 关闭旅行计划
|
||||
closeTravelPlan: (id: number) => request.post(`/travel/plans/${id}/close`)
|
||||
}
|
||||
|
||||
// 动物管理API
|
||||
export const animalAPI = {
|
||||
// 获取动物列表
|
||||
getAnimals: (params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
status?: string
|
||||
type?: string
|
||||
}) => request.get('/animals', { params }),
|
||||
|
||||
// 获取动物详情
|
||||
getAnimal: (id: number) => request.get(`/animals/${id}`),
|
||||
|
||||
// 创建动物
|
||||
createAnimal: (data: any) => request.post('/animals', data),
|
||||
|
||||
// 更新动物
|
||||
updateAnimal: (id: number, data: any) => request.put(`/animals/${id}`, data),
|
||||
|
||||
// 删除动物
|
||||
deleteAnimal: (id: number) => request.delete(`/animals/${id}`),
|
||||
|
||||
// 审核动物认领
|
||||
approveAnimalClaim: (id: number) => request.post(`/animals/claims/${id}/approve`),
|
||||
|
||||
// 拒绝动物认领
|
||||
rejectAnimalClaim: (id: number, reason: string) => request.post(`/animals/claims/${id}/reject`, { reason })
|
||||
}
|
||||
|
||||
// 订单管理API
|
||||
export const orderAPI = {
|
||||
// 获取订单列表
|
||||
getOrders: (params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
status?: string
|
||||
type?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}) => request.get('/orders', { params }),
|
||||
|
||||
// 获取订单详情
|
||||
getOrder: (id: number) => request.get(`/orders/${id}`),
|
||||
|
||||
// 更新订单状态
|
||||
updateOrderStatus: (id: number, status: string) => request.put(`/orders/${id}/status`, { status }),
|
||||
|
||||
// 导出订单
|
||||
exportOrders: (params: any) => request.get('/orders/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 推广管理API
|
||||
export const promotionAPI = {
|
||||
// 获取推广数据
|
||||
getPromotionStats: () => request.get('/promotion/stats'),
|
||||
|
||||
// 获取推广记录
|
||||
getPromotionRecords: (params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
status?: string
|
||||
}) => request.get('/promotion/records', { params }),
|
||||
|
||||
// 审核提现申请
|
||||
approveWithdrawal: (id: number) => request.post(`/promotion/withdrawals/${id}/approve`),
|
||||
|
||||
// 拒绝提现申请
|
||||
rejectWithdrawal: (id: number, reason: string) => request.post(`/promotion/withdrawals/${id}/reject`, { reason }),
|
||||
|
||||
// 导出推广数据
|
||||
exportPromotionData: (params: any) => request.get('/promotion/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 系统管理API
|
||||
export const systemAPI = {
|
||||
// 获取系统配置
|
||||
getConfig: () => request.get('/system/config'),
|
||||
|
||||
// 更新系统配置
|
||||
updateConfig: (data: any) => request.put('/system/config', data),
|
||||
|
||||
// 获取操作日志
|
||||
getOperationLogs: (params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
action?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}) => request.get('/system/logs', { params }),
|
||||
|
||||
// 清理缓存
|
||||
clearCache: () => request.post('/system/cache/clear'),
|
||||
|
||||
// 系统健康检查
|
||||
healthCheck: () => request.get('/system/health')
|
||||
}
|
||||
|
||||
export default api
|
||||
350
admin-system/src/layouts/MainLayout.vue
Normal file
350
admin-system/src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<a-layout class="main-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
:width="240"
|
||||
class="layout-sider"
|
||||
>
|
||||
<div class="logo">
|
||||
<img src="@/assets/logo.png" alt="Logo" v-if="!collapsed" />
|
||||
<h1 v-if="!collapsed">结伴客管理</h1>
|
||||
<span v-else>JK</span>
|
||||
</div>
|
||||
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
:inline-collapsed="collapsed"
|
||||
>
|
||||
<a-menu-item key="dashboard">
|
||||
<template #icon>
|
||||
<DashboardOutlined />
|
||||
</template>
|
||||
<span>仪表板</span>
|
||||
<router-link to="/dashboard" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="users">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
<span>用户管理</span>
|
||||
<router-link to="/users" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="merchants">
|
||||
<template #icon>
|
||||
<ShopOutlined />
|
||||
</template>
|
||||
<span>商家管理</span>
|
||||
<router-link to="/merchants" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="travel">
|
||||
<template #icon>
|
||||
<CompassOutlined />
|
||||
</template>
|
||||
<span>旅行管理</span>
|
||||
<router-link to="/travel" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="animals">
|
||||
<template #icon>
|
||||
<HeartOutlined />
|
||||
</template>
|
||||
<span>动物管理</span>
|
||||
<router-link to="/animals" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="orders">
|
||||
<template #icon>
|
||||
<ShoppingCartOutlined />
|
||||
</template>
|
||||
<span>订单管理</span>
|
||||
<router-link to="/orders" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="promotion">
|
||||
<template #icon>
|
||||
<GiftOutlined />
|
||||
</template>
|
||||
<span>推广管理</span>
|
||||
<router-link to="/promotion" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="system">
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
<span>系统设置</span>
|
||||
<router-link to="/system" />
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<a-layout>
|
||||
<!-- 顶部导航 -->
|
||||
<a-layout-header class="layout-header">
|
||||
<div class="header-left">
|
||||
<menu-unfold-outlined
|
||||
v-if="collapsed"
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
/>
|
||||
<menu-fold-outlined
|
||||
v-else
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
/>
|
||||
<a-breadcrumb class="breadcrumb">
|
||||
<a-breadcrumb-item>首页</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>{{ currentRouteMeta.title }}</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<a-space :size="16">
|
||||
<a-tooltip title="消息">
|
||||
<a-badge :count="5" dot>
|
||||
<bell-outlined class="header-icon" />
|
||||
</a-badge>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip title="帮助文档">
|
||||
<question-circle-outlined class="header-icon" />
|
||||
</a-tooltip>
|
||||
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-avatar
|
||||
size="small"
|
||||
:src="userAvatar"
|
||||
class="avatar"
|
||||
/>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<user-outlined />
|
||||
<span>个人中心</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<setting-outlined />
|
||||
<span>账户设置</span>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item @click="handleLogout">
|
||||
<logout-outlined />
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<span class="username">{{ userName }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<a-layout-content class="layout-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
UserOutlined,
|
||||
ShopOutlined,
|
||||
CompassOutlined,
|
||||
HeartOutlined,
|
||||
ShoppingCartOutlined,
|
||||
GiftOutlined,
|
||||
SettingOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
BellOutlined,
|
||||
QuestionCircleOutlined,
|
||||
LogoutOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const collapsed = ref(false)
|
||||
const selectedKeys = ref<string[]>(['dashboard'])
|
||||
|
||||
// 计算属性
|
||||
const currentRouteMeta = computed(() => route.meta || {})
|
||||
const userName = computed(() => appStore.state.user?.nickname || '管理员')
|
||||
const userAvatar = computed(() => appStore.state.user?.avatar || 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin')
|
||||
|
||||
// 监听路由变化
|
||||
router.afterEach((to) => {
|
||||
selectedKeys.value = [to.name as string]
|
||||
})
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
Modal.confirm({
|
||||
title: '确认退出',
|
||||
content: '您确定要退出登录吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
appStore.logout()
|
||||
message.success('退出成功')
|
||||
router.push('/login')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.main-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
z-index: 10;
|
||||
|
||||
.logo {
|
||||
height: 64px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 16px;
|
||||
border-radius: 6px;
|
||||
|
||||
img {
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-menu-dark) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item) {
|
||||
margin: 4px 0;
|
||||
border-radius: 6px;
|
||||
|
||||
&.ant-menu-item-selected {
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-right: 16px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.header-icon {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
min-height: calc(100vh - 64px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.layout-header {
|
||||
padding: 0 16px;
|
||||
|
||||
.header-left {
|
||||
.breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
admin-system/src/main.ts
Normal file
35
admin-system/src/main.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
|
||||
// 引入样式文件
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import './styles/index.less'
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 配置Pinia状态管理
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册路由
|
||||
app.use(router)
|
||||
|
||||
// 注册状态管理
|
||||
app.use(pinia)
|
||||
|
||||
// 注册Ant Design Vue
|
||||
app.use(Antd)
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
|
||||
// 开发环境调试信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🚀 结伴客后台管理系统启动成功')
|
||||
console.log('📦 版本:', import.meta.env.VITE_APP_VERSION)
|
||||
console.log('🌐 环境:', import.meta.env.MODE)
|
||||
console.log('🔗 API地址:', import.meta.env.VITE_API_BASE_URL)
|
||||
}
|
||||
193
admin-system/src/pages/Login.vue
Normal file
193
admin-system/src/pages/Login.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-form">
|
||||
<div class="login-header">
|
||||
<h1>{{ appName }}</h1>
|
||||
<p>欢迎回来,请登录您的账号</p>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="login"
|
||||
autocomplete="off"
|
||||
@finish="onFinish"
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-form-item
|
||||
name="username"
|
||||
:rules="[{ required: true, message: '请输入用户名!' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formState.username"
|
||||
size="large"
|
||||
placeholder="用户名"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
name="password"
|
||||
:rules="[{ required: true, message: '请输入密码!' }]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="formState.password"
|
||||
size="large"
|
||||
placeholder="密码"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
block
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>© 2025 结伴客系统 - 后台管理系统 v{{ appVersion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
interface FormState {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const loading = ref(false)
|
||||
|
||||
const formState = reactive<FormState>({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || '结伴客后台管理系统'
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
|
||||
|
||||
const onFinish = async (values: FormState) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟登录过程
|
||||
console.log('登录信息:', values)
|
||||
|
||||
// TODO: 调用真实登录接口
|
||||
// const response = await authAPI.login(values)
|
||||
|
||||
// 模拟登录成功
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 保存token
|
||||
localStorage.setItem('admin_token', 'mock_token_123456')
|
||||
|
||||
// 更新用户状态
|
||||
appStore.setUser({
|
||||
id: 1,
|
||||
username: values.username,
|
||||
nickname: '管理员',
|
||||
role: 'admin'
|
||||
})
|
||||
|
||||
message.success('登录成功!')
|
||||
|
||||
// 跳转到首页或重定向页面
|
||||
const redirect = router.currentRoute.value.query.redirect as string
|
||||
router.push(redirect || '/dashboard')
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
message.error('登录失败,请检查用户名和密码')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('表单验证失败:', errorInfo)
|
||||
message.warning('请填写完整的登录信息')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn) {
|
||||
border-radius: 6px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
688
admin-system/src/pages/animal/index.vue
Normal file
688
admin-system/src/pages/animal/index.vue
Normal file
@@ -0,0 +1,688 @@
|
||||
<template>
|
||||
<div class="animal-management">
|
||||
<a-page-header
|
||||
title="动物管理"
|
||||
sub-title="管理动物信息和认领记录"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增动物
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
||||
<a-tab-pane key="animals" tab="动物列表">
|
||||
<a-card>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="动物名称/编号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="全部类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="alpaca">羊驼</a-select-option>
|
||||
<a-select-option value="dog">狗狗</a-select-option>
|
||||
<a-select-option value="cat">猫咪</a-select-option>
|
||||
<a-select-option value="rabbit">兔子</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="available">可认领</a-select-option>
|
||||
<a-select-option value="claimed">已认领</a-select-option>
|
||||
<a-select-option value="reserved">预留中</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 动物表格 -->
|
||||
<a-table
|
||||
:columns="animalColumns"
|
||||
:data-source="animalList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image
|
||||
:width="60"
|
||||
:height="60"
|
||||
:src="record.image_url"
|
||||
:fallback="fallbackImage"
|
||||
style="border-radius: 6px;"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'price'">
|
||||
¥{{ record.price }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleViewAnimal(record)">
|
||||
<EyeOutlined />
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="handleEditAnimal(record)">
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" danger @click="handleDeleteAnimal(record)">
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="claims" tab="认领记录">
|
||||
<a-card>
|
||||
<!-- 认领记录搜索 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="claimSearchForm">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="claimSearchForm.keyword"
|
||||
placeholder="用户/动物名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="claimSearchForm.status"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleClaimSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleClaimReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 认领记录表格 -->
|
||||
<a-table
|
||||
:columns="claimColumns"
|
||||
:data-source="claimList"
|
||||
:loading="claimLoading"
|
||||
:pagination="claimPagination"
|
||||
:row-key="record => record.id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'animal_image'">
|
||||
<a-image
|
||||
:width="40"
|
||||
:height="40"
|
||||
:src="record.animal_image"
|
||||
:fallback="fallbackImage"
|
||||
style="border-radius: 4px;"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getClaimStatusColor(record.status)">
|
||||
{{ getClaimStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<template v-if="record.status === 'pending'">
|
||||
<a-button size="small" type="primary" @click="handleApproveClaim(record)">
|
||||
<CheckOutlined />
|
||||
通过
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="handleRejectClaim(record)">
|
||||
<CloseOutlined />
|
||||
拒绝
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-button size="small" @click="handleViewClaim(record)">
|
||||
<EyeOutlined />
|
||||
详情
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
interface Animal {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
breed: string
|
||||
age: number
|
||||
price: number
|
||||
status: string
|
||||
image_url: string
|
||||
description: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface AnimalClaim {
|
||||
id: number
|
||||
animal_id: number
|
||||
animal_name: string
|
||||
animal_image: string
|
||||
user_name: string
|
||||
user_phone: string
|
||||
status: string
|
||||
applied_at: string
|
||||
processed_at: string
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
type: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface ClaimSearchForm {
|
||||
keyword: string
|
||||
status: string
|
||||
}
|
||||
|
||||
const activeTab = ref('animals')
|
||||
const loading = ref(false)
|
||||
const claimLoading = ref(false)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const claimSearchForm = reactive<ClaimSearchForm>({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const animalList = ref<Animal[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '小白',
|
||||
type: 'alpaca',
|
||||
breed: '苏利羊驼',
|
||||
age: 2,
|
||||
price: 1000,
|
||||
status: 'available',
|
||||
image_url: 'https://api.dicebear.com/7.x/bottts/svg?seed=alpaca',
|
||||
description: '温顺可爱的羊驼',
|
||||
created_at: '2024-01-10'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '旺财',
|
||||
type: 'dog',
|
||||
breed: '金毛寻回犬',
|
||||
age: 1,
|
||||
price: 800,
|
||||
status: 'claimed',
|
||||
image_url: 'https://api.dicebear.com/7.x/bottts/svg?seed=dog',
|
||||
description: '活泼聪明的金毛',
|
||||
created_at: '2024-02-15'
|
||||
}
|
||||
])
|
||||
|
||||
const claimList = ref<AnimalClaim[]>([
|
||||
{
|
||||
id: 1,
|
||||
animal_id: 1,
|
||||
animal_name: '小白',
|
||||
animal_image: 'https://api.dicebear.com/7.x/bottts/svg?seed=alpaca',
|
||||
user_name: '张先生',
|
||||
user_phone: '13800138000',
|
||||
status: 'pending',
|
||||
applied_at: '2024-03-01',
|
||||
processed_at: ''
|
||||
}
|
||||
])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 50,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const claimPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 30,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const animalColumns = [
|
||||
{
|
||||
title: '图片',
|
||||
key: 'image',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '品种',
|
||||
dataIndex: 'breed',
|
||||
key: 'breed',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '年龄',
|
||||
dataIndex: 'age',
|
||||
key: 'age',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
customRender: ({ text }: { text: number }) => `${text}岁`
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
key: 'price',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
const claimColumns = [
|
||||
{
|
||||
title: '动物',
|
||||
key: 'animal_image',
|
||||
width: 60,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '动物名称',
|
||||
dataIndex: 'animal_name',
|
||||
key: 'animal_name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'user_phone',
|
||||
key: 'user_phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'applied_at',
|
||||
key: 'applied_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '处理时间',
|
||||
dataIndex: 'processed_at',
|
||||
key: 'processed_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
极速版 key: 'actions',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOUMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy41SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
|
||||
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
alpaca: 'pink',
|
||||
dog: 'orange',
|
||||
cat: 'blue',
|
||||
rabbit: 'green'
|
||||
}
|
||||
return colors[type as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
alpaca: '羊驼',
|
||||
dog: '狗狗',
|
||||
cat: '猫咪',
|
||||
rabbit: '兔子'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 状态映射
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
available: 'green',
|
||||
claimed: 'blue',
|
||||
reserved: 'orange'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
available: '可认领',
|
||||
claimed: '已认领',
|
||||
reserved: '预留中'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
const getClaimStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
completed: 'blue'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getClaimStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadAnimals()
|
||||
loadClaims()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadAnimals = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
message.error('加载动物列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadClaims = async () => {
|
||||
claimLoading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
message.error('加载认领记录失败')
|
||||
} finally {
|
||||
claimLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
if (key === 'animals') {
|
||||
loadAnimals()
|
||||
} else if (key === 'claims') {
|
||||
loadClaims()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadAnimals()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadAnimals()
|
||||
}
|
||||
|
||||
const handleClaimSearch = () => {
|
||||
claimPagination.current = 1
|
||||
loadClaims()
|
||||
}
|
||||
|
||||
const handleClaimReset = () => {
|
||||
Object.assign(claimSearchForm, {
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
claimPagination.current = 1
|
||||
loadClaims()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (activeTab.value === 'animals') {
|
||||
loadAnimals()
|
||||
} else {
|
||||
loadClaims()
|
||||
}
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadAnimals()
|
||||
}
|
||||
|
||||
const handleViewAnimal = (record: Animal) => {
|
||||
message.info(`查看动物: ${record.name}`)
|
||||
}
|
||||
|
||||
const handleEditAnimal = (record: Animal) => {
|
||||
message.info(`编辑动物: ${record.name}`)
|
||||
}
|
||||
|
||||
const handleDeleteAnimal = (record: Animal) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除动物 "${record.name}" 吗?`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('动物已删除')
|
||||
loadAnimals()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveClaim = (record: AnimalClaim) => {
|
||||
Modal.confirm({
|
||||
title: '确认通过',
|
||||
content: `确定要通过用户 "${record.user_name}" 的认领申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('认领申请已通过')
|
||||
loadClaims()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRejectClaim = (record: AnimalClaim) => {
|
||||
Modal.confirm({
|
||||
title: '确认拒绝',
|
||||
content: `确定要拒绝用户 "${record.user_name}" 的认领申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('认领申请已拒绝')
|
||||
loadClaims()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleViewClaim = (record: AnimalClaim) => {
|
||||
message.info(`查看认领详情: ${record.animal_name}`)
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
message.info('新增动物功能开发中')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.animal-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
245
admin-system/src/pages/dashboard/index.vue
Normal file
245
admin-system/src/pages/dashboard/index.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<a-page-header
|
||||
title="仪表板"
|
||||
sub-title="系统概览和统计数据"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button>刷新</a-button>
|
||||
<a-button type="primary">导出数据</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总用户数"
|
||||
:value="11284"
|
||||
:precision="0"
|
||||
suffix="人"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="商家数量"
|
||||
:value="356"
|
||||
:precision="0"
|
||||
suffix="家"
|
||||
>
|
||||
<template #prefix>
|
||||
<ShopOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="旅行计划"
|
||||
:value="1287"
|
||||
:precision="0"
|
||||
suffix="个"
|
||||
>
|
||||
<template #prefix>
|
||||
<CompassOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="动物认领"
|
||||
:value="542"
|
||||
:precision="0"
|
||||
suffix="只"
|
||||
>
|
||||
<template #prefix>
|
||||
<HeartOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :span="12">
|
||||
<a-card title="用户增长趋势" class="chart-card">
|
||||
<div class="chart-placeholder">
|
||||
<BarChartOutlined />
|
||||
<p>用户增长图表</p>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="订单统计" class="chart-card">
|
||||
<div class="chart-placeholder">
|
||||
<PieChartOutlined />
|
||||
<p>订单分布图表</p>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" class="activity-row">
|
||||
<a-col :span="16">
|
||||
<a-card title="最近活动" class="activity-card">
|
||||
<a-list item-layout="horizontal" :data-source="recentActivities">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta
|
||||
:description="item.description"
|
||||
>
|
||||
<template #title>
|
||||
<a>{{ item.title }}</a>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<a-avatar :src="item.avatar" />
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<div>{{ item.time }}</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card title="系统信息" class="info-card">
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="系统版本">
|
||||
{{ appVersion }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行环境">
|
||||
{{ environment }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="启动时间">
|
||||
{{ startupTime }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="内存使用">
|
||||
256MB / 2GB
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
UserOutlined,
|
||||
ShopOutlined,
|
||||
CompassOutlined,
|
||||
HeartOutlined,
|
||||
BarChartOutlined,
|
||||
PieChartOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
|
||||
const environment = import.meta.env.MODE || 'development'
|
||||
const startupTime = new Date().toLocaleString()
|
||||
|
||||
const recentActivities = [
|
||||
{
|
||||
title: '新用户注册',
|
||||
description: '用户"旅行爱好者"完成了注册',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=1',
|
||||
time: '2分钟前'
|
||||
},
|
||||
{
|
||||
title: '旅行计划创建',
|
||||
description: '用户"探险家"发布了西藏旅行计划',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=2',
|
||||
time: '5分钟前'
|
||||
},
|
||||
{
|
||||
title: '动物认领',
|
||||
description: '用户"动物之友"认领了一只羊驼',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=3',
|
||||
time: '10分钟前'
|
||||
},
|
||||
{
|
||||
title: '订单完成',
|
||||
description: '花店"鲜花坊"完成了一笔鲜花订单',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=4',
|
||||
time: '15分钟前'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.activity-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-card,
|
||||
.activity-card,
|
||||
.info-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chart-placeholder .anticon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-title) {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
420
admin-system/src/pages/merchant/index.vue
Normal file
420
admin-system/src/pages/merchant/index.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<div class="merchant-management">
|
||||
<a-page-header
|
||||
title="商家管理"
|
||||
sub-title="管理入驻商家信息"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<template #icon>
|
||||
<ShopOutlined />
|
||||
</template>
|
||||
新增商家
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-card>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="商家名称/联系人/手机号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="全部类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="flower_shop">花店</a-select-option>
|
||||
<a-select-option value="activity_organizer">活动组织</a-select-option>
|
||||
<a-select-option value="farm_owner">农场主</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="disabled">已禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 商家表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="merchantList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleView(record)">
|
||||
<EyeOutlined />
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<template v-if="record.status === 'pending'">
|
||||
<a-button size="small" type="primary" @click="handleApprove(record)">
|
||||
<CheckOutlined />
|
||||
通过
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="handleReject(record)">
|
||||
<CloseOutlined />
|
||||
拒绝
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="record.status === 'approved'">
|
||||
<a-button size="small" danger @click="handleDisable(record)">
|
||||
<StopOutlined />
|
||||
禁用
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="record.status === 'disabled'">
|
||||
<a-button size="small" type="primary" @click="handleEnable(record)">
|
||||
<PlayCircleOutlined />
|
||||
启用
|
||||
</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
ShopOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
StopOutlined,
|
||||
PlayCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
interface Merchant {
|
||||
id: number
|
||||
business_name: string
|
||||
merchant_type: string
|
||||
contact_person: string
|
||||
contact_phone: string
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
type: string
|
||||
status: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 模拟商家数据
|
||||
const merchantList = ref<Merchant[]>([
|
||||
{
|
||||
id: 1,
|
||||
business_name: '鲜花坊',
|
||||
merchant_type: 'flower_shop',
|
||||
contact_person: '张经理',
|
||||
contact_phone: '13800138000',
|
||||
status: 'approved',
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
business_name: '阳光农场',
|
||||
merchant_type: 'farm_owner',
|
||||
contact_person: '李场主',
|
||||
contact_phone: '13800138001',
|
||||
status: 'pending',
|
||||
created_at: '2024-02-20'
|
||||
}
|
||||
])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 50,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '商家名称',
|
||||
dataIndex: 'business_name',
|
||||
key: 'business_name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
dataIndex: 'contact_person',
|
||||
key: 'contact_person',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contact_phone',
|
||||
key: 'contact_phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '入驻时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
flower_shop: 'pink',
|
||||
activity_organizer: 'green',
|
||||
farm_owner: 'orange'
|
||||
}
|
||||
return colors[type as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
flower_shop: '花店',
|
||||
activity_organizer: '活动组织',
|
||||
farm_owner: '农场'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 状态映射
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
disabled: 'default'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
disabled: '已禁用'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadMerchants()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadMerchants = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 调用真实API
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
message.error('加载商家列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadMerchants()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleView = (record: Merchant) => {
|
||||
message.info(`查看商家: ${record.business_name}`)
|
||||
}
|
||||
|
||||
const handleApprove = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认通过',
|
||||
content: `确定要通过商家 "${record.business_name}" 的入驻申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('商家入驻申请已通过')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleReject = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认拒绝',
|
||||
content: `确定要拒绝商家 "${record.business_name}" 的入驻申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('商家入驻申请已拒绝')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认禁用',
|
||||
content: `确定要禁用商家 "${record.business_name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('商家已禁用')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEnable = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认启用',
|
||||
content: `确定要启用商家 "${record.business_name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('商家已启用')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
message.info('新增商家功能开发中')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.merchant-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
437
admin-system/src/pages/order/index.vue
Normal file
437
admin-system/src/pages/order/index.vue
Normal file
@@ -0,0 +1,437 @@
|
||||
<template>
|
||||
<div class="order-management">
|
||||
<a-page-header title="订单管理" sub-title="管理花束订单和交易记录">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showStats">
|
||||
<template #icon><BarChartOutlined /></template>
|
||||
销售统计
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
||||
<a-tab-pane key="orders" tab="订单列表">
|
||||
<a-card>
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="订单号">
|
||||
<a-input v-model:value="searchForm.order_no" placeholder="输入订单号" allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="searchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
|
||||
<a-select-option value="pending">待支付</a-select-option>
|
||||
<a-select-option value="paid">已支付</a-select-option>
|
||||
<a-select-option value="shipped">已发货</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="cancelled">已取消</a-select-option>
|
||||
<a-select-option value极速版="refunded">已退款</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="下单时间">
|
||||
<a-range-picker v-model:value="searchForm.orderTime" :placeholder="['开始时间', '结束时间']" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="orderColumns"
|
||||
:data-source="orderList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'order_no'">
|
||||
<a-typography-text copyable>{{ record.order_no }}</a-typography-text>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'amount'">¥{{ record.amount }}</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'payment_method'">
|
||||
<span>{{ getPaymentMethodText(record.payment_method) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleViewOrder(record)">
|
||||
<EyeOutlined />详情
|
||||
</a-button>
|
||||
|
||||
<template v-if="record.status === 'paid'">
|
||||
<a-button size="small" type="primary" @click="handleShip(record)">
|
||||
<CarOutlined />发货
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-if="record.status === 'shipped'">
|
||||
<a-button size="small" type="primary" @click="handleComplete(record)">
|
||||
<CheckCircleOutlined />完成
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-if="['pending', 'paid'].includes(record.status)">
|
||||
<a-button size="small" danger @click="handleCancel(record)">
|
||||
<极速版CloseOutlined />取消
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-if="record.status === '极速版paid'">
|
||||
<a-button size="small" danger @click="handleRefund(record)">
|
||||
<RollbackOutlined />退款
|
||||
</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="statistics" tab="销售统计">
|
||||
<a-card title="销售数据概览">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="今日订单" :value="statistics.today_orders" :precision="0" :value-style="{ color: '#3f8600' }">
|
||||
<template #prefix><ShoppingOutlined /></template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="今日销售额" :value="statistics.today_sales" :precision="2" prefix="¥" :value-style="{ color: '#cf1322' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="本月订单" :value="statistics.month_orders" :precision="0" :value-style="{ color: '#1890极速版ff' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="本月销售额" :value="statistics.month_sales" :precision="2" prefix="¥" :极速版value-style="{ color: '#722ed1' }" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<a-card title="销售趋势" style="margin-top: 16px;">
|
||||
<div style="height: 300px;">
|
||||
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">
|
||||
<BarChartOutlined style="font-size: 48px; margin-right: 12px;" />
|
||||
<span>销售趋势图表开发中</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
BarChartOutlined,
|
||||
EyeOutlined,
|
||||
CarOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseOutlined,
|
||||
RollbackOutlined,
|
||||
ShoppingOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
interface Order {
|
||||
id: number
|
||||
order_no: string
|
||||
user_id: number
|
||||
user_name: string
|
||||
user_phone: string
|
||||
amount: number
|
||||
status: string
|
||||
payment_method: string
|
||||
created_at: string
|
||||
paid_at: string
|
||||
shipped_at: string
|
||||
completed_at: string
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
order_no: string
|
||||
status: string
|
||||
orderTime: any[]
|
||||
}
|
||||
|
||||
interface Statistics {
|
||||
today_orders: number
|
||||
today_sales: number
|
||||
month_orders: number
|
||||
month_sales: number
|
||||
}
|
||||
|
||||
const activeTab = ref('orders')
|
||||
const loading = ref(false)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
order_no: '',
|
||||
status: '',
|
||||
orderTime: []
|
||||
})
|
||||
|
||||
const statistics = reactive<Statistics>({
|
||||
today_orders: 0,
|
||||
today_sales: 0,
|
||||
month_orders: 0,
|
||||
month_sales: 0
|
||||
})
|
||||
|
||||
const orderList = ref<Order[]>([
|
||||
{
|
||||
id: 1,
|
||||
order_no: 'ORD202403150001',
|
||||
user_id: 1001,
|
||||
user_name: '张先生',
|
||||
user_phone: '13800138000',
|
||||
amount: 299.99,
|
||||
status: 'paid',
|
||||
payment_method: 'wechat',
|
||||
created_at: '2024-03-15 10:30:00',
|
||||
paid_at: '2024-03-15 10:35:00',
|
||||
shipped_at: '',
|
||||
completed_at: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order_no: 'ORD202403140002',
|
||||
user_id: 1002,
|
||||
user_name: '李女士',
|
||||
user_phone: '13800138001',
|
||||
amount: 极速版199.99,
|
||||
status: 'shipped',
|
||||
payment_method: 'alipay',
|
||||
created_at: '2024-03-14 14:20:00',
|
||||
paid_at: '2024-03-14 14:25:00',
|
||||
shipped_at: '2024-03-15 09:00:00',
|
||||
completed_at: ''
|
||||
}
|
||||
])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 50,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const orderColumns = [
|
||||
{ title: '订单号', key: 'order_no', width: 160 },
|
||||
{ title: '用户', dataIndex: 'user_name', key: 'user_name', width: 100 },
|
||||
{ title: '联系电话', dataIndex: 'user极速版_phone', key: 'user_phone', width: 120 },
|
||||
{ title: '金额', key: 'amount', width: 100, align: 'center' },
|
||||
{ title: '状态', key: 'status', width: 100, align: 'center' },
|
||||
{ title: '支付方式', key: 'payment_method', width: 100, align: 'center' },
|
||||
{ title: '下单时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
|
||||
{ title: '操作', key: 'actions', width: 200, align: 'center' }
|
||||
]
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
paid: 'blue',
|
||||
shipped: 'green',
|
||||
completed: 'purple',
|
||||
cancelled: 'red',
|
||||
refunded: 'default'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '待支付',
|
||||
paid: '已支付',
|
||||
shipped: '已发货',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
refunded: '已退款'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
const getPaymentMethodText = (method: string) => {
|
||||
const texts = {
|
||||
wechat: '微信支付',
|
||||
alipay: '支付宝',
|
||||
bank: '银行卡',
|
||||
balance: '余额支付'
|
||||
}
|
||||
return texts[method as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOrders()
|
||||
loadStatistics()
|
||||
})
|
||||
|
||||
const loadOrders = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
message.error('加载订单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadStatistics = async () => {
|
||||
try {
|
||||
statistics.today_orders = 15
|
||||
statistics.today_sales = 4500.50
|
||||
statistics.month_orders = 120
|
||||
statistics.month_sales = 35600.80
|
||||
} catch (error) {
|
||||
message.error('加载统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
if (key === 'orders') {
|
||||
loadOrders()
|
||||
} else if (key === 'statistics') {
|
||||
loadStatistics()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadOrders()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
order_no: '',
|
||||
status: '',
|
||||
orderTime: []
|
||||
})
|
||||
pagination.current = 1
|
||||
loadOrders()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (activeTab.value === 'orders') {
|
||||
loadOrders()
|
||||
} else {
|
||||
loadStatistics()
|
||||
}
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange极速版'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadOrders()
|
||||
}
|
||||
|
||||
const handleViewOrder = (record: Order) => {
|
||||
message.info(`查看订单: ${record.order_no}`)
|
||||
}
|
||||
|
||||
const handleShip = async (record: Order) => {
|
||||
Modal.confirm({
|
||||
title: '确认发货',
|
||||
content: `确定要发货订单 "${record.order_no}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('订单已发货')
|
||||
loadOrders()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleComplete = async (record: Order) => {
|
||||
Modal.confirm({
|
||||
title: '确认完成',
|
||||
content: `确定要完成订单 "${record.order_no}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('订单已完成')
|
||||
loadOrders()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
极速版 }
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = async (record: Order) => {
|
||||
Modal.confirm({
|
||||
title: '确认取消',
|
||||
content: `确定要取消订单 "${record.order极速版_no}" 吗?`,
|
||||
on极速版Ok: async () => {
|
||||
try {
|
||||
message.success('订单已取消')
|
||||
loadOrders()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRefund = async (record: Order) => {
|
||||
Modal.confirm({
|
||||
title: '确认退款',
|
||||
content: `确定要退款订单 "${record.order_no}" 吗?退款金额: ¥${record.amount}`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('退款申请已提交')
|
||||
loadOrders()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showStats = () => {
|
||||
activeTab.value = 'statistics'
|
||||
loadStatistics()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.order-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
573
admin-system/src/pages/promotion/index.vue
Normal file
573
admin-system/src/pages/promotion/index.vue
Normal file
@@ -0,0 +1,573 @@
|
||||
<template>
|
||||
<div class="promotion-management">
|
||||
<a-page-header title="推广管理" sub-title="管理推广活动和奖励">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建活动
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab" @change="handle极速版TabChange">
|
||||
<a-tab-pane key="activities" tab="推广活动">
|
||||
<a-card>
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="活动名称">
|
||||
<a-input v-model:value="searchForm.name" placeholder="输入活动名称" allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="searchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
|
||||
<a-select-option value="active">进行中</a-select-option>
|
||||
<a-select-option value="upcoming">未开始</a-select-极速版option>
|
||||
<a-select-option value="ended">已结束</a-select-option>
|
||||
<a-select-option value="paused极速版">已暂停</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="活动时间">
|
||||
<a-range-picker v-model:value="searchForm.activityTime" :placeholder="['开始极速版时间', '结束时间']" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="activityColumns"
|
||||
:data-source="activityList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'reward_type'">
|
||||
<span>{{ getRewardTypeText(record.reward_type) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'reward_amount'">
|
||||
<span v-if="record.reward_type === 'cash'">¥{{ record.reward_amount }}</span>
|
||||
<span v-else-if="record.reward_type === 'points'">{{ record.reward_amount }}积分</span>
|
||||
<span v-else-if="record.reward_type === 'coupon'">{{ record.re极速版ward_amount }}元券</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'participants'">
|
||||
<a-progress
|
||||
:percent="record.current_participants / record.max_participants * 100"
|
||||
size="small"
|
||||
:show-info="false"
|
||||
/>
|
||||
<div style="font-size: 12px; text-align: center;">
|
||||
{{ record.current_participants }}/{{ record.max_participants }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleViewActivity(record)">
|
||||
<EyeOutlined />详情
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="handleEditActivity(record)">
|
||||
<EditOutlined />编辑
|
||||
</极速版a-button>
|
||||
|
||||
<template v-if="record.status === 'active'">
|
||||
极速版 <a-button size="small" danger @click="handlePauseActivity(record)">
|
||||
<PauseOutlined />暂停
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-if="record.status === 'paused'">
|
||||
<a-button size="small" type="primary" @click="handleResumeActivity(record)">
|
||||
<PlayCircleOutlined />继续
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-button size="small" danger @click="handleDeleteActivity(record)">
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="rewards" tab="奖励记录">
|
||||
<a-card>
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="rewardSearchForm">
|
||||
<a-form-item label="用户">
|
||||
<a-input v-model:value="rewardSearchForm.user" placeholder="用户名/手机号" allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="奖励类型">
|
||||
<a-select v-model:value="rewardSearchForm.reward_type" placeholder="全部类型" style="width: 120px" allow-clear>
|
||||
<a-select-option value="cash">现金</a-select-option>
|
||||
<a-select-option value="points">积分</a-select-option>
|
||||
<a-select-option value="coupon">优惠券</a-select-option>
|
||||
</a-select>
|
||||
</极速版a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="rewardSearchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
|
||||
<a-select-option value="pending">待发放</a-select-option>
|
||||
<a-select-option value="issued">已发放</a-select-option>
|
||||
<a-select-option value="failed">发放失败</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleRewardSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleRewardReset">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="rewardColumns"
|
||||
:data-source="rewardList"
|
||||
:loading="rewardLoading"
|
||||
:pagination="rewardPagination"
|
||||
:极速版row-key="record => record.id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'reward_type'">
|
||||
<span>{{ getRewardTypeText(record.reward_type) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'reward_amount'">
|
||||
<span v-if="record.reward_type === 'cash'">¥{{ record.reward_amount }}</span>
|
||||
<span v-else-if="record.reward_type === 'points'">{{ record.reward_amount }}积分</span>
|
||||
<span v-else-if="record.reward_type === 'coupon'">{{ record.reward_amount }}元券</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getRewardStatusColor(record.status)">{{ getRewardStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<template v-if="record.status === 'pending'">
|
||||
<a-button size="small" type="primary" @click="handleIssueReward(record)">
|
||||
<CheckOutlined />发放
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-button size="small" @click="handleViewRew极速版ard(record)">
|
||||
<EyeOutlined />详情
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a极速版-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PauseOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
interface PromotionActivity {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
reward_type: string极速版
|
||||
reward_amount: number
|
||||
status: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
max_participants:极速版 number
|
||||
current_participants: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface RewardRecord {
|
||||
id: number
|
||||
user_id: number
|
||||
user_name: string
|
||||
user_phone: string
|
||||
activity_id: number
|
||||
activity_name: string
|
||||
reward_type: string
|
||||
reward_amount: number
|
||||
status: string
|
||||
issued_at: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
name: string
|
||||
status: string
|
||||
activityTime: any[]
|
||||
}
|
||||
|
||||
interface RewardSearchForm {
|
||||
user: string
|
||||
reward_type: string
|
||||
status: string
|
||||
}
|
||||
|
||||
const activeTab = ref('activities')
|
||||
const loading = ref(false)
|
||||
const rewardLoading = ref(false)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
name: '',
|
||||
status: '',
|
||||
activityTime: []
|
||||
})
|
||||
|
||||
const rewardSearchForm = reactive<RewardSearchForm>({
|
||||
user: '',
|
||||
reward_type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const activityList = ref<PromotionActivity[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '邀请好友得现金',
|
||||
description: '邀请好友注册即可获得现金奖励',
|
||||
reward_type: 'cash',
|
||||
reward_amount: 10,
|
||||
status: 'active',
|
||||
start_time: '极速版2024-03-01',
|
||||
end_time: '2024-03-31',
|
||||
max_participants: 1000极速版,
|
||||
current_participants: 356,
|
||||
created_at: '2024-02-20'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '分享活动得积分',
|
||||
description: '分享活动页面即可获得积分奖励',
|
||||
reward_type: 'points',
|
||||
reward_amount: 100,
|
||||
status: 'upcoming',
|
||||
start_time: '2024-04-01',
|
||||
end_time: '202极速版4-04-30',
|
||||
max_participants: 500,
|
||||
current_p极速版articipants: 0,
|
||||
created_at: '2024-03-10'
|
||||
}
|
||||
])
|
||||
|
||||
const rewardList = ref<RewardRecord[]>([
|
||||
{
|
||||
id: 1,
|
||||
user_id: 1001,
|
||||
user_name: '张先生',
|
||||
user_phone: '13800138000',
|
||||
activity_id: 1,
|
||||
activity_name: '邀请好友极速版得现金',
|
||||
reward_type: 'cash',
|
||||
reward_amount: 10,
|
||||
status: 'issued',
|
||||
issued_at: '2024-03-05 14:30:00',
|
||||
created_at: '2024-03-05 14:25:00'
|
||||
}
|
||||
])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 50,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const rewardPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 30,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const activityColumns = [
|
||||
{ title: '活动名称', dataIndex: 'name', key: '极速版name', width: 150 },
|
||||
{ title极速版: '奖励类型', key: 'reward_type', width: 100, align: 'center' },
|
||||
{ title: '奖励金额', key: 'reward_amount', width: 100, align: 'center' },
|
||||
{ title: '状态', key: 'status', width: 100, align: 'center' },
|
||||
{ title: '活动时间', key: 'time', width: 200,
|
||||
customRender: ({ record }: { record: PromotionActivity }) =>
|
||||
`${record.start_time} 至 ${record.end_time}`
|
||||
},
|
||||
{ title: '参与人数', key: 'participants', width: 120, align: 'center' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 120 },
|
||||
{ title: '操作', key: 'actions', width: 200, align: 'center' }
|
||||
]
|
||||
|
||||
const rewardColumns = [
|
||||
{ title: '用户', dataIndex: 'user_name', key: 'user_name', width: 100 },
|
||||
{ title: '联系电话', dataIndex: 'user_phone', key: 'user_phone', width: 120 },
|
||||
{ title: '活动名称', dataIndex: 'activity_name', key: 'activity_name', width: 150 },
|
||||
{ title: '奖励类型', key: 'reward_type', width: 100, align: 'center极速版' },
|
||||
{ title: '奖励金额', key: 'reward_amount', width: 100, align: 'center' },
|
||||
{ title: '状态', key: 'status', width: 100, align: 'center' },
|
||||
{ title: '发放时间', dataIndex: 'issued_at极速版', key: 'issued_at', width: 极速版150 },
|
||||
{ title: '申请时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
|
||||
{ title: '操作', key: 'actions', width: 120, align: 'center' }
|
||||
]
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
active: 'green',
|
||||
upcoming: 'blue',
|
||||
ended: 'default',
|
||||
paused: 'orange'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string)极速版 {
|
||||
const texts = {
|
||||
active: '进行中',
|
||||
upcoming: '未开始',
|
||||
ended: '已结束',
|
||||
paused: '已暂停'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
const getRewardTypeText = (type: string) => {
|
||||
const texts = {
|
||||
cash: '现金',
|
||||
points: '积分',
|
||||
coupon: '优惠券'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
const getRewardStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending极速版: 'orange',
|
||||
issued: 'green',
|
||||
failed: 'red'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getRewardStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '待发放',
|
||||
issued: '已发放',
|
||||
failed: '发放失败'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadActivities()
|
||||
loadRewards()
|
||||
})
|
||||
|
||||
const loadActivities = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
message.error('加载活动列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadRewards = async () => {
|
||||
rewardLoading.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
message.error('加载奖励记录失败')
|
||||
} finally {
|
||||
rewardLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
if (key === 'activities') {
|
||||
loadActivities()
|
||||
} else if (key === 'rewards') {
|
||||
loadRewards()
|
||||
极速版 }
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadActivities()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
status: '',
|
||||
activityTime: []
|
||||
})
|
||||
pagination.current = 1极速版
|
||||
loadActivities()
|
||||
}
|
||||
|
||||
const handleRewardSearch = () => {
|
||||
rewardPagination.current = 1
|
||||
loadRewards()
|
||||
}
|
||||
|
||||
const handleRewardReset = () => {
|
||||
Object.assign(rewardSearchForm, {
|
||||
user: '',
|
||||
reward_type: '',
|
||||
status: ''
|
||||
})
|
||||
rewardPagination.current = 1
|
||||
loadRewards()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (activeTab.value === 'activities') {
|
||||
loadActivities()
|
||||
} else {
|
||||
loadRewards()
|
||||
}
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadActivities()
|
||||
}
|
||||
|
||||
const handleViewActivity = (record: PromotionActivity) => {
|
||||
message.info(`查看活动: ${record.name}`)
|
||||
}
|
||||
|
||||
const handleEditActivity = (record: PromotionActivity) => {
|
||||
message.info(`编辑活动: ${record.name极速版}`)
|
||||
}
|
||||
|
||||
const handlePauseActivity = async (record: PromotionActivity) => {
|
||||
Modal.confirm({
|
||||
title: '确认暂停',
|
||||
content: `确定要暂停活动 "${record.name}" 极速版吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('活动已暂停')
|
||||
loadActivities()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleResumeActivity = async (record: PromotionActivity) => {
|
||||
Modal.confirm({
|
||||
title: '确认继续',
|
||||
content: `确定要继续活动 "${record.name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('活动已继续')
|
||||
loadActivities()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteActivity = async (record: PromotionActivity) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除活动 "${record.name}" 吗?`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('活动已删除')
|
||||
loadActivities()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleIssueReward = async (record: RewardRecord) => {
|
||||
Modal.confirm({
|
||||
title: '确认发放',
|
||||
content: `确定要发放奖励给用户 "${record.user_name}" 吗?`,
|
||||
onOk: async ()极速版 => {
|
||||
try {
|
||||
message.success('奖励已发放')
|
||||
loadRewards()
|
||||
极速版 } catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleViewReward = (record: RewardRecord) => {
|
||||
message.info(`查看奖励记录: ${record.user_name}`)
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
message.info('新建活动功能开发中')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.promotion-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
289
admin-system/src/pages/system/index.vue
Normal file
289
admin-system/src/pages/system/index.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div class="system-management">
|
||||
<a-page-header title="系统管理" sub-title="管理系统设置和配置">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<ReloadOutlined />
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-card title="系统信息" size="small">
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="系统版本">v1.0.0</a-descriptions-item>
|
||||
<a-descriptions-item label="运行环境">Production</a-descriptions-item>
|
||||
<极速版a-descriptions-item label="启动时间">2024-03-15 10:00:00</a-descriptions-item>
|
||||
<a-descriptions-item label="运行时长">12天3小时</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<a-card title="数据库状态" size="small">
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="连接状态">
|
||||
<a-tag color="green">正常</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label极速版="数据库类型">MySQL</a-descriptions-item>
|
||||
<a-descriptions-item label="连接数">15极速版/100</a-descriptions-item>
|
||||
<a-descriptions-item label="查询次数">1,234次/分钟</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<a-card title="缓存状态" size="small">
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="Redis状态">
|
||||
<a-tag color="green">正常</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="内存使用">65%</a-descriptions-item>
|
||||
<a-descriptions-item label="命中率">92%</a-descriptions-item>
|
||||
<a-descriptions-item label="键数量">1,234</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" style="margin-top: 16px;">
|
||||
<a-col :span="12">
|
||||
<a-card title="服务监控" size="small">
|
||||
<a-list item-layout="horizontal" :data-source="services">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta :description="item.description">
|
||||
<template #title>
|
||||
<a-space>
|
||||
<a-tag :color="item.status === 'running' ? 'green' : 'red'">
|
||||
{{ item.status === 'running' ? '运行中' : '停止' }}
|
||||
</a-tag>
|
||||
{{ item.name }}
|
||||
</a-space>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<a-avatar :style="{ backgroundColor: item.status === 'running' ? '#52c41a' : '#ff4d4f' }">
|
||||
<DatabaseOutlined v-if="item.type === 'database'" />
|
||||
<CloudServerOutlined v-if="item.type === 'cache'" />
|
||||
<MessageOutlined v-if="item.type === 'mq'" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-button size="small" v-if="item.status === 'running'" danger @click="handleStopService(item)">
|
||||
停止
|
||||
</a-button>
|
||||
<a-button size="small" v-if="item.status === 'stopped'" type="primary" @click="handleStartService(item)">
|
||||
启动
|
||||
</a-button>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-card title="系统日志" size="small">
|
||||
<a-timeline>
|
||||
<a-t极速版imeline-item color="green">
|
||||
<p>用户登录成功 - admin (2024-03-15 14:30:22)</p>
|
||||
</a-timeline-item>
|
||||
<a-timeline-item color="blue">
|
||||
<p>数据库备份完成 - 备份文件: backup_20240315.sql (2024-03-15 14:00:00)</p>
|
||||
</a-timeline-item>
|
||||
<a-timeline-item color="orange">
|
||||
<p>系统警告 - 内存使用率超过80% (2024-03-15 13:极速版45:18)</极速版p>
|
||||
</a-timeline-item>
|
||||
<a-timeline-item color="green">
|
||||
<p>定时任务执行 - 清理过期日志 (2024-03-15 13:30:00)</p>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<div style="text-align: center; margin-top: 16px;">
|
||||
<a-button type="link" @click="viewLogs">查看完整日志</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" style="margin-top: 16px;">
|
||||
<a-col :span="24">
|
||||
<a-card title="系统极速版设置" size="small">
|
||||
<a-form :model="systemSettings" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="系统名称">
|
||||
<a-input v-model:value="systemSettings.systemName" placeholder="请输入系统名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="系统版本">
|
||||
<a-input v-model:value="systemSettings.systemVersion" placeholder="请输入系统版本极速版" />
|
||||
</a-form-item>
|
||||
</极速版a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="维护模式">
|
||||
<a-switch v-model:checked="systemSettings.maintenanceMode" />
|
||||
</极速版a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="极速版16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="会话超时(分钟)">
|
||||
<a-input-number v-model:value="systemSettings.sessionTimeout" :min="5" :max="480" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="每页显示数量">
|
||||
<a-input-number v-model:value="systemSettings.pageSize" :min="10" :max="100" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="启用API文档">
|
||||
<a-switch v-model:checked="systemSettings.enableSwagger" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="saveSettings">保存设置</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSettings">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
DatabaseOutlined,
|
||||
CloudServerOutlined,
|
||||
MessageOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
interface Service {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface SystemSettings {
|
||||
systemName: string
|
||||
systemVersion: string
|
||||
maintenanceMode: boolean
|
||||
sessionTimeout: number
|
||||
pageSize: number
|
||||
enableSwagger: boolean
|
||||
}
|
||||
|
||||
const services = ref<Service[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: 'MySQL数据库',
|
||||
type: 'database',
|
||||
description: '主数据库服务',
|
||||
status: 'running'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Redis缓存',
|
||||
type: 'cache',
|
||||
description: '缓存服务',
|
||||
status: 'running'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'RabbitMQ',
|
||||
type: 'mq',
|
||||
description: '消息队列服务',
|
||||
status: 'stopped'
|
||||
}
|
||||
])
|
||||
|
||||
const systemSettings = reactive<SystemSettings>({
|
||||
systemName: '结伴客管理系统',
|
||||
systemVersion: 'v1.0.0',
|
||||
maintenanceMode: false,
|
||||
sessionTimeout: 30,
|
||||
pageSize: 20,
|
||||
enableSwagger: true
|
||||
})
|
||||
|
||||
const handleRefresh = () => {
|
||||
message.success('系统状态已刷新')
|
||||
}
|
||||
|
||||
const handleStopService = (service: Service极速版) => {
|
||||
Modal.confirm({
|
||||
title: '确认停止',
|
||||
content: `确定要停止服务 "${service.name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
service.status = 'stopped'
|
||||
message.success('服务已停止')
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleStartService = (service: Service) => {
|
||||
Modal.confirm({
|
||||
title: '确认启动',
|
||||
content: `确定要启动服务 "${service.name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
service.status = 'running'
|
||||
message.success('服务已启动')
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewLogs = () => {
|
||||
message.info('查看系统日志功能开发中')
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
message.success('系统设置已保存')
|
||||
}
|
||||
|
||||
const resetSettings = () => {
|
||||
Object.assign(systemSettings, {
|
||||
systemName: '结伴客管理系统',
|
||||
systemVersion: 'v1.0.极速版0',
|
||||
maintenanceMode: false,
|
||||
sessionTimeout: 30,
|
||||
pageSize: 极速版20,
|
||||
enableSwagger: true
|
||||
})
|
||||
message.success('设置已重置')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.system-management {
|
||||
.ant-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-descriptions-item-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
382
admin-system/src/pages/travel/index.vue
Normal file
382
admin-system/src/pages/travel/index.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div class="travel-management">
|
||||
<a-page-header
|
||||
title="旅行管理"
|
||||
sub-title="管理旅行计划和匹配"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showStats">
|
||||
<template #icon>
|
||||
<BarChartOutlined />
|
||||
</template>
|
||||
数据统计
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-card>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="目的地">
|
||||
<a-input
|
||||
v-model:value="searchForm.destination"
|
||||
placeholder="输入目的地"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="recruiting">招募中</a-select-option>
|
||||
<a-select-option value="full">已满员</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="cancelled">已取消</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="旅行时间">
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.travelTime"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 旅行计划表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="travelList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'destination'">
|
||||
<strong>{{ record.destination }}</strong>
|
||||
<div style="font-size: 12px; color: #666;">
|
||||
{{ record.start_date }} 至 {{ record.end_date }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'budget'">
|
||||
¥{{ record.budget }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'members'">
|
||||
<a-progress
|
||||
:percent="(record.current_members / record.max_members) * 100"
|
||||
size="small"
|
||||
:show-info="false"
|
||||
/>
|
||||
<div style="font-size: 12px; text-align: center;">
|
||||
{{ record.current_members }}/{{ record.max_members }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleView(record)">
|
||||
<EyeOutlined />
|
||||
详情
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="handleMembers(record)">
|
||||
<TeamOutlined />
|
||||
成员
|
||||
</a-button>
|
||||
|
||||
<template v-if="record.status === 'recruiting'">
|
||||
<a-button size="small" type="primary" @click="handlePromote(record)">
|
||||
<RocketOutlined />
|
||||
推广
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="handleClose(record)">
|
||||
<CloseOutlined />
|
||||
关闭
|
||||
</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
BarChartOutlined,
|
||||
EyeOutlined,
|
||||
TeamOutlined,
|
||||
RocketOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
interface TravelPlan {
|
||||
id: number
|
||||
destination: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
budget: number
|
||||
max_members: number
|
||||
current_members: number
|
||||
status: string
|
||||
creator: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
destination: string
|
||||
status: string
|
||||
travelTime: any[]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
destination: '',
|
||||
status: '',
|
||||
travelTime: []
|
||||
})
|
||||
|
||||
// 模拟旅行数据
|
||||
const travelList = ref<TravelPlan[]>([
|
||||
{
|
||||
id: 1,
|
||||
destination: '西藏',
|
||||
start_date: '2024-07-01',
|
||||
end_date: '2024-07-15',
|
||||
budget: 5000,
|
||||
max_members: 6,
|
||||
current_members: 3,
|
||||
status: 'recruiting',
|
||||
creator: '旅行爱好者',
|
||||
created_at: '2024-06-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
destination: '云南',
|
||||
start_date: '2024-08-10',
|
||||
end_date: '2024-08-20',
|
||||
budget: 3000,
|
||||
max_members: 4,
|
||||
current_members: 4,
|
||||
status: 'full',
|
||||
creator: '探险家',
|
||||
created_at: '2024-07-15'
|
||||
}
|
||||
])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 50,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '旅行信息',
|
||||
key: 'destination',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '预算',
|
||||
key: 'budget',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '成员',
|
||||
key: 'members',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建者',
|
||||
dataIndex: 'creator',
|
||||
key: 'creator',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 状态映射
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
recruiting: 'blue',
|
||||
full: 'green',
|
||||
completed: 'purple',
|
||||
cancelled: 'red'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
recruiting: '招募中',
|
||||
full: '已满员',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadTravelPlans()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadTravelPlans = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 调用真实API
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
message.error('加载旅行计划失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 极速版1
|
||||
loadTravelPlans()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
destination: '',
|
||||
status: '',
|
||||
travelTime: []
|
||||
})
|
||||
pagination.current = 1
|
||||
loadTravelPlans()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadTravelPlans()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadTravelPlans()
|
||||
}
|
||||
|
||||
const handleView = (record: TravelPlan) => {
|
||||
message.info(`查看旅行计划: ${record.destination}`)
|
||||
}
|
||||
|
||||
const handleMembers = (record: TravelPlan) => {
|
||||
message.info(`查看成员: ${record.destination}`)
|
||||
}
|
||||
|
||||
const handlePromote = (record: TravelPlan) => {
|
||||
Modal.confirm({
|
||||
title: '确认推广',
|
||||
content: `确定要推广旅行计划 "${record.destination}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('旅行计划已推广')
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = (record: TravelPlan) => {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: `确定要关闭旅行计划 "${record.destination}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('旅行计划已关闭')
|
||||
loadTravelPlans()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showStats = () => {
|
||||
message.info('数据统计功能开发中')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.travel-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
650
admin-system/src/pages/user/index.vue
Normal file
650
admin-system/src/pages/user/index.vue
Normal file
@@ -0,0 +1,650 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<a-page-header
|
||||
title="用户管理"
|
||||
sub-title="管理系统用户信息"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<template #icon>
|
||||
<UserAddOutlined />
|
||||
</template>
|
||||
新增用户
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-card>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="用户名/昵称/手机号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
<a-select-option value="banned">封禁</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="注册时间">
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.registerTime"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="userList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :size="32">
|
||||
{{ record.nickname?.charAt(0) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'level'">
|
||||
<a-tag color="blue">Lv.{{ record.level }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleView(record)">
|
||||
<EyeOutlined />
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="handleEdit(record)">
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-dropdown>
|
||||
<a-button size="small">
|
||||
更多
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item
|
||||
v-if="record.status === 'active'"
|
||||
@click="handleDisable(record)"
|
||||
>
|
||||
<StopOutlined />
|
||||
禁用
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-else
|
||||
@click="handleEnable(record)"
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
启用
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="record.status !== 'banned'"
|
||||
@click="handleBan(record)"
|
||||
>
|
||||
<WarningOutlined />
|
||||
封禁
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item
|
||||
danger
|
||||
@click="handleDelete(record)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 创建/编辑用户模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="600px"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input
|
||||
v-model:value="formState.username"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="昵称" name="nickname">
|
||||
<a-input
|
||||
v-model:value="formState.nickname"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="密码" name="password" v-if="isCreate">
|
||||
<a-input-password
|
||||
v-model:value="formState.password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input
|
||||
v-model:value="formState.phone"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input
|
||||
v-model:value="formState.email"
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model:value="formState.remark"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import {
|
||||
UserAddOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
StopOutlined,
|
||||
PlayCircleOutlined,
|
||||
WarningOutlined,
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
email: string
|
||||
phone: string
|
||||
status: string
|
||||
level: number
|
||||
points: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
status: string
|
||||
registerTime: any[]
|
||||
}
|
||||
|
||||
interface FormState {
|
||||
username: string
|
||||
nickname: string
|
||||
password: string
|
||||
email: string
|
||||
phone: string
|
||||
status: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const editingUser = ref<User | null>(null)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: '',
|
||||
status: '',
|
||||
registerTime: []
|
||||
})
|
||||
|
||||
const formState = reactive<FormState>({
|
||||
username: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
status: 'active',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 4, max: 20, message: '用户名长度为4-20个字符', trigger: 'blur' }
|
||||
],
|
||||
nickname: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少6个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟用户数据
|
||||
const userList = ref<User[]>([
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
nickname: '系统管理员',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin',
|
||||
email: 'admin@jiebanke.com',
|
||||
phone: '13800138000',
|
||||
status: 'active',
|
||||
level: 10,
|
||||
points: 10000,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user001',
|
||||
nickname: '旅行爱好者',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user1',
|
||||
email: 'user001@example.com',
|
||||
phone: '13800138001',
|
||||
status: 'active',
|
||||
level: 3,
|
||||
points: 1500,
|
||||
created_at: '2024-02-15',
|
||||
updated_at: '2024-02-15'
|
||||
}
|
||||
])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 50,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户',
|
||||
key: 'avatar',
|
||||
width: 60,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
key: 'level',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '积分',
|
||||
dataIndex: 'points',
|
||||
key: 'points',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
const modalTitle = computed(() => editingUser.value ? '编辑用户' : '新增用户')
|
||||
const isCreate = computed(() => !editingUser.value)
|
||||
|
||||
// 状态映射
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
active: 'green',
|
||||
inactive: 'orange',
|
||||
banned: 'red'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
active: '正常',
|
||||
inactive: '禁用',
|
||||
banned: '封禁'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadUsers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 调用真实API
|
||||
// const response = await userAPI.getUsers({
|
||||
// page: pagination.current,
|
||||
// pageSize: pagination.pageSize,
|
||||
// ...searchForm
|
||||
// })
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
// userList.value = response.data.users
|
||||
// pagination.total = response.data.pagination.total
|
||||
} catch (error) {
|
||||
message.error('加载用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
status: '',
|
||||
registerTime: []
|
||||
})
|
||||
pagination.current = 1
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadUsers()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
editingUser.value = null
|
||||
Object.assign(formState, {
|
||||
username: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
status: 'active',
|
||||
remark: ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleView = (record: User) => {
|
||||
// TODO: 跳转到用户详情页
|
||||
message.info(`查看用户: ${record.nickname}`)
|
||||
}
|
||||
|
||||
const handleEdit = (record: User) => {
|
||||
editingUser.value = record
|
||||
Object.assign(formState, {
|
||||
username: record.username,
|
||||
nickname: record.nickname,
|
||||
password: '',
|
||||
email: record.email,
|
||||
phone: record.phone,
|
||||
status: record.status,
|
||||
remark: ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDisable = async (record: User) => {
|
||||
Modal.confirm({
|
||||
title: '确认禁用',
|
||||
content: `确定要禁用用户 "${record.nickname}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
// await userAPI.updateUser(record.id, { status: 'inactive' })
|
||||
message.success('用户已禁用')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEnable = async (record: User) => {
|
||||
Modal.confirm({
|
||||
title: '确认启用',
|
||||
content: `确定要启用用户 "${record.nickname}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
// await userAPI.updateUser(record.id, { status: 'active' })
|
||||
message.success('用户已启用')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBan = async (record: User) => {
|
||||
Modal.confirm({
|
||||
title: '确认封禁',
|
||||
content: `确定要封禁用户 "${record.nickname}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
// await userAPI.updateUser(record.id, { status: 'banned' })
|
||||
message.success('用户已封禁')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (record: User) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除用户 "${record.nickname}" 吗?此操作不可恢复。`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// await userAPI.deleteUser(record.id)
|
||||
message.success('用户已删除')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
modalLoading.value = true
|
||||
|
||||
if (editingUser.value) {
|
||||
// 编辑用户
|
||||
// await userAPI.updateUser(editingUser.value.id, formState)
|
||||
message.success('用户信息更新成功')
|
||||
} else {
|
||||
// 创建用户
|
||||
// await userAPI.createUser(formState)
|
||||
message.success('用户创建成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
142
admin-system/src/router/index.ts
Normal file
142
admin-system/src/router/index.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
// 基础路由
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard',
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/pages/Login.vue'),
|
||||
meta: { requiresAuth: false, layout: 'auth' }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/pages/dashboard/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '仪表板',
|
||||
icon: 'DashboardOutlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: () => import('@/pages/user/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '用户管理',
|
||||
icon: 'UserOutlined',
|
||||
permissions: ['user:read']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/merchants',
|
||||
name: 'Merchants',
|
||||
component: () => import('@/pages/merchant/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '商家管理',
|
||||
icon: 'ShopOutlined',
|
||||
permissions: ['merchant:read']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/travel',
|
||||
name: 'Travel',
|
||||
component: () => import('@/pages/travel/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '旅行管理',
|
||||
icon: 'CompassOutlined',
|
||||
permissions: ['travel:read']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/animals',
|
||||
name: 'Animals',
|
||||
component: () => import('@/pages/animal/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '动物管理',
|
||||
icon: 'HeartOutlined',
|
||||
permissions: ['animal:read']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/orders',
|
||||
name: 'Orders',
|
||||
component: () => import('@/pages/order/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '订单管理',
|
||||
icon: 'ShoppingCartOutlined',
|
||||
permissions: ['order:read']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/promotion',
|
||||
name: 'Promotion',
|
||||
component: () => import('@/pages/promotion/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '推广管理',
|
||||
icon: 'GiftOutlined',
|
||||
permissions: ['promotion:read']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
component: () => import('@/pages/system/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '系统设置',
|
||||
icon: 'SettingOutlined',
|
||||
permissions: ['system:read']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/pages/NotFound.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const { meta } = to
|
||||
const isAuthenticated = localStorage.getItem('admin_token') !== null
|
||||
|
||||
// 检查是否需要认证
|
||||
if (meta.requiresAuth && !isAuthenticated) {
|
||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已认证且访问登录页,重定向到首页
|
||||
if (to.name === 'Login' && isAuthenticated) {
|
||||
next({ name: 'Dashboard' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// 错误处理
|
||||
router.onError((error) => {
|
||||
console.error('🚨 路由错误:', error)
|
||||
})
|
||||
|
||||
export default router
|
||||
127
admin-system/src/stores/app.ts
Normal file
127
admin-system/src/stores/app.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface AppState {
|
||||
// 应用配置
|
||||
name: string
|
||||
version: string
|
||||
// 用户信息
|
||||
user: any
|
||||
// 系统状态
|
||||
loading: boolean
|
||||
error: string | null
|
||||
// 功能开关
|
||||
features: {
|
||||
analytics: boolean
|
||||
debug: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// 状态
|
||||
const state = ref<AppState>({
|
||||
name: import.meta.env.VITE_APP_NAME || '结伴客后台管理系统',
|
||||
version: import.meta.env.VITE_APP_VERSION || '1.0.0',
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
features: {
|
||||
analytics: import.meta.env.VITE_FEATURE_ANALYTICS === 'true',
|
||||
debug: import.meta.env.VITE_FEATURE_DEBUG === 'true'
|
||||
}
|
||||
})
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = () => !!state.value.user
|
||||
const isLoading = () => state.value.loading
|
||||
const hasError = () => state.value.error !== null
|
||||
|
||||
// Actions
|
||||
const initializeApp = async () => {
|
||||
state.value.loading = true
|
||||
try {
|
||||
// 初始化应用配置
|
||||
console.log('🚀 初始化应用:', state.value.name, state.value.version)
|
||||
|
||||
// 检查用户登录状态
|
||||
await checkAuthStatus()
|
||||
|
||||
// 初始化分析工具(如果启用)
|
||||
if (state.value.features.analytics) {
|
||||
initAnalytics()
|
||||
}
|
||||
|
||||
state.value.error = null
|
||||
} catch (error) {
|
||||
state.value.error = error instanceof Error ? error.message : '初始化失败'
|
||||
console.error('❌ 应用初始化失败:', error)
|
||||
} finally {
|
||||
state.value.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
// 检查本地存储的token
|
||||
const token = localStorage.getItem('admin_token')
|
||||
if (token) {
|
||||
// TODO: 验证token有效性
|
||||
// const user = await authAPI.verifyToken(token)
|
||||
// state.value.user = user
|
||||
console.log('✅ 用户已登录')
|
||||
} else {
|
||||
console.log('ℹ️ 用户未登录')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ 认证状态检查失败:', error)
|
||||
// 清除无效的token
|
||||
localStorage.removeItem('admin_token')
|
||||
}
|
||||
}
|
||||
|
||||
const setUser = (user: any) => {
|
||||
state.value.user = user
|
||||
}
|
||||
|
||||
const setLoading = (loading: boolean) => {
|
||||
state.value.loading = loading
|
||||
}
|
||||
|
||||
const setError = (error: string | null) => {
|
||||
state.value.error = error
|
||||
}
|
||||
|
||||
const clearError = () => {
|
||||
state.value.error = null
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
state.value.user = null
|
||||
localStorage.removeItem('admin_token')
|
||||
// TODO: 调用退出接口
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
const initAnalytics = () => {
|
||||
if (state.value.features.analytics) {
|
||||
console.log('📊 初始化分析工具')
|
||||
// TODO: 集成分析工具
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
hasError,
|
||||
// Actions
|
||||
initializeApp,
|
||||
setUser,
|
||||
setLoading,
|
||||
setError,
|
||||
clearError,
|
||||
logout
|
||||
}
|
||||
})
|
||||
8
admin-system/src/stores/index.ts
Normal file
8
admin-system/src/stores/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { useAppStore } from './app'
|
||||
// 导出其他store模块
|
||||
// export { useUserStore } from './user'
|
||||
// export { useTravelStore } from './travel'
|
||||
// export { useAnimalStore } from './animal'
|
||||
// export { useMerchantStore } from './merchant'
|
||||
// export { useOrderStore } from './order'
|
||||
// export { usePromotionStore } from './promotion'
|
||||
322
admin-system/src/styles/index.less
Normal file
322
admin-system/src/styles/index.less
Normal file
@@ -0,0 +1,322 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
font-size: 14px;
|
||||
line-height: 1.5715;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 通用工具类 */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mb-16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-24 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mt-24 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.p-16 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.p-24 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
/* 响应式断点 */
|
||||
@media (max-width: 576px) {
|
||||
.hidden-xs {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hidden-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.hidden-md {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.hidden-lg {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义Ant Design样式覆盖 */
|
||||
.ant-layout {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.ant-layout-header {
|
||||
padding: 0 24px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ant-layout-sider {
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
}
|
||||
|
||||
.ant-layout-content {
|
||||
padding: 24px;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12),
|
||||
0 5px 12px 4px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-table {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
border-radius: 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ant-btn-lg {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ant-input,
|
||||
.ant-input-affix-wrapper {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
.ant-modal {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
/* 自定义主题色 */
|
||||
:root {
|
||||
--primary-color: #1890ff;
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
--error-color: #ff4d4f;
|
||||
--info-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 暗色主题支持 */
|
||||
[data-theme='dark'] {
|
||||
--primary-color: #177ddc;
|
||||
--success-color: #49aa19;
|
||||
--warning-color: #d89614;
|
||||
--error-color: #a61d24;
|
||||
--info-color: #177ddc;
|
||||
}
|
||||
|
||||
/* 打印样式 */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.print-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高亮文本 */
|
||||
.highlight {
|
||||
background: #fffbe6;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
color: #faad14;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
26
admin-system/tsconfig.json
Normal file
26
admin-system/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
37
admin-system/vite.config.ts
Normal file
37
admin-system/vite.config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api/v1')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'antd-vendor': ['ant-design-vue', '@ant-design/icons-vue'],
|
||||
'utils-vendor': ['axios', 'dayjs', 'lodash-es']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
54
backend/.env
Normal file
54
backend/.env
Normal file
@@ -0,0 +1,54 @@
|
||||
# 服务器配置
|
||||
NODE_ENV=development
|
||||
PORT=3001
|
||||
HOST=0.0.0.0
|
||||
|
||||
# MySQL数据库配置
|
||||
DB_HOST=192.168.0.240
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=aiot$Aiot123
|
||||
DB_NAME=jiebandata
|
||||
|
||||
# 测试环境数据库
|
||||
TEST_DB_HOST=192.168.0.240
|
||||
TEST_DB_PORT=3306
|
||||
TEST_DB_USER=root
|
||||
TEST_DB_PASSWORD=aiot$Aiot123
|
||||
TEST_DB_NAME=jiebandata
|
||||
|
||||
# 生产环境数据库
|
||||
PROD_DB_HOST=129.211.213.226
|
||||
PROD_DB_PORT=9527
|
||||
PROD_DB_USER=root
|
||||
PROD_DB_PASSWORD=aiotAiot123!
|
||||
PROD_DB_NAME=jiebandata
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# RabbitMQ配置
|
||||
RABBITMQ_HOST=localhost
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USERNAME=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
RABBITMQ_VHOST=/
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_EXPIRE=7d
|
||||
|
||||
# 微信配置
|
||||
WECHAT_APPID=your-wechat-appid
|
||||
WECHAT_SECRET=your-wechat-secret
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif
|
||||
|
||||
# 调试配置
|
||||
DEBUG=jiebanke:*
|
||||
LOG_LEVEL=info
|
||||
39
backend/.env.example
Normal file
39
backend/.env.example
Normal file
@@ -0,0 +1,39 @@
|
||||
# 服务器配置
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
|
||||
# 数据库配置
|
||||
MONGODB_URI=mongodb://localhost:27017/jiebanke
|
||||
MONGODB_URI_TEST=mongodb://localhost:27017/jiebanke_test
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_EXPIRE=7d
|
||||
|
||||
# 微信配置
|
||||
WECHAT_APPID=your-wechat-appid
|
||||
WECHAT_SECRET=your-wechat-secret
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif
|
||||
|
||||
# 邮件配置(可选)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-email-password
|
||||
|
||||
# Redis配置(可选)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# 第三方API配置
|
||||
MAP_API_KEY=your-map-api-key
|
||||
SMS_API_KEY=your-sms-api-key
|
||||
|
||||
# 调试配置
|
||||
DEBUG=jiebanke:*
|
||||
LOG_LEVEL=info
|
||||
202
backend/README.md
Normal file
202
backend/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 结伴客后端服务
|
||||
|
||||
基于 Node.js + Express + MongoDB 的后端 API 服务。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 用户认证系统(JWT)
|
||||
- ✅ 微信登录集成
|
||||
- ✅ RESTful API 设计
|
||||
- ✅ 数据验证和清洗
|
||||
- ✅ 错误处理中间件
|
||||
- ✅ 请求频率限制
|
||||
- ✅ 安全防护(CORS, Helmet, XSS防护)
|
||||
- ✅ MongoDB 数据库集成
|
||||
- ✅ 环境配置管理
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js 16+
|
||||
- MongoDB 4.4+
|
||||
- npm 或 yarn
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 环境配置
|
||||
|
||||
1. 复制环境变量文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. 编辑 `.env` 文件,配置你的环境变量:
|
||||
```env
|
||||
MONGODB_URI=mongodb://localhost:27017/jiebanke
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 开发模式(带热重载)
|
||||
npm run dev
|
||||
|
||||
# 生产模式
|
||||
npm start
|
||||
|
||||
# 调试模式
|
||||
npm run debug
|
||||
```
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 运行测试
|
||||
npm test
|
||||
|
||||
# 运行测试并生成覆盖率报告
|
||||
npm run test:coverage
|
||||
|
||||
# 运行端到端测试
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
### 认证接口
|
||||
|
||||
#### 用户注册
|
||||
```
|
||||
POST /api/v1/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138000"
|
||||
}
|
||||
```
|
||||
|
||||
#### 用户登录
|
||||
```
|
||||
POST /api/v1/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取当前用户信息
|
||||
```
|
||||
GET /api/v1/auth/me
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 微信登录
|
||||
```
|
||||
POST /api/v1/auth/wechat-login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "微信授权码",
|
||||
"userInfo": {
|
||||
"nickName": "微信用户",
|
||||
"avatarUrl": "https://...",
|
||||
"gender": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── controllers/ # 控制器层
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── routes/ # 路由定义
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── app.js # Express应用配置
|
||||
│ └── server.js # 服务器入口
|
||||
├── tests/ # 测试文件
|
||||
├── .env.example # 环境变量示例
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 用户表 (users)
|
||||
- 用户基本信息
|
||||
- 认证信息
|
||||
- 统计信息
|
||||
- 第三方登录信息
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新功能
|
||||
|
||||
1. 创建数据模型 (`src/models/`)
|
||||
2. 创建控制器 (`src/controllers/`)
|
||||
3. 创建路由 (`src/routes/`)
|
||||
4. 在 `app.js` 中注册路由
|
||||
|
||||
### 代码规范
|
||||
|
||||
- 使用 ESLint 进行代码检查
|
||||
- 遵循 JavaScript Standard Style
|
||||
- 使用 async/await 处理异步操作
|
||||
- 使用错误处理中间件
|
||||
|
||||
## 部署
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t jiebanke-backend .
|
||||
|
||||
# 运行容器
|
||||
docker run -p 3000:3000 --env-file .env jiebanke-backend
|
||||
```
|
||||
|
||||
### PM2 部署
|
||||
|
||||
```bash
|
||||
npm install -g pm2
|
||||
pm2 start ecosystem.config.js
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **MongoDB 连接失败**
|
||||
- 检查 MongoDB 服务是否运行
|
||||
- 检查连接字符串是否正确
|
||||
|
||||
2. **JWT 验证失败**
|
||||
- 检查 JWT_SECRET 环境变量
|
||||
|
||||
3. **CORS 错误**
|
||||
- 检查前端域名是否在 CORS 白名单中
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请查看日志文件或联系开发团队。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
94
backend/config/env.js
Normal file
94
backend/config/env.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// 环境配置
|
||||
const path = require('path')
|
||||
require('dotenv').config({ path: path.join(__dirname, '../../.env') })
|
||||
|
||||
const config = {
|
||||
// 开发环境
|
||||
development: {
|
||||
port: process.env.PORT || 3000,
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke_dev',
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
}
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'dev-jwt-secret-key-2024',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d'
|
||||
},
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD || ''
|
||||
},
|
||||
upload: {
|
||||
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/gif']
|
||||
},
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:9000',
|
||||
credentials: true
|
||||
}
|
||||
},
|
||||
|
||||
// 测试环境
|
||||
test: {
|
||||
port: process.env.PORT || 3001,
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke_test',
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
}
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'test-jwt-secret-key-2024',
|
||||
expiresIn: '1h',
|
||||
refreshExpiresIn: '7d'
|
||||
},
|
||||
upload: {
|
||||
maxFileSize: 2 * 1024 * 1024, // 2MB
|
||||
allowedTypes: ['image/jpeg', 'image/png']
|
||||
}
|
||||
},
|
||||
|
||||
// 生产环境
|
||||
production: {
|
||||
port: process.env.PORT || 3000,
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI,
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
}
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET,
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '1d',
|
||||
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d'
|
||||
},
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST,
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD
|
||||
},
|
||||
upload: {
|
||||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/webp']
|
||||
},
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'https://your-domain.com',
|
||||
credentials: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
const getConfig = () => {
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
return config[env]
|
||||
}
|
||||
|
||||
module.exports = getConfig()
|
||||
6418
backend/package-lock.json
generated
Normal file
6418
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
backend/package.json
Normal file
52
backend/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "jiebanke-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "结伴客小程序后端API服务",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/**/*.js",
|
||||
"migrate": "node src/utils/migrate.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mini-program",
|
||||
"api",
|
||||
"express",
|
||||
"mongodb"
|
||||
],
|
||||
"author": "jiebanke-team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"amqplib": "^0.10.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-mongo-sanitize": "^2.2.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"hpp": "^0.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^8.0.3",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.14.3",
|
||||
"redis": "^5.8.2",
|
||||
"winston": "^3.11.0",
|
||||
"xss-clean": "^0.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
100
backend/src/app.js
Normal file
100
backend/src/app.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const helmet = require('helmet')
|
||||
const morgan = require('morgan')
|
||||
const rateLimit = require('express-rate-limit')
|
||||
const xss = require('xss-clean')
|
||||
const hpp = require('hpp')
|
||||
|
||||
console.log('🔧 初始化Express应用...')
|
||||
|
||||
const { globalErrorHandler, notFound } = require('./utils/errors')
|
||||
|
||||
// 路由导入
|
||||
const authRoutes = require('./routes/auth')
|
||||
// 其他路由将在这里导入
|
||||
|
||||
const app = express()
|
||||
|
||||
console.log('✅ Express应用初始化完成')
|
||||
|
||||
// 安全中间件
|
||||
app.use(helmet())
|
||||
|
||||
// CORS配置
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? ['https://your-domain.com']
|
||||
: ['http://localhost:9000', 'http://localhost:3000'],
|
||||
credentials: true
|
||||
}))
|
||||
|
||||
// 请求日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(morgan('dev'))
|
||||
} else {
|
||||
app.use(morgan('combined'))
|
||||
}
|
||||
|
||||
// 请求频率限制
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15分钟
|
||||
max: process.env.NODE_ENV === 'production' ? 100 : 1000, // 生产环境100次,开发环境1000次
|
||||
message: {
|
||||
success: false,
|
||||
code: 429,
|
||||
message: '请求过于频繁,请稍后再试',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
app.use('/api', limiter)
|
||||
|
||||
// 请求体解析
|
||||
app.use(express.json({ limit: '10kb' }))
|
||||
app.use(express.urlencoded({ extended: true, limit: '10kb' }))
|
||||
|
||||
// 数据清洗
|
||||
app.use(xss()) // 防止XSS攻击
|
||||
app.use(hpp({ // 防止参数污染
|
||||
whitelist: [
|
||||
'page',
|
||||
'pageSize',
|
||||
'sort',
|
||||
'fields',
|
||||
'price',
|
||||
'rating',
|
||||
'distance'
|
||||
]
|
||||
}))
|
||||
|
||||
// 静态文件服务
|
||||
app.use('/uploads', express.static('uploads'))
|
||||
|
||||
// 健康检查路由
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
})
|
||||
})
|
||||
|
||||
// API路由
|
||||
app.use('/api/v1/auth', authRoutes)
|
||||
// 其他API路由将在这里添加
|
||||
// app.use('/api/v1/users', userRoutes)
|
||||
// app.use('/api/v1/travel', travelRoutes)
|
||||
// app.use('/api/v1/animals', animalRoutes)
|
||||
// app.use('/api/v1/flowers', flowerRoutes)
|
||||
// app.use('/api/v1/orders', orderRoutes)
|
||||
|
||||
// 404处理
|
||||
app.use('*', notFound)
|
||||
|
||||
// 全局错误处理
|
||||
app.use(globalErrorHandler)
|
||||
|
||||
console.log('✅ 应用配置完成')
|
||||
|
||||
module.exports = app
|
||||
73
backend/src/config/database.js
Normal file
73
backend/src/config/database.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || '192.168.0.240',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'aiot$Aiot123',
|
||||
database: process.env.DB_NAME || 'jiebandata',
|
||||
connectionLimit: 10,
|
||||
// 移除无效的配置选项 acquireTimeout 和 timeout
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
// 连接池配置
|
||||
waitForConnections: true,
|
||||
queueLimit: 0
|
||||
};
|
||||
|
||||
// 创建连接池
|
||||
const pool = mysql.createPool(dbConfig);
|
||||
|
||||
// 测试数据库连接
|
||||
async function testConnection() {
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
console.log('✅ MySQL数据库连接成功');
|
||||
connection.release();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ MySQL数据库连接失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
async function query(sql, params = []) {
|
||||
try {
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('数据库查询错误:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行事务
|
||||
async function transaction(callback) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
const result = await callback(connection);
|
||||
await connection.commit();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭连接池
|
||||
async function closePool() {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool,
|
||||
query,
|
||||
transaction,
|
||||
testConnection,
|
||||
closePool
|
||||
};
|
||||
203
backend/src/config/rabbitmq.js
Normal file
203
backend/src/config/rabbitmq.js
Normal file
@@ -0,0 +1,203 @@
|
||||
const amqp = require('amqplib');
|
||||
|
||||
class RabbitMQConfig {
|
||||
constructor() {
|
||||
this.connection = null;
|
||||
this.channel = null;
|
||||
this.isConnected = false;
|
||||
this.exchanges = new Map();
|
||||
this.queues = new Map();
|
||||
}
|
||||
|
||||
// 获取连接URL
|
||||
getConnectionUrl() {
|
||||
const host = process.env.RABBITMQ_HOST || 'localhost';
|
||||
const port = process.env.RABBITMQ_PORT || 5672;
|
||||
const username = process.env.RABBITMQ_USERNAME || 'guest';
|
||||
const password = process.env.RABBITMQ_PASSWORD || 'guest';
|
||||
const vhost = process.env.RABBITMQ_VHOST || '/';
|
||||
|
||||
return `amqp://${username}:${password}@${host}:${port}/${vhost}`;
|
||||
}
|
||||
|
||||
// 连接RabbitMQ
|
||||
async connect() {
|
||||
if (this.isConnected) {
|
||||
return { connection: this.connection, channel: this.channel };
|
||||
}
|
||||
|
||||
try {
|
||||
const url = this.getConnectionUrl();
|
||||
this.connection = await amqp.connect(url);
|
||||
|
||||
this.connection.on('error', (err) => {
|
||||
console.error('RabbitMQ连接错误:', err);
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.connection.on('close', () => {
|
||||
console.log('❌ RabbitMQ连接关闭');
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.channel = await this.connection.createChannel();
|
||||
|
||||
this.channel.on('error', (err) => {
|
||||
console.error('RabbitMQ通道错误:', err);
|
||||
});
|
||||
|
||||
this.channel.on('close', () => {
|
||||
console.log('❌ RabbitMQ通道关闭');
|
||||
});
|
||||
|
||||
this.isConnected = true;
|
||||
console.log('✅ RabbitMQ连接成功');
|
||||
|
||||
// 声明默认交换器
|
||||
await this.setupDefaultExchanges();
|
||||
|
||||
return { connection: this.connection, channel: this.channel };
|
||||
} catch (error) {
|
||||
console.error('RabbitMQ连接失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认交换器
|
||||
async setupDefaultExchanges() {
|
||||
const exchanges = [
|
||||
{ name: 'jiebanke.direct', type: 'direct', durable: true },
|
||||
{ name: 'jiebanke.topic', type: 'topic', durable: true },
|
||||
{ name: 'jiebanke.fanout', type: 'fanout', durable: true },
|
||||
{ name: 'jiebanke.delay', type: 'x-delayed-message', durable: true, arguments: { 'x-delayed-type': 'direct' } }
|
||||
];
|
||||
|
||||
for (const exchange of exchanges) {
|
||||
await this.channel.assertExchange(exchange.name, exchange.type, {
|
||||
durable: exchange.durable,
|
||||
arguments: exchange.arguments
|
||||
});
|
||||
this.exchanges.set(exchange.name, exchange);
|
||||
}
|
||||
}
|
||||
|
||||
// 声明队列
|
||||
async assertQueue(queueName, options = {}) {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
const queueOptions = {
|
||||
durable: true,
|
||||
arguments: {
|
||||
'x-message-ttl': 86400000, // 24小时消息过期时间
|
||||
...options.arguments
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
const queue = await this.channel.assertQueue(queueName, queueOptions);
|
||||
this.queues.set(queueName, queue);
|
||||
return queue;
|
||||
}
|
||||
|
||||
// 绑定队列到交换器
|
||||
async bindQueue(queueName, exchangeName, routingKey = '') {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
await this.channel.bindQueue(queueName, exchangeName, routingKey);
|
||||
console.log(`✅ 队列 ${queueName} 绑定到交换器 ${exchangeName},路由键: ${routingKey}`);
|
||||
}
|
||||
|
||||
// 发布消息
|
||||
async publish(exchangeName, routingKey, message, options = {}) {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
const messageBuffer = Buffer.from(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
data: message
|
||||
}));
|
||||
|
||||
const publishOptions = {
|
||||
persistent: true,
|
||||
contentType: 'application/json',
|
||||
...options
|
||||
};
|
||||
|
||||
return this.channel.publish(exchangeName, routingKey, messageBuffer, publishOptions);
|
||||
}
|
||||
|
||||
// 消费消息
|
||||
async consume(queueName, callback, options = {}) {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
const consumeOptions = {
|
||||
noAck: false,
|
||||
...options
|
||||
};
|
||||
|
||||
return this.channel.consume(queueName, async (msg) => {
|
||||
try {
|
||||
if (msg !== null) {
|
||||
const content = JSON.parse(msg.content.toString());
|
||||
await callback(content, msg);
|
||||
this.channel.ack(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('消息处理错误:', error);
|
||||
this.channel.nack(msg, false, false); // 不重新入队
|
||||
}
|
||||
}, consumeOptions);
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async healthCheck() {
|
||||
try {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('RabbitMQ未连接');
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'healthy',
|
||||
host: process.env.RABBITMQ_HOST || 'localhost',
|
||||
port: process.env.RABBITMQ_PORT || 5672,
|
||||
connected: this.isConnected
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
error: error.message,
|
||||
host: process.env.RABBITMQ_HOST || 'localhost',
|
||||
port: process.env.RABBITMQ_PORT || 5672,
|
||||
connected: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 优雅关闭
|
||||
async close() {
|
||||
try {
|
||||
if (this.channel) {
|
||||
await this.channel.close();
|
||||
}
|
||||
if (this.connection) {
|
||||
await this.connection.close();
|
||||
}
|
||||
this.isConnected = false;
|
||||
console.log('✅ RabbitMQ连接已关闭');
|
||||
} catch (error) {
|
||||
console.error('关闭RabbitMQ连接时出错:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局RabbitMQ实例
|
||||
const rabbitMQConfig = new RabbitMQConfig();
|
||||
|
||||
module.exports = rabbitMQConfig;
|
||||
119
backend/src/config/redis.js
Normal file
119
backend/src/config/redis.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const redis = require('redis');
|
||||
|
||||
class RedisConfig {
|
||||
constructor() {
|
||||
this.client = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
// 创建Redis客户端
|
||||
createClient() {
|
||||
const redisConfig = {
|
||||
socket: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
reconnectStrategy: (retries) => {
|
||||
const delay = Math.min(retries * 100, 3000);
|
||||
console.log(`Redis连接重试第${retries + 1}次,延迟${delay}ms`);
|
||||
return delay;
|
||||
}
|
||||
},
|
||||
password: process.env.REDIS_PASSWORD || null,
|
||||
database: process.env.REDIS_DB || 0
|
||||
};
|
||||
|
||||
// 移除空配置项
|
||||
if (!redisConfig.password) delete redisConfig.password;
|
||||
|
||||
this.client = redis.createClient(redisConfig);
|
||||
|
||||
// 错误处理
|
||||
this.client.on('error', (err) => {
|
||||
console.error('Redis错误:', err);
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
// 连接成功
|
||||
this.client.on('connect', () => {
|
||||
console.log('✅ Redis连接中...');
|
||||
});
|
||||
|
||||
// 准备就绪
|
||||
this.client.on('ready', () => {
|
||||
this.isConnected = true;
|
||||
console.log('✅ Redis连接就绪');
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
this.client.on('end', () => {
|
||||
this.isConnected = false;
|
||||
console.log('❌ Redis连接断开');
|
||||
});
|
||||
|
||||
// 重连
|
||||
this.client.on('reconnecting', () => {
|
||||
console.log('🔄 Redis重新连接中...');
|
||||
});
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
// 连接Redis
|
||||
async connect() {
|
||||
if (this.client && this.isConnected) {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
// 开发环境下,如果Redis未配置,则不连接
|
||||
if (process.env.NODE_ENV === 'development' &&
|
||||
(!process.env.REDIS_HOST || process.env.REDIS_HOST === 'localhost')) {
|
||||
console.log('⚠️ 开发环境未配置Redis,跳过连接');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.createClient();
|
||||
await this.client.connect();
|
||||
return this.client;
|
||||
} catch (error) {
|
||||
console.error('Redis连接失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
async disconnect() {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
this.isConnected = false;
|
||||
console.log('✅ Redis连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取客户端状态
|
||||
getStatus() {
|
||||
return {
|
||||
isConnected: this.isConnected,
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379
|
||||
};
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async healthCheck() {
|
||||
try {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Redis未连接');
|
||||
}
|
||||
await this.client.ping();
|
||||
return { status: 'healthy', ...this.getStatus() };
|
||||
} catch (error) {
|
||||
return { status: 'unhealthy', error: error.message, ...this.getStatus() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局Redis实例
|
||||
const redisConfig = new RedisConfig();
|
||||
|
||||
module.exports = redisConfig;
|
||||
266
backend/src/controllers/authController.js
Normal file
266
backend/src/controllers/authController.js
Normal file
@@ -0,0 +1,266 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { User } = require('../models/User')
|
||||
const { AppError } = require('../utils/errors')
|
||||
const { success } = require('../utils/response')
|
||||
|
||||
// 生成JWT Token
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRE || '7d' }
|
||||
)
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
const register = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password, nickname, email, phone } = req.body
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await User.findOne({
|
||||
$or: [
|
||||
{ username },
|
||||
{ email: email || null },
|
||||
{ phone: phone || null }
|
||||
]
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.username === username) {
|
||||
throw new AppError('用户名已存在', 400)
|
||||
}
|
||||
if (existingUser.email === email) {
|
||||
throw new AppError('邮箱已存在', 400)
|
||||
}
|
||||
if (existingUser.phone === phone) {
|
||||
throw new AppError('手机号已存在', 400)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
const user = new User({
|
||||
username,
|
||||
password,
|
||||
nickname: nickname || username,
|
||||
email,
|
||||
phone
|
||||
})
|
||||
|
||||
await user.save()
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user._id)
|
||||
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
|
||||
res.status(201).json(success({
|
||||
user: user.toJSON(),
|
||||
token,
|
||||
message: '注册成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
const login = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password } = req.body
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('用户名和密码不能为空', 400)
|
||||
}
|
||||
|
||||
// 查找用户(支持用户名、邮箱、手机号登录)
|
||||
const user = await User.findOne({
|
||||
$or: [
|
||||
{ username },
|
||||
{ email: username },
|
||||
{ phone: username }
|
||||
]
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!user.isActive()) {
|
||||
throw new AppError('账户已被禁用', 403)
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await user.comparePassword(password)
|
||||
if (!isPasswordValid) {
|
||||
throw new AppError('密码错误', 401)
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user._id)
|
||||
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON(),
|
||||
token,
|
||||
message: '登录成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findById(req.userId)
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON()
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
const updateProfile = async (req, res, next) => {
|
||||
try {
|
||||
const { nickname, avatar, gender, birthday } = req.body
|
||||
const updates = {}
|
||||
|
||||
if (nickname !== undefined) updates.nickname = nickname
|
||||
if (avatar !== undefined) updates.avatar = avatar
|
||||
if (gender !== undefined) updates.gender = gender
|
||||
if (birthday !== undefined) updates.birthday = birthday
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.userId,
|
||||
updates,
|
||||
{ new: true, runValidators: true }
|
||||
)
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON(),
|
||||
message: '个人信息更新成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const changePassword = async (req, res, next) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
throw new AppError('当前密码和新密码不能为空', 400)
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw new AppError('新密码长度不能少于6位', 400)
|
||||
}
|
||||
|
||||
const user = await User.findById(req.userId)
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
const isCurrentPasswordValid = await user.comparePassword(currentPassword)
|
||||
if (!isCurrentPasswordValid) {
|
||||
throw new AppError('当前密码错误', 401)
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.password = newPassword
|
||||
await user.save()
|
||||
|
||||
res.json(success({
|
||||
message: '密码修改成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 微信登录/注册
|
||||
const wechatLogin = async (req, res, next) => {
|
||||
try {
|
||||
const { code, userInfo } = req.body
|
||||
|
||||
if (!code) {
|
||||
throw new AppError('微信授权码不能为空', 400)
|
||||
}
|
||||
|
||||
// 这里应该调用微信API获取openid和unionid
|
||||
// 模拟获取微信用户信息
|
||||
const wechatUserInfo = {
|
||||
openid: `mock_openid_${Date.now()}`,
|
||||
unionid: `mock_unionid_${Date.now()}`,
|
||||
nickname: userInfo?.nickName || '微信用户',
|
||||
avatar: userInfo?.avatarUrl || '',
|
||||
gender: userInfo?.gender === 1 ? 'male' : userInfo?.gender === 2 ? 'female' : 'unknown'
|
||||
}
|
||||
|
||||
// 查找是否已存在微信用户
|
||||
let user = await User.findOne({
|
||||
$or: [
|
||||
{ wechatOpenid: wechatUserInfo.openid },
|
||||
{ wechatUnionid: wechatUserInfo.unionid }
|
||||
]
|
||||
})
|
||||
|
||||
if (user) {
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
} else {
|
||||
// 创建新用户(微信注册)
|
||||
user = new User({
|
||||
username: `wx_${wechatUserInfo.openid.slice(-8)}`,
|
||||
password: Math.random().toString(36).slice(-8), // 随机密码
|
||||
nickname: wechatUserInfo.nickname,
|
||||
avatar: wechatUserInfo.avatar,
|
||||
gender: wechatUserInfo.gender,
|
||||
wechatOpenid: wechatUserInfo.openid,
|
||||
wechatUnionid: wechatUserInfo.unionid
|
||||
})
|
||||
await user.save()
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user._id)
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON(),
|
||||
token,
|
||||
message: '微信登录成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
getCurrentUser,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
wechatLogin
|
||||
}
|
||||
260
backend/src/controllers/authControllerMySQL.js
Normal file
260
backend/src/controllers/authControllerMySQL.js
Normal file
@@ -0,0 +1,260 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const UserMySQL = require('../models/UserMySQL');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const { success } = require('../utils/response');
|
||||
|
||||
// 生成JWT Token
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRE || '7d' }
|
||||
);
|
||||
};
|
||||
|
||||
// 用户注册
|
||||
const register = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password, nickname, email, phone } = req.body;
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (await UserMySQL.isUsernameExists(username)) {
|
||||
throw new AppError('用户名已存在', 400);
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (email && await UserMySQL.isEmailExists(email)) {
|
||||
throw new AppError('邮箱已存在', 400);
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (phone && await UserMySQL.isPhoneExists(phone)) {
|
||||
throw new AppError('手机号已存在', 400);
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// 创建新用户
|
||||
const userId = await UserMySQL.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
nickname: nickname || username,
|
||||
email,
|
||||
phone
|
||||
});
|
||||
|
||||
// 获取用户信息
|
||||
const user = await UserMySQL.findById(userId);
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(userId);
|
||||
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(userId);
|
||||
|
||||
res.status(201).json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
message: '注册成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 用户登录
|
||||
const login = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('用户名和密码不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户(支持用户名、邮箱、手机号登录)
|
||||
let user = await UserMySQL.findByUsername(username);
|
||||
if (!user) {
|
||||
user = await UserMySQL.findByEmail(username);
|
||||
}
|
||||
if (!user) {
|
||||
user = await UserMySQL.findByPhone(username);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!UserMySQL.isActive(user)) {
|
||||
throw new AppError('账户已被禁用', 403);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
throw new AppError('密码错误', 401);
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(user.id);
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
message: '登录成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async (req, res, next) => {
|
||||
try {
|
||||
const user = await UserMySQL.findById(req.userId);
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404);
|
||||
}
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user)
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户信息
|
||||
const updateProfile = async (req, res, next) => {
|
||||
try {
|
||||
const { nickname, avatar, gender, birthday } = req.body;
|
||||
const updates = {};
|
||||
|
||||
if (nickname !== undefined) updates.nickname = nickname;
|
||||
if (avatar !== undefined) updates.avatar = avatar;
|
||||
if (gender !== undefined) updates.gender = gender;
|
||||
if (birthday !== undefined) updates.birthday = birthday;
|
||||
|
||||
const success = await UserMySQL.update(req.userId, updates);
|
||||
if (!success) {
|
||||
throw new AppError('更新失败', 400);
|
||||
}
|
||||
|
||||
const user = await UserMySQL.findById(req.userId);
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
message: '个人信息更新成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改密码
|
||||
const changePassword = async (req, res, next) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
throw new AppError('当前密码和新密码不能为空', 400);
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw new AppError('新密码长度不能少于6位', 400);
|
||||
}
|
||||
|
||||
const user = await UserMySQL.findById(req.userId);
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password);
|
||||
if (!isCurrentPasswordValid) {
|
||||
throw new AppError('当前密码错误', 401);
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 12);
|
||||
|
||||
// 更新密码
|
||||
await UserMySQL.updatePassword(req.userId, hashedPassword);
|
||||
|
||||
res.json(success({
|
||||
message: '密码修改成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 微信登录/注册
|
||||
const wechatLogin = async (req, res, next) => {
|
||||
try {
|
||||
const { code, userInfo } = req.body;
|
||||
|
||||
if (!code) {
|
||||
throw new AppError('微信授权码不能为空', 400);
|
||||
}
|
||||
|
||||
// 这里应该调用微信API获取openid和unionid
|
||||
// 模拟获取微信用户信息
|
||||
const wechatUserInfo = {
|
||||
openid: `mock_openid_${Date.now()}`,
|
||||
unionid: `mock_unionid_${Date.now()}`,
|
||||
nickname: userInfo?.nickName || '微信用户',
|
||||
avatar: userInfo?.avatarUrl || '',
|
||||
gender: userInfo?.gender === 1 ? 'male' : userInfo?.gender === 2 ? 'female' : 'unknown'
|
||||
};
|
||||
|
||||
// 查找是否已存在微信用户
|
||||
let user = await UserMySQL.findByWechatOpenid(wechatUserInfo.openid);
|
||||
|
||||
if (user) {
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(user.id);
|
||||
} else {
|
||||
// 创建新用户(微信注册)
|
||||
const randomPassword = Math.random().toString(36).slice(-8);
|
||||
const hashedPassword = await bcrypt.hash(randomPassword, 12);
|
||||
|
||||
const userId = await UserMySQL.create({
|
||||
username: `wx_${wechatUserInfo.openid.slice(-8)}`,
|
||||
password: hashedPassword,
|
||||
nickname: wechatUserInfo.nickname,
|
||||
avatar: wechatUserInfo.avatar,
|
||||
gender: wechatUserInfo.gender,
|
||||
wechatOpenid: wechatUserInfo.openid,
|
||||
wechatUnionid: wechatUserInfo.unionid
|
||||
});
|
||||
|
||||
user = await UserMySQL.findById(userId);
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
message: '微信登录成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
getCurrentUser,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
wechatLogin
|
||||
};
|
||||
298
backend/src/docs/swagger.js
Normal file
298
backend/src/docs/swagger.js
Normal file
@@ -0,0 +1,298 @@
|
||||
const swaggerJsdoc = require('swagger-jsdoc');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
|
||||
// Swagger 配置选项
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: '结伴客系统 API',
|
||||
version: '1.0.0',
|
||||
description: '结伴客系统 - 旅行结伴与动物认领平台',
|
||||
contact: {
|
||||
name: '技术支持',
|
||||
email: 'support@jiebanke.com',
|
||||
url: 'https://www.jiebanke.com'
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3000/api/v1',
|
||||
description: '开发环境'
|
||||
},
|
||||
{
|
||||
url: 'https://api.jiebanke.com/api/v1',
|
||||
description: '生产环境'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
BearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
// 通用响应模型
|
||||
ApiResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '请求是否成功'
|
||||
},
|
||||
code: {
|
||||
type: 'integer',
|
||||
description: '状态码'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '消息描述'
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description: '业务数据'
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: '响应时间戳'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 错误响应模型
|
||||
ErrorResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
example: false
|
||||
},
|
||||
code: {
|
||||
type: 'integer',
|
||||
example: 400
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: '请求参数错误'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
example: '详细错误信息'
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 用户模型
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
example: 1
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
example: 'testuser'
|
||||
},
|
||||
nickname: {
|
||||
type: '极速版string',
|
||||
example: '测试用户'
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
example: 'test@example.com'
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
example: '13800138000'
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
example: 'https://example.com/avatar.jpg'
|
||||
},
|
||||
gender: {
|
||||
type: 'string',
|
||||
enum: ['male', 'female', 'unknown'],
|
||||
example: 'male'
|
||||
},
|
||||
birthday: {
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
example: '1990-01-01'
|
||||
},
|
||||
points: {
|
||||
type: 'integer',
|
||||
example: 1000
|
||||
},
|
||||
level: {
|
||||
type: 'integer',
|
||||
example: 3
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'banned'],
|
||||
example: 'active'
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 分页模型
|
||||
Pagination: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {
|
||||
type: 'integer',
|
||||
example: 100
|
||||
},
|
||||
page: {
|
||||
type: 'integer',
|
||||
example: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: 'integer',
|
||||
example: 20
|
||||
},
|
||||
totalPages: {
|
||||
type: 'integer',
|
||||
example: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
// 通用分页参数
|
||||
PageParam: {
|
||||
in: 'query',
|
||||
name: 'page',
|
||||
schema: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
default: 1
|
||||
},
|
||||
description: '页码'
|
||||
},
|
||||
PageSizeParam: {
|
||||
in: 'query',
|
||||
name: 'pageSize',
|
||||
schema: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 20
|
||||
},
|
||||
description: '每页数量'
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
// 通用响应
|
||||
UnauthorizedError: {
|
||||
description: '未授权访问',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '未授权访问',
|
||||
error: 'Token已过期或无效',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ForbiddenError: {
|
||||
description: '禁止访问',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 403,
|
||||
message: '禁止访问',
|
||||
error: '权限不足',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
NotFoundError: {
|
||||
description: '资源不存在',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '资源不存在',
|
||||
error: '请求的资源不存在',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ValidationError: {
|
||||
description: '参数验证错误',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '参数验证错误',
|
||||
error: '用户名必须为4-20个字符',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
security: [
|
||||
{
|
||||
BearerAuth: []
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: [
|
||||
'./src/routes/*.js',
|
||||
'./src/controllers/*.js',
|
||||
'./src/models/*.js'
|
||||
]
|
||||
};
|
||||
|
||||
const specs = swaggerJsdoc(options);
|
||||
|
||||
module.exports = {
|
||||
swaggerUi,
|
||||
specs,
|
||||
serve: swaggerUi.serve,
|
||||
setup: swaggerUi.setup(specs, {
|
||||
explorer: true,
|
||||
customCss: '.swagger-ui .topbar { display: none }',
|
||||
customSiteTitle: '结伴客系统 API文档'
|
||||
})
|
||||
};
|
||||
108
backend/src/middleware/auth.js
Normal file
108
backend/src/middleware/auth.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { User } = require('../models/User')
|
||||
const { AppError } = require('../utils/errors')
|
||||
|
||||
// JWT认证中间件
|
||||
const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
let token
|
||||
|
||||
// 从Authorization头获取token
|
||||
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
|
||||
token = req.headers.authorization.split(' ')[1]
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return next(new AppError('访问被拒绝,请提供有效的token', 401))
|
||||
}
|
||||
|
||||
// 验证token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key')
|
||||
|
||||
// 查找用户
|
||||
const user = await User.findById(decoded.userId)
|
||||
if (!user) {
|
||||
return next(new AppError('用户不存在', 404))
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!user.isActive()) {
|
||||
return next(new AppError('账户已被禁用', 403))
|
||||
}
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
req.userId = user._id
|
||||
req.user = user
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return next(new AppError('无效的token', 401))
|
||||
}
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return next(new AppError('token已过期', 401))
|
||||
}
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 可选认证中间件(不强制要求认证)
|
||||
const optionalAuthenticate = async (req, res, next) => {
|
||||
try {
|
||||
let token
|
||||
|
||||
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
|
||||
token = req.headers.authorization.split(' ')[1]
|
||||
}
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key')
|
||||
const user = await User.findById(decoded.userId)
|
||||
|
||||
if (user && user.isActive()) {
|
||||
req.userId = user._id
|
||||
req.user = user
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
// 忽略token验证错误,继续处理请求
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员权限检查
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new AppError('请先登录', 401))
|
||||
}
|
||||
|
||||
// 这里可以根据实际需求定义管理员权限
|
||||
// 例如:检查用户角色或权限级别
|
||||
if (req.user.level < 2) { // 假设2级以上为管理员
|
||||
return next(new AppError('权限不足,需要管理员权限', 403))
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// VIP权限检查
|
||||
const requireVip = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new AppError('请先登录', 401))
|
||||
}
|
||||
|
||||
if (!req.user.isVip()) {
|
||||
return next(new AppError('需要VIP权限', 403))
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticate,
|
||||
optionalAuthenticate,
|
||||
requireAdmin,
|
||||
requireVip
|
||||
}
|
||||
212
backend/src/models/User.js
Normal file
212
backend/src/models/User.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const mongoose = require('mongoose')
|
||||
const bcrypt = require('bcryptjs')
|
||||
|
||||
// 用户等级枚举
|
||||
const UserLevel = {
|
||||
NORMAL: 1, // 普通用户
|
||||
VIP: 2, // VIP用户
|
||||
SUPER_VIP: 3 // 超级VIP
|
||||
}
|
||||
|
||||
// 用户状态枚举
|
||||
const UserStatus = {
|
||||
ACTIVE: 'active', // 活跃
|
||||
INACTIVE: 'inactive', // 非活跃
|
||||
BANNED: 'banned' // 封禁
|
||||
}
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
// 基础信息
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
minlength: 3,
|
||||
maxlength: 20,
|
||||
match: /^[a-zA-Z0-9_]+$/
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 6
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
sparse: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
match: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
sparse: true,
|
||||
trim: true,
|
||||
match: /^1[3-9]\d{9}$/
|
||||
},
|
||||
|
||||
// 个人信息
|
||||
nickname: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 20
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
enum: ['male', 'female', 'unknown'],
|
||||
default: 'unknown'
|
||||
},
|
||||
birthday: Date,
|
||||
|
||||
// 账户信息
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
enum: Object.values(UserLevel),
|
||||
default: UserLevel.NORMAL
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: Object.values(UserStatus),
|
||||
default: UserStatus.ACTIVE
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0
|
||||
},
|
||||
|
||||
// 第三方登录
|
||||
wechatOpenid: {
|
||||
type: String,
|
||||
sparse: true
|
||||
},
|
||||
wechatUnionid: {
|
||||
type: String,
|
||||
sparse: true
|
||||
},
|
||||
|
||||
// 统计信息
|
||||
travelCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
animalAdoptCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
flowerOrderCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 时间戳
|
||||
lastLoginAt: Date,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: {
|
||||
transform: function(doc, ret) {
|
||||
delete ret.password
|
||||
delete ret.wechatOpenid
|
||||
delete ret.wechatUnionid
|
||||
return ret
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 索引 (移除与字段定义中 unique: true 和 sparse: true 重复的索引)
|
||||
userSchema.index({ createdAt: -1 })
|
||||
userSchema.index({ points: -1 })
|
||||
|
||||
// 密码加密中间件
|
||||
userSchema.pre('save', async function(next) {
|
||||
if (!this.isModified('password')) return next()
|
||||
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(12)
|
||||
this.password = await bcrypt.hash(this.password, salt)
|
||||
next()
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// 比较密码方法
|
||||
userSchema.methods.comparePassword = async function(candidatePassword) {
|
||||
return bcrypt.compare(candidatePassword, this.password)
|
||||
}
|
||||
|
||||
// 检查用户状态是否活跃
|
||||
userSchema.methods.isActive = function() {
|
||||
return this.status === UserStatus.ACTIVE
|
||||
}
|
||||
|
||||
// 检查是否为VIP用户
|
||||
userSchema.methods.isVip = function() {
|
||||
return this.level >= UserLevel.VIP
|
||||
}
|
||||
|
||||
// 添加积分
|
||||
userSchema.methods.addPoints = function(points) {
|
||||
this.points += points
|
||||
return this.save()
|
||||
}
|
||||
|
||||
// 扣除积分
|
||||
userSchema.methods.deductPoints = function(points) {
|
||||
if (this.points < points) {
|
||||
throw new Error('积分不足')
|
||||
}
|
||||
this.points -= points
|
||||
return this.save()
|
||||
}
|
||||
|
||||
// 静态方法:根据用户名查找用户
|
||||
userSchema.statics.findByUsername = function(username) {
|
||||
return this.findOne({ username })
|
||||
}
|
||||
|
||||
// 静态方法:根据邮箱查找用户
|
||||
userSchema.statics.findByEmail = function(email) {
|
||||
return this.findOne({ email })
|
||||
}
|
||||
|
||||
// 静态方法:根据手机号查找用户
|
||||
userSchema.statics.findByPhone = function(phone) {
|
||||
return this.findOne({ phone })
|
||||
}
|
||||
|
||||
// 虚拟字段:用户等级名称
|
||||
userSchema.virtual('levelName').get(function() {
|
||||
const levelNames = {
|
||||
[UserLevel.NORMAL]: '普通用户',
|
||||
[UserLevel.VIP]: 'VIP用户',
|
||||
[UserLevel.SUPER_VIP]: '超级VIP'
|
||||
}
|
||||
return levelNames[this.level] || '未知'
|
||||
})
|
||||
|
||||
const User = mongoose.model('User', userSchema)
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
UserLevel,
|
||||
UserStatus
|
||||
}
|
||||
160
backend/src/models/UserMySQL.js
Normal file
160
backend/src/models/UserMySQL.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const { query, transaction } = require('../config/database');
|
||||
|
||||
class UserMySQL {
|
||||
// 创建用户
|
||||
static async create(userData) {
|
||||
const {
|
||||
openid,
|
||||
nickname,
|
||||
avatar = '',
|
||||
gender = 'other',
|
||||
birthday = null,
|
||||
phone = null,
|
||||
email = null
|
||||
} = userData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO users (
|
||||
openid, nickname, avatar, gender, birthday, phone, email,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
|
||||
const params = [
|
||||
openid,
|
||||
nickname,
|
||||
avatar,
|
||||
gender,
|
||||
birthday,
|
||||
phone,
|
||||
email
|
||||
];
|
||||
|
||||
const result = await query(sql, params);
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
// 根据ID查找用户
|
||||
static async findById(id) {
|
||||
const sql = 'SELECT * FROM users WHERE id = ?';
|
||||
const rows = await query(sql, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据openid查找用户
|
||||
static async findByOpenid(openid) {
|
||||
const sql = 'SELECT * FROM users WHERE openid = ?';
|
||||
const rows = await query(sql, [openid]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据手机号查找用户
|
||||
static async findByPhone(phone) {
|
||||
const sql = 'SELECT * FROM users WHERE phone = ?';
|
||||
const rows = await query(sql, [phone]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据邮箱查找用户
|
||||
static async findByEmail(email) {
|
||||
const sql = 'SELECT * FROM users WHERE email = ?';
|
||||
const rows = await query(sql, [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
static async update(id, updates) {
|
||||
const allowedFields = ['nickname', 'avatar', 'gender', 'birthday', 'phone', 'email'];
|
||||
const setClauses = [];
|
||||
const params = [];
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (allowedFields.includes(key) && value !== undefined) {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setClauses.push('updated_at = NOW()');
|
||||
params.push(id);
|
||||
|
||||
const sql = `UPDATE users SET ${setClauses.join(', ')} WHERE id = ?`;
|
||||
const result = await query(sql, params);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
static async updatePassword(id, newPassword) {
|
||||
const sql = 'UPDATE users SET password = ?, updated_at = NOW() WHERE id = ?';
|
||||
const result = await query(sql, [newPassword, id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
static async updateLastLogin(id) {
|
||||
const sql = 'UPDATE users SET updated_at = NOW() WHERE id = ?';
|
||||
const result = await query(sql, [id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 检查openid是否已存在
|
||||
static async isOpenidExists(openid, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE openid = ?';
|
||||
const params = [openid];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
static async isEmailExists(email, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE email = ?';
|
||||
const params = [email];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
static async isPhoneExists(phone, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE phone = ?';
|
||||
const params = [phone];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查用户名是否已已存在 (根据openid检查)
|
||||
static async isUsernameExists(username, excludeId = null) {
|
||||
return await this.isOpenidExists(username, excludeId);
|
||||
}
|
||||
|
||||
// 安全返回用户信息(去除敏感信息)
|
||||
static sanitize(user) {
|
||||
if (!user) return null;
|
||||
|
||||
const { password, ...safeUser } = user;
|
||||
return safeUser;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserMySQL;
|
||||
33
backend/src/routes/auth.js
Normal file
33
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const express = require('express')
|
||||
const { catchAsync } = require('../utils/errors')
|
||||
const { authenticate, optionalAuthenticate } = require('../middleware/auth')
|
||||
const {
|
||||
register,
|
||||
login,
|
||||
getCurrentUser,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
wechatLogin
|
||||
} = require('../controllers/authControllerMySQL')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 用户注册
|
||||
router.post('/register', catchAsync(register))
|
||||
|
||||
// 用户登录
|
||||
router.post('/login', catchAsync(login))
|
||||
|
||||
// 微信登录
|
||||
router.post('/wechat-login', catchAsync(wechatLogin))
|
||||
|
||||
// 获取当前用户信息(需要认证)
|
||||
router.get('/me', authenticate, catchAsync(getCurrentUser))
|
||||
|
||||
// 更新用户信息(需要认证)
|
||||
router.put('/profile', authenticate, catchAsync(updateProfile))
|
||||
|
||||
// 修改密码(需要认证)
|
||||
router.put('/password', authenticate, catchAsync(changePassword))
|
||||
|
||||
module.exports = router
|
||||
172
backend/src/server.js
Normal file
172
backend/src/server.js
Normal file
@@ -0,0 +1,172 @@
|
||||
require('dotenv').config()
|
||||
const app = require('./app')
|
||||
const { testConnection } = require('./config/database')
|
||||
const redisConfig = require('./config/redis')
|
||||
const rabbitMQConfig = require('./config/rabbitmq')
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
const HOST = process.env.HOST || '0.0.0.0'
|
||||
|
||||
// 显示启动横幅
|
||||
console.log('========================================')
|
||||
console.log('🚀 服务器启动中...')
|
||||
console.log(`📅 时间: ${new Date().toISOString()}`)
|
||||
console.log(`📌 版本: 1.0.0`)
|
||||
console.log('========================================\n')
|
||||
|
||||
// 显示环境信息
|
||||
console.log('🔍 环境配置:')
|
||||
console.log(`🔹 NODE_ENV: ${process.env.NODE_ENV || 'development'}`)
|
||||
console.log(`🔹 PORT: ${PORT}`)
|
||||
console.log(`🔹 HOST: ${HOST}`)
|
||||
console.log(`🔹 DATABASE_URL: ${process.env.DATABASE_URL ? '已配置' : '未配置'}`)
|
||||
console.log(`🔹 REDIS_URL: ${process.env.REDIS_URL ? '已配置' : '未配置'}`)
|
||||
console.log(`🔹 RABBITMQ_URL: ${process.env.RABBITMQ_URL ? '已配置' : '未配置'}\n`)
|
||||
|
||||
// 优雅关闭处理
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 未捕获的异常:')
|
||||
console.error(`🔹 消息: ${err.message}`)
|
||||
console.error(`🔹 堆栈: ${err.stack}`)
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 未处理的Promise拒绝:')
|
||||
console.error(`🔹 消息: ${err.message}`)
|
||||
console.error(`🔹 堆栈: ${err.stack}`)
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
const startServer = async () => {
|
||||
try {
|
||||
console.log('\n========================================')
|
||||
console.log('🔍 正在初始化服务...')
|
||||
console.log('========================================\n')
|
||||
|
||||
console.log('🔍 测试数据库连接...')
|
||||
// 测试数据库连接
|
||||
await testConnection()
|
||||
console.log('✅ 数据库连接测试成功')
|
||||
console.log('📌 数据库连接池配置:', {
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER
|
||||
})
|
||||
|
||||
// 连接Redis(可选)
|
||||
try {
|
||||
console.log('\n🔍 初始化Redis连接...')
|
||||
console.log(`📌 Redis配置: ${process.env.REDIS_URL || '使用默认配置'}`)
|
||||
await redisConfig.connect()
|
||||
console.log('✅ Redis连接成功')
|
||||
const info = await redisConfig.getInfo()
|
||||
console.log('📌 Redis服务器信息:', info.server)
|
||||
console.log('📌 Redis内存信息:', info.memory)
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Redis连接失败,继续以无缓存模式运行')
|
||||
console.warn(`🔹 错误详情: ${error.message}`)
|
||||
}
|
||||
|
||||
// 连接RabbitMQ(可选)
|
||||
try {
|
||||
console.log('\n🔍 初始化RabbitMQ连接...')
|
||||
console.log(`📌 RabbitMQ配置: ${process.env.RABBITMQ_URL || '使用默认配置'}`)
|
||||
await rabbitMQConfig.connect()
|
||||
console.log('✅ RabbitMQ连接成功')
|
||||
const connInfo = rabbitMQConfig.getConnectionInfo()
|
||||
console.log('📌 RabbitMQ连接信息:', connInfo)
|
||||
} catch (error) {
|
||||
console.warn('⚠️ RabbitMQ连接失败,继续以无消息队列模式运行')
|
||||
console.warn(`🔹 错误详情: ${error.message}`)
|
||||
}
|
||||
|
||||
// 启动HTTP服务器
|
||||
console.log('\n🔍 启动HTTP服务器...')
|
||||
const server = app.listen(PORT, HOST, () => {
|
||||
console.log('========================================')
|
||||
console.log('✅ 服务器启动成功!')
|
||||
console.log(`🚀 访问地址: http://${HOST}:${PORT}`)
|
||||
console.log(`📊 环境: ${process.env.NODE_ENV || 'development'}`)
|
||||
console.log(`⏰ 启动时间: ${new Date().toLocaleString()}`)
|
||||
console.log('💾 数据库: MySQL')
|
||||
console.log(`🔴 Redis: ${redisConfig.isConnected() ? '已连接' : '未连接'}`)
|
||||
console.log(`🐰 RabbitMQ: ${rabbitMQConfig.isConnected() ? '已连接' : '未连接'}`)
|
||||
console.log('========================================\n')
|
||||
})
|
||||
|
||||
// 优雅关闭
|
||||
const gracefulShutdown = async (signal) => {
|
||||
console.log('\n========================================')
|
||||
console.log(`🛑 收到 ${signal} 信号,开始优雅关闭流程...`)
|
||||
console.log(`⏰ 时间: ${new Date().toLocaleString()}`)
|
||||
console.log('========================================\n')
|
||||
|
||||
// 设置超时计时器
|
||||
const shutdownTimer = setTimeout(() => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 关闭操作超时,强制退出')
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
}, 10000)
|
||||
|
||||
try {
|
||||
// 关闭HTTP服务器
|
||||
console.log('🔐 关闭HTTP服务器...')
|
||||
await new Promise((resolve) => server.close(resolve))
|
||||
console.log('✅ HTTP服务器已关闭')
|
||||
|
||||
// 关闭Redis连接
|
||||
if (redisConfig.isConnected()) {
|
||||
console.log('🔐 关闭Redis连接...')
|
||||
await redisConfig.disconnect()
|
||||
console.log('✅ Redis连接已关闭')
|
||||
}
|
||||
|
||||
// 关闭RabbitMQ连接
|
||||
if (rabbitMQConfig.isConnected()) {
|
||||
console.log('🔐 关闭RabbitMQ连接...')
|
||||
await rabbitMQConfig.close()
|
||||
console.log('✅ RabbitMQ连接已关闭')
|
||||
}
|
||||
|
||||
console.log('\n========================================')
|
||||
console.log('👋 服务器已完全关闭')
|
||||
console.log('========================================')
|
||||
|
||||
clearTimeout(shutdownTimer)
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
console.error('========================================')
|
||||
console.error('❌ 关闭过程中发生错误:', error.message)
|
||||
console.error('========================================')
|
||||
clearTimeout(shutdownTimer)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册关闭信号
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则启动服务器
|
||||
if (require.main === module) {
|
||||
console.log('\n🔧 启动模式: 直接运行')
|
||||
console.log(`📌 调用堆栈: ${new Error().stack.split('\n')[1].trim()}`)
|
||||
console.log('🔄 开始启动服务器...\n')
|
||||
startServer()
|
||||
}
|
||||
|
||||
module.exports = app
|
||||
96
backend/src/utils/database.js
Normal file
96
backend/src/utils/database.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.mongoose = mongoose
|
||||
this.isConnected = false
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (this.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 连接数据库
|
||||
const mongodbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke'
|
||||
await this.mongoose.connect(mongodbUri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
|
||||
this.isConnected = true
|
||||
console.log('✅ MongoDB连接成功')
|
||||
|
||||
// 监听连接事件
|
||||
this.mongoose.connection.on('error', (error) => {
|
||||
console.error('❌ MongoDB连接错误:', error)
|
||||
this.isConnected = false
|
||||
})
|
||||
|
||||
this.mongoose.connection.on('disconnected', () => {
|
||||
console.warn('⚠️ MongoDB连接断开')
|
||||
this.isConnected = false
|
||||
})
|
||||
|
||||
this.mongoose.connection.on('reconnected', () => {
|
||||
console.log('🔁 MongoDB重新连接成功')
|
||||
this.isConnected = true
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ MongoDB连接失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (!this.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.mongoose.disconnect()
|
||||
this.isConnected = false
|
||||
console.log('✅ MongoDB连接已关闭')
|
||||
} catch (error) {
|
||||
console.error('❌ MongoDB断开连接失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async healthCheck() {
|
||||
try {
|
||||
await this.mongoose.connection.db.admin().ping()
|
||||
return { status: 'healthy', connected: this.isConnected }
|
||||
} catch (error) {
|
||||
return { status: 'unhealthy', connected: this.isConnected, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接状态
|
||||
getStatus() {
|
||||
return {
|
||||
connected: this.isConnected,
|
||||
readyState: this.mongoose.connection.readyState,
|
||||
host: this.mongoose.connection.host,
|
||||
name: this.mongoose.connection.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const database = new Database()
|
||||
|
||||
// 进程退出时关闭数据库连接
|
||||
process.on('SIGINT', async () => {
|
||||
await database.disconnect()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await database.disconnect()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
module.exports = database
|
||||
79
backend/src/utils/errors.js
Normal file
79
backend/src/utils/errors.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// 自定义应用错误类
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode) {
|
||||
super(message)
|
||||
this.statusCode = statusCode
|
||||
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'
|
||||
this.isOperational = true
|
||||
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
}
|
||||
}
|
||||
|
||||
// 异步错误处理包装器
|
||||
const catchAsync = (fn) => {
|
||||
return (req, res, next) => {
|
||||
fn(req, res, next).catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
// 404错误处理
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new AppError(`无法找到 ${req.originalUrl}`, 404)
|
||||
next(error)
|
||||
}
|
||||
|
||||
// 全局错误处理中间件
|
||||
const globalErrorHandler = (err, req, res, next) => {
|
||||
err.statusCode = err.statusCode || 500
|
||||
err.status = err.status || 'error'
|
||||
err.message = err.message || '服务器内部错误'
|
||||
|
||||
// 开发环境详细错误信息
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
res.status(err.statusCode).json({
|
||||
status: err.status,
|
||||
error: err,
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
})
|
||||
} else {
|
||||
// 生产环境简化错误信息
|
||||
res.status(err.statusCode).json({
|
||||
status: err.status,
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MongoDB重复键错误处理
|
||||
const handleDuplicateFieldsDB = (err) => {
|
||||
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0]
|
||||
const message = `字段值 ${value} 已存在,请使用其他值`
|
||||
return new AppError(message, 400)
|
||||
}
|
||||
|
||||
// MongoDB验证错误处理
|
||||
const handleValidationErrorDB = (err) => {
|
||||
const errors = Object.values(err.errors).map(el => el.message)
|
||||
const message = `输入数据无效: ${errors.join('. ')}`
|
||||
return new AppError(message, 400)
|
||||
}
|
||||
|
||||
// JWT错误处理
|
||||
const handleJWTError = () =>
|
||||
new AppError('无效的token,请重新登录', 401)
|
||||
|
||||
const handleJWTExpiredError = () =>
|
||||
new AppError('token已过期,请重新登录', 401)
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
catchAsync,
|
||||
notFound,
|
||||
globalErrorHandler,
|
||||
handleDuplicateFieldsDB,
|
||||
handleValidationErrorDB,
|
||||
handleJWTError,
|
||||
handleJWTExpiredError
|
||||
}
|
||||
70
backend/src/utils/response.js
Normal file
70
backend/src/utils/response.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// 成功响应格式
|
||||
const success = (data = null, message = '操作成功') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页响应格式
|
||||
const paginate = (data, pagination, message = '获取成功') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message,
|
||||
data: {
|
||||
list: data,
|
||||
pagination: {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
totalPages: Math.ceil(pagination.total / pagination.pageSize)
|
||||
}
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 错误响应格式
|
||||
const error = (message = '操作失败', code = 400, errors = null) => {
|
||||
return {
|
||||
success: false,
|
||||
code,
|
||||
message,
|
||||
errors,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建成功响应
|
||||
const created = (data = null, message = '创建成功') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 201,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 无内容响应
|
||||
const noContent = (message = '无内容') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 204,
|
||||
message,
|
||||
data: null,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
success,
|
||||
paginate,
|
||||
error,
|
||||
created,
|
||||
noContent
|
||||
}
|
||||
103
backend/test-api.js
Normal file
103
backend/test-api.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const http = require('http');
|
||||
|
||||
// 测试健康检查接口
|
||||
function testHealthCheck() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: 3000,
|
||||
path: '/health',
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('✅ 健康检查接口测试成功');
|
||||
console.log('状态码:', res.statusCode);
|
||||
console.log('响应:', JSON.parse(data));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('❌ 健康检查接口测试失败:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// 测试认证接口
|
||||
function testAuthAPI() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const postData = JSON.stringify({
|
||||
username: 'testuser',
|
||||
password: 'testpass123'
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: 3000,
|
||||
path: '/api/v1/auth/login',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
}
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('\n✅ 认证接口测试成功');
|
||||
console.log('状态码:', res.statusCode);
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
console.log('响应:', response);
|
||||
} catch (e) {
|
||||
console.log('原始响应:', data);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('❌ 认证接口测试失败:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('🚀 开始测试API接口...\n');
|
||||
|
||||
try {
|
||||
await testHealthCheck();
|
||||
await testAuthAPI();
|
||||
console.log('\n🎉 所有测试完成!');
|
||||
} catch (error) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行此文件,则执行测试
|
||||
if (require.main === module) {
|
||||
runTests();
|
||||
}
|
||||
|
||||
module.exports = { runTests };
|
||||
103
create-database-simple.js
Normal file
103
create-database-simple.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const mysql = require('mysql2');
|
||||
|
||||
// 数据库配置
|
||||
const configs = [
|
||||
{
|
||||
name: '测试环境',
|
||||
host: '192.168.0.240',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
password: 'aiot$Aiot123'
|
||||
},
|
||||
{
|
||||
name: '生产环境',
|
||||
host: '129.211.213.226',
|
||||
port: 9527,
|
||||
user: 'root',
|
||||
password: 'aiotAiot123!'
|
||||
}
|
||||
];
|
||||
|
||||
// 简单的SQL语句(避免编码问题)
|
||||
const sqlStatements = [
|
||||
"CREATE DATABASE IF NOT EXISTS jiebandata CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||
"USE jiebandata",
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
openid VARCHAR(64) UNIQUE NOT NULL,
|
||||
nickname VARCHAR(50) NOT NULL,
|
||||
avatar VARCHAR(255),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB`
|
||||
];
|
||||
|
||||
function executeSQL(connection, sql, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(description);
|
||||
connection.query(sql, (err, results) => {
|
||||
if (err) {
|
||||
console.error('失败:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('成功');
|
||||
resolve(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function setupDatabase(config) {
|
||||
console.log(`\n开始设置 ${config.name} 数据库...`);
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password
|
||||
});
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
connection.connect((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
for (let i = 0; i < sqlStatements.length; i++) {
|
||||
await executeSQL(connection, sqlStatements[i], `执行SQL ${i + 1}/${sqlStatements.length}`);
|
||||
}
|
||||
|
||||
console.log('✅ 数据库设置完成');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库设置失败:', error.message);
|
||||
return false;
|
||||
} finally {
|
||||
connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🎯 结伴客数据库初始化');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
let successCount = 0;
|
||||
|
||||
for (const config of configs) {
|
||||
const success = await setupDatabase(config);
|
||||
if (success) successCount++;
|
||||
console.log('\n' + '='.repeat(50));
|
||||
}
|
||||
|
||||
console.log(`📊 完成: ${successCount}/${configs.length} 个环境成功`);
|
||||
|
||||
if (successCount > 0) {
|
||||
console.log('\n🎉 数据库初始化完成!');
|
||||
console.log('现在可以运行测试脚本来验证数据库结构。');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
203
docs/API-README.md
Normal file
203
docs/API-README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 结伴客系统 API 使用指南
|
||||
|
||||
## 📖 概述
|
||||
|
||||
本文档提供了结伴客系统完整的API接口说明,包括认证、用户管理、旅行服务、动物认领、商家服务、推广奖励等核心功能接口。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Node.js 16+
|
||||
- MySQL 8.0+
|
||||
- Redis (可选)
|
||||
- RabbitMQ (可选)
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd scripts
|
||||
npm install
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
```bash
|
||||
# 运行完整API测试
|
||||
npm test
|
||||
|
||||
# 仅测试健康检查
|
||||
npm run test:health
|
||||
```
|
||||
|
||||
## 🔐 认证方式
|
||||
|
||||
所有需要认证的接口都需要在请求头中包含Bearer Token:
|
||||
|
||||
```http
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
### 获取Token的流程:
|
||||
1. 用户注册或登录
|
||||
2. 从响应中获取token
|
||||
3. 在后续请求的Header中包含token
|
||||
|
||||
## 📋 核心接口
|
||||
|
||||
### 用户认证
|
||||
- `POST /auth/register` - 用户注册
|
||||
- `POST /auth/login` - 用户登录
|
||||
- `POST /auth/wechat-login` - 微信登录
|
||||
- `GET /auth/me` - 获取当前用户信息
|
||||
|
||||
### 旅行服务
|
||||
- `POST /travel/plans` - 创建旅行计划
|
||||
- `GET /travel/plans` - 获取旅行计划列表
|
||||
- `GET /travel/matches` - 匹配旅行伙伴
|
||||
|
||||
### 动物认领
|
||||
- `GET /animals` - 获取可认领动物列表
|
||||
- `POST /animals/{id}/claim` - 认领动物
|
||||
- `GET /animals/claims` - 获取认领记录
|
||||
|
||||
### 商家服务
|
||||
- `POST /merchants/register` - 商家注册
|
||||
- `POST /merchants/products` - 发布商品/服务
|
||||
- `GET /merchants/orders` - 获取商家订单
|
||||
|
||||
### 推广奖励
|
||||
- `GET /promotion/link` - 获取推广链接
|
||||
- `GET /promotion/stats` - 获取推广数据
|
||||
- `POST /promotion/withdraw` - 申请提现
|
||||
|
||||
### 官网接口
|
||||
- `POST /website/merchant/apply` - 提交商家入驻申请
|
||||
- `GET /website/cases` - 获取成功案例列表
|
||||
|
||||
## 🎯 响应格式
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
// 具体业务数据
|
||||
},
|
||||
"timestamp": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 400,
|
||||
"message": "错误信息",
|
||||
"error": "详细错误描述",
|
||||
"timestamp": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **时间格式**: 所有时间字段使用ISO 8601格式 (YYYY-MM-DDTHH:mm:ss.sssZ)
|
||||
2. **金额单位**: 人民币元,保留两位小数
|
||||
3. **图片URL**: 必须使用HTTPS协议
|
||||
4. **频率限制**: API调用频率限制为每分钟100次
|
||||
5. **敏感操作**: 需要二次验证确保安全
|
||||
|
||||
## 🔧 开发建议
|
||||
|
||||
### 1. 错误处理
|
||||
```javascript
|
||||
try {
|
||||
const response = await api.post('/auth/login', credentials);
|
||||
// 处理成功响应
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
// 处理未授权错误
|
||||
} else if (error.response?.status === 429) {
|
||||
// 处理频率限制错误
|
||||
} else {
|
||||
// 处理其他错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 请求重试
|
||||
对于重要的请求,建议实现重试机制:
|
||||
|
||||
```javascript
|
||||
async function requestWithRetry(url, data, retries = 3) {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await api.post(url, data);
|
||||
} catch (error) {
|
||||
if (i === retries - 1) throw error;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Token刷新
|
||||
实现token自动刷新机制:
|
||||
|
||||
```javascript
|
||||
// 响应拦截器处理token过期
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// 刷新token逻辑
|
||||
const newToken = await refreshToken();
|
||||
error.config.headers.Authorization = `Bearer ${newToken}`;
|
||||
return api.request(error.config);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## 📊 监控和日志
|
||||
|
||||
建议对API调用进行监控和日志记录:
|
||||
|
||||
1. **性能监控**: 记录每个接口的响应时间
|
||||
2. **错误监控**: 记录API调用错误和异常
|
||||
3. **使用统计**: 统计接口调用频率和用户行为
|
||||
4. **安全审计**: 记录敏感操作和登录尝试
|
||||
|
||||
## 🚨 常见问题
|
||||
|
||||
### Q1: 如何处理重复注册?
|
||||
A: 注册接口会返回409状态码,提示"用户已存在"
|
||||
|
||||
### Q2: 如何重置密码?
|
||||
A: 目前需要通过客服渠道重置,后续会开发密码重置功能
|
||||
|
||||
### Q3: 如何获取商家资质?
|
||||
A: 商家需要准备营业执照等资质文件,通过官网提交申请
|
||||
|
||||
### Q4: API调用频率限制是多少?
|
||||
A: 每分钟100次请求,超过限制会返回429状态码
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有API使用问题,请联系:
|
||||
- 邮箱: support@jiebanke.com
|
||||
- 电话: 400-123-4567
|
||||
- 微信: jiebanke-support
|
||||
|
||||
## 📄 版本历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| v1.0 | 2025-01-01 | 初始版本发布 |
|
||||
| v1.1 | 2025-02-01 | 新增微信登录接口 |
|
||||
| v1.2 | 2025-03-01 | 优化错误处理机制 |
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-01-01
|
||||
**文档版本**: v1.0
|
||||
851
docs/api-documentation.md
Normal file
851
docs/api-documentation.md
Normal file
@@ -0,0 +1,851 @@
|
||||
# 结伴客系统 API 接口文档
|
||||
|
||||
## 基础信息
|
||||
|
||||
**Base URL**: `http://localhost:3000/api/v1`
|
||||
|
||||
**认证方式**: Bearer Token (JWT)
|
||||
|
||||
**响应格式**: JSON
|
||||
|
||||
## 响应格式
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
// 具体数据
|
||||
},
|
||||
"timestamp": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 400,
|
||||
"message": "错误信息",
|
||||
"error": "详细错误描述",
|
||||
"timestamp": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 认证接口
|
||||
|
||||
### 1. 用户注册
|
||||
|
||||
**Endpoint**: `POST /auth/register`
|
||||
|
||||
**描述**: 创建新用户账号
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"username": "string, required, 用户名(4-20字符)",
|
||||
"password": "string, required, 密码(6-20字符)",
|
||||
"nickname": "string, optional, 昵称",
|
||||
"email": "string, optional, 邮箱",
|
||||
"phone": "string, optional, 手机号",
|
||||
"gender": "string, optional, 性别(male/female/unknown)",
|
||||
"birthday": "string, optional, 生日(YYYY-MM-DD)"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 201,
|
||||
"message": "注册成功",
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138000",
|
||||
"avatar": "",
|
||||
"gender": "unknown",
|
||||
"points": 0,
|
||||
"level": 1,
|
||||
"status": "active",
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 用户登录
|
||||
|
||||
**Endpoint**: `POST /auth/login`
|
||||
|
||||
**描述**: 用户登录获取访问令牌
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"username": "string, required, 用户名/邮箱/手机号",
|
||||
"password": "string, required, 密码"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.1 微信登录
|
||||
|
||||
**Endpoint**: `POST /auth/wechat-login`
|
||||
|
||||
**描述**: 微信授权登录
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code": "string, required, 微信授权码",
|
||||
"userInfo": {
|
||||
"nickName": "string, optional, 微信昵称",
|
||||
"avatarUrl": "string, optional, 微信头像",
|
||||
"gender": "number, optional, 性别(0:未知,1:男,2:女)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应**: 同登录接口
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138000",
|
||||
"avatar": "",
|
||||
"gender": "unknown",
|
||||
"points": 100,
|
||||
"level": 2,
|
||||
"status": "active",
|
||||
"last_login_at": "2025-01-01T00:00:00.000Z"
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 微信登录
|
||||
|
||||
**Endpoint**: `POST /auth/wechat-login`
|
||||
|
||||
**描述**: 微信授权登录
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code": "string, required, 微信授权码",
|
||||
"userInfo": {
|
||||
"nickName": "string, optional, 微信昵称",
|
||||
"avatarUrl": "string, optional, 微信头像",
|
||||
"gender": "number, optional, 性别(0:未知,1:男,2:女)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应**: 同登录接口
|
||||
|
||||
### 4. 获取当前用户信息
|
||||
|
||||
**Endpoint**: `GET /auth/me`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**描述**: 获取当前登录用户信息
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138000",
|
||||
"avatar": "https://example.com/avatar.jpg",
|
||||
"gender": "male",
|
||||
"birthday": "1990-01-01",
|
||||
"points": 1000,
|
||||
"level": 3,
|
||||
"balance": 500.00,
|
||||
"travel_count": 5,
|
||||
"animal_adopt_count": 2,
|
||||
"flower_order_count": 3,
|
||||
"status": "active",
|
||||
"created_at": "2025-01-01T00:00:00.000Z",
|
||||
"updated_at": "2025-01-01T00:00:00.000Z",
|
||||
"last_login_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 更新用户信息
|
||||
|
||||
**Endpoint**: `PUT /auth/profile`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**描述**: 更新用户个人信息
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"nickname": "string, optional, 昵称",
|
||||
"avatar": "string, optional, 头像URL",
|
||||
"gender": "string, optional, 性别(male/female/unknown)",
|
||||
"birthday": "string, optional, 生日(YYYY-MM-DD)"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**: 同获取用户信息接口
|
||||
|
||||
### 6. 修改密码
|
||||
|
||||
**Endpoint**: `PUT /auth/password`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**描述**: 修改用户密码
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"currentPassword": "string, required, 当前密码",
|
||||
"newPassword": "string, required, 新密码(6-20字符)"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "密码修改成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 用户接口
|
||||
|
||||
### 1. 获取用户列表
|
||||
|
||||
**Endpoint**: `GET /users`
|
||||
|
||||
**认证**: 需要Bearer Token (管理员权限)
|
||||
|
||||
**查询参数**:
|
||||
- `page`: number, optional, 页码 (默认: 1)
|
||||
- `pageSize`: number, optional, 每页数量 (默认: 20)
|
||||
- `search`: string, optional, 搜索关键词
|
||||
- `status`: string, optional, 状态过滤(active/inactive/banned)
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"username": "user1",
|
||||
"nickname": "用户1",
|
||||
"email": "user1@example.com",
|
||||
"phone": "13800138001",
|
||||
"status": "active",
|
||||
"level": 2,
|
||||
"created_at": "2025-01-01极速版T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPages": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取用户详情
|
||||
|
||||
**Endpoint**: `GET /users/:id`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138000",
|
||||
"avatar": "https://example.com/avatar.jpg",
|
||||
"gender": "male",
|
||||
"points": 1000,
|
||||
"level": 3,
|
||||
"status": "active",
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 旅行接口
|
||||
|
||||
### 1. 创建旅行计划
|
||||
|
||||
**Endpoint**: `POST /travel/plans`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"destination": "string, required, 目的地",
|
||||
"start_date": "string, required, 开始日期(YYYY-MM-DD)",
|
||||
"end_date": "string, required, 结束日期(YYYY-MM-DD)",
|
||||
"budget": "number, required, 预算",
|
||||
"interests": "string, optional, 兴趣偏好",
|
||||
"visibility": "string, optional, 可见范围(public/friends/private)"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 201,
|
||||
"message": "旅行计划创建成功",
|
||||
"data": {
|
||||
"plan": {
|
||||
"id": 1,
|
||||
"title": "西藏自驾游",
|
||||
"description": "寻找志同道合的旅友一起探索西藏",
|
||||
"destination": "西藏",
|
||||
"start_date": "2025-07-01",
|
||||
"end_date": "2025-07-15",
|
||||
"budget": 5000,
|
||||
"max_members": 4,
|
||||
"current_members": 1,
|
||||
"status": "recruiting",
|
||||
"tags": ["自驾", "摄影", "探险"],
|
||||
"creator_id": 1,
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取旅行计划列表
|
||||
|
||||
**Endpoint**: `GET /travel/plans`
|
||||
|
||||
**查询参数**:
|
||||
- `page`: number, optional, 页码
|
||||
- `pageSize`: number, optional, 每页数量
|
||||
- `destination`: string, optional, 目的地搜索
|
||||
- `start_date`: string, optional, 开始日期之后
|
||||
- `end_date`: string, optional, 结束日期之前
|
||||
- `status`: string, optional, 状态(recruiting/in_progress/completed/cancelled)
|
||||
- `tags`: string, optional, 标签过滤(多个用逗号分隔)
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"plans": [
|
||||
{
|
||||
"id": 1,
|
||||
"destination": "西藏",
|
||||
"start_date": "2025-07-01",
|
||||
"end_date": "2025-07-15",
|
||||
"budget": 5000.00,
|
||||
"interests": "自驾,摄影,探险",
|
||||
"visibility": "public",
|
||||
"creator": {
|
||||
"id": 1,
|
||||
"nickname": "旅行达人",
|
||||
"avatar": "https://example.com/avatar.jpg"
|
||||
},
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPages": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 匹配旅行伙伴
|
||||
|
||||
**Endpoint**: `GET /travel/matches`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**查询参数**:
|
||||
- `plan_id`: number, required, 旅行计划ID
|
||||
- `page`: number, optional, 页码
|
||||
- `pageSize`: number, optional, 每页数量
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 2,
|
||||
"destination": "西藏",
|
||||
"start_date": "2025-07-02",
|
||||
"end_date": "2025-07-08",
|
||||
"budget": 4500.00,
|
||||
"interests": "徒步,美食",
|
||||
"match_score": 0.85,
|
||||
"user": {
|
||||
"id": 2,
|
||||
"nickname": "旅行伙伴",
|
||||
"avatar": "https://example.com/avatar2.jpg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"size": 10,
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 动物认领接口
|
||||
|
||||
### 1. 发布动物认领
|
||||
|
||||
**Endpoint**: `POST /animals`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "string, required, 动物名称",
|
||||
"species": "string, required, 物种",
|
||||
"breed": "string, optional, 品种",
|
||||
"age": "number, optional, 年龄",
|
||||
"gender": "string, optional, 性别(male/female/unknown)",
|
||||
"description": "string, required, 描述",
|
||||
"location": "string, required, 位置",
|
||||
"images": "array, optional, 图片URL数组",
|
||||
"vaccination_status": "string, optional, 疫苗接种情况",
|
||||
"sterilization_status": "string, optional, 绝育情况"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 201,
|
||||
"message": "动物认领发布成功",
|
||||
"data": {
|
||||
"animal": {
|
||||
"id": 1,
|
||||
"name": "小白",
|
||||
"species": "猫",
|
||||
"breed": "中华田园猫",
|
||||
"age": 2,
|
||||
"gender": "male",
|
||||
"description": "非常温顺的猫咪,寻找有爱心的主人",
|
||||
"location": "北京市朝阳区",
|
||||
"status": "available",
|
||||
"images": ["https://example.com/cat1.jpg"],
|
||||
"creator_id": 1,
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 商家服务接口
|
||||
|
||||
### 1. 商家注册
|
||||
|
||||
**Endpoint**: `POST /merchants/register`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"merchant_type": "string, required, 商家类型(flower_shop/activity_organizer/farm_owner)",
|
||||
"business_name": "string, required, 商家名称",
|
||||
"business_license": "string, optional, 营业执照URL",
|
||||
"contact_person": "string, required, 联系人",
|
||||
"contact_phone": "string, required, 联系电话",
|
||||
"address": "string, optional, 地址",
|
||||
"description": "string, optional, 商家介绍"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 201,
|
||||
"message": "商家注册申请已提交",
|
||||
"data": {
|
||||
"merchant": {
|
||||
"id": 1,
|
||||
"user_id": 1,
|
||||
"merchant_type": "farm_owner",
|
||||
"business_name": "XX农场",
|
||||
"business_license": "https://example.com/license.jpg",
|
||||
"contact_person": "张三",
|
||||
"contact_phone": "13800138000",
|
||||
"address": "北京市朝阳区XX路XX号",
|
||||
"description": "专业养殖羊驼的农场",
|
||||
"status": "pending",
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 发布商品/服务
|
||||
|
||||
**Endpoint**: `POST /merchants/products`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "string, required, 商品名称",
|
||||
"description": "string, required, 商品描述",
|
||||
"price": "number, required, 价格",
|
||||
"image_url": "string, optional, 图片URL",
|
||||
"category": "string, required, 商品类别",
|
||||
"status": "string, optional, 状态(available/unavailable)"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 201,
|
||||
"message": "商品发布成功",
|
||||
"data": {
|
||||
"product": {
|
||||
"id": 1,
|
||||
"merchant_id": 1,
|
||||
"name": "羊驼认领体验",
|
||||
"description": "提供一个月的羊驼认领体验服务",
|
||||
"price": 1000.00,
|
||||
"image_url": "https://example.com/product.jpg",
|
||||
"category": "animal_claim",
|
||||
"status": "available",
|
||||
"created_at": "202极速版5-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 获取商家订单
|
||||
|
||||
**Endpoint**: `GET /merchants/orders`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**查询参数**:
|
||||
- `page`: number, optional, 页码
|
||||
- `pageSize`: number, optional, 每页数量
|
||||
- `status`: string, optional, 订单状态
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"orders": [
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": 2,
|
||||
"order_number": "ORD202501010001",
|
||||
"total_amount": 1000.00,
|
||||
"status": "paid",
|
||||
"ordered_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPages": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 鲜花订购接口
|
||||
|
||||
### 1. 创建鲜花订单
|
||||
|
||||
**Endpoint**: `POST /flowers/orders`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"product_id": "number, required, 商品ID",
|
||||
"quantity": "number, required, 数量",
|
||||
"recipient_name": "string, required, 收花人姓名",
|
||||
"recipient_phone": "string, required, 收花人电话",
|
||||
"delivery_address": "string, required, 配送地址",
|
||||
"delivery_date": "string, required, 配送日期(YYYY-MM-DD)",
|
||||
"delivery_time": "string, required, 配送时间段",
|
||||
"message": "string, optional, 祝福语",
|
||||
"special_instructions": "string, optional, 特殊说明"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 201,
|
||||
"message": "订单创建成功",
|
||||
"data": {
|
||||
"order": {
|
||||
"极速版id": 1,
|
||||
"order_number": "F202501010001",
|
||||
"product_id": 1,
|
||||
"quantity": 1,
|
||||
"total_amount": 199.00,
|
||||
"status": "pending",
|
||||
"recipient_name": "张三",
|
||||
"recipient_phone": "13800138000",
|
||||
"delivery_address": "北京市朝阳区xxx路xxx号",
|
||||
"delivery_date": "2025-01-01",
|
||||
"delivery_time": "09:00-12:00",
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 推广奖励接口
|
||||
|
||||
### 1. 获取推广链接
|
||||
|
||||
**Endpoint**: `GET /promotion/link`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"promotion_link": "https://example.com/promotion?ref=user123",
|
||||
"qr_code": "https://example.com/qrcode.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取推广数据
|
||||
|
||||
**Endpoint**: `GET /promotion/stats`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"total_referrals": 50,
|
||||
"successful_registrations": 25,
|
||||
"total_rewards": 500.00,
|
||||
"available_rewards": 300.00,
|
||||
"withdrawn_rewards": 200.00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 申请提现
|
||||
|
||||
**Endpoint**: `POST /promotion/withdraw`
|
||||
|
||||
**认证**: 需要Bearer Token
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"amount": "number, required, 提现金额",
|
||||
"payment_method": "string, required, 支付方式(wechat/alipay)"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "提现申请已提交",
|
||||
"data": {
|
||||
"withdrawal_id": "WD202501010001",
|
||||
"status": "processing"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统接口
|
||||
|
||||
### 1. 健康检查
|
||||
|
||||
**Endpoint**: `GET /health`
|
||||
|
||||
**描述**: 检查服务健康状态
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"timestamp": "2025-01-01T00:00:00.000Z",
|
||||
"uptime": 12345.67,
|
||||
"environment": "development",
|
||||
"services": {
|
||||
"database": "connected",
|
||||
"redis": "disconnected",
|
||||
"rabbitmq": "disconnected"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 官网接口
|
||||
|
||||
### 1. 提交商家入驻申请
|
||||
|
||||
**Endpoint**: `POST /website/merchant/apply`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"business_name": "string, required, 商家名称",
|
||||
"contact_person": "string, required, 联系人",
|
||||
"contact_phone": "string, required, 联系电话",
|
||||
"email": "string, optional, 邮箱",
|
||||
"description": "string, optional, 商家描述"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 201,
|
||||
"message": "入驻申请已提交",
|
||||
"data": {
|
||||
"application_id": 1,
|
||||
"status": "pending"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取成功案例列表
|
||||
|
||||
**Endpoint**: `GET /website/cases`
|
||||
|
||||
**查询参数**:
|
||||
- `page`: number, optional, 页码
|
||||
- `pageSize`: number, optional, 每页数量
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"data": {
|
||||
"cases": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "XX农场成功入驻案例",
|
||||
"description": "XX农场通过平台实现了数字化转型",
|
||||
"image_url": "https://example.com/case1.jpg",
|
||||
"created_at": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPages": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 | 处理建议 |
|
||||
|--------|------|----------|
|
||||
| 200 | 成功 | 操作成功 |
|
||||
| 201 | 创建成功 | 资源创建成功 |
|
||||
| 400 | 请求错误 | 检查请求参数 |
|
||||
| 401 | 未授权 | 需要登录认证 |
|
||||
| 403 | 禁止访问 | 权限不足 |
|
||||
| 404 | 资源不存在 | 检查资源ID |
|
||||
| 409 | 资源冲突 | 资源已存在 |
|
||||
| 429 | 请求过多 | 降低请求频率 |
|
||||
| 500 | 服务器错误 | 联系管理员 |
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| v1.0 | 2025-01-01 | 初始版本发布 |
|
||||
| v1.1 | 2025-02-01 | 新增微信登录接口 |
|
||||
| v1.2 | 2025-03-01 | 优化错误处理机制 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有时间格式均为 ISO 8601 格式 (YYYY-MM-DDTHH:mm:ss.sssZ)
|
||||
2. 金额单位为元,保留两位小数
|
||||
3. 图片URL需要支持HTTPS
|
||||
4. 敏感操作需要二次验证
|
||||
5. API调用频率限制为每分钟100次
|
||||
@@ -1,34 +1,95 @@
|
||||
# 结伴客系统架构文档
|
||||
|
||||
## 1. 架构图
|
||||
## 1. 系统架构概览
|
||||
|
||||
```
|
||||
+------------------+ +------------+ +------------------+
|
||||
| 微信小程序 | | | | |
|
||||
| (uni-app) | --> | API网关 | --> | 后端服务 |
|
||||
+------------------+ | | | (Node.js+Express)|
|
||||
+------------+ | |
|
||||
+------------------+
|
||||
|
|
||||
+-------+-------+
|
||||
| |
|
||||
+-------v-----+ +-----v-------+
|
||||
| MySQL数据库 | | Redis缓存 |
|
||||
+-------------+ +-------------+
|
||||
|
|
||||
+-------v-------+
|
||||
| RabbitMQ消息队列 |
|
||||
+-----------------+
|
||||
### 1.1 架构图
|
||||
|
||||
+------------------+
|
||||
| 后台管理系统 |
|
||||
| (Vue.js 3) |
|
||||
+------------------+
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "客户端层"
|
||||
MP[微信小程序<br/>uni-app]
|
||||
ADMIN[后台管理系统<br/>Vue.js 3 + Ant Design]
|
||||
WEBSITE[官网系统<br/>HTML5 + Bootstrap]
|
||||
end
|
||||
|
||||
+------------------+
|
||||
| 官网系统 |
|
||||
| (HTML5+Bootstrap) |
|
||||
+------------------+
|
||||
subgraph "接入层"
|
||||
GATEWAY[API网关<br/>Nginx + Node.js]
|
||||
end
|
||||
|
||||
subgraph "应用服务层"
|
||||
AUTH[认证服务]
|
||||
USER[用户服务]
|
||||
TRAVEL[旅行服务]
|
||||
ANIMAL[动物服务]
|
||||
MERCHANT[商家服务]
|
||||
PAYMENT[支付服务]
|
||||
PROMOTION[推广服务]
|
||||
end
|
||||
|
||||
subgraph "基础设施层"
|
||||
DB[MySQL数据库<br/>主从复制]
|
||||
CACHE[Redis缓存<br/>集群模式]
|
||||
MQ[RabbitMQ<br/>消息队列]
|
||||
STORAGE[对象存储<br/>腾讯云COS]
|
||||
end
|
||||
|
||||
subgraph "监控运维层"
|
||||
MONITOR[监控系统<br/>Prometheus + Grafana]
|
||||
LOG[日志系统<br/>ELK Stack]
|
||||
CI_CD[CI/CD<br/>Jenkins + Docker]
|
||||
end
|
||||
|
||||
MP --> GATEWAY
|
||||
ADMIN --> GATEWAY
|
||||
WEBSITE --> GATEWAY
|
||||
|
||||
GATEWAY --> AUTH
|
||||
GATEWAY --> USER
|
||||
GATEWAY --> TRAVEL
|
||||
GATEWAY --> ANIMAL
|
||||
GATEWAY --> MERCHANT
|
||||
GATEWAY --> PAYMENT
|
||||
GATEWAY --> PROMOTION
|
||||
|
||||
AUTH --> DB
|
||||
USER --> DB
|
||||
TRAVEL --> DB
|
||||
ANIMAL --> DB
|
||||
MERCHANT --> DB
|
||||
PAYMENT --> DB
|
||||
PROMOTION --> DB
|
||||
|
||||
AUTH --> CACHE
|
||||
USER --> CACHE
|
||||
TRAVEL --> CACHE
|
||||
ANIMAL --> CACHE
|
||||
MERCHANT --> CACHE
|
||||
|
||||
PAYMENT --> MQ
|
||||
PROMOTION --> MQ
|
||||
|
||||
AUTH --> STORAGE
|
||||
USER --> STORAGE
|
||||
ANIMAL --> STORAGE
|
||||
MERCHANT --> STORAGE
|
||||
|
||||
MONITOR -.-> AUTH
|
||||
MONITOR -.-> USER
|
||||
MONITOR -.-> TRAVEL
|
||||
MONITOR -.-> ANIMAL
|
||||
MONITOR -.-> MERCHANT
|
||||
|
||||
LOG -.-> AUTH
|
||||
LOG -.-> USER
|
||||
LOG -.-> TRAVEL
|
||||
LOG -.-> ANIMAL
|
||||
LOG -.-> MERCHANT
|
||||
|
||||
CI_CD -.-> AUTH
|
||||
CI_CD -.-> USER
|
||||
CI_CD -.-> TRAVEL
|
||||
CI_CD -.-> ANIMAL
|
||||
CI_CD -.-> MERCHANT
|
||||
```
|
||||
|
||||
## 2. 项目结构
|
||||
@@ -263,15 +324,13 @@
|
||||
## 2. 技术栈选型
|
||||
|
||||
### 2.1 后端技术栈
|
||||
- 编程语言:Node.js (TypeScript)
|
||||
- 框架:Express.js
|
||||
- API规范:RESTful API
|
||||
- 容器化:Docker
|
||||
- 容器编排:Kubernetes
|
||||
- API网关:Kong
|
||||
- 监控:Prometheus + Grafana
|
||||
- 日志:ELK Stack (Elasticsearch, Logstash, Kibana)
|
||||
|
||||
API服务: Node.js + Express.js + TypeScript + RESTful API
|
||||
数据库: MySQL
|
||||
缓存系统: Redis
|
||||
消息队列: RabbitMQ(用于异步处理)
|
||||
文件存储: 腾讯云对象存储
|
||||
实时通信: WebSocket(用于大屏数据推送和实时通知)
|
||||
API文档: Swagger
|
||||
### 2.2 前端技术栈
|
||||
- 小程序框架:uni-app
|
||||
- 开发语言:JavaScript/TypeScript
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# 结伴客系统详细设计文档
|
||||
|
||||
## 2.3 数据库配置
|
||||
|
||||
### 2.3.1 测试环境
|
||||
- **主机**: 192.168.0.240 (MySQL主机地址)
|
||||
- **端口**: 3306 (MySQL端口)
|
||||
- **用户名**: root
|
||||
- **密码**: aiot$Aiot123
|
||||
- **数据库**: jiebandata
|
||||
|
||||
### 2.3.2 生产环境
|
||||
- **主机**: 129.211.213.226
|
||||
- **端口**: 9527
|
||||
- **用户名**: root
|
||||
- **密码**: aiotAiot123!
|
||||
- **数据库**: jiebandata
|
||||
## 1. 数据库设计
|
||||
|
||||
### 1.1 ER图
|
||||
|
||||
183
init-database-clean.js
Normal file
183
init-database-clean.js
Normal file
@@ -0,0 +1,183 @@
|
||||
const mysql = require('mysql2');
|
||||
|
||||
// 数据库配置
|
||||
const configs = [
|
||||
{
|
||||
name: '测试环境',
|
||||
host: '192.168.0.240',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
password: 'aiot$Aiot123'
|
||||
},
|
||||
{
|
||||
name: '生产环境',
|
||||
host: '129.211.213.226',
|
||||
port: 9527,
|
||||
user: 'root',
|
||||
password: 'aiotAiot123!'
|
||||
}
|
||||
];
|
||||
|
||||
// 创建数据库的SQL语句
|
||||
const createDatabaseSQL = `
|
||||
CREATE DATABASE IF NOT EXISTS jiebandata
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf极4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建用户表的SQL语句
|
||||
const createUsersTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
openid VARCHAR(64) UNIQUE NOT NULL,
|
||||
nickname VARCHAR(50) NOT NULL,
|
||||
avatar VARCHAR(255),
|
||||
gender ENUM('male', 'female', 'other'),
|
||||
birthday DATE,
|
||||
phone VARCHAR(20) UNIQUE,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_openid (openid),
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建商家表的SQL语句
|
||||
const createMerchantsTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.merchants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
merchant_type ENUM('flower_shop', 'activity_organizer', 'farm_owner') NOT NULL,
|
||||
business_name VARCHAR(100) NOT NULL,
|
||||
business_license VARCHAR(255),
|
||||
contact_person VARCHAR(50) NOT NULL,
|
||||
contact_phone VARCHAR(20) NOT NULL,
|
||||
address VARCHAR(255),
|
||||
description TEXT,
|
||||
status ENUM('pending', 'approved', 'rejected', 'suspended') NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建旅行计划表的SQL语句
|
||||
const createTravelPlansTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.travel_plans (
|
||||
id极INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
destination VARCHAR(100) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
budget DECIMAL(10,2) NOT NULL,
|
||||
interests TEXT,
|
||||
visibility ENUM('public', 'friends', 'private') NOT NULL DEFAULT 'public',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_destination (destination),
|
||||
INDEX idx_dates (start_date, end_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
function executeSQL(connection, sql, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`🛠️ ${description}...`);
|
||||
connection.query(sql, (err, results) => {
|
||||
if (err) {
|
||||
console.error(`❌ ${description}失败:`, err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ ${description}成功`);
|
||||
resolve(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeDatabase(config) {
|
||||
console.log(`\n🚀 开始初始化 ${config.name} 数据库...`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
multipleStatements: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 连接数据库
|
||||
await new Promise((resolve, reject) => {
|
||||
connection.connect((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// 创建数据库
|
||||
await executeSQL(connection, createDatabaseSQL, '创建数据库 jiebandata');
|
||||
|
||||
// 使用新创建的数据库
|
||||
await executeSQL(connection, 'USE jiebandata', '切换到 jiebandata 数据库');
|
||||
|
||||
// 创建用户表
|
||||
await executeSQL(connection, createUsersTableSQL, '创建用户表');
|
||||
|
||||
// 创建商家表
|
||||
await executeSQL(connection, createMerchantsTableSQL, '创建商家表');
|
||||
|
||||
// 创建旅行计划表
|
||||
await executeSQL(connection, createTravelPlansTableSQL, '创建旅行计划表');
|
||||
|
||||
console.log('✅ 数据库初始化完成!');
|
||||
|
||||
return { success: true, message: '数据库初始化成功' };
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库初始化失败:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
// 关闭极接
|
||||
connection.end();
|
||||
console.log('✅ 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🎯 结伴客系统数据库初始化工具');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const config of configs) {
|
||||
const result = await initializeDatabase(config);
|
||||
results.push({
|
||||
environment: config.name,
|
||||
success: result.success,
|
||||
message: result.message || result.error
|
||||
});
|
||||
console.log('\n' + '='.repeat(60));
|
||||
}
|
||||
|
||||
// 输出汇总结果
|
||||
console.log('📋 初始化结果汇总:');
|
||||
results.forEach(result => {
|
||||
console.log(`${result.environment}: ${result.success ? '✅ 成功' : '❌ 失败'} - ${result.message}`);
|
||||
});
|
||||
|
||||
if (results.some(r => r.success)) {
|
||||
console.log('\n🎉 数据库初始化完成!现在可以运行测试验证数据库结构。');
|
||||
} else {
|
||||
console.log('\n⚠️ 所有环境初始化失败,请检查网络连接和数据库权限');
|
||||
}
|
||||
}
|
||||
|
||||
// 运行初始化
|
||||
main().catch(console.error);
|
||||
188
init-database-fixed.js
Normal file
188
init-database-fixed.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const mysql = require('mysql2');
|
||||
|
||||
// 数据库配置
|
||||
const configs = [
|
||||
{
|
||||
name: '测试环境',
|
||||
host: '192.168.0.240',
|
||||
port: 3306,
|
||||
user: '极',
|
||||
password: 'aiot$Aiot123'
|
||||
},
|
||||
{
|
||||
name: '生产环境',
|
||||
host: '129.211.213.226',
|
||||
port: 9527,
|
||||
user: 'root',
|
||||
password: 'aiotAiot123!'
|
||||
}
|
||||
];
|
||||
|
||||
// 创建数据库的SQL语句
|
||||
const createDatabaseSQL = `
|
||||
CREATE DATABASE IF NOT EXISTS jiebandata
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建用户表的SQL语句
|
||||
const createUsersTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
openid VARCHAR(64) UNIQUE NOT NULL,
|
||||
nickname VARCHAR(50) NOT NULL,
|
||||
avatar VARCHAR(255),
|
||||
gender ENUM('male', 'female', 'other'),
|
||||
birthday DATE,
|
||||
phone VARCHAR(20) UNIQUE,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_openid (openid),
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf极4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建商家表的SQL语句
|
||||
const createMerchantsTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.merchants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
merchant_type ENUM('flower_shop', 'activity_organizer', 'farm_owner') NOT NULL,
|
||||
business_name VARCHAR(100) NOT NULL,
|
||||
business_license VARCHAR(255),
|
||||
contact_person VARCHAR极50) NOT NULL,
|
||||
contact_phone VARCHAR(20) NOT NULL,
|
||||
address VARCHAR(255),
|
||||
description TEXT,
|
||||
status ENUM('pending', 'approved', 'rejected', 'suspended') NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建旅行计划表的SQL语句
|
||||
const createTravelPlansTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.travel_plans (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
destination VARCHAR(100) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
budget DECIMAL(10,2) NOT NULL,
|
||||
interests TEXT,
|
||||
visibility ENUM('public', 'friends', 'private') NOT NULL DEFAULT 'public',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_destination (destination),
|
||||
INDEX idx_dates (start_date, end_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 所有表创建语句
|
||||
const tableCreationSQLs = [
|
||||
createUsersTableSQL,
|
||||
createMerchantsTableSQL,
|
||||
createTravelPlansTableSQL
|
||||
];
|
||||
|
||||
function executeSQL(connection, sql, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`🛠️ ${description}...`);
|
||||
connection.query(sql, (err, results) => {
|
||||
if (err) {
|
||||
console.error(`❌ ${description}失败:`, err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ ${description}成功`);
|
||||
resolve(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeDatabase(config) {
|
||||
console.log(`\n🚀 开始初始化 ${config.name} 数据库...`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
multipleStatements: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 连接数据库
|
||||
await new Promise((resolve, reject) => {
|
||||
connection.connect((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// 创建数据库
|
||||
await executeSQL(connection, createDatabaseSQL, '创建数据库 jiebandata');
|
||||
|
||||
// 使用新创建的数据库
|
||||
await executeSQL(connection, 'USE jiebandata', '切换到 jiebandata 数据库');
|
||||
|
||||
// 创建所有表
|
||||
for (let i = 0; i < tableCreationSQLs.length; i++) {
|
||||
await executeSQL(connection, tableCreationSQLs[i], `创建表 ${i + 1}/${tableCreationSQLs.length}`);
|
||||
}
|
||||
|
||||
console.log('✅ 数据库初始化完成!');
|
||||
|
||||
return { success: true, message: '数据库初始化成功' };
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库初始化失败:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
// 关闭连接
|
||||
connection.end();
|
||||
console.log('✅ 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🎯 结伴客系统数据库初始化工具');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const config of configs) {
|
||||
const result = await initializeDatabase(config);
|
||||
results.push({
|
||||
environment: config.name,
|
||||
success: result.success,
|
||||
message: result.message || result.error
|
||||
});
|
||||
console.log('\n' + '='.repeat(60));
|
||||
}
|
||||
|
||||
// 输出汇总结果
|
||||
console.log('📋 初始化结果汇总:');
|
||||
results.forEach(result => {
|
||||
console.log(`${result.environment}: ${result.success ? '✅ 成功' : '❌ 失败'} - ${result.message}`);
|
||||
});
|
||||
|
||||
// 检查是否所有环境都成功
|
||||
const allSuccess = results.every(result => result.success);
|
||||
if (allSuccess) {
|
||||
console.log('\n🎉 所有环境数据库初始化成功!');
|
||||
} else {
|
||||
console.log('\n⚠️ 部分环境初始化失败,请检查网络连接和数据库权限');
|
||||
}
|
||||
}
|
||||
|
||||
// 运行初始化
|
||||
main().catch(console.error);
|
||||
208
init-database.js
Normal file
208
init-database.js
Normal file
@@ -0,0 +1,208 @@
|
||||
const mysql = require('mysql2');
|
||||
|
||||
// 数据库配置
|
||||
const configs = [
|
||||
{
|
||||
name: '测试环境',
|
||||
host: '192.168.0.240',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
password: 'aiot$Aiot123'
|
||||
},
|
||||
{
|
||||
name: '生产环境',
|
||||
host: '129.211.213.226',
|
||||
port: 9527,
|
||||
user: 'root',
|
||||
password: 'aiotAiot123!'
|
||||
}
|
||||
];
|
||||
|
||||
// 创建数据库的SQL语句
|
||||
const createDatabaseSQL = `
|
||||
CREATE DATABASE IF NOT EXISTS jiebandata
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建用户表的SQL语句
|
||||
const createUsersTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
openid VARCHAR(64) UNIQUE NOT NULL,
|
||||
nickname VARCHAR(50) NOT NULL,
|
||||
avatar VARCHAR(255),
|
||||
gender ENUM('male', 'female', 'other'),
|
||||
birthday DATE,
|
||||
phone VARCHAR(20) UNIQUE,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_openid (openid),
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建商家表的SQL语句
|
||||
const createMerchantsTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.merchants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
merchant_type ENUM('flower_shop', 'activity_organizer', 'farm_owner') NOT NULL,
|
||||
business_name VARCHAR(100) NOT NULL,
|
||||
business_license VARCHAR(255),
|
||||
contact_person VARCHAR(50) NOT NULL,
|
||||
contact_phone VARCHAR(20) NOT NULL,
|
||||
address VARCHAR(255),
|
||||
description TEXT,
|
||||
status ENUM('pending', 'approved', 'rejected', 'suspended') NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 创建旅行计划表的SQL语句
|
||||
const createTravelPlansTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS jiebandata.travel_plans (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
destination VARCHAR(100) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
budget DECIMAL(10,2) NOT NULL,
|
||||
interests TEXT,
|
||||
visibility ENUM('public', 'friends', 'private') NOT NULL DEFAULT 'public',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT极ESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_destination (destination),
|
||||
INDEX idx_dates (start_date, end_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
// 所有表创建语句
|
||||
const tableCreationSQLs = [
|
||||
createUsersTableSQL,
|
||||
createMerchantsTableSQL,
|
||||
createTravelPlansTableSQL
|
||||
];
|
||||
|
||||
function executeSQL(connection, sql, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`🛠️ ${description}...`);
|
||||
connection.query(sql, (err, results) => {
|
||||
if (err) {
|
||||
console.error(`❌ ${description}失败:`, err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ ${description}成功`);
|
||||
resolve(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeDatabase(config) {
|
||||
console.log(`\n🚀 开始初始化 ${config.name} 数据库...`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
multipleStatements: true // 允许执行多条SQL语句
|
||||
});
|
||||
|
||||
try {
|
||||
// 连接数据库
|
||||
await new Promise((resolve, reject) => {
|
||||
connection.connect((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// 创建数据库
|
||||
await executeSQL(connection, createDatabaseSQL, '创建数据库 jiebandata');
|
||||
|
||||
// 使用新创建的数据库
|
||||
await executeSQL(connection, 'USE jiebandata', '切换到 jiebandata 数据库');
|
||||
|
||||
// 创建所有表
|
||||
for (let i = 0; i < tableCreationSQLs.length; i++) {
|
||||
await executeSQL(connection, tableCreationSQLs[i], `创建表 ${i + 1}/${tableCreationSQLs.length}`);
|
||||
}
|
||||
|
||||
// 插入示例数据
|
||||
console.log('📝 插入示例数据...');
|
||||
|
||||
// 插入示例用户
|
||||
await executeSQL(connection, `
|
||||
INSERT IGNORE INTO users (openid, nickname, avatar, gender, phone, email) VALUES
|
||||
('test_openid_1', '测试用户1', 'https://example.com/avatar1.jpg', 'male', '13800138001', 'user1@example.com'),
|
||||
('test_openid_2', '测试用户2', 'https://example.com/avatar2.jpg', 'female', '13800138002', 'user2@example.com')
|
||||
`, '插入示例用户数据');
|
||||
|
||||
// 验证数据插入
|
||||
const [users] = await new Promise((resolve, reject) => {
|
||||
connection.query('SELECT COUNT(*) as count FROM users', (err, results) => {
|
||||
if (err) reject(err);
|
||||
else resolve(results);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`✅ 数据库初始化完成!用户表中有 ${users[0].count} 条记录`);
|
||||
|
||||
return { success: true, message: '数据库初始化成功' };
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库初始化失败:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
// 关闭连接
|
||||
connection.end();
|
||||
console.log('✅ 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🎯 结伴客系统数据库初始化工具');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const config of configs) {
|
||||
const result = await initializeDatabase(config);
|
||||
results.push({
|
||||
environment: config.name,
|
||||
success: result.success,
|
||||
message: result.message || result.error
|
||||
});
|
||||
console.log('\n' + '='.repeat(60));
|
||||
}
|
||||
|
||||
// 输出汇总结果
|
||||
console.log('📋 初始化结果汇总:');
|
||||
results.forEach(result => {
|
||||
console.log(`${result.environment}: ${result.success ? '✅ 成功' : '❌ 失败'} - ${result.message}`);
|
||||
});
|
||||
|
||||
// 检查是否所有环境都成功
|
||||
const allSuccess = results.every(result => result.success);
|
||||
if (allSuccess) {
|
||||
console.log('\n🎉 所有环境数据库初始化成功!');
|
||||
} else {
|
||||
console.log('\n⚠️ 部分环境初始化失败,请检查网络连接和数据库权限');
|
||||
}
|
||||
}
|
||||
|
||||
// 运行初始化
|
||||
main().catch(console.error);
|
||||
|
||||
module.exports = { initializeDatabase };
|
||||
17
mini-program/App.vue
Normal file
17
mini-program/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
</style>
|
||||
87
mini-program/api/config.js
Normal file
87
mini-program/api/config.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// API基础配置
|
||||
const config = {
|
||||
// 开发环境
|
||||
development: {
|
||||
baseURL: 'http://localhost:3000/api',
|
||||
timeout: 10000
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
baseURL: 'https://api.jiebanke.com/api',
|
||||
timeout: 15000
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
const getConfig = () => {
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
return config[env]
|
||||
}
|
||||
|
||||
// API端点
|
||||
const endpoints = {
|
||||
// 用户相关
|
||||
USER: {
|
||||
LOGIN: '/auth/login',
|
||||
REGISTER: '/auth/register',
|
||||
PROFILE: '/user/profile',
|
||||
UPDATE_PROFILE: '/user/profile',
|
||||
UPLOAD_AVATAR: '/user/avatar'
|
||||
},
|
||||
|
||||
// 旅行计划
|
||||
TRAVEL: {
|
||||
LIST: '/travel/list',
|
||||
DETAIL: '/travel/detail',
|
||||
CREATE: '/travel/create',
|
||||
JOIN: '/travel/join',
|
||||
MY_PLANS: '/travel/my-plans',
|
||||
SEARCH: '/travel/search'
|
||||
},
|
||||
|
||||
// 动物认养
|
||||
ANIMAL: {
|
||||
LIST: '/animal/list',
|
||||
DETAIL: '/animal/detail',
|
||||
ADOPT: '/animal/adopt',
|
||||
MY_ANIMALS: '/animal/my-animals',
|
||||
CATEGORIES: '/animal/categories'
|
||||
},
|
||||
|
||||
// 送花服务
|
||||
FLOWER: {
|
||||
LIST: '/flower/list',
|
||||
DETAIL: '/flower/detail',
|
||||
ORDER: '/flower/order',
|
||||
MY_ORDERS: '/flower/my-orders',
|
||||
CATEGORIES: '/flower/categories'
|
||||
},
|
||||
|
||||
// 订单管理
|
||||
ORDER: {
|
||||
LIST: '/order/list',
|
||||
DETAIL: '/order/detail',
|
||||
CANCEL: '/order/cancel',
|
||||
PAY: '/order/pay',
|
||||
CONFIRM: '/order/confirm'
|
||||
},
|
||||
|
||||
// 支付相关
|
||||
PAYMENT: {
|
||||
CREATE: '/payment/create',
|
||||
QUERY: '/payment/query',
|
||||
REFUND: '/payment/refund'
|
||||
},
|
||||
|
||||
// 系统相关
|
||||
SYSTEM: {
|
||||
CONFIG: '/system/config',
|
||||
NOTICE: '/system/notice',
|
||||
FEEDBACK: '/system/feedback'
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
...getConfig(),
|
||||
endpoints
|
||||
}
|
||||
169
mini-program/api/example.js
Normal file
169
mini-program/api/example.js
Normal file
@@ -0,0 +1,169 @@
|
||||
// API 使用示例
|
||||
import api, { userService, travelService, homeService, apiUtils } from './index.js'
|
||||
|
||||
// 示例1: 用户登录
|
||||
async function exampleLogin() {
|
||||
try {
|
||||
const result = await userService.login({
|
||||
username: 'user123',
|
||||
password: 'password123'
|
||||
})
|
||||
|
||||
console.log('登录成功:', result)
|
||||
// 保存token
|
||||
api.setAuthToken(result.token)
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
api.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 示例2: 获取首页数据
|
||||
async function exampleGetHomeData() {
|
||||
try {
|
||||
const homeData = await homeService.getHomeData()
|
||||
console.log('首页数据:', homeData)
|
||||
|
||||
// 或者使用错误处理包装器
|
||||
const { success, data, error } = await api.withErrorHandling(
|
||||
homeService.getHomeData(),
|
||||
'首页数据加载成功'
|
||||
)
|
||||
|
||||
if (success) {
|
||||
// 处理数据
|
||||
console.log('处理首页数据:', data)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
api.handleError(error, '首页数据加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 示例3: 分页获取旅行计划
|
||||
async function exampleGetTravelPlans() {
|
||||
try {
|
||||
const pagination = apiUtils.generatePagination(1, 10)
|
||||
const travels = await travelService.getList({
|
||||
...pagination,
|
||||
keyword: '西藏'
|
||||
})
|
||||
|
||||
console.log('旅行计划列表:', travels)
|
||||
|
||||
} catch (error) {
|
||||
api.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 示例4: 文件上传
|
||||
async function exampleUploadAvatar() {
|
||||
try {
|
||||
// 选择图片
|
||||
const res = await uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album']
|
||||
})
|
||||
|
||||
const filePath = res.tempFilePaths[0]
|
||||
const uploadResult = await userService.uploadAvatar(filePath)
|
||||
|
||||
console.log('头像上传成功:', uploadResult)
|
||||
|
||||
} catch (error) {
|
||||
api.handleError(error, '头像上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 示例5: 完整的页面数据加载流程
|
||||
async function loadPageData() {
|
||||
try {
|
||||
// 并行加载多个数据
|
||||
const [banners, travels, animals, flowers] = await Promise.all([
|
||||
homeService.getBanners(),
|
||||
homeService.getRecommendedTravels(),
|
||||
homeService.getHotAnimals(),
|
||||
homeService.getFeaturedFlowers()
|
||||
])
|
||||
|
||||
return {
|
||||
banners: banners || [],
|
||||
travelPlans: travels || [],
|
||||
animals: animals || [],
|
||||
flowers: flowers || []
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
api.handleError(error)
|
||||
// 返回默认数据或空数据
|
||||
return {
|
||||
banners: [],
|
||||
travelPlans: [],
|
||||
animals: [],
|
||||
flowers: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 示例6: 订单支付流程
|
||||
async function examplePaymentFlow(orderId) {
|
||||
try {
|
||||
// 1. 创建支付
|
||||
const payment = await paymentService.create({
|
||||
orderId,
|
||||
amount: 100,
|
||||
paymentMethod: 'wechat'
|
||||
})
|
||||
|
||||
// 2. 调用微信支付
|
||||
const payResult = await uni.requestPayment({
|
||||
timeStamp: payment.timeStamp,
|
||||
nonceStr: payment.nonceStr,
|
||||
package: payment.package,
|
||||
signType: payment.signType,
|
||||
paySign: payment.paySign
|
||||
})
|
||||
|
||||
// 3. 确认支付结果
|
||||
if (payResult.errMsg === 'requestPayment:ok') {
|
||||
// 支付成功,更新订单状态
|
||||
await orderService.pay(orderId)
|
||||
console.log('支付成功')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
api.handleError(error, '支付失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出示例函数
|
||||
export default {
|
||||
exampleLogin,
|
||||
exampleGetHomeData,
|
||||
exampleGetTravelPlans,
|
||||
exampleUploadAvatar,
|
||||
loadPageData,
|
||||
examplePaymentFlow
|
||||
}
|
||||
|
||||
// 在页面中使用示例
|
||||
/*
|
||||
// 在Vue组件的methods中
|
||||
methods: {
|
||||
async loadData() {
|
||||
const pageData = await loadPageData()
|
||||
this.banners = pageData.banners
|
||||
this.travelPlans = pageData.travelPlans
|
||||
this.animals = pageData.animals
|
||||
this.flowers = pageData.flowers
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
await exampleLogin()
|
||||
// 登录成功后重新加载数据
|
||||
await this.loadData()
|
||||
}
|
||||
}
|
||||
*/
|
||||
99
mini-program/api/index.js
Normal file
99
mini-program/api/index.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// API模块主入口
|
||||
import request from './request.js'
|
||||
import * as services from './services.js'
|
||||
|
||||
// 导出所有服务
|
||||
export * from './services.js'
|
||||
|
||||
// 导出请求实例
|
||||
export { request }
|
||||
|
||||
// 默认导出
|
||||
export default {
|
||||
// 请求实例
|
||||
request,
|
||||
|
||||
// 所有服务
|
||||
...services,
|
||||
|
||||
// 工具函数
|
||||
utils: services.apiUtils,
|
||||
|
||||
// 初始化配置
|
||||
init(config = {}) {
|
||||
// 可以在这里进行一些初始化配置
|
||||
console.log('API模块初始化完成', config)
|
||||
},
|
||||
|
||||
// 设置认证token
|
||||
setAuthToken(token) {
|
||||
uni.setStorageSync('token', token)
|
||||
},
|
||||
|
||||
// 获取认证token
|
||||
getAuthToken() {
|
||||
return uni.getStorageSync('token')
|
||||
},
|
||||
|
||||
// 清除认证信息
|
||||
clearAuth() {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
},
|
||||
|
||||
// 检查是否已登录
|
||||
isLoggedIn() {
|
||||
return !!this.getAuthToken()
|
||||
},
|
||||
|
||||
// 统一错误处理
|
||||
handleError(error, defaultMessage = '操作失败') {
|
||||
const errorMessage = error.message || defaultMessage
|
||||
const errorCode = error.code || 'UNKNOWN_ERROR'
|
||||
|
||||
// 可以根据错误码进行不同的处理
|
||||
switch (errorCode) {
|
||||
case 'UNAUTHORIZED':
|
||||
this.clearAuth()
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
break
|
||||
case 'NETWORK_ERROR':
|
||||
uni.showToast({ title: '网络连接失败', icon: 'none' })
|
||||
break
|
||||
default:
|
||||
uni.showToast({ title: errorMessage, icon: 'none' })
|
||||
}
|
||||
|
||||
return { error: true, code: errorCode, message: errorMessage }
|
||||
},
|
||||
|
||||
// 请求包装器,自动处理错误
|
||||
async withErrorHandling(promise, successMessage = '操作成功') {
|
||||
try {
|
||||
const result = await promise
|
||||
if (successMessage) {
|
||||
uni.showToast({ title: successMessage, icon: 'success' })
|
||||
}
|
||||
return { success: true, data: result }
|
||||
} catch (error) {
|
||||
return this.handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自动初始化
|
||||
const api = {
|
||||
request,
|
||||
...services
|
||||
}
|
||||
|
||||
// 全局错误处理
|
||||
const originalRequest = uni.request
|
||||
uni.request = function(config) {
|
||||
return originalRequest.call(this, config).catch(error => {
|
||||
console.error('请求失败:', error)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export default api
|
||||
312
mini-program/api/mock.js
Normal file
312
mini-program/api/mock.js
Normal file
@@ -0,0 +1,312 @@
|
||||
// Mock数据 - 用于开发和测试
|
||||
import { OrderStatus, TravelStatus } from './types.js'
|
||||
|
||||
// Mock用户数据
|
||||
export const mockUsers = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
nickname: '旅行爱好者',
|
||||
avatar: '/static/user/avatar1.jpg',
|
||||
phone: '138****1234',
|
||||
points: 150,
|
||||
level: 2,
|
||||
createTime: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user2',
|
||||
nickname: '动物保护者',
|
||||
avatar: '/static/user/avatar2.jpg',
|
||||
email: 'user2@example.com',
|
||||
points: 300,
|
||||
level: 3,
|
||||
createTime: '2024-02-20'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock轮播图数据
|
||||
export const mockBanners = [
|
||||
{
|
||||
id: 1,
|
||||
image: '/static/banners/banner1.jpg',
|
||||
title: '西藏之旅',
|
||||
link: '/pages/travel/list',
|
||||
type: 'travel'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: '/static/banners/banner2.jpg',
|
||||
title: '动物认养',
|
||||
link: '/pages/animal/list',
|
||||
type: 'animal'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: '/static/banners/banner3.jpg',
|
||||
title: '鲜花配送',
|
||||
link: '/pages/flower/list',
|
||||
type: 'flower'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock旅行计划数据
|
||||
export const mockTravelPlans = [
|
||||
{
|
||||
id: 1,
|
||||
title: '西藏拉萨深度游',
|
||||
destination: '西藏拉萨',
|
||||
coverImage: '/static/travel/tibet.jpg',
|
||||
startDate: '2024-10-01',
|
||||
endDate: '2024-10-07',
|
||||
budget: 5000,
|
||||
currentMembers: 3,
|
||||
maxMembers: 6,
|
||||
description: '探索西藏神秘文化,感受高原风情',
|
||||
status: TravelStatus.RECRUITING,
|
||||
creator: mockUsers[0],
|
||||
createTime: '2024-08-20'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '云南大理休闲游',
|
||||
destination: '云南大理',
|
||||
coverImage: '/static/travel/yunnan.jpg',
|
||||
startDate: '2024-10-05',
|
||||
endDate: '2024-10-12',
|
||||
budget: 3500,
|
||||
currentMembers: 2,
|
||||
maxMembers: 4,
|
||||
description: '漫步古城,享受洱海风光',
|
||||
status: TravelStatus.RECRUITING,
|
||||
creator: mockUsers[1],
|
||||
createTime: '2024-08-22'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock动物数据
|
||||
export const mockAnimals = [
|
||||
{
|
||||
id: 1,
|
||||
name: '小羊驼',
|
||||
species: '羊驼',
|
||||
price: 1000,
|
||||
image: '/static/animals/alpaca.jpg',
|
||||
description: '温顺可爱的羊驼,适合家庭认养',
|
||||
location: '西藏牧场',
|
||||
isHot: true,
|
||||
status: 'available',
|
||||
createTime: '2024-08-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '小绵羊',
|
||||
species: '绵羊',
|
||||
price: 800,
|
||||
image: '/static/animals/sheep.jpg',
|
||||
description: '毛茸茸的小绵羊,非常温顺',
|
||||
location: '内蒙古草原',
|
||||
isHot: false,
|
||||
status: 'available',
|
||||
createTime: '2024-08-18'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock花束数据
|
||||
export const mockFlowers = [
|
||||
{
|
||||
id: 1,
|
||||
name: '浪漫玫瑰',
|
||||
description: '11朵红玫瑰,象征热烈的爱情',
|
||||
price: 199,
|
||||
image: '/static/flowers/rose.jpg',
|
||||
category: '爱情',
|
||||
stock: 50,
|
||||
createTime: '2024-08-10'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '向日葵花束',
|
||||
description: '9朵向日葵,象征阳光和希望',
|
||||
price: 179,
|
||||
image: '/static/flowers/sunflower.jpg',
|
||||
category: '祝福',
|
||||
stock: 30,
|
||||
createTime: '2024-08-12'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock订单数据
|
||||
export const mockOrders = [
|
||||
{
|
||||
id: 'ORD202408270001',
|
||||
orderNo: '202408270001',
|
||||
type: 'travel',
|
||||
title: '西藏拉萨深度游',
|
||||
image: '/static/travel/tibet.jpg',
|
||||
price: 5000,
|
||||
count: 1,
|
||||
totalAmount: 5000,
|
||||
status: OrderStatus.UNPAID,
|
||||
createTime: '2024-08-27 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 'ORD202408270002',
|
||||
orderNo: '202408270002',
|
||||
type: 'animal',
|
||||
title: '小羊驼认养',
|
||||
image: '/static/animals/alpaca.jpg',
|
||||
price: 1000,
|
||||
count: 1,
|
||||
totalAmount: 1000,
|
||||
status: OrderStatus.PAID,
|
||||
createTime: '2024-08-27 11:30:00',
|
||||
payTime: '2024-08-27 11:35:00'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock系统公告
|
||||
export const mockNotices = [
|
||||
{
|
||||
id: 1,
|
||||
title: '系统维护通知',
|
||||
content: '系统将于今晚进行维护,预计耗时2小时',
|
||||
type: 'system',
|
||||
isActive: true,
|
||||
createTime: '2024-08-26'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '新功能上线',
|
||||
content: '送花服务正式上线,欢迎体验',
|
||||
type: 'feature',
|
||||
isActive: true,
|
||||
createTime: '2024-08-27'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock API响应
|
||||
export const mockResponses = {
|
||||
// 成功响应
|
||||
success: (data, message = '操作成功') => ({
|
||||
code: 0,
|
||||
message,
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
}),
|
||||
|
||||
// 错误响应
|
||||
error: (code, message) => ({
|
||||
code,
|
||||
message,
|
||||
timestamp: Date.now()
|
||||
}),
|
||||
|
||||
// 分页响应
|
||||
pagination: (list, total, page = 1, pageSize = 10) => ({
|
||||
code: 0,
|
||||
message: '操作成功',
|
||||
data: {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
},
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
// Mock服务函数
|
||||
export const mockService = {
|
||||
// 模拟延迟
|
||||
delay: (ms = 500) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||
|
||||
// 模拟登录
|
||||
mockLogin: async (username, password) => {
|
||||
await mockService.delay(1000)
|
||||
|
||||
if (username === 'admin' && password === '123456') {
|
||||
return mockResponses.success({
|
||||
token: 'mock-token-123456',
|
||||
refreshToken: 'mock-refresh-token-123456',
|
||||
userInfo: mockUsers[0]
|
||||
})
|
||||
}
|
||||
|
||||
return mockResponses.error(1001, '用户名或密码错误')
|
||||
},
|
||||
|
||||
// 模拟获取首页数据
|
||||
mockHomeData: async () => {
|
||||
await mockService.delay(800)
|
||||
return mockResponses.success({
|
||||
banners: mockBanners,
|
||||
travelPlans: mockTravelPlans,
|
||||
animals: mockAnimals,
|
||||
flowers: mockFlowers,
|
||||
notices: mockNotices
|
||||
})
|
||||
},
|
||||
|
||||
// 模拟获取列表数据
|
||||
mockList: async (type, page = 1, pageSize = 10) => {
|
||||
await mockService.delay(600)
|
||||
|
||||
const allData = {
|
||||
travel: mockTravelPlans,
|
||||
animal: mockAnimals,
|
||||
flower: mockFlowers,
|
||||
order: mockOrders
|
||||
}
|
||||
|
||||
const data = allData[type] || []
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const paginatedData = data.slice(start, end)
|
||||
|
||||
return mockResponses.pagination(paginatedData, data.length, page, pageSize)
|
||||
},
|
||||
|
||||
// 模拟获取详情
|
||||
mockDetail: async (type, id) => {
|
||||
await mockService.delay(500)
|
||||
|
||||
const allData = {
|
||||
travel: mockTravelPlans,
|
||||
animal: mockAnimals,
|
||||
flower: mockFlowers,
|
||||
order: mockOrders
|
||||
}
|
||||
|
||||
const data = allData[type] || []
|
||||
const item = data.find(item => item.id === id || item.id === parseInt(id))
|
||||
|
||||
if (item) {
|
||||
return mockResponses.success(item)
|
||||
}
|
||||
|
||||
return mockResponses.error(404, '数据不存在')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有Mock数据
|
||||
export default {
|
||||
mockUsers,
|
||||
mockBanners,
|
||||
mockTravelPlans,
|
||||
mockAnimals,
|
||||
mockFlowers,
|
||||
mockOrders,
|
||||
mockNotices,
|
||||
mockResponses,
|
||||
mockService
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
/*
|
||||
// 在开发环境中使用Mock数据
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 可以在这里替换真实的API调用
|
||||
}
|
||||
*/
|
||||
230
mini-program/api/request.js
Normal file
230
mini-program/api/request.js
Normal file
@@ -0,0 +1,230 @@
|
||||
import config from './config.js'
|
||||
|
||||
// 请求队列(用于处理token刷新)
|
||||
const requestQueue = []
|
||||
let isRefreshing = false
|
||||
|
||||
class Request {
|
||||
constructor() {
|
||||
this.baseURL = config.baseURL
|
||||
this.timeout = config.timeout
|
||||
this.interceptors = {
|
||||
request: [],
|
||||
response: []
|
||||
}
|
||||
}
|
||||
|
||||
// 添加请求拦截器
|
||||
addRequestInterceptor(interceptor) {
|
||||
this.interceptors.request.push(interceptor)
|
||||
}
|
||||
|
||||
// 添加响应拦截器
|
||||
addResponseInterceptor(interceptor) {
|
||||
this.interceptors.response.push(interceptor)
|
||||
}
|
||||
|
||||
// 执行请求拦截器
|
||||
async runRequestInterceptors(config) {
|
||||
for (const interceptor of this.interceptors.request) {
|
||||
config = await interceptor(config)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// 执行响应拦截器
|
||||
async runResponseInterceptors(response) {
|
||||
for (const interceptor of this.interceptors.response) {
|
||||
response = await interceptor(response)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// 核心请求方法
|
||||
async request(options) {
|
||||
try {
|
||||
// 合并配置
|
||||
const requestConfig = {
|
||||
url: options.url.startsWith('http') ? options.url : `${this.baseURL}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.header
|
||||
},
|
||||
data: options.data,
|
||||
timeout: this.timeout
|
||||
}
|
||||
|
||||
// 执行请求拦截器
|
||||
const finalConfig = await this.runRequestInterceptors(requestConfig)
|
||||
|
||||
// 发起请求
|
||||
const response = await uni.request(finalConfig)
|
||||
|
||||
// 执行响应拦截器
|
||||
const finalResponse = await this.runResponseInterceptors(response)
|
||||
|
||||
return finalResponse[1] // uni.request返回的是数组[error, success]
|
||||
} catch (error) {
|
||||
console.error('Request error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// GET请求
|
||||
get(url, data = {}, options = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
data,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// POST请求
|
||||
post(url, data = {}, options = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
put(url, data = {}, options = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
delete(url, data = {}, options = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
data,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
upload(url, filePath, formData = {}, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${this.baseURL}${url}`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
formData,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
...options
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
download(url, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.downloadFile({
|
||||
url: `${this.baseURL}${url}`,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
...options
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 创建请求实例
|
||||
const request = new Request()
|
||||
|
||||
// 添加请求拦截器 - Token处理
|
||||
request.addRequestInterceptor(async (config) => {
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
config.header.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// 添加响应拦截器 - 错误处理
|
||||
request.addResponseInterceptor(async (response) => {
|
||||
const { statusCode, data } = response
|
||||
|
||||
if (statusCode === 200) {
|
||||
if (data.code === 0) {
|
||||
return data.data
|
||||
} else {
|
||||
// 业务错误
|
||||
const error = new Error(data.message || '业务错误')
|
||||
error.code = data.code
|
||||
throw error
|
||||
}
|
||||
} else if (statusCode === 401) {
|
||||
// Token过期,尝试刷新
|
||||
return handleTokenExpired(response)
|
||||
} else {
|
||||
// 网络错误
|
||||
throw new Error(`网络错误: ${statusCode}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Token过期处理
|
||||
async function handleTokenExpired(response) {
|
||||
if (isRefreshing) {
|
||||
// 如果正在刷新,将请求加入队列
|
||||
return new Promise((resolve) => {
|
||||
requestQueue.push(() => resolve(request.request(response.config)))
|
||||
})
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
const refreshToken = uni.getStorageSync('refreshToken')
|
||||
|
||||
if (!refreshToken) {
|
||||
// 没有refreshToken,跳转到登录页
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
throw new Error('请重新登录')
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试刷新Token
|
||||
const result = await request.post(config.endpoints.USER.REFRESH_TOKEN, {
|
||||
refreshToken
|
||||
})
|
||||
|
||||
// 保存新Token
|
||||
uni.setStorageSync('token', result.token)
|
||||
uni.setStorageSync('refreshToken', result.refreshToken)
|
||||
|
||||
// 重试原始请求
|
||||
const retryResponse = await request.request(response.config)
|
||||
|
||||
// 处理队列中的请求
|
||||
processRequestQueue()
|
||||
|
||||
return retryResponse
|
||||
} catch (error) {
|
||||
// 刷新失败,清空Token并跳转登录
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
throw error
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理请求队列
|
||||
function processRequestQueue() {
|
||||
while (requestQueue.length > 0) {
|
||||
const retry = requestQueue.shift()
|
||||
retry()
|
||||
}
|
||||
}
|
||||
|
||||
export default request
|
||||
189
mini-program/api/services.js
Normal file
189
mini-program/api/services.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import request from './request.js'
|
||||
import config from './config.js'
|
||||
|
||||
const { endpoints } = config
|
||||
|
||||
// 用户服务
|
||||
export const userService = {
|
||||
// 登录
|
||||
login: (data) => request.post(endpoints.USER.LOGIN, data),
|
||||
|
||||
// 注册
|
||||
register: (data) => request.post(endpoints.USER.REGISTER, data),
|
||||
|
||||
// 获取用户信息
|
||||
getProfile: () => request.get(endpoints.USER.PROFILE),
|
||||
|
||||
// 更新用户信息
|
||||
updateProfile: (data) => request.put(endpoints.USER.UPDATE_PROFILE, data),
|
||||
|
||||
// 上传头像
|
||||
uploadAvatar: (filePath) => request.upload(endpoints.USER.UPLOAD_AVATAR, filePath),
|
||||
|
||||
// 退出登录
|
||||
logout: () => {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// 旅行计划服务
|
||||
export const travelService = {
|
||||
// 获取旅行计划列表
|
||||
getList: (params = {}) => request.get(endpoints.TRAVEL.LIST, params),
|
||||
|
||||
// 获取旅行计划详情
|
||||
getDetail: (id) => request.get(`${endpoints.TRAVEL.DETAIL}/${id}`),
|
||||
|
||||
// 创建旅行计划
|
||||
create: (data) => request.post(endpoints.TRAVEL.CREATE, data),
|
||||
|
||||
// 加入旅行计划
|
||||
join: (travelId) => request.post(`${endpoints.TRAVEL.JOIN}/${travelId}`),
|
||||
|
||||
// 获取我的旅行计划
|
||||
getMyPlans: (params = {}) => request.get(endpoints.TRAVEL.MY_PLANS, params),
|
||||
|
||||
// 搜索旅行计划
|
||||
search: (keyword, params = {}) => request.get(endpoints.TRAVEL.SEARCH, { keyword, ...params })
|
||||
}
|
||||
|
||||
// 动物认养服务
|
||||
export const animalService = {
|
||||
// 获取动物列表
|
||||
getList: (params = {}) => request.get(endpoints.ANIMAL.LIST, params),
|
||||
|
||||
// 获取动物详情
|
||||
getDetail: (id) => request.get(`${endpoints.ANIMAL.DETAIL}/${id}`),
|
||||
|
||||
// 认养动物
|
||||
adopt: (animalId, data) => request.post(`${endpoints.ANIMAL.ADOPT}/${animalId}`, data),
|
||||
|
||||
// 获取我的动物
|
||||
getMyAnimals: (params = {}) => request.get(endpoints.ANIMAL.MY_ANIMALS, params),
|
||||
|
||||
// 获取动物分类
|
||||
getCategories: () => request.get(endpoints.ANIMAL.CATEGORIES)
|
||||
}
|
||||
|
||||
// 送花服务
|
||||
export const flowerService = {
|
||||
// 获取花束列表
|
||||
getList: (params = {}) => request.get(endpoints.FLOWER.LIST, params),
|
||||
|
||||
// 获取花束详情
|
||||
getDetail: (id) => request.get(`${endpoints.FLOWER.DETAIL}/${id}`),
|
||||
|
||||
// 下单
|
||||
order: (data) => request.post(endpoints.FLOWER.ORDER, data),
|
||||
|
||||
// 获取我的订单
|
||||
getMyOrders: (params = {}) => request.get(endpoints.FLOWER.MY_ORDERS, params),
|
||||
|
||||
// 获取花束分类
|
||||
getCategories: () => request.get(endpoints.FLOWER.CATEGORIES)
|
||||
}
|
||||
|
||||
// 订单服务
|
||||
export const orderService = {
|
||||
// 获取订单列表
|
||||
getList: (params = {}) => request.get(endpoints.ORDER.LIST, params),
|
||||
|
||||
// 获取订单详情
|
||||
getDetail: (id) => request.get(`${endpoints.ORDER.DETAIL}/${id}`),
|
||||
|
||||
// 取消订单
|
||||
cancel: (id) => request.post(`${endpoints.ORDER.CANCEL}/${id}`),
|
||||
|
||||
// 支付订单
|
||||
pay: (id) => request.post(`${endpoints.ORDER.PAY}/${id}`),
|
||||
|
||||
// 确认收货
|
||||
confirm: (id) => request.post(`${endpoints.ORDER.CONFIRM}/${id}`)
|
||||
}
|
||||
|
||||
// 支付服务
|
||||
export const paymentService = {
|
||||
// 创建支付
|
||||
create: (data) => request.post(endpoints.PAYMENT.CREATE, data),
|
||||
|
||||
// 查询支付状态
|
||||
query: (paymentId) => request.get(`${endpoints.PAYMENT.QUERY}/${paymentId}`),
|
||||
|
||||
// 退款
|
||||
refund: (paymentId, data) => request.post(`${endpoints.PAYMENT.REFUND}/${paymentId}`, data)
|
||||
}
|
||||
|
||||
// 系统服务
|
||||
export const systemService = {
|
||||
// 获取系统配置
|
||||
getConfig: () => request.get(endpoints.SYSTEM.CONFIG),
|
||||
|
||||
// 获取公告列表
|
||||
getNotices: (params = {}) => request.get(endpoints.SYSTEM.NOTICE, params),
|
||||
|
||||
// 提交反馈
|
||||
submitFeedback: (data) => request.post(endpoints.SYSTEM.FEEDBACK, data)
|
||||
}
|
||||
|
||||
// 首页数据服务
|
||||
export const homeService = {
|
||||
// 获取首页数据
|
||||
getHomeData: () => request.get('/home/data'),
|
||||
|
||||
// 获取轮播图
|
||||
getBanners: () => request.get('/home/banners'),
|
||||
|
||||
// 获取推荐旅行计划
|
||||
getRecommendedTravels: () => request.get('/home/recommended-travels'),
|
||||
|
||||
// 获取热门动物
|
||||
getHotAnimals: () => request.get('/home/hot-animals'),
|
||||
|
||||
// 获取精选花束
|
||||
getFeaturedFlowers: () => request.get('/home/featured-flowers')
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
export const apiUtils = {
|
||||
// 生成分页参数
|
||||
generatePagination: (page = 1, pageSize = 10) => ({
|
||||
page,
|
||||
pageSize,
|
||||
skip: (page - 1) * pageSize
|
||||
}),
|
||||
|
||||
// 处理上传进度
|
||||
handleUploadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
return percent
|
||||
},
|
||||
|
||||
// 处理下载进度
|
||||
handleDownloadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
return percent
|
||||
},
|
||||
|
||||
// 格式化错误信息
|
||||
formatError: (error) => {
|
||||
if (error.code) {
|
||||
return error.message
|
||||
}
|
||||
return '网络连接失败,请检查网络设置'
|
||||
}
|
||||
}
|
||||
|
||||
// 默认导出所有服务
|
||||
export default {
|
||||
userService,
|
||||
travelService,
|
||||
animalService,
|
||||
flowerService,
|
||||
orderService,
|
||||
paymentService,
|
||||
systemService,
|
||||
homeService,
|
||||
apiUtils
|
||||
}
|
||||
242
mini-program/api/types.js
Normal file
242
mini-program/api/types.js
Normal file
@@ -0,0 +1,242 @@
|
||||
// API 类型定义
|
||||
/**
|
||||
* 基础响应类型
|
||||
* @template T
|
||||
* @typedef {Object} BaseResponse
|
||||
* @property {number} code - 响应码 (0表示成功)
|
||||
* @property {string} message - 响应消息
|
||||
* @property {T} [data] - 响应数据
|
||||
* @property {number} [timestamp] - 时间戳
|
||||
*/
|
||||
|
||||
/**
|
||||
* 分页参数
|
||||
* @typedef {Object} PaginationParams
|
||||
* @property {number} [page] - 页码
|
||||
* @property {number} [pageSize] - 每页数量
|
||||
* @property {string} [keyword] - 搜索关键词
|
||||
*/
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
* @template T
|
||||
* @typedef {Object} PaginationResponse
|
||||
* @property {T[]} list - 数据列表
|
||||
* @property {number} total - 总数量
|
||||
* @property {number} page - 当前页码
|
||||
* @property {number} pageSize - 每页数量
|
||||
* @property {number} totalPages - 总页数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
* @typedef {Object} UserInfo
|
||||
* @property {number} id - 用户ID
|
||||
* @property {string} username - 用户名
|
||||
* @property {string} nickname - 昵称
|
||||
* @property {string} avatar - 头像URL
|
||||
* @property {string} [phone] - 手机号
|
||||
* @property {string} [email] - 邮箱
|
||||
* @property {number} points - 积分
|
||||
* @property {number} level - 会员等级
|
||||
* @property {string} createTime - 创建时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* 登录请求参数
|
||||
* @typedef {Object} LoginParams
|
||||
* @property {string} username - 用户名
|
||||
* @property {string} password - 密码
|
||||
*/
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
* @typedef {Object} LoginResponse
|
||||
* @property {string} token - 访问令牌
|
||||
* @property {string} refreshToken - 刷新令牌
|
||||
* @property {UserInfo} userInfo - 用户信息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 旅行计划
|
||||
* @typedef {Object} TravelPlan
|
||||
* @property {number} id - 计划ID
|
||||
* @property {string} title - 标题
|
||||
* @property {string} destination - 目的地
|
||||
* @property {string} coverImage - 封面图
|
||||
* @property {string} startDate - 开始日期
|
||||
* @property {string} endDate - 结束日期
|
||||
* @property {number} budget - 预算
|
||||
* @property {number} currentMembers - 当前成员数
|
||||
* @property {number} maxMembers - 最大成员数
|
||||
* @property {string} description - 描述
|
||||
* @property {string} status - 状态
|
||||
* @property {UserInfo} creator - 创建者
|
||||
* @property {string} createTime - 创建时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* 动物信息
|
||||
* @typedef {Object} AnimalInfo
|
||||
* @property {number} id - 动物ID
|
||||
* @property {string} name - 名称
|
||||
* @property {string} species - 种类
|
||||
* @property {number} price - 价格
|
||||
* @property {string} image - 图片
|
||||
* @property {string} description - 描述
|
||||
* @property {string} location - 位置
|
||||
* @property {boolean} isHot - 是否热门
|
||||
* @property {string} status - 状态
|
||||
* @property {string} createTime - 创建时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* 花束信息
|
||||
* @typedef {Object} FlowerInfo
|
||||
* @property {number} id - 花束ID
|
||||
* @property {string} name - 名称
|
||||
* @property {string} description - 描述
|
||||
* @property {number} price - 价格
|
||||
* @property {string} image - 图片
|
||||
* @property {string} category - 分类
|
||||
* @property {number} stock - 库存
|
||||
* @property {string} createTime - 创建时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* 订单信息
|
||||
* @typedef {Object} OrderInfo
|
||||
* @property {string} id - 订单ID
|
||||
* @property {string} orderNo - 订单编号
|
||||
* @property {string} type - 订单类型 (travel/animal/flower)
|
||||
* @property {string} title - 订单标题
|
||||
* @property {string} image - 图片
|
||||
* @property {number} price - 单价
|
||||
* @property {number} count - 数量
|
||||
* @property {number} totalAmount - 总金额
|
||||
* @property {string} status - 订单状态
|
||||
* @property {string} createTime - 创建时间
|
||||
* @property {string} [payTime] - 支付时间
|
||||
* @property {string} [completeTime] - 完成时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* 支付信息
|
||||
* @typedef {Object} PaymentInfo
|
||||
* @property {string} paymentId - 支付ID
|
||||
* @property {number} amount - 支付金额
|
||||
* @property {string} status - 支付状态
|
||||
* @property {string} createTime - 创建时间
|
||||
* @property {string} [payTime] - 支付时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* 系统公告
|
||||
* @typedef {Object} SystemNotice
|
||||
* @property {number} id - 公告ID
|
||||
* @property {string} title - 标题
|
||||
* @property {string} content - 内容
|
||||
* @property {string} type - 类型
|
||||
* @property {boolean} isActive - 是否激活
|
||||
* @property {string} createTime - 创建时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* 错误码定义
|
||||
* @enum {number}
|
||||
*/
|
||||
export const ErrorCode = {
|
||||
SUCCESS: 0,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
INTERNAL_ERROR: 500,
|
||||
NETWORK_ERROR: 1001,
|
||||
PARAM_ERROR: 1002,
|
||||
BUSINESS_ERROR: 1003
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态
|
||||
* @enum {string}
|
||||
*/
|
||||
export const OrderStatus = {
|
||||
UNPAID: 'unpaid', // 待付款
|
||||
PAID: 'paid', // 已付款
|
||||
DELIVERED: 'delivered', // 已发货
|
||||
COMPLETED: 'completed', // 已完成
|
||||
CANCELLED: 'cancelled' // 已取消
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付状态
|
||||
* @enum {string}
|
||||
*/
|
||||
export const PaymentStatus = {
|
||||
PENDING: 'pending', // 待支付
|
||||
SUCCESS: 'success', // 支付成功
|
||||
FAILED: 'failed', // 支付失败
|
||||
REFUNDED: 'refunded' // 已退款
|
||||
}
|
||||
|
||||
/**
|
||||
* 旅行计划状态
|
||||
* @enum {string}
|
||||
*/
|
||||
export const TravelStatus = {
|
||||
RECRUITING: 'recruiting', // 招募中
|
||||
FULL: 'full', // 已满员
|
||||
ONGOING: 'ongoing', // 进行中
|
||||
COMPLETED: 'completed', // 已完成
|
||||
CANCELLED: 'cancelled' // 已取消
|
||||
}
|
||||
|
||||
// 导出所有类型
|
||||
export default {
|
||||
BaseResponse,
|
||||
PaginationParams,
|
||||
PaginationResponse,
|
||||
UserInfo,
|
||||
LoginParams,
|
||||
LoginResponse,
|
||||
TravelPlan,
|
||||
AnimalInfo,
|
||||
FlowerInfo,
|
||||
OrderInfo,
|
||||
PaymentInfo,
|
||||
SystemNotice,
|
||||
ErrorCode,
|
||||
OrderStatus,
|
||||
PaymentStatus,
|
||||
TravelStatus
|
||||
}
|
||||
|
||||
// 类型检查工具
|
||||
export const TypeUtils = {
|
||||
/**
|
||||
* 检查是否为有效用户信息
|
||||
* @param {any} obj
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isUserInfo(obj) {
|
||||
return obj && typeof obj.id === 'number' && typeof obj.username === 'string'
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否为有效旅行计划
|
||||
* @param {any} obj
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTravelPlan(obj) {
|
||||
return obj && typeof obj.id === 'number' && typeof obj.destination === 'string'
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否为有效订单信息
|
||||
* @param {any} obj
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isOrderInfo(obj) {
|
||||
return obj && typeof obj.id === 'string' && typeof obj.orderNo === 'string'
|
||||
}
|
||||
}
|
||||
20
mini-program/index.html
Normal file
20
mini-program/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
mini-program/main.js
Normal file
22
mini-program/main.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import App from './App'
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
72
mini-program/manifest.json
Normal file
72
mini-program/manifest.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name" : "mini-program",
|
||||
"appid" : "__UNI__6B62B6B",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
39
mini-program/package.json
Normal file
39
mini-program/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "jiebanke-mini-program",
|
||||
"version": "1.0.0",
|
||||
"description": "结伴客微信小程序",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
|
||||
"build": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
|
||||
"dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve",
|
||||
"build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
|
||||
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
|
||||
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "^3.0.0",
|
||||
"@dcloudio/uni-components": "^3.0.0",
|
||||
"@dcloudio/uni-h5": "^3.0.0",
|
||||
"@dcloudio/uni-mp-weixin": "^3.0.0",
|
||||
"vue": "^3.2.0",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.0.0",
|
||||
"@dcloudio/uni-cli-shared": "^3.0.0",
|
||||
"@dcloudio/uni-migration": "^3.0.0",
|
||||
"@dcloudio/uni-template-compiler": "^3.0.0",
|
||||
"@dcloudio/vue-cli-plugin-uni": "^3.0.0",
|
||||
"@vue/cli-service": "^5.0.0",
|
||||
"cross-env": "^7.0.0",
|
||||
"sass": "^1.32.0",
|
||||
"sass-loader": "^12.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
17
mini-program/pages.json
Normal file
17
mini-program/pages.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "uni-app"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
178
mini-program/pages/animal/detail.vue
Normal file
178
mini-program/pages/animal/detail.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 动物图片轮播 -->
|
||||
<swiper class="animal-swiper" :indicator-dots="true">
|
||||
<swiper-item v-for="(img, index) in animal.images" :key="index">
|
||||
<image :src="img" mode="aspectFill" class="swiper-image"></image>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 动物基本信息 -->
|
||||
<view class="animal-info">
|
||||
<text class="name">{{ animal.name }}</text>
|
||||
<view class="meta">
|
||||
<text class="species">{{ animal.species }}</text>
|
||||
<text class="price">¥{{ animal.price }}</text>
|
||||
</view>
|
||||
<text class="location">{{ animal.location }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 动物详情 -->
|
||||
<view class="section">
|
||||
<text class="section-title">动物介绍</text>
|
||||
<text class="description">{{ animal.description }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 认养信息 -->
|
||||
<view class="section">
|
||||
<text class="section-title">认养说明</text>
|
||||
<text class="adoption-info">{{ animal.adoptionInfo }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-bar">
|
||||
<button class="btn adopt" @click="handleAdopt">立即认养</button>
|
||||
<button class="btn contact" @click="handleContact">联系管理员</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
animal: {
|
||||
id: 1,
|
||||
name: '小羊驼',
|
||||
species: '羊驼',
|
||||
price: 1000,
|
||||
location: '西藏牧场',
|
||||
images: [
|
||||
'/static/animals/alpaca1.jpg',
|
||||
'/static/animals/alpaca2.jpg'
|
||||
],
|
||||
description: '这是一只可爱的羊驼,性格温顺,喜欢与人互动。',
|
||||
adoptionInfo: '认养后您将获得:\n1. 专属认养证书\n2. 定期照片和视频\n3. 牧场参观机会'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAdopt() {
|
||||
uni.showModal({
|
||||
title: '确认认养',
|
||||
content: '确定要认养这只可爱的' + this.animal.name + '吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: '认养成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
handleContact() {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '13800138000'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.animal-swiper {
|
||||
height: 400rpx;
|
||||
}
|
||||
|
||||
.swiper-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.animal-info {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.species {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 28rpx;
|
||||
color: #ff9500;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.location {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 30rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.description, .adoption-info {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.adoption-info {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
margin: 0 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.adopt {
|
||||
background-color: #ff2d55;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.contact {
|
||||
background-color: #fff;
|
||||
color: #ff2d55;
|
||||
border: 1rpx solid #ff2d55;
|
||||
}
|
||||
</style>
|
||||
260
mini-program/pages/flower/order.vue
Normal file
260
mini-program/pages/flower/order.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 花束选择 -->
|
||||
<view class="section">
|
||||
<text class="section-title">选择花束</text>
|
||||
<scroll-view class="flower-list" scroll-x>
|
||||
<view
|
||||
v-for="(item, index) in flowers"
|
||||
:key="index"
|
||||
class="flower-item"
|
||||
:class="{active: selectedFlower === index}"
|
||||
@click="selectFlower(index)"
|
||||
>
|
||||
<image :src="item.image" class="flower-image"></image>
|
||||
<text class="flower-name">{{ item.name }}</text>
|
||||
<text class="flower-price">¥{{ item.price }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 收花人信息 -->
|
||||
<view class="section">
|
||||
<text class="section-title">收花人信息</text>
|
||||
<view class="form-item">
|
||||
<text class="label">姓名</text>
|
||||
<input v-model="receiver.name" placeholder="请输入收花人姓名" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">电话</text>
|
||||
<input v-model="receiver.phone" type="number" placeholder="请输入收花人电话" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">地址</text>
|
||||
<input v-model="receiver.address" placeholder="请输入详细地址" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 祝福语 -->
|
||||
<view class="section">
|
||||
<text class="section-title">祝福语</text>
|
||||
<textarea
|
||||
v-model="greeting"
|
||||
placeholder="写下您的祝福..."
|
||||
class="greeting-input"
|
||||
></textarea>
|
||||
</view>
|
||||
|
||||
<!-- 订单汇总 -->
|
||||
<view class="order-summary">
|
||||
<text class="summary-text">总计: ¥{{ flowers[selectedFlower].price }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="action-bar">
|
||||
<button class="submit-btn" @click="submitOrder">立即下单</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectedFlower: 0,
|
||||
flowers: [
|
||||
{
|
||||
name: '浪漫玫瑰',
|
||||
price: 199,
|
||||
image: '/static/flowers/rose.jpg'
|
||||
},
|
||||
{
|
||||
name: '温馨康乃馨',
|
||||
price: 159,
|
||||
image: '/static/flowers/carnation.jpg'
|
||||
},
|
||||
{
|
||||
name: '向日葵花束',
|
||||
price: 179,
|
||||
image: '/static/flowers/sunflower.jpg'
|
||||
},
|
||||
{
|
||||
name: '百合花束',
|
||||
price: 219,
|
||||
image: '/static/flowers/lily.jpg'
|
||||
}
|
||||
],
|
||||
receiver: {
|
||||
name: '',
|
||||
phone: '',
|
||||
address: ''
|
||||
},
|
||||
greeting: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectFlower(index) {
|
||||
this.selectedFlower = index
|
||||
},
|
||||
submitOrder() {
|
||||
if (!this.validateForm()) return
|
||||
|
||||
uni.showLoading({
|
||||
title: '提交中...'
|
||||
})
|
||||
|
||||
// 模拟API请求
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '下单成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.resetForm()
|
||||
}, 1500)
|
||||
},
|
||||
validateForm() {
|
||||
if (!this.receiver.name) {
|
||||
uni.showToast({ title: '请输入收花人姓名', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.receiver.phone) {
|
||||
uni.showToast({ title: '请输入收花人电话', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.receiver.address) {
|
||||
uni.showToast({ title: '请输入收花地址', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
resetForm() {
|
||||
this.receiver = {
|
||||
name: '',
|
||||
phone: '',
|
||||
address: ''
|
||||
}
|
||||
this.greeting = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 30rpx;
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flower-list {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flower-item {
|
||||
display: inline-block;
|
||||
width: 200rpx;
|
||||
margin-right: 20rpx;
|
||||
text-align: center;
|
||||
padding: 10rpx;
|
||||
border-radius: 10rpx;
|
||||
border: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.flower-item.active {
|
||||
border-color: #ff2d55;
|
||||
background-color: #fff5f7;
|
||||
}
|
||||
|
||||
.flower-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.flower-name {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.flower-price {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #ff2d55;
|
||||
margin-top: 5rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 10rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 80rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.greeting-input {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
position: fixed;
|
||||
bottom: 120rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
font-size: 32rpx;
|
||||
color: #ff2d55;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #ff2d55;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
687
mini-program/pages/index/index.vue
Normal file
687
mini-program/pages/index/index.vue
Normal file
@@ -0,0 +1,687 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 顶部状态栏占位 -->
|
||||
<view class="status-bar"></view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<uni-icons type="search" size="16" color="#999"></uni-icons>
|
||||
<input type="text" placeholder="搜索目的地、用户、动物" placeholder-class="placeholder" @focus="navigateToSearch" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 轮播图 -->
|
||||
<swiper class="banner-swiper" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="500">
|
||||
<swiper-item v-for="(item, index) in banners" :key="index" @click="navigateTo(item.link)">
|
||||
<image :src="item.image" mode="aspectFill" class="banner-image" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 功能入口 -->
|
||||
<view class="feature-grid">
|
||||
<view class="feature-item" @click="navigateTo('/pages/travel/list')">
|
||||
<view class="feature-icon travel">
|
||||
<uni-icons type="map" size="24" color="#007aff"></uni-icons>
|
||||
</view>
|
||||
<text class="feature-text">找搭子</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="navigateTo('/pages/animal/list')">
|
||||
<view class="feature-icon animal">
|
||||
<uni-icons type="heart" size="24" color="#ff2d55"></uni-icons>
|
||||
</view>
|
||||
<text class="feature-text">认领动物</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="navigateTo('/pages/flower/order')">
|
||||
<view class="feature-icon flower">
|
||||
<uni-icons type="flower" size="24" color="#ff9500"></uni-icons>
|
||||
</view>
|
||||
<text class="feature-text">送花服务</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="navigateTo('/pages/promotion/invite')">
|
||||
<view class="feature-icon promotion">
|
||||
<uni-icons type="gift" size="24" color="#34c759"></uni-icons>
|
||||
</view>
|
||||
<text class="feature-text">推广奖励</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 公告栏 -->
|
||||
<view class="notice-bar">
|
||||
<uni-icons type="sound" size="16" color="#ff9500"></uni-icons>
|
||||
<swiper class="notice-swiper" vertical autoplay circular :interval="3000">
|
||||
<swiper-item v-for="(notice, index) in notices" :key="index">
|
||||
<text class="notice-text">{{notice}}</text>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- 推荐旅行计划 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">推荐旅行计划</text>
|
||||
<text class="section-more" @click="navigateTo('/pages/travel/list')">更多</text>
|
||||
</view>
|
||||
<scroll-view class="plan-list" scroll-x="true" show-scrollbar="false">
|
||||
<view class="plan-card" v-for="(plan, index) in travelPlans" :key="index" @click="navigateTo(`/pages/travel/detail?id=${plan.id}`)">
|
||||
<image :src="plan.coverImage" mode="aspectFill" class="plan-image" />
|
||||
<view class="plan-info">
|
||||
<text class="plan-destination">{{ plan.destination }}</text>
|
||||
<text class="plan-date">{{ plan.startDate }} - {{ plan.endDate }}</text>
|
||||
<view class="plan-meta">
|
||||
<text class="plan-budget">¥{{ plan.budget }}</text>
|
||||
<text class="plan-members">{{ plan.currentMembers }}/{{ plan.maxMembers }}人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 热门动物 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">热门动物</text>
|
||||
<text class="section-more" @click="navigateTo('/pages/animal/list')">更多</text>
|
||||
</view>
|
||||
<view class="animal-grid">
|
||||
<view class="animal-card" v-for="(animal, index) in animals" :key="index" @click="navigateTo(`/pages/animal/detail?id=${animal.id}`)">
|
||||
<image :src="animal.image" mode="aspectFill" class="animal-image" />
|
||||
<view class="animal-tag" v-if="animal.isHot">热门</view>
|
||||
<view class="animal-info">
|
||||
<text class="animal-name">{{ animal.name }}</text>
|
||||
<text class="animal-species">{{ animal.species }}</text>
|
||||
<text class="animal-price">¥{{ animal.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 送花服务 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精选花束</text>
|
||||
<text class="section-more" @click="navigateTo('/pages/flower/list')">更多</text>
|
||||
</view>
|
||||
<scroll-view class="flower-list" scroll-x="true" show-scrollbar="false">
|
||||
<view class="flower-card" v-for="(flower, index) in flowers" :key="index" @click="navigateTo(`/pages/flower/detail?id=${flower.id}`)">
|
||||
<image :src="flower.image" mode="aspectFill" class="flower-image" />
|
||||
<view class="flower-info">
|
||||
<text class="flower-name">{{ flower.name }}</text>
|
||||
<text class="flower-desc">{{ flower.description }}</text>
|
||||
<text class="flower-price">¥{{ flower.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item active">
|
||||
<uni-icons type="home" size="24" color="#007aff"></uni-icons>
|
||||
<text class="tab-text">首页</text>
|
||||
</view>
|
||||
<view class="tab-item" @click="navigateTo('/pages/discover/index')">
|
||||
<uni-icons type="compass" size="24" color="#999"></uni-icons>
|
||||
<text class="tab-text">发现</text>
|
||||
</view>
|
||||
<view class="tab-item" @click="navigateTo('/pages/message/index')">
|
||||
<uni-icons type="chat" size="24" color="#999"></uni-icons>
|
||||
<text class="tab-text">消息</text>
|
||||
<view class="badge" v-if="unreadCount > 0">{{unreadCount > 99 ? '99+' : unreadCount}}</view>
|
||||
</view>
|
||||
<view class="tab-item" @click="navigateTo('/pages/user/center')">
|
||||
<uni-icons type="person" size="24" color="#999"></uni-icons>
|
||||
<text class="tab-text">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
banners: [
|
||||
{ image: '/static/banners/banner1.jpg', link: '/pages/travel/list' },
|
||||
{ image: '/static/banners/banner2.jpg', link: '/pages/animal/list' },
|
||||
{ image: '/static/banners/banner3.jpg', link: '/pages/flower/list' }
|
||||
],
|
||||
notices: [
|
||||
'欢迎来到结伴客,找到志同道合的旅伴!',
|
||||
'新用户注册即送50积分,可兑换精美礼品',
|
||||
'推荐好友加入,双方各得30积分奖励'
|
||||
],
|
||||
travelPlans: [
|
||||
{
|
||||
id: 1,
|
||||
destination: '西藏拉萨',
|
||||
startDate: '10月1日',
|
||||
endDate: '10月7日',
|
||||
budget: 5000,
|
||||
currentMembers: 3,
|
||||
maxMembers: 6,
|
||||
coverImage: '/static/travel/tibet.jpg'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
destination: '云南大理',
|
||||
startDate: '10月5日',
|
||||
endDate: '10月12日',
|
||||
budget: 3500,
|
||||
currentMembers: 2,
|
||||
maxMembers: 4,
|
||||
coverImage: '/static/travel/yunnan.jpg'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
destination: '新疆喀什',
|
||||
startDate: '10月10日',
|
||||
endDate: '10月20日',
|
||||
budget: 6000,
|
||||
currentMembers: 4,
|
||||
maxMembers: 8,
|
||||
coverImage: '/static/travel/xinjiang.jpg'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
destination: '青海湖',
|
||||
startDate: '10月15日',
|
||||
endDate: '10月20日',
|
||||
budget: 4000,
|
||||
currentMembers: 2,
|
||||
maxMembers: 5,
|
||||
coverImage: '/static/travel/qinghai.jpg'
|
||||
}
|
||||
],
|
||||
animals: [
|
||||
{
|
||||
id: 1,
|
||||
name: '小羊驼',
|
||||
species: '羊驼',
|
||||
price: 1000,
|
||||
isHot: true,
|
||||
image: '/static/animals/alpaca.jpg'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '小绵羊',
|
||||
species: '绵羊',
|
||||
price: 800,
|
||||
isHot: false,
|
||||
image: '/static/animals/sheep.jpg'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '小花猪',
|
||||
species: '猪',
|
||||
price: 600,
|
||||
isHot: true,
|
||||
image: '/static/animals/pig.jpg'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '小公鸡',
|
||||
species: '鸡',
|
||||
price: 300,
|
||||
isHot: false,
|
||||
image: '/static/animals/chicken.jpg'
|
||||
}
|
||||
],
|
||||
flowers: [
|
||||
{
|
||||
id: 1,
|
||||
name: '浪漫玫瑰',
|
||||
description: '11朵红玫瑰',
|
||||
price: 199,
|
||||
image: '/static/flowers/rose.jpg'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '向日葵花束',
|
||||
description: '9朵向日葵',
|
||||
price: 179,
|
||||
image: '/static/flowers/sunflower.jpg'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '百合花束',
|
||||
description: '7朵白百合',
|
||||
price: 229,
|
||||
image: '/static/flowers/lily.jpg'
|
||||
}
|
||||
],
|
||||
unreadCount: 5
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 获取系统信息设置状态栏高度
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.statusBarHeight = systemInfo.statusBarHeight;
|
||||
|
||||
// 加载数据
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
// 实际开发中,这里应该从API获取数据
|
||||
console.log('加载首页数据');
|
||||
},
|
||||
navigateTo(url) {
|
||||
uni.navigateTo({
|
||||
url: url
|
||||
});
|
||||
},
|
||||
navigateToSearch() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/index'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 移除图标字体相关样式 */
|
||||
|
||||
/* 页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
padding-bottom: 100rpx; /* 为底部导航留出空间 */
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: var(--status-bar-height);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 50rpx;
|
||||
padding: 15rpx 30rpx;
|
||||
}
|
||||
|
||||
.search-input .iconfont {
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.search-input input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.banner-swiper {
|
||||
height: 300rpx;
|
||||
margin: 0 30rpx 30rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20rpx 30rpx 30rpx;
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.feature-icon .iconfont {
|
||||
font-size: 50rpx;
|
||||
}
|
||||
|
||||
.feature-icon.travel {
|
||||
background-color: rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon.travel .iconfont {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.feature-icon.animal {
|
||||
background-color: rgba(255, 45, 85, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon.animal .iconfont {
|
||||
color: #ff2d55;
|
||||
}
|
||||
|
||||
.feature-icon.flower {
|
||||
background-color: rgba(255, 149, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon.flower .iconfont {
|
||||
color: #ff9500;
|
||||
}
|
||||
|
||||
.feature-icon.promotion {
|
||||
background-color: rgba(52, 199, 89, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon.promotion .iconfont {
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.notice-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff8e6;
|
||||
padding: 15rpx 30rpx;
|
||||
margin: 0 30rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.notice-bar .iconfont {
|
||||
font-size: 32rpx;
|
||||
color: #ff9500;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.notice-swiper {
|
||||
height: 40rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
font-size: 24rpx;
|
||||
color: #ff9500;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30rpx;
|
||||
background-color: #ffffff;
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 30rpx 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
position: relative;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8rpx;
|
||||
height: 30rpx;
|
||||
background-color: #007aff;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.plan-list {
|
||||
white-space: nowrap;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
display: inline-block;
|
||||
width: 300rpx;
|
||||
margin-right: 20rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 15rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.plan-image {
|
||||
width: 100%;
|
||||
height: 180rpx;
|
||||
}
|
||||
|
||||
.plan-info {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.plan-destination {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.plan-date {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.plan-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.plan-budget {
|
||||
font-size: 26rpx;
|
||||
color: #ff9500;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.plan-members {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.animal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.animal-card {
|
||||
background: #ffffff;
|
||||
border-radius: 15rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.animal-image {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.animal-tag {
|
||||
position: absolute;
|
||||
top: 15rpx;
|
||||
right: 15rpx;
|
||||
background-color: #ff2d55;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 5rpx 10rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.animal-info {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.animal-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.animal-species {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.animal-price {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #ff9500;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.flower-list {
|
||||
white-space: nowrap;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.flower-card {
|
||||
display: inline-block;
|
||||
width: 240rpx;
|
||||
margin-right: 20rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 15rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.flower-image {
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
}
|
||||
|
||||
.flower-info {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.flower-name {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.flower-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-bottom: 10rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.flower-price {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #ff9500;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100rpx;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item .iconfont {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .iconfont {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 50%;
|
||||
transform: translateX(10rpx);
|
||||
background-color: #ff2d55;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6rpx;
|
||||
}
|
||||
</style>
|
||||
167
mini-program/pages/travel/detail.vue
Normal file
167
mini-program/pages/travel/detail.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 封面图 -->
|
||||
<image :src="plan.coverImage" mode="widthFix" class="cover-image"></image>
|
||||
|
||||
<!-- 计划标题和基本信息 -->
|
||||
<view class="plan-header">
|
||||
<text class="title">{{ plan.destination }}</text>
|
||||
<view class="meta">
|
||||
<text class="date">{{ plan.startDate }} - {{ plan.endDate }}</text>
|
||||
<text class="budget">预算: ¥{{ plan.budget }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 行程详情 -->
|
||||
<view class="section">
|
||||
<text class="section-title">行程安排</text>
|
||||
<view class="schedule" v-for="(day, index) in plan.schedule" :key="index">
|
||||
<text class="day">第{{ index + 1 }}天</text>
|
||||
<text class="content">{{ day }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 同行要求 -->
|
||||
<view class="section">
|
||||
<text class="section-title">同行要求</text>
|
||||
<text class="requirements">{{ plan.requirements }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-bar">
|
||||
<button class="btn join" @click="handleJoin">加入计划</button>
|
||||
<button class="btn contact" @click="handleContact">联系发起人</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
plan: {
|
||||
id: 1,
|
||||
destination: '西藏',
|
||||
startDate: '10月1日',
|
||||
endDate: '10月7日',
|
||||
budget: 5000,
|
||||
coverImage: '/static/travel/tibet.jpg',
|
||||
schedule: [
|
||||
'拉萨市区游览,参观布达拉宫、大昭寺',
|
||||
'前往纳木错,欣赏圣湖美景',
|
||||
'林芝地区游览,参观雅鲁藏布大峡谷',
|
||||
'返回拉萨,自由活动',
|
||||
'日喀则地区游览,参观扎什伦布寺',
|
||||
'珠峰大本营一日游',
|
||||
'返回拉萨,结束行程'
|
||||
],
|
||||
requirements: '1. 年龄18-35岁\n2. 身体健康,能适应高原环境\n3. 有团队精神,乐于分享'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleJoin() {
|
||||
uni.showToast({
|
||||
title: '已申请加入',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
handleContact() {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '13800138000'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20rpx;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 30rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.schedule {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.day {
|
||||
font-size: 28rpx;
|
||||
color: #007aff;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.requirements {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
white-space: pre-line;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
margin: 0 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.join {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.contact {
|
||||
background-color: #fff;
|
||||
color: #007aff;
|
||||
border: 1rpx solid #007aff;
|
||||
}
|
||||
</style>
|
||||
160
mini-program/pages/user/center.vue
Normal file
160
mini-program/pages/user/center.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-header">
|
||||
<image :src="user.avatar" class="avatar"></image>
|
||||
<view class="user-info">
|
||||
<text class="username">{{ user.nickname }}</text>
|
||||
<text class="member-level">{{ user.memberLevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单状态 -->
|
||||
<view class="order-status">
|
||||
<view
|
||||
class="status-item"
|
||||
v-for="item in orderStatus"
|
||||
:key="item.type"
|
||||
@click="navigateToOrder(item.type)"
|
||||
>
|
||||
<text class="count">{{ item.count }}</text>
|
||||
<text class="text">{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<view class="function-list">
|
||||
<view
|
||||
class="function-item"
|
||||
v-for="item in functions"
|
||||
:key="item.text"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<uni-icons :type="item.icon" size="20" color="#666"></uni-icons>
|
||||
<text class="text">{{ item.text }}</text>
|
||||
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
avatar: '/static/user/avatar.jpg',
|
||||
nickname: '旅行爱好者',
|
||||
memberLevel: '黄金会员'
|
||||
},
|
||||
orderStatus: [
|
||||
{ type: 'all', text: '全部订单', count: 5 },
|
||||
{ type: 'unpaid', text: '待付款', count: 1 },
|
||||
{ type: 'undelivered', text: '待发货', count: 1 },
|
||||
{ type: 'delivered', text: '待收货', count: 2 },
|
||||
{ type: 'completed', text: '已完成', count: 1 }
|
||||
],
|
||||
functions: [
|
||||
{ icon: 'heart', text: '我的认养', url: '/pages/user/adoptions' },
|
||||
{ icon: 'map', text: '我的旅行计划', url: '/pages/user/travels' },
|
||||
{ icon: 'gift', text: '我的送花订单', url: '/pages/user/flowers' },
|
||||
{ icon: 'star', text: '我的收藏', url: '/pages/user/favorites' },
|
||||
{ icon: 'settings', text: '账户设置', url: '/pages/user/settings' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigateTo(url) {
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
navigateToOrder(type) {
|
||||
uni.navigateTo({ url: `/pages/user/orders?type=${type}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.member-level {
|
||||
font-size: 24rpx;
|
||||
color: #ff9500;
|
||||
background-color: #fff8e6;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
padding: 30rpx 0;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.function-list {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.function-item .text {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
480
mini-program/pages/user/orders.vue
Normal file
480
mini-program/pages/user/orders.vue
Normal file
@@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 订单类型选项卡 -->
|
||||
<view class="tab-bar">
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === index }"
|
||||
@click="switchTab(index)"
|
||||
>
|
||||
<text>{{ tab.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<scroll-view
|
||||
class="order-list"
|
||||
scroll-y="true"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view v-if="orders.length === 0" class="empty-tip">
|
||||
<image src="/static/user/empty-order.png" class="empty-image"></image>
|
||||
<text>暂无相关订单</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="(order, index) in orders"
|
||||
:key="index"
|
||||
class="order-item"
|
||||
@click="navigateToDetail(order.id)"
|
||||
>
|
||||
<!-- 订单头部 -->
|
||||
<view class="order-header">
|
||||
<text class="order-type">{{ getOrderTypeName(order.type) }}</text>
|
||||
<text class="order-status">{{ getStatusText(order.status) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 订单内容 -->
|
||||
<view class="order-content">
|
||||
<image :src="order.image" class="order-image"></image>
|
||||
<view class="order-info">
|
||||
<text class="order-title">{{ order.title }}</text>
|
||||
<text class="order-desc">{{ order.description }}</text>
|
||||
<view class="order-price-box">
|
||||
<text class="order-price">¥{{ order.price }}</text>
|
||||
<text class="order-count">x{{ order.count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单底部 -->
|
||||
<view class="order-footer">
|
||||
<text class="order-total">共{{ order.count }}件商品 合计:¥{{ order.price * order.count }}</text>
|
||||
<view class="order-actions">
|
||||
<button
|
||||
v-if="order.status === 'unpaid'"
|
||||
class="action-btn primary"
|
||||
@click.stop="payOrder(order.id)"
|
||||
>立即付款</button>
|
||||
<button
|
||||
v-if="order.status === 'delivered'"
|
||||
class="action-btn primary"
|
||||
@click.stop="confirmReceive(order.id)"
|
||||
>确认收货</button>
|
||||
<button
|
||||
v-if="order.status === 'completed'"
|
||||
class="action-btn"
|
||||
@click.stop="reviewOrder(order.id)"
|
||||
>评价</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click.stop="deleteOrder(order.id)"
|
||||
>删除订单</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loading" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-if="noMore && orders.length > 0" class="no-more">
|
||||
<text>没有更多订单了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
{ text: '全部', type: 'all' },
|
||||
{ text: '待付款', type: 'unpaid' },
|
||||
{ text: '待发货', type: 'undelivered' },
|
||||
{ text: '待收货', type: 'delivered' },
|
||||
{ text: '已完成', type: 'completed' }
|
||||
],
|
||||
currentTab: 0,
|
||||
orders: [],
|
||||
page: 1,
|
||||
loading: false,
|
||||
noMore: false
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 如果有传入类型参数,切换到对应选项卡
|
||||
if (options.type) {
|
||||
const index = this.tabs.findIndex(tab => tab.type === options.type)
|
||||
if (index !== -1) {
|
||||
this.currentTab = index
|
||||
}
|
||||
}
|
||||
this.loadOrders()
|
||||
},
|
||||
methods: {
|
||||
switchTab(index) {
|
||||
if (this.currentTab === index) return
|
||||
this.currentTab = index
|
||||
this.page = 1
|
||||
this.orders = []
|
||||
this.noMore = false
|
||||
this.loadOrders()
|
||||
},
|
||||
loadOrders() {
|
||||
if (this.loading || this.noMore) return
|
||||
|
||||
this.loading = true
|
||||
|
||||
// 模拟API请求
|
||||
setTimeout(() => {
|
||||
const type = this.tabs[this.currentTab].type
|
||||
const newOrders = this.getMockOrders(type, this.page)
|
||||
|
||||
if (newOrders.length === 0) {
|
||||
this.noMore = true
|
||||
} else {
|
||||
this.orders = [...this.orders, ...newOrders]
|
||||
this.page++
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
}, 1000)
|
||||
},
|
||||
loadMore() {
|
||||
this.loadOrders()
|
||||
},
|
||||
getMockOrders(type, page) {
|
||||
// 模拟数据,实际应从API获取
|
||||
const allOrders = [
|
||||
{
|
||||
id: '1001',
|
||||
type: 'travel',
|
||||
status: 'unpaid',
|
||||
title: '西藏旅行计划',
|
||||
description: '10月1日-10月7日',
|
||||
price: 5000,
|
||||
count: 1,
|
||||
image: '/static/travel/tibet.jpg'
|
||||
},
|
||||
{
|
||||
id: '1002',
|
||||
type: 'animal',
|
||||
status: 'undelivered',
|
||||
title: '小羊驼认养',
|
||||
description: '认养期限:1年',
|
||||
price: 1000,
|
||||
count: 1,
|
||||
image: '/static/animals/alpaca.jpg'
|
||||
},
|
||||
{
|
||||
id: '1003',
|
||||
type: 'flower',
|
||||
status: 'delivered',
|
||||
title: '浪漫玫瑰花束',
|
||||
description: '11朵红玫瑰',
|
||||
price: 199,
|
||||
count: 1,
|
||||
image: '/static/flowers/rose.jpg'
|
||||
},
|
||||
{
|
||||
id: '1004',
|
||||
type: 'flower',
|
||||
status: 'completed',
|
||||
title: '向日葵花束',
|
||||
description: '9朵向日葵',
|
||||
price: 179,
|
||||
count: 1,
|
||||
image: '/static/flowers/sunflower.jpg'
|
||||
}
|
||||
]
|
||||
|
||||
// 根据类型筛选
|
||||
let filteredOrders = allOrders
|
||||
if (type !== 'all') {
|
||||
filteredOrders = allOrders.filter(order => order.status === type)
|
||||
}
|
||||
|
||||
// 分页
|
||||
const pageSize = 5
|
||||
const start = (page - 1) * pageSize
|
||||
const end = page * pageSize
|
||||
|
||||
return filteredOrders.slice(start, end)
|
||||
},
|
||||
getOrderTypeName(type) {
|
||||
const typeMap = {
|
||||
travel: '旅行计划',
|
||||
animal: '动物认养',
|
||||
flower: '送花服务'
|
||||
}
|
||||
return typeMap[type] || '订单'
|
||||
},
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
unpaid: '待付款',
|
||||
undelivered: '待发货',
|
||||
delivered: '待收货',
|
||||
completed: '已完成'
|
||||
}
|
||||
return statusMap[status] || '未知状态'
|
||||
},
|
||||
navigateToDetail(id) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/order-detail?id=${id}`
|
||||
})
|
||||
},
|
||||
payOrder(id) {
|
||||
uni.showModal({
|
||||
title: '支付提示',
|
||||
content: '确定要支付此订单吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 模拟支付流程
|
||||
uni.showLoading({
|
||||
title: '支付中...'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 更新订单状态
|
||||
this.updateOrderStatus(id, 'undelivered')
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
confirmReceive(id) {
|
||||
uni.showModal({
|
||||
title: '确认收货',
|
||||
content: '确认已收到商品吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: '确认成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 更新订单状态
|
||||
this.updateOrderStatus(id, 'completed')
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
reviewOrder(id) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/review?id=${id}`
|
||||
})
|
||||
},
|
||||
deleteOrder(id) {
|
||||
uni.showModal({
|
||||
title: '删除订单',
|
||||
content: '确定要删除此订单吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 从列表中移除
|
||||
const index = this.orders.findIndex(order => order.id === id)
|
||||
if (index !== -1) {
|
||||
this.orders.splice(index, 1)
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateOrderStatus(id, newStatus) {
|
||||
const order = this.orders.find(order => order.id === id)
|
||||
if (order) {
|
||||
order.status = newStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
background-color: #fff;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.order-type {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 28rpx;
|
||||
color: #ff9500;
|
||||
}
|
||||
|
||||
.order-content {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.order-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.order-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.order-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.order-price-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.order-price {
|
||||
font-size: 28rpx;
|
||||
color: #ff2d55;
|
||||
}
|
||||
|
||||
.order-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.order-total {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-left: 20rpx;
|
||||
font-size: 24rpx;
|
||||
padding: 0 30rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
background-color: #fff;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading, .no-more {
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
25
mini-program/project.config.json
Normal file
25
mini-program/project.config.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"setting": {
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"uglifyFileName": false,
|
||||
"enhance": true,
|
||||
"packNpmRelationList": [],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"useCompilerPlugins": false,
|
||||
"minifyWXML": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx0f61cff5f12e13fc",
|
||||
"editorSetting": {}
|
||||
}
|
||||
14
mini-program/project.private.config.json
Normal file
14
mini-program/project.private.config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"libVersion": "3.9.2",
|
||||
"projectname": "mini-program",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
}
|
||||
BIN
mini-program/static/logo.png
Normal file
BIN
mini-program/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
13
mini-program/uni.promisify.adaptor.js
Normal file
13
mini-program/uni.promisify.adaptor.js
Normal file
@@ -0,0 +1,13 @@
|
||||
uni.addInterceptor({
|
||||
returnValue (res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
76
mini-program/uni.scss
Normal file
76
mini-program/uni.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
127
package-lock.json
generated
Normal file
127
package-lock.json
generated
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "jiebanke",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"mysql2": "^3.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
|
||||
"integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.14.3",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz",
|
||||
"integrity": "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.1",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"long": "^5.2.1",
|
||||
"lru.min": "^1.0.0",
|
||||
"named-placeholders": "^1.1.3",
|
||||
"seq-queue": "^0.0.5",
|
||||
"sqlstring": "^2.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
|
||||
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^7.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/seq-queue": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mysql2": "^3.14.3"
|
||||
}
|
||||
}
|
||||
22
scripts/package.json
Normal file
22
scripts/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "jiebanke-api-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "结伴客系统API测试脚本",
|
||||
"main": "test-api.js",
|
||||
"scripts": {
|
||||
"test": "node test-api.js",
|
||||
"test:health": "node -e \"require('./test-api.js').api.get('/health').then(r => console.log('健康检查:', r.data)).catch(console.error)\""
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"keywords": [
|
||||
"api-testing",
|
||||
"jiebanke",
|
||||
"travel",
|
||||
"animal-adoption"
|
||||
],
|
||||
"author": "结伴客开发团队",
|
||||
"license": "MIT"
|
||||
}
|
||||
129
scripts/test-api.js
Normal file
129
scripts/test-api.js
Normal file
@@ -0,0 +1,129 @@
|
||||
// 结伴客系统API测试脚本
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'http://localhost:3000/api/v1';
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// 测试用例
|
||||
async function testAPI() {
|
||||
console.log('🚀 开始测试结伴客系统API...\n');
|
||||
|
||||
try {
|
||||
// 1. 测试健康检查
|
||||
console.log('1. 测试健康检查接口...');
|
||||
const healthResponse = await api.get('/health');
|
||||
console.log('✅ 健康检查成功:', healthResponse.data);
|
||||
|
||||
// 2. 测试用户注册
|
||||
console.log('\n2. 测试用户注册接口...');
|
||||
const registerData = {
|
||||
username: 'testuser',
|
||||
password: 'test123',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
gender: 'male',
|
||||
birthday: '1990-01-01'
|
||||
};
|
||||
|
||||
try {
|
||||
const registerResponse = await api.post('/auth/register', registerData);
|
||||
console.log('✅ 用户注册成功:', registerResponse.data);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 409) {
|
||||
console.log('⚠️ 用户已存在,跳过注册测试');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 测试用户登录
|
||||
console.log('\n3. 测试用户登录接口...');
|
||||
const loginData = {
|
||||
username: 'testuser',
|
||||
password: 'test123'
|
||||
};
|
||||
|
||||
const loginResponse = await api.post('/auth/login', loginData);
|
||||
console.log('✅ 用户登录成功');
|
||||
|
||||
const token = loginResponse.data.data.token;
|
||||
|
||||
// 设置认证头
|
||||
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
// 4. 测试获取用户信息
|
||||
console.log('\n4. 测试获取用户信息接口...');
|
||||
const userInfoResponse = await api.get('/auth/me');
|
||||
console.log('✅ 获取用户信息成功:', userInfoResponse.data.data.user.nickname);
|
||||
|
||||
// 5. 测试创建旅行计划
|
||||
console.log('\n5. 测试创建旅行计划接口...');
|
||||
const travelPlanData = {
|
||||
destination: '西藏',
|
||||
start_date: '2025-07-01',
|
||||
end_date: '2025-07-15',
|
||||
budget: 5000,
|
||||
interests: '自驾,摄影,探险',
|
||||
visibility: 'public'
|
||||
};
|
||||
|
||||
const travelResponse = await api.post('/travel/plans', travelPlanData);
|
||||
console.log('✅ 创建旅行计划成功:', travelResponse.data.data.plan.destination);
|
||||
|
||||
// 6. 测试获取旅行计划列表
|
||||
console.log('\n6. 测试获取旅行计划列表接口...');
|
||||
const travelListResponse = await api.get('/travel/plans');
|
||||
console.log('✅ 获取旅行计划列表成功:', travelListResponse.data.data.plans.length + '条记录');
|
||||
|
||||
// 7. 测试商家注册
|
||||
console.log('\n7. 测试商家注册接口...');
|
||||
const merchantData = {
|
||||
merchant_type: 'farm_owner',
|
||||
business_name: '测试农场',
|
||||
contact_person: '测试联系人',
|
||||
contact_phone: '13800138001',
|
||||
address: '测试地址',
|
||||
description: '测试农场描述'
|
||||
};
|
||||
|
||||
try {
|
||||
const merchantResponse = await api.post('/merchants/register', merchantData);
|
||||
console.log('✅ 商家注册成功:', merchantResponse.data.data.merchant.business_name);
|
||||
} catch (error) {
|
||||
console.log('⚠️ 商家注册测试跳过:', error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
console.log('\n🎉 所有API测试完成!');
|
||||
console.log('\n📋 测试总结:');
|
||||
console.log('✅ 健康检查接口 - 正常');
|
||||
console.log('✅ 用户认证接口 - 正常');
|
||||
console.log('✅ 旅行计划接口 - 正常');
|
||||
console.log('✅ 商家服务接口 - 部分测试');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ API测试失败:');
|
||||
if (error.response) {
|
||||
console.error('状态码:', error.response.status);
|
||||
console.error('错误信息:', error.response.data);
|
||||
} else {
|
||||
console.error('错误信息:', error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
if (require.main === module) {
|
||||
testAPI().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = { testAPI, api };
|
||||
72
simple-mysql-test.js
Normal file
72
simple-mysql-test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const mysql = require('mysql2');
|
||||
|
||||
// 测试环境配置
|
||||
const testConfig = {
|
||||
host: '192.168.0.240',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
password: 'aiot$Aiot123'
|
||||
};
|
||||
|
||||
// 生产环境配置
|
||||
const prodConfig = {
|
||||
host: '129.211.213.226',
|
||||
port: 9527,
|
||||
user: 'root',
|
||||
password: 'aiotAiot123!'
|
||||
};
|
||||
|
||||
function testConnection(config, environment) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n🔗 测试 ${environment} 连接...`);
|
||||
|
||||
const connection = mysql.createConnection(config);
|
||||
|
||||
connection.connect((err) => {
|
||||
if (err) {
|
||||
console.error('❌ 连接失败:', err.message);
|
||||
connection.end();
|
||||
resolve({ success: false, error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 连接成功');
|
||||
|
||||
// 测试简单查询
|
||||
connection.query('SELECT VERSION() as version', (err, results) => {
|
||||
if (err) {
|
||||
console.error('❌ 查询失败:', err.message);
|
||||
} else {
|
||||
console.log('📋 MySQL版本:', results[0].version);
|
||||
}
|
||||
|
||||
connection.end();
|
||||
resolve({ success: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 MySQL连接测试');
|
||||
console.log('='.repeat(40));
|
||||
|
||||
// 测试测试环境
|
||||
const testResult = await testConnection(testConfig, '测试环境');
|
||||
|
||||
console.log('\n' + '='.repeat(40));
|
||||
|
||||
// 测试生产环境
|
||||
const prodResult = await testConnection(prodConfig, '生产环境');
|
||||
|
||||
console.log('\n' + '='.repeat(40));
|
||||
console.log('📋 测试结果:');
|
||||
console.log('测试环境:', testResult.success ? '✅ 成功' : '❌ 失败');
|
||||
console.log('生产环境:', prodResult.success ? '✅ 成功' : '❌ 失败');
|
||||
|
||||
if (testResult.success && prodResult.success) {
|
||||
console.log('\n🎉 两个环境连接都成功!');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
156
test-mysql-connection.js
Normal file
156
test-mysql-connection.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const mysql = require('mysql2');
|
||||
|
||||
// 测试环境数据库配置(先不指定数据库)
|
||||
const testConfig = {
|
||||
host: '192.168.0.240',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
password: 'aiot$Aiot123'
|
||||
};
|
||||
|
||||
// 生产环境数据库配置(先不指定数据库)
|
||||
const prodConfig = {
|
||||
host: '129.211.213.226',
|
||||
port: 9527,
|
||||
user: 'root',
|
||||
password: 'aiotAiot123!'
|
||||
};
|
||||
|
||||
function testConnection(config, environment) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n🔗 正在测试 ${environment} 数据库连接...`);
|
||||
console.log(`主机: ${config.host}`);
|
||||
console.log(`端口: ${config.port}`);
|
||||
|
||||
// 创建连接
|
||||
const connection = mysql.createConnection(config);
|
||||
|
||||
connection.connect((err) => {
|
||||
if (err) {
|
||||
console.error('❌ 数据库连接失败:', err.message);
|
||||
connection.end();
|
||||
return resolve({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
console.log('✅ 数据库连接成功!');
|
||||
|
||||
// 测试查询
|
||||
console.log('📊 执行测试查询...');
|
||||
connection.query('SELECT 1 as test_result', (err, rows) => {
|
||||
if (err) {
|
||||
console.error('❌ 测试查询失败:', err.message);
|
||||
connection.end();
|
||||
return resolve({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
console.log('✅ 测试查询成功:', rows[0]);
|
||||
|
||||
// 获取数据库版本
|
||||
connection.query('SELECT VERSION() as version', (err, versionRows) => {
|
||||
if (err) {
|
||||
console.error('❌ 获取版本失败:', err.message);
|
||||
connection.end();
|
||||
return resolve({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
console.log('📋 MySQL版本:', versionRows[0].version);
|
||||
|
||||
// 检查所有数据库
|
||||
connection.query('SHOW DATABASES', (err, databases) => {
|
||||
if (err) {
|
||||
console.error('❌ 获取数据库列表失败:', err.message);
|
||||
connection.end();
|
||||
return resolve({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
console.log('\n📋 所有数据库:');
|
||||
console.log('='.repeat(40));
|
||||
databases.forEach(db => {
|
||||
console.log(db.Database);
|
||||
});
|
||||
|
||||
// 检查jiebandata数据库是否存在
|
||||
const jiebandataExists = databases.some(db => db.Database === 'jiebandata');
|
||||
console.log(`\n📊 jiebandata数据库: ${jiebandataExists ? '✅ 存在' : '❌ 不存在'}`);
|
||||
|
||||
if (jiebandataExists) {
|
||||
// 获取jiebandata数据库中的表
|
||||
connection.query(`
|
||||
SELECT TABLE_NAME, TABLE_ROWS, CREATE_TIME, UPDATE_TIME
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'jiebandata'
|
||||
ORDER BY TABLE_NAME
|
||||
`, (err, tables) => {
|
||||
if (err) {
|
||||
console.error('❌ 获取表信息失败:', err.message);
|
||||
connection.end();
|
||||
return resolve({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
console.log(`\n📋 jiebandata数据库中的表:`);
|
||||
console.log('='.repeat(80));
|
||||
console.log('表名'.padEnd(25) + '行数'.padEnd(10) + '创建时间'.padEnd(20) + '更新时间');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
if (tables.length === 0) {
|
||||
console.log('暂无表');
|
||||
} else {
|
||||
tables.forEach(table => {
|
||||
console.log(
|
||||
table.TABLE_NAME.padEnd(25) +
|
||||
(table.TABLE_ROWS || '0').toString().padEnd(10) +
|
||||
(table.CREATE_TIME ? new Date(table.CREATE_TIME).toLocaleString() : 'N/A').padEnd(20) +
|
||||
(table.UPDATE_TIME ? new Date(table.UPDATE_TIME).toLocaleString() : 'N/A')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
connection.end();
|
||||
console.log('\n✅ 连接已正常关闭');
|
||||
|
||||
resolve({ success: true, databaseExists: jiebandataExists, tables: tables.length });
|
||||
});
|
||||
} else {
|
||||
// 关闭连接
|
||||
connection.end();
|
||||
console.log('\n✅ 连接已正常关闭');
|
||||
resolve({ success: true, databaseExists: false, tables: 0 });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 开始测试结伴客系统数据库连接');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 测试测试环境
|
||||
const testResult = await testConnection(testConfig, '测试环境');
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
|
||||
// 测试生产环境
|
||||
const prodResult = await testConnection(prodConfig, '生产环境');
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📋 测试结果汇总:');
|
||||
console.log('测试环境:', testResult.success ? '✅ 成功' : '❌ 失败');
|
||||
console.log('生产环境:', prodResult.success ? '✅ 成功' : '❌ 失败');
|
||||
|
||||
if (testResult.success && testResult.tables > 0) {
|
||||
console.log(`测试环境表数量: ${testResult.tables}`);
|
||||
}
|
||||
if (prodResult.success && prodResult.tables > 0) {
|
||||
console.log(`生产环境表数量: ${prodResult.tables}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
main().catch(console.error);
|
||||
|
||||
// 导出测试函数供其他模块使用
|
||||
module.exports = { testConnection, testConfig, prodConfig };
|
||||
8
website/.idea/.gitignore
generated
vendored
Normal file
8
website/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
10
website/.idea/UniappTool.xml
generated
Normal file
10
website/.idea/UniappTool.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="cn.fjdmy.uniapp.UniappProjectDataService">
|
||||
<option name="generalBasePath" value="$PROJECT_DIR$" />
|
||||
<option name="manifestPath" value="$PROJECT_DIR$/manifest.json" />
|
||||
<option name="pagesPath" value="$PROJECT_DIR$/pages.json" />
|
||||
<option name="scanNum" value="1" />
|
||||
<option name="type" value="store" />
|
||||
</component>
|
||||
</project>
|
||||
8
website/.idea/modules.xml
generated
Normal file
8
website/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/website.iml" filepath="$PROJECT_DIR$/.idea/website.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
website/.idea/vcs.xml
generated
Normal file
6
website/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
9
website/.idea/website.iml
generated
Normal file
9
website/.idea/website.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Reference in New Issue
Block a user