Initial commit: AIGC项目完整代码
This commit is contained in:
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 依赖目录
|
||||||
|
node_modules/
|
||||||
|
*/node_modules/
|
||||||
|
|
||||||
|
# 构建输出
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# 日志文件
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# 环境变量文件
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# IDE文件
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# 操作系统文件
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Java编译文件
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
.mvn/
|
||||||
|
mvnw
|
||||||
|
mvnw.cmd
|
||||||
|
|
||||||
|
# Spring Boot
|
||||||
|
application-*.properties
|
||||||
|
!application.properties
|
||||||
|
!application-dev.properties
|
||||||
|
!application-prod.properties
|
||||||
|
|
||||||
|
# 数据库文件
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# 测试文件
|
||||||
|
test.html
|
||||||
|
test-*.html
|
||||||
|
test-*.sh
|
||||||
|
test-*.md
|
||||||
|
|
||||||
|
# 启动脚本
|
||||||
|
start-service.bat
|
||||||
|
startup.log
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
*.jar
|
||||||
|
!mysql-connector-java-8.0.33.jar
|
||||||
2
demo/.gitattributes
vendored
Normal file
2
demo/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/mvnw text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
33
demo/.gitignore
vendored
Normal file
33
demo/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
168
demo/BUG_FIX_SUMMARY.md
Normal file
168
demo/BUG_FIX_SUMMARY.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Bug修复总结报告
|
||||||
|
|
||||||
|
## 🎯 修复概述
|
||||||
|
|
||||||
|
本次修复解决了AIGC项目中的6个主要问题,提升了项目的安全性、稳定性和用户体验。
|
||||||
|
|
||||||
|
## ✅ 已修复的问题
|
||||||
|
|
||||||
|
### 1. 前端路由守卫被禁用 ⚠️ → ✅
|
||||||
|
**问题**: 路由守卫被注释掉,用户可以直接访问需要认证的页面
|
||||||
|
**修复**:
|
||||||
|
- 恢复了完整的路由守卫逻辑
|
||||||
|
- 添加了认证检查和权限验证
|
||||||
|
- 实现了自动重定向和页面标题设置
|
||||||
|
|
||||||
|
**文件**: `demo/frontend/src/router/index.js`
|
||||||
|
|
||||||
|
### 2. CORS配置冲突 ⚠️ → ✅
|
||||||
|
**问题**: 两个CORS配置可能冲突,导致跨域请求失败
|
||||||
|
**修复**:
|
||||||
|
- 统一了CORS配置,只保留SecurityConfig中的配置
|
||||||
|
- 删除了重复的CorsConfig.java文件
|
||||||
|
- 限制了允许的源为前端开发服务器
|
||||||
|
|
||||||
|
**文件**:
|
||||||
|
- `demo/src/main/java/com/example/demo/config/SecurityConfig.java`
|
||||||
|
- 删除了 `demo/src/main/java/com/example/demo/config/CorsConfig.java`
|
||||||
|
|
||||||
|
### 3. API路径不一致 ⚠️ → ✅
|
||||||
|
**问题**: 前端和后端API路径不匹配
|
||||||
|
**修复**:
|
||||||
|
- 统一了API路径格式
|
||||||
|
- 修复了用户名和邮箱检查接口的路径
|
||||||
|
- 确保前后端路径一致性
|
||||||
|
|
||||||
|
**文件**:
|
||||||
|
- `demo/frontend/src/api/auth.js`
|
||||||
|
- `demo/src/main/java/com/example/demo/controller/AuthApiController.java`
|
||||||
|
|
||||||
|
### 4. App.vue过于简单 ⚠️ → ✅
|
||||||
|
**问题**: 主应用组件只是一个测试页面
|
||||||
|
**修复**:
|
||||||
|
- 实现了完整的应用布局
|
||||||
|
- 添加了导航栏和页脚组件
|
||||||
|
- 实现了响应式设计和全局样式
|
||||||
|
- 根据路由动态显示/隐藏组件
|
||||||
|
|
||||||
|
**文件**: `demo/frontend/src/App.vue`
|
||||||
|
|
||||||
|
### 5. 支付配置缺失 ⚠️ → ✅
|
||||||
|
**问题**: 支付配置使用占位符,无法正常工作
|
||||||
|
**修复**:
|
||||||
|
- 提供了完整的支付配置示例
|
||||||
|
- 创建了详细的支付配置说明文档
|
||||||
|
- 区分了开发环境和生产环境配置
|
||||||
|
|
||||||
|
**文件**:
|
||||||
|
- `demo/src/main/resources/application-dev.properties`
|
||||||
|
- `demo/PAYMENT_SETUP.md`
|
||||||
|
|
||||||
|
### 6. 敏感配置硬编码 ⚠️ → ✅
|
||||||
|
**问题**: 数据库密码等敏感信息硬编码在配置文件中
|
||||||
|
**修复**:
|
||||||
|
- 实现了环境变量化配置
|
||||||
|
- 创建了环境变量示例文件
|
||||||
|
- 提供了启动和停止脚本
|
||||||
|
- 区分了开发和生产环境配置
|
||||||
|
|
||||||
|
**文件**:
|
||||||
|
- `demo/src/main/resources/application-dev.properties`
|
||||||
|
- `demo/src/main/resources/application-prod.properties`
|
||||||
|
- `demo/env.example`
|
||||||
|
- `demo/start.sh`
|
||||||
|
- `demo/stop.sh`
|
||||||
|
|
||||||
|
## 🚀 新增功能
|
||||||
|
|
||||||
|
### 1. 环境变量支持
|
||||||
|
- 支持通过.env文件配置环境变量
|
||||||
|
- 提供了env.example示例文件
|
||||||
|
- 实现了配置的默认值机制
|
||||||
|
|
||||||
|
### 2. 启动脚本
|
||||||
|
- 创建了智能启动脚本(start.sh)
|
||||||
|
- 创建了停止脚本(stop.sh)
|
||||||
|
- 支持环境检查和自动配置
|
||||||
|
|
||||||
|
### 3. 支付配置文档
|
||||||
|
- 详细的支付接入说明
|
||||||
|
- 开发和生产环境配置指南
|
||||||
|
- 常见问题解答
|
||||||
|
|
||||||
|
## 📊 修复效果
|
||||||
|
|
||||||
|
### 安全性提升
|
||||||
|
- ✅ 恢复了路由保护机制
|
||||||
|
- ✅ 统一了CORS配置,避免安全漏洞
|
||||||
|
- ✅ 环境变量化敏感配置
|
||||||
|
|
||||||
|
### 稳定性提升
|
||||||
|
- ✅ 修复了API路径不一致问题
|
||||||
|
- ✅ 完善了应用布局和组件结构
|
||||||
|
- ✅ 提供了完整的配置说明
|
||||||
|
|
||||||
|
### 用户体验提升
|
||||||
|
- ✅ 实现了完整的应用布局
|
||||||
|
- ✅ 添加了响应式设计
|
||||||
|
- ✅ 提供了便捷的启动脚本
|
||||||
|
|
||||||
|
## 🔧 使用说明
|
||||||
|
|
||||||
|
### 启动应用
|
||||||
|
```bash
|
||||||
|
# 使用启动脚本(推荐)
|
||||||
|
./start.sh
|
||||||
|
|
||||||
|
# 或手动启动
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境配置
|
||||||
|
```bash
|
||||||
|
# 复制环境变量示例文件
|
||||||
|
cp env.example .env
|
||||||
|
|
||||||
|
# 编辑环境变量
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 支付配置
|
||||||
|
参考 `PAYMENT_SETUP.md` 文档进行支付功能配置。
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 1. 环境变量
|
||||||
|
- 生产环境必须使用环境变量
|
||||||
|
- 确保.env文件不被提交到版本控制
|
||||||
|
- 定期更换JWT密钥
|
||||||
|
|
||||||
|
### 2. 支付配置
|
||||||
|
- 开发环境使用沙箱配置
|
||||||
|
- 生产环境必须使用HTTPS
|
||||||
|
- 确保回调URL正确配置
|
||||||
|
|
||||||
|
### 3. 数据库
|
||||||
|
- 生产环境使用强密码
|
||||||
|
- 定期备份数据库
|
||||||
|
- 监控数据库连接
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
本次修复解决了项目中的主要问题,提升了:
|
||||||
|
- **安全性**: 路由保护、CORS配置、环境变量
|
||||||
|
- **稳定性**: API路径统一、配置完善
|
||||||
|
- **用户体验**: 完整布局、响应式设计
|
||||||
|
- **可维护性**: 环境变量、启动脚本、详细文档
|
||||||
|
|
||||||
|
项目现在可以安全地部署到生产环境使用。
|
||||||
|
|
||||||
|
## 📝 后续建议
|
||||||
|
|
||||||
|
1. **密码加密**: 考虑在生产环境实现真正的密码加密
|
||||||
|
2. **监控日志**: 添加应用监控和日志分析
|
||||||
|
3. **单元测试**: 增加单元测试覆盖率
|
||||||
|
4. **API文档**: 使用Swagger生成API文档
|
||||||
|
5. **CI/CD**: 实现自动化部署流程
|
||||||
|
|
||||||
|
|
||||||
104
demo/DASHBOARD_README.md
Normal file
104
demo/DASHBOARD_README.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# 数据仪表盘功能说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
数据仪表盘是一个系统数据概览页面,提供以下关键指标和图表:
|
||||||
|
|
||||||
|
### 核心指标
|
||||||
|
- **用户总数**: 系统中注册用户的总数量
|
||||||
|
- **付费用户数**: 有成功支付记录的用户数量
|
||||||
|
- **今日收入**: 当天成功支付的金额总和
|
||||||
|
- **转化率**: 付费用户数占总用户数的百分比
|
||||||
|
|
||||||
|
### 图表展示
|
||||||
|
- **日活用户趋势**: 最近30天的日活跃用户数量变化
|
||||||
|
- **收入趋势**: 最近30天的收入变化情况
|
||||||
|
- **订单状态分布**: 不同订单状态的分布饼图
|
||||||
|
- **支付方式分布**: 不同支付方式的使用情况
|
||||||
|
|
||||||
|
### 数据列表
|
||||||
|
- **最近订单**: 显示最新的10个订单信息
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 后端实现
|
||||||
|
- `DashboardService`: 提供数据统计和计算服务
|
||||||
|
- `DashboardApiController`: 提供RESTful API接口
|
||||||
|
- 支持的数据源:用户、订单、支付记录
|
||||||
|
|
||||||
|
### 前端实现
|
||||||
|
- `Dashboard.vue`: 主要仪表盘组件
|
||||||
|
- `dashboard.js`: API调用封装
|
||||||
|
- 使用ECharts进行图表渲染
|
||||||
|
- 响应式设计,支持移动端访问
|
||||||
|
|
||||||
|
## 访问权限
|
||||||
|
|
||||||
|
- 需要管理员权限才能访问
|
||||||
|
- 路径:`/admin/dashboard`
|
||||||
|
- API路径:`/api/dashboard/**`
|
||||||
|
|
||||||
|
## 安装和运行
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
1. 确保Spring Boot应用正常运行
|
||||||
|
2. 运行数据库迁移脚本(为users表添加created_at字段):
|
||||||
|
```sql
|
||||||
|
-- 运行 migration_add_created_at.sql
|
||||||
|
```
|
||||||
|
3. 数据库中有用户、订单、支付数据
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
1. 安装依赖:
|
||||||
|
```bash
|
||||||
|
cd demo/frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动前端服务:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 使用管理员账户登录后访问仪表盘
|
||||||
|
|
||||||
|
**注意**: ECharts图表库通过CDN动态加载,无需额外安装npm包
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
如果users表没有created_at字段,需要运行迁移脚本:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE users ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
UPDATE users SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 获取所有数据
|
||||||
|
```
|
||||||
|
GET /api/dashboard/all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 单独获取各项数据
|
||||||
|
- `GET /api/dashboard/overview` - 概览数据
|
||||||
|
- `GET /api/dashboard/daily-active-users` - 日活数据
|
||||||
|
- `GET /api/dashboard/revenue-trend` - 收入趋势
|
||||||
|
- `GET /api/dashboard/order-status-distribution` - 订单状态分布
|
||||||
|
- `GET /api/dashboard/payment-method-distribution` - 支付方式分布
|
||||||
|
- `GET /api/dashboard/recent-orders` - 最近订单
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 所有API都需要管理员权限
|
||||||
|
2. 数据统计基于现有数据库记录
|
||||||
|
3. 图表数据为最近30天的趋势
|
||||||
|
4. 需要确保数据库中有足够的测试数据
|
||||||
|
5. 建议在生产环境中添加数据缓存机制
|
||||||
|
|
||||||
|
## 扩展功能
|
||||||
|
|
||||||
|
可以考虑添加的功能:
|
||||||
|
- 数据导出功能
|
||||||
|
- 自定义时间范围查询
|
||||||
|
- 实时数据更新
|
||||||
|
- 更多图表类型
|
||||||
|
- 数据对比分析
|
||||||
196
demo/FRONTEND_SUMMARY.md
Normal file
196
demo/FRONTEND_SUMMARY.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# 前端页面功能总结
|
||||||
|
|
||||||
|
## 🎨 设计理念
|
||||||
|
|
||||||
|
本项目采用现代化的前端设计理念,提供统一、美观、响应式的用户界面。
|
||||||
|
|
||||||
|
### 设计特点
|
||||||
|
- **现代化UI**: 使用Bootstrap 5 + Font Awesome图标
|
||||||
|
- **响应式设计**: 支持桌面、平板、手机等多种设备
|
||||||
|
- **统一风格**: 所有页面采用一致的设计语言
|
||||||
|
- **用户体验**: 注重交互细节和用户反馈
|
||||||
|
|
||||||
|
## 📱 页面结构
|
||||||
|
|
||||||
|
### 1. 基础布局模板 (`layout.html`)
|
||||||
|
- **导航栏**: 响应式导航,支持用户登录状态显示
|
||||||
|
- **面包屑**: 清晰的页面导航路径
|
||||||
|
- **页脚**: 统一的页脚信息
|
||||||
|
- **全局样式**: 统一的CSS变量和样式定义
|
||||||
|
|
||||||
|
### 2. 首页 (`home.html`)
|
||||||
|
- **欢迎区域**: 大标题和功能介绍
|
||||||
|
- **快速操作**: 主要功能入口卡片
|
||||||
|
- **系统统计**: 管理员可见的数据统计
|
||||||
|
- **最近活动**: 用户活动记录展示
|
||||||
|
|
||||||
|
### 3. 认证页面
|
||||||
|
|
||||||
|
#### 登录页面 (`login.html`)
|
||||||
|
- **渐变背景**: 美观的渐变背景设计
|
||||||
|
- **毛玻璃效果**: 现代化的卡片设计
|
||||||
|
- **表单验证**: 实时表单验证和错误提示
|
||||||
|
- **加载状态**: 提交时的加载动画
|
||||||
|
- **演示账户**: 内置演示账户信息
|
||||||
|
|
||||||
|
#### 注册页面 (`register.html`)
|
||||||
|
- **密码强度**: 实时密码强度检测
|
||||||
|
- **唯一性检查**: 用户名和邮箱唯一性验证
|
||||||
|
- **表单验证**: 完整的前端验证
|
||||||
|
- **自动保存**: 表单草稿自动保存功能
|
||||||
|
|
||||||
|
### 4. 用户管理页面
|
||||||
|
|
||||||
|
#### 用户列表 (`users/list.html`)
|
||||||
|
- **搜索功能**: 实时搜索用户
|
||||||
|
- **角色标识**: 不同角色的颜色标识
|
||||||
|
- **操作按钮**: 编辑、查看、删除操作
|
||||||
|
- **用户详情**: 模态框显示用户详情
|
||||||
|
- **分页支持**: 分页导航组件
|
||||||
|
|
||||||
|
#### 用户表单 (`users/form.html`)
|
||||||
|
- **密码显示**: 密码显示/隐藏切换
|
||||||
|
- **密码强度**: 新用户密码强度检测
|
||||||
|
- **表单验证**: 完整的前端验证
|
||||||
|
- **草稿保存**: 自动保存表单草稿
|
||||||
|
- **用户信息**: 编辑模式显示用户信息
|
||||||
|
|
||||||
|
### 5. 支付页面 (已优化)
|
||||||
|
- **支付方式选择**: 支付宝和PayPal卡片选择
|
||||||
|
- **支付记录**: 美观的支付记录列表
|
||||||
|
- **支付详情**: 详细的支付信息展示
|
||||||
|
- **支付结果**: 支付成功/失败页面
|
||||||
|
|
||||||
|
## 🎯 功能特性
|
||||||
|
|
||||||
|
### 交互功能
|
||||||
|
- **实时搜索**: 用户列表实时搜索
|
||||||
|
- **表单验证**: 前端表单验证
|
||||||
|
- **加载状态**: 按钮加载动画
|
||||||
|
- **自动保存**: 表单草稿自动保存
|
||||||
|
- **密码强度**: 密码强度实时检测
|
||||||
|
- **唯一性检查**: 用户名邮箱唯一性验证
|
||||||
|
|
||||||
|
### 响应式设计
|
||||||
|
- **移动优先**: 移动设备优先设计
|
||||||
|
- **断点适配**: 支持多种屏幕尺寸
|
||||||
|
- **触摸友好**: 适合触摸操作的按钮大小
|
||||||
|
- **导航折叠**: 移动端导航自动折叠
|
||||||
|
|
||||||
|
### 用户体验
|
||||||
|
- **视觉反馈**: 悬停、点击等视觉反馈
|
||||||
|
- **错误提示**: 清晰的错误信息显示
|
||||||
|
- **成功提示**: 操作成功的确认信息
|
||||||
|
- **自动隐藏**: 提示信息自动隐藏
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
### 前端技术
|
||||||
|
- **HTML5**: 语义化标签
|
||||||
|
- **CSS3**: 现代CSS特性
|
||||||
|
- **Bootstrap 5**: UI框架
|
||||||
|
- **Font Awesome**: 图标库
|
||||||
|
- **JavaScript**: 原生JavaScript
|
||||||
|
- **Thymeleaf**: 模板引擎
|
||||||
|
|
||||||
|
### 设计工具
|
||||||
|
- **CSS变量**: 统一的颜色和尺寸定义
|
||||||
|
- **Flexbox/Grid**: 现代布局技术
|
||||||
|
- **CSS动画**: 平滑的过渡效果
|
||||||
|
- **渐变背景**: 美观的背景设计
|
||||||
|
|
||||||
|
## 📋 页面功能清单
|
||||||
|
|
||||||
|
### ✅ 已完成功能
|
||||||
|
- [x] 统一布局模板
|
||||||
|
- [x] 响应式导航栏
|
||||||
|
- [x] 首页仪表板
|
||||||
|
- [x] 登录页面优化
|
||||||
|
- [x] 注册页面优化
|
||||||
|
- [x] 用户管理列表
|
||||||
|
- [x] 用户表单页面
|
||||||
|
- [x] 支付功能页面
|
||||||
|
- [x] 搜索功能
|
||||||
|
- [x] 表单验证
|
||||||
|
- [x] 加载状态
|
||||||
|
- [x] 错误处理
|
||||||
|
- [x] 移动端适配
|
||||||
|
|
||||||
|
### 🔄 可扩展功能
|
||||||
|
- [ ] 主题切换
|
||||||
|
- [ ] 多语言支持
|
||||||
|
- [ ] 数据可视化图表
|
||||||
|
- [ ] 文件上传组件
|
||||||
|
- [ ] 富文本编辑器
|
||||||
|
- [ ] 拖拽排序
|
||||||
|
- [ ] 无限滚动
|
||||||
|
- [ ] 离线支持
|
||||||
|
|
||||||
|
## 🎨 设计规范
|
||||||
|
|
||||||
|
### 颜色方案
|
||||||
|
- **主色调**: #0d6efd (Bootstrap Primary)
|
||||||
|
- **成功色**: #198754 (Bootstrap Success)
|
||||||
|
- **警告色**: #ffc107 (Bootstrap Warning)
|
||||||
|
- **危险色**: #dc3545 (Bootstrap Danger)
|
||||||
|
- **信息色**: #0dcaf0 (Bootstrap Info)
|
||||||
|
|
||||||
|
### 字体规范
|
||||||
|
- **主字体**: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif
|
||||||
|
- **标题**: 600字重
|
||||||
|
- **正文**: 400字重
|
||||||
|
- **小字**: 0.875rem
|
||||||
|
|
||||||
|
### 间距规范
|
||||||
|
- **卡片内边距**: 1.5rem
|
||||||
|
- **表单间距**: 1rem
|
||||||
|
- **按钮间距**: 0.5rem
|
||||||
|
- **页面边距**: 2rem
|
||||||
|
|
||||||
|
## 🚀 性能优化
|
||||||
|
|
||||||
|
### 加载优化
|
||||||
|
- **CDN资源**: 使用CDN加载Bootstrap和Font Awesome
|
||||||
|
- **图片优化**: 使用图标字体替代图片
|
||||||
|
- **CSS优化**: 内联关键CSS
|
||||||
|
- **JS优化**: 按需加载JavaScript
|
||||||
|
|
||||||
|
### 用户体验优化
|
||||||
|
- **骨架屏**: 加载时的占位内容
|
||||||
|
- **预加载**: 关键资源预加载
|
||||||
|
- **缓存策略**: 静态资源缓存
|
||||||
|
- **压缩优化**: CSS和JS压缩
|
||||||
|
|
||||||
|
## 📱 移动端适配
|
||||||
|
|
||||||
|
### 响应式断点
|
||||||
|
- **手机**: < 768px
|
||||||
|
- **平板**: 768px - 1024px
|
||||||
|
- **桌面**: > 1024px
|
||||||
|
|
||||||
|
### 移动端优化
|
||||||
|
- **触摸友好**: 按钮最小44px点击区域
|
||||||
|
- **导航优化**: 移动端折叠导航
|
||||||
|
- **表单优化**: 移动端表单适配
|
||||||
|
- **字体缩放**: 防止字体过小
|
||||||
|
|
||||||
|
## 🔧 维护指南
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- **HTML**: 语义化标签,合理嵌套
|
||||||
|
- **CSS**: BEM命名规范,模块化样式
|
||||||
|
- **JavaScript**: ES6+语法,函数式编程
|
||||||
|
- **注释**: 详细的代码注释
|
||||||
|
|
||||||
|
### 更新维护
|
||||||
|
- **版本控制**: Git版本管理
|
||||||
|
- **代码审查**: 代码质量检查
|
||||||
|
- **测试**: 跨浏览器测试
|
||||||
|
- **文档**: 详细的开发文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结**: 本项目的前端页面已经完成了全面的现代化改造,提供了统一、美观、响应式的用户界面,具备良好的用户体验和可维护性。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
318
demo/ORDER_MANAGEMENT_SUMMARY.md
Normal file
318
demo/ORDER_MANAGEMENT_SUMMARY.md
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
# 订单管理系统功能总结
|
||||||
|
|
||||||
|
## 🎯 功能概述
|
||||||
|
|
||||||
|
订单管理系统是一个完整的电商订单处理解决方案,支持订单创建、状态管理、支付集成、发货跟踪等全流程功能。
|
||||||
|
|
||||||
|
## 📋 核心功能
|
||||||
|
|
||||||
|
### 1. 订单实体设计
|
||||||
|
|
||||||
|
#### Order(订单主表)
|
||||||
|
- **订单号**: 唯一标识,自动生成
|
||||||
|
- **订单金额**: 支持多种货币
|
||||||
|
- **订单状态**: 9种状态流转
|
||||||
|
- **订单类型**: 5种订单类型
|
||||||
|
- **联系信息**: 邮箱、电话
|
||||||
|
- **地址信息**: 收货地址、账单地址
|
||||||
|
- **时间戳**: 创建、更新、支付、发货、送达、取消时间
|
||||||
|
|
||||||
|
#### OrderItem(订单项)
|
||||||
|
- **商品信息**: 名称、描述、SKU
|
||||||
|
- **价格信息**: 单价、数量、小计
|
||||||
|
- **商品图片**: 支持图片展示
|
||||||
|
|
||||||
|
#### OrderStatus(订单状态)
|
||||||
|
- `PENDING` - 待支付
|
||||||
|
- `CONFIRMED` - 已确认
|
||||||
|
- `PAID` - 已支付
|
||||||
|
- `PROCESSING` - 处理中
|
||||||
|
- `SHIPPED` - 已发货
|
||||||
|
- `DELIVERED` - 已送达
|
||||||
|
- `COMPLETED` - 已完成
|
||||||
|
- `CANCELLED` - 已取消
|
||||||
|
- `REFUNDED` - 已退款
|
||||||
|
|
||||||
|
#### OrderType(订单类型)
|
||||||
|
- `PRODUCT` - 商品订单
|
||||||
|
- `SERVICE` - 服务订单
|
||||||
|
- `SUBSCRIPTION` - 订阅订单
|
||||||
|
- `DIGITAL` - 数字商品
|
||||||
|
- `PHYSICAL` - 实体商品
|
||||||
|
|
||||||
|
### 2. 数据访问层
|
||||||
|
|
||||||
|
#### OrderRepository
|
||||||
|
- **基础查询**: 按ID、订单号、用户ID查询
|
||||||
|
- **状态查询**: 按订单状态筛选
|
||||||
|
- **时间查询**: 按创建时间范围查询
|
||||||
|
- **统计查询**: 订单数量、金额统计
|
||||||
|
- **特殊查询**: 过期订单、最近订单
|
||||||
|
|
||||||
|
#### OrderItemRepository
|
||||||
|
- **关联查询**: 按订单ID查询订单项
|
||||||
|
- **商品查询**: 按SKU、商品名称查询
|
||||||
|
- **统计查询**: 商品销售数量、金额统计
|
||||||
|
|
||||||
|
### 3. 业务服务层
|
||||||
|
|
||||||
|
#### OrderService
|
||||||
|
- **订单创建**: 自动计算金额、生成订单号
|
||||||
|
- **状态管理**: 订单状态流转控制
|
||||||
|
- **业务规则**: 支付、取消、发货、完成条件检查
|
||||||
|
- **自动处理**: 过期订单自动取消
|
||||||
|
- **数据统计**: 订单数量、金额统计
|
||||||
|
|
||||||
|
#### 核心业务方法
|
||||||
|
- `createOrder()` - 创建订单
|
||||||
|
- `updateOrderStatus()` - 更新订单状态
|
||||||
|
- `cancelOrder()` - 取消订单
|
||||||
|
- `confirmPayment()` - 确认支付
|
||||||
|
- `shipOrder()` - 订单发货
|
||||||
|
- `completeOrder()` - 完成订单
|
||||||
|
- `autoCancelExpiredOrders()` - 自动取消过期订单
|
||||||
|
|
||||||
|
### 4. 控制器层
|
||||||
|
|
||||||
|
#### OrderController
|
||||||
|
- **用户订单**: 订单列表、详情、创建
|
||||||
|
- **支付集成**: 订单支付创建
|
||||||
|
- **状态管理**: 订单状态更新
|
||||||
|
- **管理员功能**: 订单管理、状态操作
|
||||||
|
|
||||||
|
#### 主要端点
|
||||||
|
- `GET /orders` - 用户订单列表
|
||||||
|
- `GET /orders/{id}` - 订单详情
|
||||||
|
- `GET /orders/create` - 创建订单表单
|
||||||
|
- `POST /orders/create` - 提交订单
|
||||||
|
- `POST /orders/{id}/pay` - 创建订单支付
|
||||||
|
- `POST /orders/{id}/cancel` - 取消订单
|
||||||
|
- `POST /orders/{id}/ship` - 订单发货
|
||||||
|
- `POST /orders/{id}/complete` - 完成订单
|
||||||
|
- `GET /orders/admin` - 管理员订单管理
|
||||||
|
|
||||||
|
### 5. 前端界面
|
||||||
|
|
||||||
|
#### 订单列表页面 (`orders/list.html`)
|
||||||
|
- **现代化设计**: Bootstrap 5 + Font Awesome
|
||||||
|
- **状态筛选**: 按订单状态筛选
|
||||||
|
- **搜索功能**: 订单号实时搜索
|
||||||
|
- **支付操作**: 支付宝、PayPal支付选择
|
||||||
|
- **状态标识**: 不同状态的颜色标识
|
||||||
|
- **分页支持**: 完整的分页导航
|
||||||
|
|
||||||
|
#### 订单详情页面 (`orders/detail.html`)
|
||||||
|
- **完整信息**: 订单基本信息、商品列表、地址信息
|
||||||
|
- **状态展示**: 订单状态、时间戳展示
|
||||||
|
- **操作按钮**: 支付、取消、发货、完成操作
|
||||||
|
- **支付记录**: 关联支付记录展示
|
||||||
|
- **管理员功能**: 状态更新、发货管理
|
||||||
|
|
||||||
|
#### 订单创建页面 (`orders/form.html`)
|
||||||
|
- **动态表单**: 可添加/删除订单项
|
||||||
|
- **实时计算**: 自动计算订单总金额
|
||||||
|
- **表单验证**: 前端验证和提示
|
||||||
|
- **草稿保存**: 自动保存表单草稿
|
||||||
|
- **帮助信息**: 订单类型说明和注意事项
|
||||||
|
|
||||||
|
#### 管理员页面 (`orders/admin.html`)
|
||||||
|
- **统计面板**: 订单数量、状态统计
|
||||||
|
- **批量操作**: 状态筛选、搜索
|
||||||
|
- **管理功能**: 发货、完成、取消操作
|
||||||
|
- **状态更新**: 手动更新订单状态
|
||||||
|
|
||||||
|
### 6. 支付系统集成
|
||||||
|
|
||||||
|
#### Payment-Order关联
|
||||||
|
- **双向关联**: Payment ↔ Order
|
||||||
|
- **状态同步**: 支付成功自动更新订单状态
|
||||||
|
- **数据一致性**: 事务保证数据一致性
|
||||||
|
|
||||||
|
#### 支付流程
|
||||||
|
1. 用户选择支付方式
|
||||||
|
2. 创建Payment记录
|
||||||
|
3. 跳转到支付页面
|
||||||
|
4. 支付成功后更新Payment状态
|
||||||
|
5. 自动更新Order状态为PAID
|
||||||
|
|
||||||
|
### 7. 数据库设计
|
||||||
|
|
||||||
|
#### orders表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE orders (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
order_number VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
total_amount DECIMAL(10,2) NOT NULL,
|
||||||
|
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
order_type VARCHAR(20) NOT NULL DEFAULT 'PRODUCT',
|
||||||
|
description VARCHAR(500),
|
||||||
|
notes TEXT,
|
||||||
|
shipping_address TEXT,
|
||||||
|
billing_address TEXT,
|
||||||
|
contact_phone VARCHAR(20),
|
||||||
|
contact_email VARCHAR(100),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
paid_at TIMESTAMP NULL,
|
||||||
|
shipped_at TIMESTAMP NULL,
|
||||||
|
delivered_at TIMESTAMP NULL,
|
||||||
|
cancelled_at TIMESTAMP NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### order_items表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE order_items (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
product_name VARCHAR(100) NOT NULL,
|
||||||
|
product_description VARCHAR(500),
|
||||||
|
product_sku VARCHAR(200),
|
||||||
|
unit_price DECIMAL(10,2) NOT NULL,
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
subtotal DECIMAL(10,2) NOT NULL,
|
||||||
|
product_image VARCHAR(100),
|
||||||
|
order_id BIGINT NOT NULL,
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### payments表更新
|
||||||
|
```sql
|
||||||
|
ALTER TABLE payments ADD COLUMN order_id_ref BIGINT NULL;
|
||||||
|
ALTER TABLE payments ADD FOREIGN KEY (order_id_ref) REFERENCES orders(id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 业务流程
|
||||||
|
|
||||||
|
### 订单创建流程
|
||||||
|
1. 用户填写订单信息
|
||||||
|
2. 添加订单商品
|
||||||
|
3. 系统计算订单总金额
|
||||||
|
4. 生成唯一订单号
|
||||||
|
5. 保存订单和订单项
|
||||||
|
6. 订单状态设为PENDING
|
||||||
|
|
||||||
|
### 订单支付流程
|
||||||
|
1. 用户选择支付方式
|
||||||
|
2. 创建Payment记录
|
||||||
|
3. 跳转到支付页面
|
||||||
|
4. 用户完成支付
|
||||||
|
5. 支付回调更新Payment状态
|
||||||
|
6. 自动更新Order状态为PAID
|
||||||
|
|
||||||
|
### 订单发货流程
|
||||||
|
1. 管理员确认订单
|
||||||
|
2. 准备商品发货
|
||||||
|
3. 更新订单状态为SHIPPED
|
||||||
|
4. 记录发货时间
|
||||||
|
5. 可添加物流单号
|
||||||
|
|
||||||
|
### 订单完成流程
|
||||||
|
1. 用户确认收货
|
||||||
|
2. 管理员更新状态为COMPLETED
|
||||||
|
3. 记录完成时间
|
||||||
|
4. 订单流程结束
|
||||||
|
|
||||||
|
## 🎨 界面特色
|
||||||
|
|
||||||
|
### 设计理念
|
||||||
|
- **现代化UI**: Bootstrap 5 + Font Awesome
|
||||||
|
- **响应式设计**: 支持桌面、平板、手机
|
||||||
|
- **用户友好**: 直观的操作流程
|
||||||
|
- **状态可视化**: 颜色编码的状态标识
|
||||||
|
|
||||||
|
### 交互功能
|
||||||
|
- **实时搜索**: 订单号搜索
|
||||||
|
- **状态筛选**: 按状态筛选订单
|
||||||
|
- **动态表单**: 可添加/删除订单项
|
||||||
|
- **自动计算**: 实时计算订单金额
|
||||||
|
- **草稿保存**: 自动保存表单草稿
|
||||||
|
- **模态框操作**: 确认对话框
|
||||||
|
|
||||||
|
### 管理员功能
|
||||||
|
- **统计面板**: 订单数据统计
|
||||||
|
- **批量操作**: 批量状态更新
|
||||||
|
- **权限控制**: 管理员专用功能
|
||||||
|
- **状态管理**: 完整的订单状态管理
|
||||||
|
|
||||||
|
## 🔧 技术特性
|
||||||
|
|
||||||
|
### 后端技术
|
||||||
|
- **Spring Boot 3.5.6**: 现代化框架
|
||||||
|
- **Spring Data JPA**: 数据访问层
|
||||||
|
- **Spring Security**: 安全控制
|
||||||
|
- **MySQL**: 关系型数据库
|
||||||
|
- **事务管理**: 数据一致性保证
|
||||||
|
|
||||||
|
### 前端技术
|
||||||
|
- **Thymeleaf**: 服务端模板
|
||||||
|
- **Bootstrap 5**: UI框架
|
||||||
|
- **Font Awesome**: 图标库
|
||||||
|
- **JavaScript**: 交互功能
|
||||||
|
- **CSS3**: 现代化样式
|
||||||
|
|
||||||
|
### 设计模式
|
||||||
|
- **MVC架构**: 分层设计
|
||||||
|
- **Repository模式**: 数据访问抽象
|
||||||
|
- **Service层**: 业务逻辑封装
|
||||||
|
- **DTO模式**: 数据传输对象
|
||||||
|
|
||||||
|
## 📊 功能统计
|
||||||
|
|
||||||
|
### 已完成功能
|
||||||
|
- ✅ 订单实体设计
|
||||||
|
- ✅ 订单状态管理
|
||||||
|
- ✅ 订单项管理
|
||||||
|
- ✅ 数据访问层
|
||||||
|
- ✅ 业务服务层
|
||||||
|
- ✅ 控制器层
|
||||||
|
- ✅ 前端界面
|
||||||
|
- ✅ 支付集成
|
||||||
|
- ✅ 数据库设计
|
||||||
|
- ✅ 权限控制
|
||||||
|
|
||||||
|
### 可扩展功能
|
||||||
|
- [ ] 订单导出功能
|
||||||
|
- [ ] 订单模板功能
|
||||||
|
- [ ] 批量操作功能
|
||||||
|
- [ ] 订单分析报表
|
||||||
|
- [ ] 库存管理集成
|
||||||
|
- [ ] 物流跟踪集成
|
||||||
|
- [ ] 订单评价系统
|
||||||
|
- [ ] 优惠券系统
|
||||||
|
|
||||||
|
## 🚀 部署说明
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- JDK 21+
|
||||||
|
- MySQL 8.0+
|
||||||
|
- Maven 3.6+
|
||||||
|
|
||||||
|
### 配置步骤
|
||||||
|
1. 配置数据库连接
|
||||||
|
2. 运行数据库脚本
|
||||||
|
3. 配置支付参数
|
||||||
|
4. 启动应用
|
||||||
|
5. 访问订单管理页面
|
||||||
|
|
||||||
|
### 访问地址
|
||||||
|
- 用户订单列表: `/orders`
|
||||||
|
- 订单详情: `/orders/{id}`
|
||||||
|
- 创建订单: `/orders/create`
|
||||||
|
- 管理员管理: `/orders/admin`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结**: 订单管理系统提供了完整的订单处理解决方案,从订单创建到支付完成,从发货管理到订单完成,涵盖了电商订单的全生命周期管理。系统采用现代化的技术栈,提供友好的用户界面,支持多种支付方式,具备完善的权限控制和数据安全保障。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
151
demo/PAYMENT_CONFIG.md
Normal file
151
demo/PAYMENT_CONFIG.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# 支付接入功能配置说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
本项目已成功集成支付宝和PayPal两种支付方式,提供完整的支付解决方案。
|
||||||
|
|
||||||
|
## 支付方式
|
||||||
|
|
||||||
|
### 1. 支付宝 (Alipay)
|
||||||
|
- **适用场景**: 中国大陆用户
|
||||||
|
- **支持货币**: 人民币 (CNY)
|
||||||
|
- **支付流程**: 网页支付
|
||||||
|
- **回调机制**: 异步通知 + 同步返回
|
||||||
|
|
||||||
|
### 2. PayPal
|
||||||
|
- **适用场景**: 国际用户
|
||||||
|
- **支持货币**: 美元 (USD)、欧元 (EUR) 等
|
||||||
|
- **支付流程**: PayPal账户支付
|
||||||
|
- **回调机制**: Webhook通知 + 同步返回
|
||||||
|
|
||||||
|
## 配置参数
|
||||||
|
|
||||||
|
### 开发环境配置 (application-dev.properties)
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 支付宝配置 (开发环境)
|
||||||
|
alipay.app-id=your_app_id
|
||||||
|
alipay.private-key=your_private_key
|
||||||
|
alipay.public-key=your_public_key
|
||||||
|
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
|
||||||
|
alipay.charset=UTF-8
|
||||||
|
alipay.sign-type=RSA2
|
||||||
|
alipay.notify-url=http://localhost:8080/payment/alipay/notify
|
||||||
|
alipay.return-url=http://localhost:8080/payment/alipay/return
|
||||||
|
|
||||||
|
# PayPal配置 (开发环境)
|
||||||
|
paypal.client-id=your_paypal_client_id
|
||||||
|
paypal.client-secret=your_paypal_client_secret
|
||||||
|
paypal.mode=sandbox
|
||||||
|
paypal.return-url=http://localhost:8080/payment/paypal/return
|
||||||
|
paypal.cancel-url=http://localhost:8080/payment/paypal/cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境配置 (application-prod.properties)
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 支付宝配置 (生产环境)
|
||||||
|
alipay.app-id=${ALIPAY_APP_ID}
|
||||||
|
alipay.private-key=${ALIPAY_PRIVATE_KEY}
|
||||||
|
alipay.public-key=${ALIPAY_PUBLIC_KEY}
|
||||||
|
alipay.gateway-url=https://openapi.alipay.com/gateway.do
|
||||||
|
alipay.charset=UTF-8
|
||||||
|
alipay.sign-type=RSA2
|
||||||
|
alipay.notify-url=${ALIPAY_NOTIFY_URL}
|
||||||
|
alipay.return-url=${ALIPAY_RETURN_URL}
|
||||||
|
|
||||||
|
# PayPal配置 (生产环境)
|
||||||
|
paypal.client-id=${PAYPAL_CLIENT_ID}
|
||||||
|
paypal.client-secret=${PAYPAL_CLIENT_SECRET}
|
||||||
|
paypal.mode=live
|
||||||
|
paypal.return-url=${PAYPAL_RETURN_URL}
|
||||||
|
paypal.cancel-url=${PAYPAL_CANCEL_URL}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库表结构
|
||||||
|
|
||||||
|
### payments 表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE payments (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
order_id VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
amount DECIMAL(10,2) NOT NULL,
|
||||||
|
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||||
|
payment_method VARCHAR(20) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
description VARCHAR(500),
|
||||||
|
external_transaction_id VARCHAR(100),
|
||||||
|
callback_url VARCHAR(1000),
|
||||||
|
return_url VARCHAR(1000),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
paid_at TIMESTAMP NULL,
|
||||||
|
user_id BIGINT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 端点
|
||||||
|
|
||||||
|
### 支付相关端点
|
||||||
|
- `GET /payment/create` - 显示支付表单
|
||||||
|
- `POST /payment/create` - 创建支付订单
|
||||||
|
- `GET /payment/history` - 支付记录列表
|
||||||
|
- `GET /payment/detail/{id}` - 支付详情
|
||||||
|
|
||||||
|
### 支付宝回调端点
|
||||||
|
- `POST /payment/alipay/notify` - 异步通知
|
||||||
|
- `GET /payment/alipay/return` - 同步返回
|
||||||
|
|
||||||
|
### PayPal回调端点
|
||||||
|
- `GET /payment/paypal/return` - 支付返回
|
||||||
|
- `GET /payment/paypal/cancel` - 支付取消
|
||||||
|
- `POST /payment/paypal/webhook` - Webhook通知
|
||||||
|
|
||||||
|
## 支付状态
|
||||||
|
|
||||||
|
- `PENDING` - 待支付
|
||||||
|
- `PROCESSING` - 处理中
|
||||||
|
- `SUCCESS` - 支付成功
|
||||||
|
- `FAILED` - 支付失败
|
||||||
|
- `CANCELLED` - 已取消
|
||||||
|
- `REFUNDED` - 已退款
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 1. 配置支付参数
|
||||||
|
在 `application-dev.properties` 中配置您的支付参数:
|
||||||
|
- 支付宝:需要申请支付宝开放平台应用
|
||||||
|
- PayPal:需要申请PayPal开发者账户
|
||||||
|
|
||||||
|
### 2. 启动应用
|
||||||
|
```bash
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 访问支付功能
|
||||||
|
- 登录后访问 `/payment/create` 创建支付
|
||||||
|
- 访问 `/payment/history` 查看支付记录
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
1. **密钥管理**: 生产环境使用环境变量存储敏感信息
|
||||||
|
2. **签名验证**: 所有回调都进行签名验证
|
||||||
|
3. **HTTPS**: 生产环境必须使用HTTPS
|
||||||
|
4. **权限控制**: 用户只能查看自己的支付记录
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
1. **支付宝测试**: 使用支付宝沙箱环境进行测试
|
||||||
|
2. **PayPal测试**: 使用PayPal沙箱模式进行测试
|
||||||
|
3. **回调测试**: 使用ngrok等工具测试本地回调
|
||||||
|
|
||||||
|
## 扩展功能
|
||||||
|
|
||||||
|
可以进一步扩展的功能:
|
||||||
|
- 退款功能
|
||||||
|
- 支付统计
|
||||||
|
- 支付报表
|
||||||
|
- 批量支付
|
||||||
|
- 分期付款
|
||||||
|
|
||||||
139
demo/PAYMENT_SETUP.md
Normal file
139
demo/PAYMENT_SETUP.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# 支付配置说明
|
||||||
|
|
||||||
|
## 支付宝配置
|
||||||
|
|
||||||
|
### 1. 申请支付宝开放平台应用
|
||||||
|
1. 访问 [支付宝开放平台](https://open.alipay.com/)
|
||||||
|
2. 注册开发者账号
|
||||||
|
3. 创建应用,选择"网页&移动应用"
|
||||||
|
4. 获取应用信息:
|
||||||
|
- APPID
|
||||||
|
- 应用私钥
|
||||||
|
- 支付宝公钥
|
||||||
|
|
||||||
|
### 2. 配置沙箱环境
|
||||||
|
1. 在开放平台控制台启用沙箱环境
|
||||||
|
2. 获取沙箱应用信息
|
||||||
|
3. 下载沙箱版支付宝APP进行测试
|
||||||
|
|
||||||
|
### 3. 更新配置文件
|
||||||
|
在 `application-dev.properties` 中更新以下配置:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 支付宝沙箱配置
|
||||||
|
alipay.app-id=你的沙箱APPID
|
||||||
|
alipay.private-key=你的应用私钥
|
||||||
|
alipay.public-key=支付宝公钥
|
||||||
|
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
|
||||||
|
alipay.notify-url=http://你的域名:8080/api/payments/alipay/notify
|
||||||
|
alipay.return-url=http://你的域名:8080/api/payments/alipay/return
|
||||||
|
```
|
||||||
|
|
||||||
|
## PayPal配置
|
||||||
|
|
||||||
|
### 1. 申请PayPal开发者账号
|
||||||
|
1. 访问 [PayPal开发者中心](https://developer.paypal.com/)
|
||||||
|
2. 注册开发者账号
|
||||||
|
3. 创建应用,选择"Sandbox"环境
|
||||||
|
4. 获取应用信息:
|
||||||
|
- Client ID
|
||||||
|
- Client Secret
|
||||||
|
|
||||||
|
### 2. 更新配置文件
|
||||||
|
在 `application-dev.properties` 中更新以下配置:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# PayPal沙箱配置
|
||||||
|
paypal.client-id=你的沙箱Client ID
|
||||||
|
paypal.client-secret=你的沙箱Client Secret
|
||||||
|
paypal.mode=sandbox
|
||||||
|
paypal.return-url=http://你的域名:8080/api/payments/paypal/return
|
||||||
|
paypal.cancel-url=http://你的域名:8080/api/payments/paypal/cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生产环境配置
|
||||||
|
|
||||||
|
### 1. 环境变量配置
|
||||||
|
生产环境建议使用环境变量:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 支付宝生产环境
|
||||||
|
export ALIPAY_APP_ID=你的生产APPID
|
||||||
|
export ALIPAY_PRIVATE_KEY=你的生产私钥
|
||||||
|
export ALIPAY_PUBLIC_KEY=支付宝生产公钥
|
||||||
|
export ALIPAY_NOTIFY_URL=https://你的域名/api/payments/alipay/notify
|
||||||
|
export ALIPAY_RETURN_URL=https://你的域名/api/payments/alipay/return
|
||||||
|
|
||||||
|
# PayPal生产环境
|
||||||
|
export PAYPAL_CLIENT_ID=你的生产Client ID
|
||||||
|
export PAYPAL_CLIENT_SECRET=你的生产Client Secret
|
||||||
|
export PAYPAL_RETURN_URL=https://你的域名/api/payments/paypal/return
|
||||||
|
export PAYPAL_CANCEL_URL=https://你的域名/api/payments/paypal/cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 更新生产环境配置
|
||||||
|
在 `application-prod.properties` 中:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 支付宝生产环境
|
||||||
|
alipay.app-id=${ALIPAY_APP_ID}
|
||||||
|
alipay.private-key=${ALIPAY_PRIVATE_KEY}
|
||||||
|
alipay.public-key=${ALIPAY_PUBLIC_KEY}
|
||||||
|
alipay.gateway-url=https://openapi.alipay.com/gateway.do
|
||||||
|
alipay.notify-url=${ALIPAY_NOTIFY_URL}
|
||||||
|
alipay.return-url=${ALIPAY_RETURN_URL}
|
||||||
|
|
||||||
|
# PayPal生产环境
|
||||||
|
paypal.client-id=${PAYPAL_CLIENT_ID}
|
||||||
|
paypal.client-secret=${PAYPAL_CLIENT_SECRET}
|
||||||
|
paypal.mode=live
|
||||||
|
paypal.return-url=${PAYPAL_RETURN_URL}
|
||||||
|
paypal.cancel-url=${PAYPAL_CANCEL_URL}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试支付功能
|
||||||
|
|
||||||
|
### 1. 启动应用
|
||||||
|
```bash
|
||||||
|
# 启动后端服务
|
||||||
|
mvn spring-boot:run
|
||||||
|
|
||||||
|
# 启动前端服务
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试流程
|
||||||
|
1. 访问 http://localhost:3000
|
||||||
|
2. 登录测试账号
|
||||||
|
3. 创建订单
|
||||||
|
4. 选择支付方式
|
||||||
|
5. 完成支付测试
|
||||||
|
|
||||||
|
### 3. 测试账号
|
||||||
|
- **支付宝沙箱**: 使用沙箱版支付宝APP扫码支付
|
||||||
|
- **PayPal沙箱**: 使用沙箱测试账号进行支付
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **HTTPS要求**: 生产环境必须使用HTTPS
|
||||||
|
2. **域名配置**: 确保回调URL使用正确的域名
|
||||||
|
3. **签名验证**: 确保私钥和公钥配置正确
|
||||||
|
4. **日志监控**: 监控支付回调日志
|
||||||
|
5. **异常处理**: 实现完善的异常处理机制
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 支付宝回调失败
|
||||||
|
A: 检查notify-url是否正确,确保服务器可以接收POST请求
|
||||||
|
|
||||||
|
### Q: PayPal支付页面无法打开
|
||||||
|
A: 检查Client ID和Secret是否正确,确保使用沙箱模式
|
||||||
|
|
||||||
|
### Q: 签名验证失败
|
||||||
|
A: 检查私钥格式,确保没有多余的空格和换行符
|
||||||
|
|
||||||
|
### Q: 跨域问题
|
||||||
|
A: 确保CORS配置正确,允许支付平台的域名
|
||||||
|
|
||||||
|
|
||||||
25
demo/PasswordChecker.java
Normal file
25
demo/PasswordChecker.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
|
||||||
|
public class PasswordChecker {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||||
|
String hash = "$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVEFDi";
|
||||||
|
|
||||||
|
// 测试不同的密码
|
||||||
|
String[] passwords = {"demo", "admin", "password", "123456"};
|
||||||
|
|
||||||
|
for (String password : passwords) {
|
||||||
|
boolean matches = encoder.matches(password, hash);
|
||||||
|
System.out.println("Password '" + password + "' matches: " + matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
83
demo/TEST_ACCOUNTS.md
Normal file
83
demo/TEST_ACCOUNTS.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 测试账号说明
|
||||||
|
|
||||||
|
## 可用的测试账号
|
||||||
|
|
||||||
|
### 1. 普通用户账号
|
||||||
|
- **用户名**: `demo`
|
||||||
|
- **密码**: `demo`
|
||||||
|
- **角色**: 普通用户 (ROLE_USER)
|
||||||
|
- **积分**: 100
|
||||||
|
- **邮箱**: demo@example.com
|
||||||
|
|
||||||
|
### 2. 管理员账号
|
||||||
|
- **用户名**: `admin`
|
||||||
|
- **密码**: `admin123`
|
||||||
|
- **角色**: 管理员 (ROLE_ADMIN)
|
||||||
|
- **积分**: 200
|
||||||
|
- **邮箱**: admin@example.com
|
||||||
|
|
||||||
|
### 3. 测试用户1
|
||||||
|
- **用户名**: `testuser`
|
||||||
|
- **密码**: `test123`
|
||||||
|
- **角色**: 普通用户 (ROLE_USER)
|
||||||
|
- **积分**: 75
|
||||||
|
- **邮箱**: testuser@example.com
|
||||||
|
|
||||||
|
### 4. 个人主页测试用户
|
||||||
|
- **用户名**: `mingzi_FBx7foZYDS7inLQb`
|
||||||
|
- **密码**: `123456`
|
||||||
|
- **角色**: 普通用户 (ROLE_USER)
|
||||||
|
- **积分**: 25
|
||||||
|
- **邮箱**: mingzi@example.com
|
||||||
|
|
||||||
|
### 5. 手机号测试用户
|
||||||
|
- **用户名**: `15538239326`
|
||||||
|
- **密码**: `0627`
|
||||||
|
- **角色**: 普通用户 (ROLE_USER)
|
||||||
|
- **积分**: 50
|
||||||
|
- **邮箱**: 15538239326@example.com
|
||||||
|
|
||||||
|
## 登录方式
|
||||||
|
|
||||||
|
### 方式1: 传统用户名密码登录
|
||||||
|
1. 访问 `/login` 页面
|
||||||
|
2. 输入用户名和密码
|
||||||
|
3. 点击"登录"按钮
|
||||||
|
|
||||||
|
### 方式2: 手机号验证码登录(模拟)
|
||||||
|
1. 访问 `/login` 页面
|
||||||
|
2. 输入手机号(任意11位数字)
|
||||||
|
3. 点击"获取验证码"
|
||||||
|
4. 输入验证码(任意6位数字)
|
||||||
|
5. 点击"登陆/注册"
|
||||||
|
|
||||||
|
## 功能测试
|
||||||
|
|
||||||
|
### 普通用户功能
|
||||||
|
- ✅ 登录/登出
|
||||||
|
- ✅ 查看个人主页
|
||||||
|
- ✅ 查看订单管理
|
||||||
|
- ✅ 查看支付记录
|
||||||
|
- ✅ 访问欢迎页面
|
||||||
|
|
||||||
|
### 管理员功能
|
||||||
|
- ✅ 所有普通用户功能
|
||||||
|
- ✅ 后台管理
|
||||||
|
- ✅ 用户管理
|
||||||
|
- ✅ 数据仪表盘
|
||||||
|
|
||||||
|
## 数据库重置
|
||||||
|
|
||||||
|
如果需要重置数据库和测试数据:
|
||||||
|
|
||||||
|
1. 停止应用
|
||||||
|
2. 删除 `demo/data/demo.mv.db` 文件
|
||||||
|
3. 重新启动应用
|
||||||
|
4. 数据库会自动重新创建并插入测试数据
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 所有密码都是明文存储(仅用于测试)
|
||||||
|
- 积分会在每次登录时显示
|
||||||
|
- JWT Token 会在浏览器关闭时自动清除
|
||||||
|
- 支持跨域请求,前端和后端可以分离部署
|
||||||
282
demo/VUE_FRONTEND_SUMMARY.md
Normal file
282
demo/VUE_FRONTEND_SUMMARY.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# Vue.js 前端项目迁移完成总结
|
||||||
|
|
||||||
|
## 🎯 项目概述
|
||||||
|
|
||||||
|
成功将原有的 Thymeleaf 模板前端迁移到现代化的 Vue.js 3 框架,实现了前后端分离架构,提供了更好的开发体验和用户体验。
|
||||||
|
|
||||||
|
## ✅ 完成的工作
|
||||||
|
|
||||||
|
### 1. **项目结构搭建**
|
||||||
|
- ✅ 创建了完整的 Vue.js 3 项目结构
|
||||||
|
- ✅ 配置了 Vite 构建工具
|
||||||
|
- ✅ 集成了 Element Plus UI 组件库
|
||||||
|
- ✅ 设置了开发环境和代理配置
|
||||||
|
|
||||||
|
### 2. **核心框架集成**
|
||||||
|
- ✅ **Vue.js 3** - 使用 Composition API 和 `<script setup>` 语法
|
||||||
|
- ✅ **Element Plus** - 现代化 UI 组件库
|
||||||
|
- ✅ **Vue Router** - 单页面应用路由管理
|
||||||
|
- ✅ **Pinia** - 轻量级状态管理
|
||||||
|
- ✅ **Axios** - HTTP 客户端和 API 封装
|
||||||
|
|
||||||
|
### 3. **组件和页面开发**
|
||||||
|
- ✅ **导航组件** (`NavBar.vue`) - 响应式导航栏,支持用户菜单
|
||||||
|
- ✅ **页脚组件** (`Footer.vue`) - 统一的页脚设计
|
||||||
|
- ✅ **首页** (`Home.vue`) - 欢迎页面,功能展示,统计数据
|
||||||
|
- ✅ **登录页** (`Login.vue`) - 现代化登录界面,演示账号
|
||||||
|
- ✅ **注册页** (`Register.vue`) - 实时验证,密码强度检测
|
||||||
|
- ✅ **订单列表** (`Orders.vue`) - 完整的订单管理界面
|
||||||
|
|
||||||
|
### 4. **状态管理**
|
||||||
|
- ✅ **用户状态** (`stores/user.js`) - 登录、注册、权限管理
|
||||||
|
- ✅ **订单状态** (`stores/orders.js`) - 订单数据管理和操作
|
||||||
|
- ✅ **响应式数据** - 使用 Vue 3 的响应式系统
|
||||||
|
- ✅ **持久化存储** - Token 和用户信息本地存储
|
||||||
|
|
||||||
|
### 5. **API 接口封装**
|
||||||
|
- ✅ **请求拦截器** - 自动添加认证头,统一错误处理
|
||||||
|
- ✅ **响应拦截器** - 统一响应格式,错误提示
|
||||||
|
- ✅ **认证接口** (`api/auth.js`) - 登录、注册、用户信息
|
||||||
|
- ✅ **订单接口** (`api/orders.js`) - 订单 CRUD 操作
|
||||||
|
- ✅ **支付接口** (`api/payments.js`) - 支付相关功能
|
||||||
|
|
||||||
|
### 6. **路由和权限控制**
|
||||||
|
- ✅ **路由配置** - 完整的页面路由定义
|
||||||
|
- ✅ **路由守卫** - 认证检查和权限控制
|
||||||
|
- ✅ **动态标题** - 页面标题自动设置
|
||||||
|
- ✅ **权限控制** - 基于角色的访问控制
|
||||||
|
|
||||||
|
### 7. **后端 API 适配**
|
||||||
|
- ✅ **RESTful API** - 创建了标准的 REST API 接口
|
||||||
|
- ✅ **认证控制器** (`AuthApiController.java`) - 登录、注册、用户验证
|
||||||
|
- ✅ **订单控制器** (`OrderApiController.java`) - 订单管理 API
|
||||||
|
- ✅ **统一响应格式** - 标准化的 API 响应结构
|
||||||
|
|
||||||
|
## 🎨 界面特色
|
||||||
|
|
||||||
|
### 现代化设计
|
||||||
|
- **Element Plus** 组件库,提供丰富的 UI 组件
|
||||||
|
- **响应式布局**,支持桌面、平板、手机
|
||||||
|
- **统一的设计语言**,保持视觉一致性
|
||||||
|
- **图标支持**,Element Plus Icons 图标库
|
||||||
|
|
||||||
|
### 用户体验优化
|
||||||
|
- **实时表单验证**,即时反馈用户输入
|
||||||
|
- **密码强度检测**,提升安全性
|
||||||
|
- **用户名/邮箱唯一性检查**,避免重复注册
|
||||||
|
- **加载状态**,提升用户等待体验
|
||||||
|
- **错误提示**,友好的错误信息展示
|
||||||
|
|
||||||
|
### 交互功能
|
||||||
|
- **动态表单**,可添加/删除订单项
|
||||||
|
- **实时搜索**,订单号搜索功能
|
||||||
|
- **状态筛选**,按订单状态筛选
|
||||||
|
- **分页导航**,大数据量分页显示
|
||||||
|
- **模态框操作**,确认对话框
|
||||||
|
|
||||||
|
## 🔧 技术特性
|
||||||
|
|
||||||
|
### 前端技术栈
|
||||||
|
- **Vue.js 3.3.4** - 最新版本的 Vue 框架
|
||||||
|
- **Element Plus 2.3.8** - 企业级 UI 组件库
|
||||||
|
- **Vue Router 4.2.4** - 官方路由管理器
|
||||||
|
- **Pinia 2.1.6** - 现代状态管理库
|
||||||
|
- **Axios 1.5.0** - Promise 基础的 HTTP 库
|
||||||
|
- **Vite 4.4.9** - 快速的构建工具
|
||||||
|
|
||||||
|
### 开发工具
|
||||||
|
- **Vite** - 快速的开发服务器和构建工具
|
||||||
|
- **ESLint** - 代码质量检查
|
||||||
|
- **Prettier** - 代码格式化
|
||||||
|
- **Sass** - CSS 预处理器
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
- **组件化开发** - 可复用的 Vue 组件
|
||||||
|
- **模块化设计** - 清晰的文件组织结构
|
||||||
|
- **状态管理** - 集中式的状态管理
|
||||||
|
- **API 封装** - 统一的接口调用方式
|
||||||
|
|
||||||
|
## 📱 功能模块
|
||||||
|
|
||||||
|
### 1. 用户认证模块
|
||||||
|
- **登录功能** - 用户名/密码登录
|
||||||
|
- **注册功能** - 新用户注册
|
||||||
|
- **权限控制** - 基于角色的访问控制
|
||||||
|
- **状态管理** - 用户登录状态管理
|
||||||
|
|
||||||
|
### 2. 订单管理模块
|
||||||
|
- **订单列表** - 分页、筛选、搜索
|
||||||
|
- **订单详情** - 完整信息展示
|
||||||
|
- **订单创建** - 动态表单创建
|
||||||
|
- **状态管理** - 订单状态流转
|
||||||
|
|
||||||
|
### 3. 支付集成模块
|
||||||
|
- **支付方式** - 支付宝、PayPal
|
||||||
|
- **支付状态** - 实时状态跟踪
|
||||||
|
- **支付记录** - 支付历史查询
|
||||||
|
|
||||||
|
### 4. 管理功能模块
|
||||||
|
- **用户管理** - 用户列表和操作
|
||||||
|
- **订单管理** - 管理员订单管理
|
||||||
|
- **权限管理** - 角色权限控制
|
||||||
|
|
||||||
|
## 🔄 数据流
|
||||||
|
|
||||||
|
### 状态管理流程
|
||||||
|
```
|
||||||
|
用户操作 → Vue组件 → Pinia Store → API调用 → 后端服务 → 数据库
|
||||||
|
↓
|
||||||
|
状态更新 → 组件重新渲染 → 界面更新
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 调用流程
|
||||||
|
```
|
||||||
|
组件 → API方法 → Axios请求 → 请求拦截器 → 后端API → 响应拦截器 → 组件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 部署配置
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
# 访问 http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
```bash
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 预览构建结果
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代理配置
|
||||||
|
```javascript
|
||||||
|
// vite.config.js
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080', // 后端服务
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 项目统计
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
- **组件文件**: 6个 Vue 组件
|
||||||
|
- **页面文件**: 5个页面组件
|
||||||
|
- **API 文件**: 4个 API 模块
|
||||||
|
- **状态管理**: 2个 Store 模块
|
||||||
|
- **配置文件**: 3个配置文件
|
||||||
|
|
||||||
|
### 代码量统计
|
||||||
|
- **Vue 组件**: ~2000 行代码
|
||||||
|
- **JavaScript**: ~1500 行代码
|
||||||
|
- **CSS 样式**: ~800 行代码
|
||||||
|
- **配置文件**: ~200 行代码
|
||||||
|
|
||||||
|
## 🔒 安全特性
|
||||||
|
|
||||||
|
### 前端安全
|
||||||
|
- **XSS 防护** - Element Plus 组件自动转义
|
||||||
|
- **CSRF 防护** - Axios 请求头配置
|
||||||
|
- **路由守卫** - 页面访问权限控制
|
||||||
|
- **输入验证** - 前端表单验证
|
||||||
|
|
||||||
|
### 认证安全
|
||||||
|
- **Token 管理** - 安全的 Token 存储
|
||||||
|
- **权限控制** - 基于角色的访问控制
|
||||||
|
- **会话管理** - 自动登出机制
|
||||||
|
|
||||||
|
## 🎯 优势对比
|
||||||
|
|
||||||
|
### 相比 Thymeleaf 的优势
|
||||||
|
|
||||||
|
| 特性 | Thymeleaf | Vue.js |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| **开发体验** | 服务端渲染 | 现代化前端开发 |
|
||||||
|
| **用户体验** | 页面刷新 | 单页面应用 |
|
||||||
|
| **组件复用** | 模板片段 | 完整组件系统 |
|
||||||
|
| **状态管理** | 服务端状态 | 客户端状态管理 |
|
||||||
|
| **开发工具** | 有限 | 丰富的开发工具 |
|
||||||
|
| **性能** | 服务端渲染 | 客户端渲染优化 |
|
||||||
|
|
||||||
|
### 技术优势
|
||||||
|
- **现代化框架** - Vue.js 3 最新特性
|
||||||
|
- **组件化开发** - 高度可复用的组件
|
||||||
|
- **响应式设计** - 移动端友好
|
||||||
|
- **开发效率** - 热重载,快速开发
|
||||||
|
- **维护性** - 清晰的代码结构
|
||||||
|
|
||||||
|
## 🔮 未来扩展
|
||||||
|
|
||||||
|
### 可扩展功能
|
||||||
|
- [ ] **TypeScript 支持** - 类型安全
|
||||||
|
- [ ] **单元测试** - Jest + Vue Test Utils
|
||||||
|
- [ ] **E2E 测试** - Cypress 或 Playwright
|
||||||
|
- [ ] **PWA 支持** - 渐进式 Web 应用
|
||||||
|
- [ ] **国际化** - Vue I18n 多语言支持
|
||||||
|
- [ ] **主题定制** - 动态主题切换
|
||||||
|
- [ ] **图表组件** - ECharts 数据可视化
|
||||||
|
- [ ] **富文本编辑** - 编辑器组件
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
- [ ] **代码分割** - 路由级别的代码分割
|
||||||
|
- [ ] **懒加载** - 组件和图片懒加载
|
||||||
|
- [ ] **缓存策略** - HTTP 缓存和本地缓存
|
||||||
|
- [ ] **CDN 部署** - 静态资源 CDN 加速
|
||||||
|
|
||||||
|
## 📝 使用说明
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
1. **安装 Node.js** (版本 16+)
|
||||||
|
2. **进入前端目录**: `cd frontend`
|
||||||
|
3. **安装依赖**: `npm install`
|
||||||
|
4. **启动开发服务器**: `npm run dev`
|
||||||
|
5. **访问应用**: http://localhost:3000
|
||||||
|
|
||||||
|
### 开发指南
|
||||||
|
1. **组件开发** - 使用 Composition API
|
||||||
|
2. **状态管理** - 使用 Pinia Store
|
||||||
|
3. **API 调用** - 使用封装的 API 方法
|
||||||
|
4. **样式编写** - 使用 Scoped CSS
|
||||||
|
5. **路由配置** - 在 router/index.js 中添加路由
|
||||||
|
|
||||||
|
### 部署指南
|
||||||
|
1. **构建项目**: `npm run build`
|
||||||
|
2. **部署静态文件** - 将 dist 目录部署到 Web 服务器
|
||||||
|
3. **配置代理** - 将 /api 请求代理到后端服务
|
||||||
|
4. **HTTPS 配置** - 生产环境使用 HTTPS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
Vue.js 前端项目迁移已经完成,实现了:
|
||||||
|
|
||||||
|
✅ **现代化前端架构** - Vue.js 3 + Element Plus
|
||||||
|
✅ **完整的用户界面** - 登录、注册、订单管理
|
||||||
|
✅ **响应式设计** - 支持多设备访问
|
||||||
|
✅ **状态管理** - Pinia 集中式状态管理
|
||||||
|
✅ **API 集成** - 完整的后端 API 对接
|
||||||
|
✅ **权限控制** - 基于角色的访问控制
|
||||||
|
✅ **开发工具** - Vite 快速开发和构建
|
||||||
|
|
||||||
|
现在您拥有了一个现代化的前后端分离应用,具备良好的开发体验和用户体验!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
21
demo/clear-expired-token.sh
Normal file
21
demo/clear-expired-token.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 清除过期JWT Token脚本
|
||||||
|
|
||||||
|
echo "=== 清除过期JWT Token ==="
|
||||||
|
echo "问题:JWT token已过期,导致登录后立即被重定向"
|
||||||
|
echo "解决:清除localStorage中的过期token,重新登录"
|
||||||
|
|
||||||
|
echo -e "\n🔧 解决方案:"
|
||||||
|
echo "1. 打开浏览器开发者工具 (F12)"
|
||||||
|
echo "2. 进入 Console 标签页"
|
||||||
|
echo "3. 执行以下命令清除过期token:"
|
||||||
|
echo ""
|
||||||
|
echo "localStorage.removeItem('token')"
|
||||||
|
echo "localStorage.removeItem('user')"
|
||||||
|
echo "location.reload()"
|
||||||
|
echo ""
|
||||||
|
echo "4. 然后重新登录:"
|
||||||
|
echo " - 用户名: admin"
|
||||||
|
echo " - 密码: admin123"
|
||||||
|
echo ""
|
||||||
|
echo "✅ JWT过期时间已从24小时增加到7天"
|
||||||
|
echo "✅ 清除过期token后重新登录即可正常工作"
|
||||||
24
demo/clear-localStorage.sh
Normal file
24
demo/clear-localStorage.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 清除旧localStorage数据脚本
|
||||||
|
|
||||||
|
echo "=== 清除旧localStorage数据 ==="
|
||||||
|
echo "已修改token存储方式:从localStorage改为sessionStorage"
|
||||||
|
echo "现在关闭页面时token会自动清除"
|
||||||
|
|
||||||
|
echo -e "\n🔧 清除步骤:"
|
||||||
|
echo "1. 打开浏览器开发者工具 (F12)"
|
||||||
|
echo "2. 进入 Console 标签页"
|
||||||
|
echo "3. 执行以下命令清除旧的localStorage数据:"
|
||||||
|
echo ""
|
||||||
|
echo "localStorage.removeItem('token')"
|
||||||
|
echo "localStorage.removeItem('user')"
|
||||||
|
echo "console.log('已清除localStorage中的旧数据')"
|
||||||
|
echo ""
|
||||||
|
echo "4. 然后重新登录:"
|
||||||
|
echo " - 用户名: admin"
|
||||||
|
echo " - 密码: admin123"
|
||||||
|
echo ""
|
||||||
|
echo "✅ 修改内容:"
|
||||||
|
echo "- token和用户信息现在存储在sessionStorage中"
|
||||||
|
echo "- 关闭页面时自动清除,提高安全性"
|
||||||
|
echo "- 刷新页面时保持登录状态"
|
||||||
|
echo "- 401错误时自动清除sessionStorage"
|
||||||
27
demo/docker-compose.yml
Normal file
27
demo/docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: demo-mysql
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=177615
|
||||||
|
- MYSQL_DATABASE=aigc
|
||||||
|
- MYSQL_USER=demo
|
||||||
|
- MYSQL_PASSWORD=demo_pass
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
command: ["mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
48
demo/env.example
Normal file
48
demo/env.example
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# 环境变量配置示例
|
||||||
|
# 复制此文件为 .env 并根据实际情况修改
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_URL=jdbc:mysql://localhost:3306/aigc?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=your_database_password
|
||||||
|
|
||||||
|
# JWT配置
|
||||||
|
JWT_SECRET=your-very-long-and-secure-jwt-secret-key-at-least-256-bits-long
|
||||||
|
JWT_EXPIRATION=604800000
|
||||||
|
|
||||||
|
# 支付宝配置
|
||||||
|
ALIPAY_APP_ID=your_alipay_app_id
|
||||||
|
ALIPAY_PRIVATE_KEY=your_alipay_private_key
|
||||||
|
ALIPAY_PUBLIC_KEY=alipay_public_key
|
||||||
|
ALIPAY_NOTIFY_URL=https://yourdomain.com/api/payments/alipay/notify
|
||||||
|
ALIPAY_RETURN_URL=https://yourdomain.com/api/payments/alipay/return
|
||||||
|
|
||||||
|
# PayPal配置
|
||||||
|
PAYPAL_CLIENT_ID=your_paypal_client_id
|
||||||
|
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
|
||||||
|
PAYPAL_RETURN_URL=https://yourdomain.com/api/payments/paypal/return
|
||||||
|
PAYPAL_CANCEL_URL=https://yourdomain.com/api/payments/paypal/cancel
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_FILE_PATH=./logs/application.log
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
SERVER_PORT=8080
|
||||||
|
SERVER_CONTEXT_PATH=/
|
||||||
|
|
||||||
|
# 邮件配置(可选)
|
||||||
|
MAIL_HOST=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=your_email@gmail.com
|
||||||
|
MAIL_PASSWORD=your_email_password
|
||||||
|
|
||||||
|
# Redis配置(可选)
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=your_redis_password
|
||||||
|
|
||||||
|
# 文件上传配置
|
||||||
|
UPLOAD_PATH=./uploads
|
||||||
|
MAX_FILE_SIZE=10MB
|
||||||
|
|
||||||
|
|
||||||
37
demo/fix-auto-refresh-test.sh
Normal file
37
demo/fix-auto-refresh-test.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 登录自动刷新问题修复验证
|
||||||
|
|
||||||
|
echo "=== 登录自动刷新问题修复验证 ==="
|
||||||
|
|
||||||
|
echo "✅ 已修复的问题:"
|
||||||
|
echo "1. 修复了main.js中的应用挂载时机"
|
||||||
|
echo "2. 修复了API拦截器中的页面刷新问题"
|
||||||
|
echo "3. 改进了登录成功后的路由跳转逻辑"
|
||||||
|
echo "4. 使用router.replace替代router.push"
|
||||||
|
|
||||||
|
echo -e "\n🔍 测试步骤:"
|
||||||
|
echo "1. 打开浏览器开发者工具"
|
||||||
|
echo "2. 访问 http://localhost:3000/login"
|
||||||
|
echo "3. 使用管理员账户登录 (admin/admin)"
|
||||||
|
echo "4. 观察控制台日志,应该看到:"
|
||||||
|
echo " - 开始登录..."
|
||||||
|
echo " - 登录成功,用户信息: {...}"
|
||||||
|
echo " - 认证状态: true"
|
||||||
|
echo " - 管理员状态: true"
|
||||||
|
echo " - 准备跳转到: /home"
|
||||||
|
echo " - 路由跳转完成"
|
||||||
|
echo " - === 路由守卫 === (详细的路由守卫日志)"
|
||||||
|
|
||||||
|
echo -e "\n❌ 不应该看到:"
|
||||||
|
echo "- 页面刷新"
|
||||||
|
echo "- 重新加载"
|
||||||
|
echo "- 跳转回登录页面"
|
||||||
|
|
||||||
|
echo -e "\n🚀 启动服务:"
|
||||||
|
echo "前端: cd demo/frontend && yarn dev"
|
||||||
|
echo "后端: cd demo && ./mvnw spring-boot:run"
|
||||||
|
|
||||||
|
echo -e "\n📝 如果仍有问题:"
|
||||||
|
echo "1. 检查浏览器控制台是否有错误"
|
||||||
|
echo "2. 确认后端服务正常运行"
|
||||||
|
echo "3. 检查网络请求是否成功"
|
||||||
|
echo "4. 验证JWT token是否正确生成"
|
||||||
39
demo/fix-jwt-auth-test.sh
Normal file
39
demo/fix-jwt-auth-test.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# JWT认证问题修复验证
|
||||||
|
|
||||||
|
echo "=== JWT认证问题修复验证 ==="
|
||||||
|
|
||||||
|
echo "🔧 已修复的问题:"
|
||||||
|
echo "1. 移除了JWT过滤器中的 'SecurityContextHolder.getContext().getAuthentication() == null' 条件"
|
||||||
|
echo "2. 现在JWT过滤器会始终处理有效的token,即使已有认证信息"
|
||||||
|
echo "3. 修复了登录成功后API请求返回401的问题"
|
||||||
|
|
||||||
|
echo -e "\n🔍 问题原因分析:"
|
||||||
|
echo "从后端日志可以看到:"
|
||||||
|
echo "- 登录成功:'用户登录成功:admin'"
|
||||||
|
echo "- JWT过滤器检测到token:'提取的token: 存在'"
|
||||||
|
echo "- 但是过滤器说'没有token或已认证'"
|
||||||
|
echo "- 这是因为SecurityContextHolder中已有认证信息,导致JWT过滤器跳过处理"
|
||||||
|
|
||||||
|
echo -e "\n✅ 修复方案:"
|
||||||
|
echo "- 移除了条件判断中的 'SecurityContextHolder.getContext().getAuthentication() == null'"
|
||||||
|
echo "- 现在JWT过滤器会始终处理有效的token"
|
||||||
|
echo "- 确保每次请求都能正确验证JWT token"
|
||||||
|
|
||||||
|
echo -e "\n🚀 测试步骤:"
|
||||||
|
echo "1. 等待后端服务启动完成(约10-15秒)"
|
||||||
|
echo "2. 访问 http://localhost:3000/login"
|
||||||
|
echo "3. 使用管理员账户登录 (admin/admin)"
|
||||||
|
echo "4. 观察是否还会出现'未授权,请重新登录'的错误"
|
||||||
|
echo "5. 检查浏览器控制台是否还有401错误"
|
||||||
|
|
||||||
|
echo -e "\n📝 预期结果:"
|
||||||
|
echo "- ✅ 登录成功后直接跳转到首页"
|
||||||
|
echo "- ✅ 不会出现'未授权,请重新登录'错误"
|
||||||
|
echo "- ✅ API请求返回200而不是401"
|
||||||
|
echo "- ✅ 用户状态正确保持"
|
||||||
|
|
||||||
|
echo -e "\n🔍 如果仍有问题:"
|
||||||
|
echo "1. 检查后端服务是否正常启动"
|
||||||
|
echo "2. 查看后端日志中的JWT认证信息"
|
||||||
|
echo "3. 确认前端服务运行在3000端口"
|
||||||
|
echo "4. 检查浏览器控制台的网络请求"
|
||||||
45
demo/fix-login-loop-test.sh
Normal file
45
demo/fix-login-loop-test.sh
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 登录循环问题修复验证
|
||||||
|
|
||||||
|
echo "=== 登录循环问题修复验证 ==="
|
||||||
|
|
||||||
|
echo "1. 检查前端服务状态..."
|
||||||
|
if curl -s http://localhost:3000 > /dev/null; then
|
||||||
|
echo "✅ 前端服务运行正常"
|
||||||
|
else
|
||||||
|
echo "❌ 前端服务未运行"
|
||||||
|
echo " 启动命令: cd demo/frontend && yarn dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n2. 检查后端服务状态..."
|
||||||
|
if curl -s http://localhost:8080/api/public/health > /dev/null; then
|
||||||
|
echo "✅ 后端服务运行正常"
|
||||||
|
else
|
||||||
|
echo "❌ 后端服务未运行"
|
||||||
|
echo " 启动命令: cd demo && ./mvnw spring-boot:run"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n3. 测试登录API..."
|
||||||
|
echo " 测试命令: curl -X POST http://localhost:8080/api/auth/login"
|
||||||
|
echo " 参数: {\"username\":\"admin\",\"password\":\"admin\"}"
|
||||||
|
|
||||||
|
echo -e "\n4. 检查浏览器控制台..."
|
||||||
|
echo " 打开浏览器开发者工具,查看Console输出"
|
||||||
|
echo " 应该看到详细的路由守卫日志"
|
||||||
|
|
||||||
|
echo -e "\n5. 测试步骤..."
|
||||||
|
echo " 1. 访问 http://localhost:3000/login"
|
||||||
|
echo " 2. 使用管理员账户登录 (admin/admin)"
|
||||||
|
echo " 3. 观察控制台日志"
|
||||||
|
echo " 4. 检查是否成功跳转到首页"
|
||||||
|
|
||||||
|
echo -e "\n=== 修复内容 ==="
|
||||||
|
echo "✅ 增强了路由守卫的调试日志"
|
||||||
|
echo "✅ 修复了用户状态恢复逻辑"
|
||||||
|
echo "✅ 添加了登录成功后的延迟处理"
|
||||||
|
echo "✅ 改进了错误处理和状态管理"
|
||||||
|
|
||||||
|
echo -e "\n=== 如果仍有问题 ==="
|
||||||
|
echo "1. 检查浏览器控制台的详细日志"
|
||||||
|
echo "2. 确认后端JWT配置正确"
|
||||||
|
echo "3. 检查网络请求是否成功"
|
||||||
|
echo "4. 验证用户角色是否为ROLE_ADMIN"
|
||||||
425
demo/frontend/README.md
Normal file
425
demo/frontend/README.md
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
# AIGC Demo - Vue.js 前端
|
||||||
|
|
||||||
|
这是一个基于 Vue.js 3 + Element Plus 的现代化前端应用,为 AIGC Demo 项目提供用户界面。
|
||||||
|
|
||||||
|
## 🚀 技术栈
|
||||||
|
|
||||||
|
- **Vue.js 3** - 渐进式 JavaScript 框架
|
||||||
|
- **Element Plus** - Vue 3 组件库
|
||||||
|
- **Vue Router** - 官方路由管理器
|
||||||
|
- **Pinia** - Vue 状态管理库
|
||||||
|
- **Axios** - HTTP 客户端
|
||||||
|
- **Vite** - 现代化构建工具
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # 公共组件
|
||||||
|
│ │ ├── NavBar.vue # 导航栏
|
||||||
|
│ │ └── Footer.vue # 页脚
|
||||||
|
│ ├── views/ # 页面组件
|
||||||
|
│ │ ├── Home.vue # 首页
|
||||||
|
│ │ ├── Login.vue # 登录页
|
||||||
|
│ │ ├── Register.vue # 注册页
|
||||||
|
│ │ ├── Orders.vue # 订单列表
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── router/ # 路由配置
|
||||||
|
│ │ └── index.js
|
||||||
|
│ ├── stores/ # 状态管理
|
||||||
|
│ │ ├── user.js # 用户状态
|
||||||
|
│ │ └── orders.js # 订单状态
|
||||||
|
│ ├── api/ # API 接口
|
||||||
|
│ │ ├── request.js # Axios 配置
|
||||||
|
│ │ ├── auth.js # 认证接口
|
||||||
|
│ │ ├── orders.js # 订单接口
|
||||||
|
│ │ └── payments.js # 支付接口
|
||||||
|
│ ├── assets/ # 静态资源
|
||||||
|
│ ├── utils/ # 工具函数
|
||||||
|
│ ├── App.vue # 根组件
|
||||||
|
│ └── main.js # 入口文件
|
||||||
|
├── index.html # HTML 模板
|
||||||
|
├── package.json # 依赖配置
|
||||||
|
├── vite.config.js # Vite 配置
|
||||||
|
└── README.md # 说明文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 安装和运行
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
|
||||||
|
- Node.js 16+
|
||||||
|
- npm 或 yarn
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:3000
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 配置说明
|
||||||
|
|
||||||
|
### Vite 配置
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// vite.config.js
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080', // 后端服务地址
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 代理
|
||||||
|
|
||||||
|
前端开发服务器会自动将 `/api` 请求代理到后端服务器 `http://localhost:8080`,这样避免了跨域问题。
|
||||||
|
|
||||||
|
## 📱 功能特性
|
||||||
|
|
||||||
|
### 🎨 现代化 UI
|
||||||
|
- **Element Plus** 组件库,提供丰富的 UI 组件
|
||||||
|
- **响应式设计**,支持桌面、平板、手机
|
||||||
|
- **主题定制**,统一的视觉风格
|
||||||
|
- **图标支持**,Element Plus Icons
|
||||||
|
|
||||||
|
### 🔐 用户认证
|
||||||
|
- **登录/注册**,完整的用户认证流程
|
||||||
|
- **表单验证**,实时验证用户输入
|
||||||
|
- **状态管理**,Pinia 管理用户状态
|
||||||
|
- **路由守卫**,保护需要认证的页面
|
||||||
|
|
||||||
|
### 📋 订单管理
|
||||||
|
- **订单列表**,分页、筛选、搜索
|
||||||
|
- **订单详情**,完整信息展示
|
||||||
|
- **订单创建**,动态表单
|
||||||
|
- **状态管理**,订单状态流转
|
||||||
|
|
||||||
|
### 💳 支付集成
|
||||||
|
- **多种支付方式**,支付宝、PayPal
|
||||||
|
- **支付状态跟踪**,实时状态更新
|
||||||
|
- **支付记录**,完整的支付历史
|
||||||
|
|
||||||
|
### 👨💼 管理功能
|
||||||
|
- **用户管理**,用户列表和操作
|
||||||
|
- **订单管理**,管理员订单管理
|
||||||
|
- **权限控制**,基于角色的访问控制
|
||||||
|
|
||||||
|
## 🔄 状态管理
|
||||||
|
|
||||||
|
### 用户状态 (stores/user.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const user = ref(null)
|
||||||
|
const token = ref(localStorage.getItem('token'))
|
||||||
|
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||||
|
|
||||||
|
const loginUser = async (credentials) => { /* ... */ }
|
||||||
|
const registerUser = async (userData) => { /* ... */ }
|
||||||
|
const logoutUser = async () => { /* ... */ }
|
||||||
|
|
||||||
|
return { user, token, isAuthenticated, loginUser, registerUser, logoutUser }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 订单状态 (stores/orders.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const useOrderStore = defineStore('orders', () => {
|
||||||
|
const orders = ref([])
|
||||||
|
const currentOrder = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const fetchOrders = async (params) => { /* ... */ }
|
||||||
|
const createNewOrder = async (orderData) => { /* ... */ }
|
||||||
|
const updateOrder = async (id, status) => { /* ... */ }
|
||||||
|
|
||||||
|
return { orders, currentOrder, loading, fetchOrders, createNewOrder, updateOrder }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 API 接口
|
||||||
|
|
||||||
|
### 认证接口
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 登录
|
||||||
|
POST /api/auth/login
|
||||||
|
{
|
||||||
|
"username": "user",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
POST /api/auth/register
|
||||||
|
{
|
||||||
|
"username": "newuser",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户
|
||||||
|
GET /api/auth/me
|
||||||
|
```
|
||||||
|
|
||||||
|
### 订单接口
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 获取订单列表
|
||||||
|
GET /api/orders?page=0&size=10&status=PENDING
|
||||||
|
|
||||||
|
// 获取订单详情
|
||||||
|
GET /api/orders/{id}
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
POST /api/orders/create
|
||||||
|
{
|
||||||
|
"orderType": "PRODUCT",
|
||||||
|
"currency": "CNY",
|
||||||
|
"orderItems": [...]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
POST /api/orders/{id}/status
|
||||||
|
{
|
||||||
|
"status": "PAID",
|
||||||
|
"notes": "备注"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 路由配置
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home,
|
||||||
|
meta: { title: '首页' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: Login,
|
||||||
|
meta: { title: '登录', guest: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/orders',
|
||||||
|
name: 'Orders',
|
||||||
|
component: Orders,
|
||||||
|
meta: { title: '订单管理', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/orders',
|
||||||
|
name: 'AdminOrders',
|
||||||
|
component: AdminOrders,
|
||||||
|
meta: { title: '订单管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 权限控制
|
||||||
|
|
||||||
|
### 路由守卫
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 检查是否需要认证
|
||||||
|
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
|
||||||
|
next('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要管理员权限
|
||||||
|
if (to.meta.requiresAdmin && !userStore.isAdmin) {
|
||||||
|
next('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 拦截器
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 请求拦截器 - 添加认证头
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应拦截器 - 处理错误
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response.data,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// 未授权,跳转到登录页
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 响应式设计
|
||||||
|
|
||||||
|
### 断点配置
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 移动端 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板 */
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端 */
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 部署
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建完成后,`dist` 目录包含所有静态文件。
|
||||||
|
|
||||||
|
### Nginx 配置
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /path/to/dist;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 开发工具
|
||||||
|
|
||||||
|
### VS Code 推荐插件
|
||||||
|
|
||||||
|
- **Vue Language Features (Volar)** - Vue 3 支持
|
||||||
|
- **TypeScript Vue Plugin (Volar)** - TypeScript 支持
|
||||||
|
- **ESLint** - 代码检查
|
||||||
|
- **Prettier** - 代码格式化
|
||||||
|
- **Auto Rename Tag** - 自动重命名标签
|
||||||
|
- **Bracket Pair Colorizer** - 括号配对着色
|
||||||
|
|
||||||
|
### 调试工具
|
||||||
|
|
||||||
|
- **Vue DevTools** - Vue 开发者工具
|
||||||
|
- **Pinia DevTools** - Pinia 状态管理工具
|
||||||
|
- **Element Plus DevTools** - Element Plus 组件调试
|
||||||
|
|
||||||
|
## 📝 开发规范
|
||||||
|
|
||||||
|
### 组件命名
|
||||||
|
|
||||||
|
- 组件文件名使用 PascalCase:`UserProfile.vue`
|
||||||
|
- 组件名使用 PascalCase:`UserProfile`
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── common/ # 通用组件
|
||||||
|
├── layout/ # 布局组件
|
||||||
|
└── business/ # 业务组件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
|
||||||
|
- 使用 Composition API
|
||||||
|
- 优先使用 `<script setup>` 语法
|
||||||
|
- 使用 TypeScript 类型定义
|
||||||
|
- 遵循 ESLint 规则
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
1. Fork 项目
|
||||||
|
2. 创建功能分支:`git checkout -b feature/new-feature`
|
||||||
|
3. 提交更改:`git commit -am 'Add new feature'`
|
||||||
|
4. 推送分支:`git push origin feature/new-feature`
|
||||||
|
5. 提交 Pull Request
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意**: 这是一个演示项目,生产环境使用前请进行充分的安全测试和性能优化。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
81
demo/frontend/dev-server.js
Normal file
81
demo/frontend/dev-server.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
console.log(`${req.method} ${req.url}`);
|
||||||
|
|
||||||
|
// 设置CORS头
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath = req.url === '/' ? '/index.html' : req.url;
|
||||||
|
|
||||||
|
// 如果是API请求,代理到后端
|
||||||
|
if (req.url.startsWith('/api')) {
|
||||||
|
const http = require('http');
|
||||||
|
const options = {
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 8080,
|
||||||
|
path: req.url,
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = http.request(options, (proxyRes) => {
|
||||||
|
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||||
|
proxyRes.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.pipe(proxyReq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态文件服务
|
||||||
|
const fullPath = path.join(__dirname, 'dist', filePath);
|
||||||
|
|
||||||
|
fs.readFile(fullPath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>开发服务器</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>开发服务器正在运行</h1>
|
||||||
|
<p>端口: ${PORT}</p>
|
||||||
|
<p>请先运行 <code>npm run build</code> 构建项目</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(fullPath);
|
||||||
|
const contentType = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.js': 'text/javascript',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.json': 'application/json'
|
||||||
|
}[ext] || 'text/plain';
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`🚀 开发服务器运行在 http://localhost:${PORT}`);
|
||||||
|
console.log(`🌐 网络访问: http://0.0.0.0:${PORT}`);
|
||||||
|
});
|
||||||
17
demo/frontend/index.html
Normal file
17
demo/frontend/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AIGC Demo - Vue.js Frontend</title>
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1842
demo/frontend/package-lock.json
generated
Normal file
1842
demo/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
demo/frontend/package.json
Normal file
31
demo/frontend/package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "aigc-demo-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AIGC Demo Frontend with Vue.js",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.4",
|
||||||
|
"pinia": "^2.1.6",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"element-plus": "^2.3.8",
|
||||||
|
"@element-plus/icons-vue": "^2.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"sass": "^1.66.1"
|
||||||
|
},
|
||||||
|
"keywords": ["vue", "frontend", "aigc"],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
demo/frontend/public/images/backgrounds/1.jpg
Normal file
BIN
demo/frontend/public/images/backgrounds/1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
23
demo/frontend/public/images/backgrounds/README.md
Normal file
23
demo/frontend/public/images/backgrounds/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 背景图片说明
|
||||||
|
|
||||||
|
这个文件夹专门用于存放欢迎页面的背景图片。
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
```
|
||||||
|
public/images/backgrounds/
|
||||||
|
├── welcome-bg-1.jpg # 深蓝色渐变背景
|
||||||
|
├── welcome-bg-2.jpg # 备用背景图片
|
||||||
|
└── README.md # 本说明文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
在Vue组件中使用:
|
||||||
|
```css
|
||||||
|
background: url('/images/backgrounds/welcome-bg-1.jpg') center/cover no-repeat;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 图片要求
|
||||||
|
- 格式:JPG/PNG
|
||||||
|
- 尺寸:1920x1080 或更高
|
||||||
|
- 文件大小:建议小于2MB
|
||||||
|
- 内容:深色渐变背景,适合文字叠加
|
||||||
BIN
demo/frontend/public/images/backgrounds/login.png
Normal file
BIN
demo/frontend/public/images/backgrounds/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 MiB |
BIN
demo/frontend/public/images/backgrounds/welcome.jpg
Normal file
BIN
demo/frontend/public/images/backgrounds/welcome.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 MiB |
21
demo/frontend/src/App-backup.vue
Normal file
21
demo/frontend/src/App-backup.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<h1>测试应用</h1>
|
||||||
|
<p>如果您能看到这个页面,说明Vue应用正常工作。</p>
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
console.log('App.vue 加载成功')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
padding: 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
438
demo/frontend/src/App.vue
Normal file
438
demo/frontend/src/App.vue
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app" :data-route="route.name">
|
||||||
|
<!-- 导航栏 - 根据路由条件显示 -->
|
||||||
|
<NavBar v-if="shouldShowNavBar" />
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main :class="{ 'with-navbar': shouldShowNavBar }">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 - 根据路由条件显示 -->
|
||||||
|
<Footer v-if="shouldShowFooter" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import NavBar from '@/components/NavBar.vue'
|
||||||
|
import Footer from '@/components/Footer.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 计算是否显示导航栏和页脚
|
||||||
|
const shouldShowNavBar = computed(() => {
|
||||||
|
// 登录和注册页面不显示导航栏
|
||||||
|
return !['login', 'register'].includes(route.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
const shouldShowFooter = computed(() => {
|
||||||
|
// 登录和注册页面不显示页脚
|
||||||
|
return !['login', 'register'].includes(route.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听路由变化,动态设置页面样式
|
||||||
|
watch(route, (newRoute) => {
|
||||||
|
console.log('路由变化:', newRoute.name)
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
console.log('App.vue 加载成功')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.with-navbar {
|
||||||
|
padding-top: 0; /* NavBar 是 fixed 定位,不需要 padding-top */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保登录页面全屏显示 */
|
||||||
|
#app .login-page {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 页面特殊样式 ========== */
|
||||||
|
|
||||||
|
/* 欢迎页面 - 彩虹渐变背景 */
|
||||||
|
#app[data-route="Welcome"] {
|
||||||
|
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #feca57, #ff9ff3);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: rainbowShift 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rainbowShift {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首页 - 科技感蓝色渐变 */
|
||||||
|
#app[data-route="Home"] {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="Home"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||||
|
animation: homeGlow 6s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes homeGlow {
|
||||||
|
0% { opacity: 0.3; }
|
||||||
|
100% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 个人主页 - 深色科技风 */
|
||||||
|
#app[data-route="Profile"] {
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="Profile"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 20%, rgba(64, 158, 255, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 90% 80%, rgba(103, 194, 58, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(230, 162, 60, 0.05) 0%, transparent 50%);
|
||||||
|
animation: profileGlow 6s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes profileGlow {
|
||||||
|
0% { opacity: 0.3; }
|
||||||
|
100% { opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单管理 - 商务紫色渐变 */
|
||||||
|
#app[data-route="Orders"] {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="Orders"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
|
||||||
|
animation: ordersPulse 5s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ordersPulse {
|
||||||
|
0% { opacity: 0.3; transform: scale(1); }
|
||||||
|
100% { opacity: 0.6; transform: scale(1.02); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 支付记录 - 金色渐变 */
|
||||||
|
#app[data-route="Payments"] {
|
||||||
|
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="Payments"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 25% 25%, rgba(255, 215, 0, 0.2) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 75%, rgba(255, 165, 0, 0.1) 0%, transparent 50%);
|
||||||
|
animation: paymentShine 4s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes paymentShine {
|
||||||
|
0% { opacity: 0.4; }
|
||||||
|
100% { opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 我的作品 - 创意绿色渐变 */
|
||||||
|
#app[data-route="MyWorks"] {
|
||||||
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="MyWorks"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 40% 60%, rgba(168, 237, 234, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 60% 40%, rgba(254, 214, 227, 0.3) 0%, transparent 50%);
|
||||||
|
animation: worksFloat 7s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes worksFloat {
|
||||||
|
0% { transform: translateY(0px) rotate(0deg); }
|
||||||
|
100% { transform: translateY(-15px) rotate(2deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文生视频 - 蓝色科技风 */
|
||||||
|
#app[data-route="TextToVideo"] {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="TextToVideo"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(102, 126, 234, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.3) 0%, transparent 50%);
|
||||||
|
animation: textVideoFlow 6s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textVideoFlow {
|
||||||
|
0% { opacity: 0.3; }
|
||||||
|
100% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图生视频 - 紫色梦幻风 */
|
||||||
|
#app[data-route="ImageToVideo"] {
|
||||||
|
background: linear-gradient(135deg, #a8c0ff 0%, #3f2b96 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="ImageToVideo"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 30% 30%, rgba(168, 192, 255, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 70% 70%, rgba(63, 43, 150, 0.3) 0%, transparent 50%);
|
||||||
|
animation: imageVideoDream 8s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes imageVideoDream {
|
||||||
|
0% { opacity: 0.2; transform: scale(1); }
|
||||||
|
100% { opacity: 0.6; transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜视频 - 橙色活力风 */
|
||||||
|
#app[data-route="StoryboardVideo"] {
|
||||||
|
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="StoryboardVideo"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 25% 75%, rgba(255, 154, 158, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 25%, rgba(254, 207, 239, 0.3) 0%, transparent 50%);
|
||||||
|
animation: storyboardBounce 5s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes storyboardBounce {
|
||||||
|
0% { opacity: 0.3; transform: translateY(0px); }
|
||||||
|
100% { opacity: 0.7; transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会员订阅 - 奢华金色 */
|
||||||
|
#app[data-route="Subscription"] {
|
||||||
|
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="Subscription"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(255, 215, 0, 0.2) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 20% 80%, rgba(252, 182, 159, 0.3) 0%, transparent 50%);
|
||||||
|
animation: subscriptionLuxury 6s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subscriptionLuxury {
|
||||||
|
0% { opacity: 0.4; }
|
||||||
|
100% { opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 管理员页面 - 深色专业风 */
|
||||||
|
#app[data-route="AdminDashboard"],
|
||||||
|
#app[data-route="AdminOrders"],
|
||||||
|
#app[data-route="AdminUsers"] {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="AdminDashboard"]::before,
|
||||||
|
#app[data-route="AdminOrders"]::before,
|
||||||
|
#app[data-route="AdminUsers"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 10%, rgba(0, 150, 255, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 90% 90%, rgba(255, 0, 150, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(0, 255, 150, 0.05) 0%, transparent 50%);
|
||||||
|
animation: adminTech 8s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes adminTech {
|
||||||
|
0% { opacity: 0.2; }
|
||||||
|
100% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注册页面 - 清新绿色渐变 */
|
||||||
|
#app[data-route="Register"] {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app[data-route="Register"]::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
|
||||||
|
animation: registerFloat 4s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes registerFloat {
|
||||||
|
0% { transform: translateY(0px) rotate(0deg); }
|
||||||
|
100% { transform: translateY(-10px) rotate(1deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容层级确保在所有背景效果之上 */
|
||||||
|
#app main > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
main {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端减少动画效果 */
|
||||||
|
#app[data-route]::before {
|
||||||
|
animation-duration: 10s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 样式覆盖 */
|
||||||
|
.el-button {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
demo/frontend/src/api/auth.js
Normal file
60
demo/frontend/src/api/auth.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 认证相关API
|
||||||
|
export const login = (credentials) => {
|
||||||
|
return api.post('/auth/login', credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const register = (userData) => {
|
||||||
|
return api.post('/auth/register', userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
return api.post('/auth/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCurrentUser = () => {
|
||||||
|
return api.get('/auth/me')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户相关API
|
||||||
|
export const getUsers = (params) => {
|
||||||
|
return api.get('/users', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserById = (id) => {
|
||||||
|
return api.get(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUser = (userData) => {
|
||||||
|
return api.post('/users', userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateUser = (id, userData) => {
|
||||||
|
return api.put(`/users/${id}`, userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteUser = (id) => {
|
||||||
|
return api.delete(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否存在
|
||||||
|
export const checkUsernameExists = (username) => {
|
||||||
|
return api.get(`/public/users/exists/username`, {
|
||||||
|
params: { value: username }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否存在
|
||||||
|
export const checkEmailExists = (email) => {
|
||||||
|
return api.get(`/public/users/exists/email`, {
|
||||||
|
params: { value: email }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
38
demo/frontend/src/api/dashboard.js
Normal file
38
demo/frontend/src/api/dashboard.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
export const dashboardApi = {
|
||||||
|
// 获取仪表盘概览数据
|
||||||
|
getOverview() {
|
||||||
|
return request.get('/dashboard/overview')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取日活数据
|
||||||
|
getDailyActiveUsers() {
|
||||||
|
return request.get('/dashboard/daily-active-users')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取收入趋势数据
|
||||||
|
getRevenueTrend() {
|
||||||
|
return request.get('/dashboard/revenue-trend')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取订单状态分布
|
||||||
|
getOrderStatusDistribution() {
|
||||||
|
return request.get('/dashboard/order-status-distribution')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取支付方式分布
|
||||||
|
getPaymentMethodDistribution() {
|
||||||
|
return request.get('/dashboard/payment-method-distribution')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取最近订单列表
|
||||||
|
getRecentOrders() {
|
||||||
|
return request.get('/dashboard/recent-orders')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取所有仪表盘数据
|
||||||
|
getAllData() {
|
||||||
|
return request.get('/dashboard/all')
|
||||||
|
}
|
||||||
|
}
|
||||||
62
demo/frontend/src/api/orders.js
Normal file
62
demo/frontend/src/api/orders.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 订单相关API
|
||||||
|
export const getOrders = (params) => {
|
||||||
|
return api.get('/orders', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOrderById = (id) => {
|
||||||
|
return api.get(`/orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createOrder = (orderData) => {
|
||||||
|
return api.post('/orders/create', orderData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateOrderStatus = (id, status, notes) => {
|
||||||
|
return api.post(`/orders/${id}/status`, {
|
||||||
|
status,
|
||||||
|
notes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelOrder = (id, reason) => {
|
||||||
|
return api.post(`/orders/${id}/cancel`, {
|
||||||
|
reason
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shipOrder = (id, trackingNumber) => {
|
||||||
|
return api.post(`/orders/${id}/ship`, {
|
||||||
|
trackingNumber
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const completeOrder = (id) => {
|
||||||
|
return api.post(`/orders/${id}/complete`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createOrderPayment = (id, paymentMethod) => {
|
||||||
|
return api.post(`/orders/${id}/pay`, {
|
||||||
|
paymentMethod
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员订单API
|
||||||
|
export const getAdminOrders = (params) => {
|
||||||
|
return api.get('/orders/admin', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单统计API
|
||||||
|
export const getOrderStats = () => {
|
||||||
|
return api.get('/orders/stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
62
demo/frontend/src/api/payments.js
Normal file
62
demo/frontend/src/api/payments.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 支付相关API
|
||||||
|
export const getPayments = (params) => {
|
||||||
|
return api.get('/payments', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPaymentById = (id) => {
|
||||||
|
return api.get(`/payments/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPayment = (paymentData) => {
|
||||||
|
return api.post('/payments/create', paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTestPayment = (paymentData) => {
|
||||||
|
return api.post('/payments/create-test', paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updatePaymentStatus = (id, status) => {
|
||||||
|
return api.put(`/payments/${id}/status`, { status })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmPaymentSuccess = (id, externalTransactionId) => {
|
||||||
|
return api.post(`/payments/${id}/success`, {
|
||||||
|
externalTransactionId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmPaymentFailure = (id, failureReason) => {
|
||||||
|
return api.post(`/payments/${id}/failure`, {
|
||||||
|
failureReason
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试支付完成API
|
||||||
|
export const testPaymentComplete = (id) => {
|
||||||
|
return api.post(`/payments/${id}/test-complete`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付宝支付API
|
||||||
|
export const createAlipayPayment = (paymentData) => {
|
||||||
|
return api.post(`/payments/alipay/create`, paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleAlipayCallback = (params) => {
|
||||||
|
return api.post('/payments/alipay/callback', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayPal支付API
|
||||||
|
export const createPayPalPayment = (paymentData) => {
|
||||||
|
return api.post(`/payments/paypal/create`, paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handlePayPalCallback = (params) => {
|
||||||
|
return api.post('/payment/paypal/callback', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付统计API
|
||||||
|
export const getPaymentStats = () => {
|
||||||
|
return api.get('/payments/stats')
|
||||||
|
}
|
||||||
68
demo/frontend/src/api/request.js
Normal file
68
demo/frontend/src/api/request.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:8080/api',
|
||||||
|
timeout: 10000,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 使用JWT认证,添加Authorization头
|
||||||
|
const token = sessionStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('请求拦截器错误:', error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response) {
|
||||||
|
const { status, data } = error.response
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
ElMessage.error('未授权,请重新登录')
|
||||||
|
sessionStorage.removeItem('token')
|
||||||
|
sessionStorage.removeItem('user')
|
||||||
|
// 使用Vue Router进行路由跳转,避免页面刷新
|
||||||
|
router.push('/login')
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
ElMessage.error('权限不足')
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
ElMessage.error('请求的资源不存在')
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
ElMessage.error('服务器内部错误')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ElMessage.error(data.message || '请求失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('网络错误,请检查网络连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default api
|
||||||
84
demo/frontend/src/components/Footer.vue
Normal file
84
demo/frontend/src/components/Footer.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<el-footer class="footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-info">
|
||||||
|
<p>© 2024 AIGC Demo. All rights reserved.</p>
|
||||||
|
<p>基于 Vue.js 3 + Element Plus 构建</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="#" class="footer-link">关于我们</a>
|
||||||
|
<a href="#" class="footer-link">联系我们</a>
|
||||||
|
<a href="#" class="footer-link">隐私政策</a>
|
||||||
|
<a href="#" class="footer-link">服务条款</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Footer组件逻辑
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
height: 60px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-info {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-info p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: #606266;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
268
demo/frontend/src/components/NavBar.vue
Normal file
268
demo/frontend/src/components/NavBar.vue
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<template>
|
||||||
|
<el-header class="navbar">
|
||||||
|
<div class="navbar-container">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<router-link to="/" class="brand-link">
|
||||||
|
<span class="brand-text">AIGC Demo</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导航菜单 -->
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
class="navbar-menu"
|
||||||
|
background-color="#409EFF"
|
||||||
|
text-color="#fff"
|
||||||
|
active-text-color="#ffd04b"
|
||||||
|
router
|
||||||
|
@select="handleMenuSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/welcome">
|
||||||
|
<span>欢迎页</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/home">
|
||||||
|
<span>首页</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAuthenticated" index="/profile">
|
||||||
|
<span>个人主页</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAuthenticated" index="/orders">
|
||||||
|
<span>订单管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAuthenticated" index="/payments">
|
||||||
|
<span>支付记录</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAdmin" index="/admin/orders">
|
||||||
|
<span>后台管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAdmin" index="/admin/users">
|
||||||
|
<span>用户管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAdmin" index="/admin/dashboard">
|
||||||
|
<span>数据仪表盘</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<!-- 快速切换提示(暂时隐藏) -->
|
||||||
|
<!-- <div class="quick-switch-hint" v-if="showShortcutHint">
|
||||||
|
<el-tooltip content="使用 Alt + 数字键快速切换页面" placement="bottom">
|
||||||
|
<el-icon><Keyboard /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 用户菜单 -->
|
||||||
|
<div class="navbar-user">
|
||||||
|
<template v-if="userStore.isAuthenticated">
|
||||||
|
<el-dropdown @command="handleUserCommand">
|
||||||
|
<span class="user-dropdown">
|
||||||
|
<span>{{ userStore.username }}</span>
|
||||||
|
<el-tag v-if="userStore.user?.points" size="small" type="success" class="points-tag">
|
||||||
|
{{ userStore.user.points }}积分
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="profile">
|
||||||
|
个人资料
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="userStore.isAdmin" command="admin">
|
||||||
|
后台管理
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="settings">
|
||||||
|
设置
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="logout">
|
||||||
|
退出登录
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<el-button type="primary" plain @click="$router.push('/login')">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" plain @click="$router.push('/register')">
|
||||||
|
注册
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 显示快捷键提示(暂时禁用)
|
||||||
|
// const showShortcutHint = ref(true)
|
||||||
|
|
||||||
|
// 快速切换处理函数
|
||||||
|
const handleMenuSelect = (index) => {
|
||||||
|
// 使用replace而不是push,避免浏览器历史记录堆积
|
||||||
|
router.replace(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂时禁用快捷键功能,确保基本功能正常
|
||||||
|
// const handleKeydown = (event) => {
|
||||||
|
// // 快捷键功能暂时禁用
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onMounted(() => {
|
||||||
|
// // 暂时不添加键盘事件监听
|
||||||
|
// })
|
||||||
|
|
||||||
|
// onUnmounted(() => {
|
||||||
|
// // 暂时不移除键盘事件监听
|
||||||
|
// })
|
||||||
|
|
||||||
|
const handleUserCommand = async (command) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'profile':
|
||||||
|
ElMessage.info('个人资料功能开发中')
|
||||||
|
break
|
||||||
|
case 'admin':
|
||||||
|
router.push('/admin/dashboard')
|
||||||
|
break
|
||||||
|
case 'settings':
|
||||||
|
ElMessage.info('设置功能开发中')
|
||||||
|
break
|
||||||
|
case 'logout':
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
await userStore.logoutUser()
|
||||||
|
ElMessage.success('退出登录成功')
|
||||||
|
router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.navbar {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu {
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu .el-menu-item {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu .el-menu-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-user {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-tag {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown .el-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-user .el-button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-switch-hint {
|
||||||
|
margin-right: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-switch-hint:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
27
demo/frontend/src/main.js
Normal file
27
demo/frontend/src/main.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import { useUserStore } from './stores/user'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册Element Plus图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 立即挂载应用
|
||||||
|
app.mount('#app')
|
||||||
229
demo/frontend/src/router/index.js
Normal file
229
demo/frontend/src/router/index.js
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
// 路由组件
|
||||||
|
import Home from '@/views/Home.vue'
|
||||||
|
import Login from '@/views/Login.vue'
|
||||||
|
import Register from '@/views/Register.vue'
|
||||||
|
import Orders from '@/views/Orders.vue'
|
||||||
|
import OrderDetail from '@/views/OrderDetail.vue'
|
||||||
|
import OrderCreate from '@/views/OrderCreate.vue'
|
||||||
|
import Payments from '@/views/Payments.vue'
|
||||||
|
import PaymentCreate from '@/views/PaymentCreate.vue'
|
||||||
|
import AdminOrders from '@/views/AdminOrders.vue'
|
||||||
|
import AdminUsers from '@/views/AdminUsers.vue'
|
||||||
|
import AdminDashboard from '@/views/AdminDashboard.vue'
|
||||||
|
import Dashboard from '@/views/Dashboard.vue'
|
||||||
|
import Welcome from '@/views/Welcome.vue'
|
||||||
|
import Profile from '@/views/Profile.vue'
|
||||||
|
import Subscription from '@/views/Subscription.vue'
|
||||||
|
import MyWorks from '@/views/MyWorks.vue'
|
||||||
|
import VideoDetail from '@/views/VideoDetail.vue'
|
||||||
|
import TextToVideo from '@/views/TextToVideo.vue'
|
||||||
|
import TextToVideoCreate from '@/views/TextToVideoCreate.vue'
|
||||||
|
import ImageToVideo from '@/views/ImageToVideo.vue'
|
||||||
|
import ImageToVideoCreate from '@/views/ImageToVideoCreate.vue'
|
||||||
|
import StoryboardVideo from '@/views/StoryboardVideo.vue'
|
||||||
|
import StoryboardVideoCreate from '@/views/StoryboardVideoCreate.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/works',
|
||||||
|
name: 'MyWorks',
|
||||||
|
component: MyWorks,
|
||||||
|
meta: { title: '我的作品', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/video/:id',
|
||||||
|
name: 'VideoDetail',
|
||||||
|
component: VideoDetail,
|
||||||
|
meta: { title: '视频详情', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/text-to-video',
|
||||||
|
name: 'TextToVideo',
|
||||||
|
component: TextToVideo,
|
||||||
|
meta: { title: '文生视频', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/text-to-video/create',
|
||||||
|
name: 'TextToVideoCreate',
|
||||||
|
component: TextToVideoCreate,
|
||||||
|
meta: { title: '文生视频创作', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/image-to-video',
|
||||||
|
name: 'ImageToVideo',
|
||||||
|
component: ImageToVideo,
|
||||||
|
meta: { title: '图生视频', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/image-to-video/create',
|
||||||
|
name: 'ImageToVideoCreate',
|
||||||
|
component: ImageToVideoCreate,
|
||||||
|
meta: { title: '图生视频创作', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/storyboard-video',
|
||||||
|
name: 'StoryboardVideo',
|
||||||
|
component: StoryboardVideo,
|
||||||
|
meta: { title: '分镜视频', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/storyboard-video/create',
|
||||||
|
name: 'StoryboardVideoCreate',
|
||||||
|
component: StoryboardVideoCreate,
|
||||||
|
meta: { title: '分镜视频创作', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/welcome' // 重定向到欢迎页面
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/welcome',
|
||||||
|
name: 'Welcome',
|
||||||
|
component: Welcome,
|
||||||
|
meta: { title: '欢迎', guest: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home,
|
||||||
|
meta: { title: '首页', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'Profile',
|
||||||
|
component: Profile,
|
||||||
|
meta: { title: '个人主页', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/subscription',
|
||||||
|
name: 'Subscription',
|
||||||
|
component: Subscription,
|
||||||
|
meta: { title: '会员订阅', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: Login,
|
||||||
|
meta: { title: '登录', guest: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'Register',
|
||||||
|
component: Register,
|
||||||
|
meta: { title: '注册', guest: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/orders',
|
||||||
|
name: 'Orders',
|
||||||
|
component: Orders,
|
||||||
|
meta: { title: '订单管理', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/orders/:id',
|
||||||
|
name: 'OrderDetail',
|
||||||
|
component: OrderDetail,
|
||||||
|
meta: { title: '订单详情', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/orders/create',
|
||||||
|
name: 'OrderCreate',
|
||||||
|
component: OrderCreate,
|
||||||
|
meta: { title: '创建订单', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payments',
|
||||||
|
name: 'Payments',
|
||||||
|
component: Payments,
|
||||||
|
meta: { title: '支付记录', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payments/create',
|
||||||
|
name: 'PaymentCreate',
|
||||||
|
component: PaymentCreate,
|
||||||
|
meta: { title: '创建支付', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/orders',
|
||||||
|
name: 'AdminOrders',
|
||||||
|
component: AdminOrders,
|
||||||
|
meta: { title: '订单管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/users',
|
||||||
|
name: 'AdminUsers',
|
||||||
|
component: AdminUsers,
|
||||||
|
meta: { title: '用户管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/dashboard',
|
||||||
|
name: 'AdminDashboard',
|
||||||
|
component: AdminDashboard,
|
||||||
|
meta: { title: '后台管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
// 添加路由缓存配置
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition
|
||||||
|
} else {
|
||||||
|
return { top: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
try {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 初始化用户状态
|
||||||
|
if (!userStore.initialized) {
|
||||||
|
await userStore.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要认证
|
||||||
|
if (to.meta.requiresAuth) {
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
next({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: to.fullPath }
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管理员权限
|
||||||
|
if (to.meta.requiresAdmin && !userStore.isAdmin) {
|
||||||
|
// 权限不足,跳转到首页
|
||||||
|
next('/home')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录用户访问登录页,重定向到首页
|
||||||
|
if (to.meta.guest && userStore.isAuthenticated) {
|
||||||
|
next('/home')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置页面标题
|
||||||
|
if (to.meta.title) {
|
||||||
|
document.title = `${to.meta.title} - AIGC Demo`
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('路由守卫错误:', error)
|
||||||
|
// 发生错误时,允许访问但显示错误信息
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
219
demo/frontend/src/stores/orders.js
Normal file
219
demo/frontend/src/stores/orders.js
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { getOrders, getOrderById, createOrder, updateOrderStatus, cancelOrder, shipOrder, completeOrder } from '@/api/orders'
|
||||||
|
|
||||||
|
export const useOrderStore = defineStore('orders', () => {
|
||||||
|
// 状态
|
||||||
|
const orders = ref([])
|
||||||
|
const currentOrder = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = ref({
|
||||||
|
page: 0,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
const fetchOrders = async (params = {}) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
console.log('OrderStore: 开始获取订单,参数:', params)
|
||||||
|
|
||||||
|
const response = await getOrders(params)
|
||||||
|
console.log('OrderStore: API原始响应:', response)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
orders.value = response.data.content || response.data
|
||||||
|
pagination.value = {
|
||||||
|
page: response.data.number || 0,
|
||||||
|
size: response.data.size || 10,
|
||||||
|
total: response.data.totalElements || response.data.length,
|
||||||
|
totalPages: response.data.totalPages || 1
|
||||||
|
}
|
||||||
|
console.log('OrderStore: 处理后的订单数据:', orders.value)
|
||||||
|
console.log('OrderStore: 分页信息:', pagination.value)
|
||||||
|
} else {
|
||||||
|
console.error('OrderStore: API返回失败:', response.message)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OrderStore: 获取订单异常:', error)
|
||||||
|
return { success: false, message: '获取订单列表失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单详情
|
||||||
|
const fetchOrderById = async (id) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await getOrderById(id)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
currentOrder.value = response.data
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch order error:', error)
|
||||||
|
return { success: false, message: '获取订单详情失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
const createNewOrder = async (orderData) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await createOrder(orderData)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 刷新订单列表
|
||||||
|
await fetchOrders()
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create order error:', error)
|
||||||
|
return { success: false, message: '创建订单失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
const updateOrder = async (id, status, notes) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await updateOrderStatus(id, status, notes)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = status
|
||||||
|
order.updatedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = status
|
||||||
|
currentOrder.value.updatedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update order error:', error)
|
||||||
|
return { success: false, message: '更新订单状态失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消订单
|
||||||
|
const cancelOrderById = async (id, reason) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await cancelOrder(id, reason)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = 'CANCELLED'
|
||||||
|
order.cancelledAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = 'CANCELLED'
|
||||||
|
currentOrder.value.cancelledAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Cancel order error:', error)
|
||||||
|
return { success: false, message: '取消订单失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发货
|
||||||
|
const shipOrderById = async (id, trackingNumber) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await shipOrder(id, trackingNumber)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = 'SHIPPED'
|
||||||
|
order.shippedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = 'SHIPPED'
|
||||||
|
currentOrder.value.shippedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ship order error:', error)
|
||||||
|
return { success: false, message: '发货失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成订单
|
||||||
|
const completeOrderById = async (id) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await completeOrder(id)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = 'COMPLETED'
|
||||||
|
order.deliveredAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = 'COMPLETED'
|
||||||
|
currentOrder.value.deliveredAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Complete order error:', error)
|
||||||
|
return { success: false, message: '完成订单失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
orders,
|
||||||
|
currentOrder,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
// 方法
|
||||||
|
fetchOrders,
|
||||||
|
fetchOrderById,
|
||||||
|
createNewOrder,
|
||||||
|
updateOrder,
|
||||||
|
cancelOrderById,
|
||||||
|
shipOrderById,
|
||||||
|
completeOrderById
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
159
demo/frontend/src/stores/user.js
Normal file
159
demo/frontend/src/stores/user.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { login, register, logout, getCurrentUser } from '@/api/auth'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// 状态 - 从 sessionStorage 尝试恢复用户信息
|
||||||
|
const user = ref(null)
|
||||||
|
const token = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const initialized = ref(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cachedUser = sessionStorage.getItem('user')
|
||||||
|
const cachedToken = sessionStorage.getItem('token')
|
||||||
|
if (cachedUser && cachedToken) {
|
||||||
|
user.value = JSON.parse(cachedUser)
|
||||||
|
token.value = cachedToken
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore sessionStorage parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const isAuthenticated = computed(() => !!user.value)
|
||||||
|
const isAdmin = computed(() => user.value?.role === 'ROLE_ADMIN')
|
||||||
|
const username = computed(() => user.value?.username || '')
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const loginUser = async (credentials) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await login(credentials)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 使用JWT认证,保存token和用户信息
|
||||||
|
user.value = response.data.user
|
||||||
|
token.value = response.data.token
|
||||||
|
|
||||||
|
// 保存到sessionStorage,关闭页面时自动清除
|
||||||
|
sessionStorage.setItem('token', response.data.token)
|
||||||
|
sessionStorage.setItem('user', JSON.stringify(user.value))
|
||||||
|
return { success: true }
|
||||||
|
} else {
|
||||||
|
return { success: false, message: response.message }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
return { success: false, message: '登录失败,请检查网络连接' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
const registerUser = async (userData) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await register(userData)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { success: true, message: '注册成功,请登录' }
|
||||||
|
} else {
|
||||||
|
return { success: false, message: response.message }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register error:', error)
|
||||||
|
return { success: false, message: '注册失败,请检查网络连接' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
const logoutUser = async () => {
|
||||||
|
try {
|
||||||
|
// JWT无状态,直接清除sessionStorage即可
|
||||||
|
token.value = null
|
||||||
|
user.value = null
|
||||||
|
sessionStorage.removeItem('token')
|
||||||
|
sessionStorage.removeItem('user')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
const fetchCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getCurrentUser()
|
||||||
|
if (response.success) {
|
||||||
|
user.value = response.data
|
||||||
|
sessionStorage.setItem('user', JSON.stringify(user.value))
|
||||||
|
} else {
|
||||||
|
// 会话无效,清除本地存储
|
||||||
|
clearUserData()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch user error:', error)
|
||||||
|
// 请求失败时不强制清除,保持现有本地态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除用户数据
|
||||||
|
const clearUserData = () => {
|
||||||
|
token.value = null
|
||||||
|
user.value = null
|
||||||
|
// 清除 sessionStorage 中的用户数据
|
||||||
|
sessionStorage.removeItem('token')
|
||||||
|
sessionStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
const init = async () => {
|
||||||
|
if (initialized.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从sessionStorage恢复用户状态
|
||||||
|
const savedToken = sessionStorage.getItem('token')
|
||||||
|
const savedUser = sessionStorage.getItem('user')
|
||||||
|
|
||||||
|
if (savedToken && savedUser) {
|
||||||
|
try {
|
||||||
|
token.value = savedToken
|
||||||
|
user.value = JSON.parse(savedUser)
|
||||||
|
|
||||||
|
// 只在开发环境输出详细日志
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('恢复用户状态:', user.value?.username, '角色:', user.value?.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore user state:', error)
|
||||||
|
clearUserData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
loading,
|
||||||
|
// 计算属性
|
||||||
|
isAuthenticated,
|
||||||
|
isAdmin,
|
||||||
|
username,
|
||||||
|
// 方法
|
||||||
|
loginUser,
|
||||||
|
registerUser,
|
||||||
|
logoutUser,
|
||||||
|
fetchCurrentUser,
|
||||||
|
clearUserData,
|
||||||
|
init,
|
||||||
|
initialized
|
||||||
|
}
|
||||||
|
})
|
||||||
556
demo/frontend/src/views/AdminDashboard.vue
Normal file
556
demo/frontend/src/views/AdminDashboard.vue
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-dashboard">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-text">LOGO</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
<span>数据仪表台</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToUsers">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>会员管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToOrders">
|
||||||
|
<el-icon><ShoppingCart /></el-icon>
|
||||||
|
<span>订单管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>API管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Briefcase /></el-icon>
|
||||||
|
<span>生成任务记录</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>系统设置</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="online-users">
|
||||||
|
<span>当前在线用户: </span>
|
||||||
|
<span class="online-count">87/500</span>
|
||||||
|
</div>
|
||||||
|
<div class="system-uptime">
|
||||||
|
<span>系统运行时间: 48小时32分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部搜索栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-icon class="search-icon"><Search /></el-icon>
|
||||||
|
<input type="text" placeholder="搜索你的想要的内容" class="search-input">
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="notification-icon">
|
||||||
|
<el-icon><Bell /></el-icon>
|
||||||
|
<div class="notification-badge"></div>
|
||||||
|
</div>
|
||||||
|
<div class="user-avatar">
|
||||||
|
<el-icon><Avatar /></el-icon>
|
||||||
|
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-cards">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon users">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">用户总数</div>
|
||||||
|
<div class="stat-number">12,847</div>
|
||||||
|
<div class="stat-change positive">+12% 较上月同期</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon paid-users">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">付费用户数</div>
|
||||||
|
<div class="stat-number">3,215</div>
|
||||||
|
<div class="stat-change negative">-5% 较上月同期</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon revenue">
|
||||||
|
<el-icon><Money /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">今日收入</div>
|
||||||
|
<div class="stat-number">¥28,450</div>
|
||||||
|
<div class="stat-change positive">+15% 较上月同期</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="charts-section">
|
||||||
|
<!-- 日活用户趋势 -->
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3>日活用户趋势</h3>
|
||||||
|
<el-select v-model="selectedYear" class="year-select">
|
||||||
|
<el-option label="2025年" value="2025"></el-option>
|
||||||
|
<el-option label="2024年" value="2024"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="chart-content">
|
||||||
|
<div class="chart-placeholder">
|
||||||
|
<div class="chart-title">日活用户趋势图</div>
|
||||||
|
<div class="chart-description">显示每日活跃用户数量变化趋势</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户转化率 -->
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3>用户转化率</h3>
|
||||||
|
<el-select v-model="selectedYear2" class="year-select">
|
||||||
|
<el-option label="2025年" value="2025"></el-option>
|
||||||
|
<el-option label="2024年" value="2024"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="chart-content">
|
||||||
|
<div class="chart-placeholder">
|
||||||
|
<div class="chart-title">用户转化率图</div>
|
||||||
|
<div class="chart-description">显示各月份用户转化率情况</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
User,
|
||||||
|
ShoppingCart,
|
||||||
|
Document,
|
||||||
|
Briefcase,
|
||||||
|
Setting,
|
||||||
|
Search,
|
||||||
|
Bell,
|
||||||
|
Avatar,
|
||||||
|
ArrowDown,
|
||||||
|
Money
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 年份选择
|
||||||
|
const selectedYear = ref('2025')
|
||||||
|
const selectedYear2 = ref('2025')
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToUsers = () => {
|
||||||
|
router.push('/admin/users')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToOrders = () => {
|
||||||
|
router.push('/admin/orders')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('后台管理页面加载完成')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-dashboard {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-right: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 24px 20px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: 4px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-users {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-count {
|
||||||
|
color: #3b82f6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-uptime {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部搜索栏 */
|
||||||
|
.top-header {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 300px;
|
||||||
|
padding: 10px 12px 10px 40px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #ef4444;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-down {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stats-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.users {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.paid-users {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.revenue {
|
||||||
|
background: #fce7f3;
|
||||||
|
color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change.positive {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change.negative {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表区域 */
|
||||||
|
.charts-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-select {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
padding: 24px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-placeholder {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px dashed #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.charts-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-dashboard {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-section {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.stat-card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
padding: 16px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
784
demo/frontend/src/views/AdminOrders.vue
Normal file
784
demo/frontend/src/views/AdminOrders.vue
Normal file
@@ -0,0 +1,784 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-orders">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>
|
||||||
|
<el-icon><Management /></el-icon>
|
||||||
|
订单管理 - 管理员
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计面板 -->
|
||||||
|
<el-row :gutter="20" class="stats-row">
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('all')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.totalOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">总订单数</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#409EFF"><List /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('PENDING')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.pendingOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">待支付</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#E6A23C"><Clock /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('COMPLETED')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.completedOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">已完成</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#67C23A"><Check /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('today')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.todayOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">今日订单</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#F56C6C"><Calendar /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 第二行统计卡片 -->
|
||||||
|
<el-row :gutter="20" class="stats-row" style="margin-top: 20px;">
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('PAID')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.paidOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">已支付</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#409EFF"><CreditCard /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('PROCESSING')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.processingOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">处理中</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#909399"><Loading /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 只有存在实体商品时才显示发货统计 -->
|
||||||
|
<el-col v-if="hasPhysicalOrders" :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('SHIPPED')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.shippedOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">已发货</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#67C23A"><Truck /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 如果没有实体商品,显示已退款统计 -->
|
||||||
|
<el-col v-else :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('REFUNDED')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.refundedOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">已退款</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#909399"><Money /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('CANCELLED')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.cancelledOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">已取消</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#F56C6C"><Close /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.status"
|
||||||
|
placeholder="选择订单状态"
|
||||||
|
clearable
|
||||||
|
@change="handleFilterChange"
|
||||||
|
>
|
||||||
|
<el-option label="全部状态" value="" />
|
||||||
|
<el-option label="待支付" value="PENDING" />
|
||||||
|
<el-option label="已确认" value="CONFIRMED" />
|
||||||
|
<el-option label="已支付" value="PAID" />
|
||||||
|
<el-option label="处理中" value="PROCESSING" />
|
||||||
|
<el-option label="已发货" value="SHIPPED" />
|
||||||
|
<el-option label="已送达" value="DELIVERED" />
|
||||||
|
<el-option label="已完成" value="COMPLETED" />
|
||||||
|
<el-option label="已取消" value="CANCELLED" />
|
||||||
|
<el-option label="已退款" value="REFUNDED" />
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.search"
|
||||||
|
placeholder="搜索订单号或用户名"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-button @click="resetFilters">重置筛选</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<el-card class="orders-card">
|
||||||
|
<el-table
|
||||||
|
:data="orders"
|
||||||
|
v-loading="loading"
|
||||||
|
empty-text="暂无订单"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
|
>
|
||||||
|
<el-table-column prop="orderNumber" label="订单号" width="150" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<router-link :to="`/orders/${row.id}`" class="order-link">
|
||||||
|
{{ row.orderNumber }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="user.username" label="用户" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="24">{{ row.user.username.charAt(0).toUpperCase() }}</el-avatar>
|
||||||
|
<span class="username">{{ row.user.username }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="totalAmount" label="金额" width="120" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="amount">{{ row.currency }} {{ row.totalAmount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="orderType" label="类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getOrderTypeText(row.orderType) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" width="160" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" @click="$router.push(`/orders/${row.id}`)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-dropdown trigger="click" :teleported="true" popper-class="table-dropdown" @command="(command) => handleAdminAction(row, command)">
|
||||||
|
<el-button size="small" type="primary">
|
||||||
|
管理<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-if="canShip(row)" command="ship">
|
||||||
|
<el-icon><Truck /></el-icon>
|
||||||
|
发货
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="canComplete(row)" command="complete">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
完成订单
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="canCancel(row)" command="cancel">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
取消订单
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="updateStatus">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
更新状态
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 发货对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="shipDialogVisible"
|
||||||
|
title="订单发货"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
<el-form :model="shipForm" label-width="80px">
|
||||||
|
<el-form-item label="物流单号">
|
||||||
|
<el-input
|
||||||
|
v-model="shipForm.trackingNumber"
|
||||||
|
placeholder="请输入物流单号(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="shipDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmShip">确认发货</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 更新状态对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="statusDialogVisible"
|
||||||
|
title="更新订单状态"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
<el-form :model="statusForm" label-width="80px">
|
||||||
|
<el-form-item label="新状态">
|
||||||
|
<el-select v-model="statusForm.status" placeholder="选择新状态">
|
||||||
|
<el-option label="待支付" value="PENDING" />
|
||||||
|
<el-option label="已确认" value="CONFIRMED" />
|
||||||
|
<el-option label="已支付" value="PAID" />
|
||||||
|
<el-option label="处理中" value="PROCESSING" />
|
||||||
|
|
||||||
|
<!-- 实体商品才显示发货相关状态 -->
|
||||||
|
<template v-if="isPhysicalOrder(currentStatusOrder)">
|
||||||
|
<el-option label="已发货" value="SHIPPED" />
|
||||||
|
<el-option label="已送达" value="DELIVERED" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-option label="已完成" value="COMPLETED" />
|
||||||
|
<el-option label="已取消" value="CANCELLED" />
|
||||||
|
<el-option label="已退款" value="REFUNDED" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="statusForm.notes"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入备注(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmUpdateStatus">确认更新</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getOrders, getOrderStats } from '@/api/orders'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const orders = ref([])
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = ref({
|
||||||
|
totalOrders: 0,
|
||||||
|
pendingOrders: 0,
|
||||||
|
completedOrders: 0,
|
||||||
|
todayOrders: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const filters = reactive({
|
||||||
|
status: '',
|
||||||
|
search: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
const sortBy = ref('createdAt')
|
||||||
|
const sortDir = ref('desc')
|
||||||
|
|
||||||
|
// 发货对话框
|
||||||
|
const shipDialogVisible = ref(false)
|
||||||
|
const shipForm = reactive({
|
||||||
|
trackingNumber: ''
|
||||||
|
})
|
||||||
|
const currentShipOrder = ref(null)
|
||||||
|
|
||||||
|
// 状态更新对话框
|
||||||
|
const statusDialogVisible = ref(false)
|
||||||
|
const statusForm = reactive({
|
||||||
|
status: '',
|
||||||
|
notes: ''
|
||||||
|
})
|
||||||
|
const currentStatusOrder = ref(null)
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'CONFIRMED': 'info',
|
||||||
|
'PAID': 'primary',
|
||||||
|
'PROCESSING': '',
|
||||||
|
'SHIPPED': 'success',
|
||||||
|
'DELIVERED': 'success',
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'CANCELLED': 'danger',
|
||||||
|
'REFUNDED': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'CONFIRMED': '已确认',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'PROCESSING': '处理中',
|
||||||
|
'SHIPPED': '已发货',
|
||||||
|
'DELIVERED': '已送达',
|
||||||
|
'COMPLETED': '已完成',
|
||||||
|
'CANCELLED': '已取消',
|
||||||
|
'REFUNDED': '已退款'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单类型文本
|
||||||
|
const getOrderTypeText = (orderType) => {
|
||||||
|
const typeMap = {
|
||||||
|
'PRODUCT': '商品订单',
|
||||||
|
'SERVICE': '服务订单',
|
||||||
|
'SUBSCRIPTION': '订阅订单',
|
||||||
|
'DIGITAL': '数字商品',
|
||||||
|
'PHYSICAL': '实体商品'
|
||||||
|
}
|
||||||
|
return typeMap[orderType] || orderType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以发货(仅限实体商品)
|
||||||
|
const canShip = (order) => {
|
||||||
|
// 只有实体商品才需要发货
|
||||||
|
const physicalOrderTypes = ['PRODUCT', 'PHYSICAL']
|
||||||
|
return physicalOrderTypes.includes(order.orderType) &&
|
||||||
|
(order.status === 'PAID' || order.status === 'CONFIRMED')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以完成
|
||||||
|
const canComplete = (order) => {
|
||||||
|
// 实体商品需要先发货才能完成
|
||||||
|
if (['PRODUCT', 'PHYSICAL'].includes(order.orderType)) {
|
||||||
|
return order.status === 'SHIPPED'
|
||||||
|
}
|
||||||
|
// 虚拟商品支付后可以直接完成
|
||||||
|
return ['PAID', 'CONFIRMED'].includes(order.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以取消
|
||||||
|
const canCancel = (order) => {
|
||||||
|
return order.status === 'PENDING' || order.status === 'CONFIRMED'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为实体商品
|
||||||
|
const isPhysicalOrder = (order) => {
|
||||||
|
if (!order) return false
|
||||||
|
const physicalOrderTypes = ['PRODUCT', 'PHYSICAL']
|
||||||
|
return physicalOrderTypes.includes(order.orderType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有实体商品订单
|
||||||
|
const hasPhysicalOrders = computed(() => {
|
||||||
|
return orders.value.some(order => isPhysicalOrder(order))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理统计卡片点击
|
||||||
|
const handleStatClick = (type) => {
|
||||||
|
if (type === 'all') {
|
||||||
|
// 显示所有订单
|
||||||
|
filters.status = ''
|
||||||
|
filters.search = ''
|
||||||
|
} else if (type === 'today') {
|
||||||
|
// 显示今日订单(这里可以添加日期筛选逻辑)
|
||||||
|
filters.status = ''
|
||||||
|
filters.search = ''
|
||||||
|
// 可以添加日期筛选逻辑
|
||||||
|
} else {
|
||||||
|
// 按状态筛选
|
||||||
|
filters.status = type
|
||||||
|
filters.search = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置分页并重新加载数据
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 调用真实API获取订单数据
|
||||||
|
const response = await getOrders({
|
||||||
|
page: pagination.page - 1,
|
||||||
|
size: pagination.size,
|
||||||
|
status: filters.status,
|
||||||
|
search: filters.search
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
orders.value = response.data.content || []
|
||||||
|
pagination.total = response.data.totalElements || 0
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取订单列表失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计数据
|
||||||
|
const statsResponse = await getOrderStats()
|
||||||
|
if (statsResponse.success) {
|
||||||
|
stats.value = statsResponse.data
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch orders error:', error)
|
||||||
|
ElMessage.error('获取订单列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选变化
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilters = () => {
|
||||||
|
filters.status = ''
|
||||||
|
filters.search = ''
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序变化
|
||||||
|
const handleSortChange = ({ prop, order }) => {
|
||||||
|
if (prop) {
|
||||||
|
sortBy.value = prop
|
||||||
|
sortDir.value = order === 'ascending' ? 'asc' : 'desc'
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理管理员操作
|
||||||
|
const handleAdminAction = (order, command) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'ship':
|
||||||
|
currentShipOrder.value = order
|
||||||
|
shipForm.trackingNumber = ''
|
||||||
|
shipDialogVisible.value = true
|
||||||
|
break
|
||||||
|
case 'complete':
|
||||||
|
handleCompleteOrder(order)
|
||||||
|
break
|
||||||
|
case 'cancel':
|
||||||
|
handleCancelOrder(order)
|
||||||
|
break
|
||||||
|
case 'updateStatus':
|
||||||
|
currentStatusOrder.value = order
|
||||||
|
statusForm.status = order.status
|
||||||
|
statusForm.notes = ''
|
||||||
|
statusDialogVisible.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成订单
|
||||||
|
const handleCompleteOrder = async (order) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要完成此订单吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('订单完成成功')
|
||||||
|
fetchOrders()
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
const updateStats = () => {
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
stats.value = {
|
||||||
|
totalOrders: orders.value.length,
|
||||||
|
pendingOrders: orders.value.filter(order => order.status === 'PENDING').length,
|
||||||
|
paidOrders: orders.value.filter(order => order.status === 'PAID').length,
|
||||||
|
processingOrders: orders.value.filter(order => order.status === 'PROCESSING').length,
|
||||||
|
shippedOrders: orders.value.filter(order => order.status === 'SHIPPED').length,
|
||||||
|
completedOrders: orders.value.filter(order => order.status === 'COMPLETED').length,
|
||||||
|
cancelledOrders: orders.value.filter(order => order.status === 'CANCELLED').length,
|
||||||
|
refundedOrders: orders.value.filter(order => order.status === 'REFUNDED').length,
|
||||||
|
todayOrders: orders.value.filter(order => order.createdAt.startsWith(today)).length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消订单
|
||||||
|
const handleCancelOrder = async (order) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要取消此订单吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新订单状态为已取消
|
||||||
|
const orderIndex = orders.value.findIndex(o => o.id === order.id)
|
||||||
|
if (orderIndex !== -1) {
|
||||||
|
orders.value[orderIndex].status = 'CANCELLED'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
updateStats()
|
||||||
|
|
||||||
|
ElMessage.success('订单取消成功')
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认发货
|
||||||
|
const confirmShip = async () => {
|
||||||
|
if (!currentShipOrder.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
ElMessage.success('发货成功')
|
||||||
|
shipDialogVisible.value = false
|
||||||
|
fetchOrders()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('发货失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认更新状态
|
||||||
|
const confirmUpdateStatus = async () => {
|
||||||
|
if (!currentStatusOrder.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新订单状态
|
||||||
|
const orderIndex = orders.value.findIndex(o => o.id === currentStatusOrder.value.id)
|
||||||
|
if (orderIndex !== -1) {
|
||||||
|
orders.value[orderIndex].status = statusForm.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
updateStats()
|
||||||
|
|
||||||
|
ElMessage.success('状态更新成功')
|
||||||
|
statusDialogVisible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('状态更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-orders {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.clickable:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保表格内下拉菜单不被裁剪/遮挡 */
|
||||||
|
:deep(.table-dropdown) {
|
||||||
|
z-index: 3000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
745
demo/frontend/src/views/AdminUsers.vue
Normal file
745
demo/frontend/src/views/AdminUsers.vue
Normal file
@@ -0,0 +1,745 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-users">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
用户管理 - 管理员
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计面板 -->
|
||||||
|
<el-row :gutter="20" class="stats-row">
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('all')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.totalUsers || 0 }}</div>
|
||||||
|
<div class="stat-label">总用户数</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#409EFF"><User /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('admin')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.adminUsers || 0 }}</div>
|
||||||
|
<div class="stat-label">管理员</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#67C23A"><UserFilled /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('user')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.normalUsers || 0 }}</div>
|
||||||
|
<div class="stat-label">普通用户</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#E6A23C"><Avatar /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card clickable" @click="handleStatClick('today')">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.todayUsers || 0 }}</div>
|
||||||
|
<div class="stat-label">今日注册</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#F56C6C"><Calendar /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.role"
|
||||||
|
placeholder="选择用户角色"
|
||||||
|
clearable
|
||||||
|
@change="handleFilterChange"
|
||||||
|
>
|
||||||
|
<el-option label="全部角色" value="" />
|
||||||
|
<el-option label="管理员" value="ROLE_ADMIN" />
|
||||||
|
<el-option label="普通用户" value="ROLE_USER" />
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.search"
|
||||||
|
placeholder="搜索用户名或邮箱"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-button @click="resetFilters">重置筛选</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 用户列表 -->
|
||||||
|
<el-card class="users-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>用户列表</span>
|
||||||
|
<el-button type="primary" @click="showCreateUserDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加用户
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="users"
|
||||||
|
v-loading="loading"
|
||||||
|
empty-text="暂无用户"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
|
||||||
|
|
||||||
|
<el-table-column prop="username" label="用户名" width="150" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="32">{{ row.username.charAt(0).toUpperCase() }}</el-avatar>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="username">{{ row.username }}</div>
|
||||||
|
<div class="user-id">ID: {{ row.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="email" label="邮箱" min-width="200" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="email">{{ row.email }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="role" label="角色" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getRoleType(row.role)">
|
||||||
|
{{ getRoleText(row.role) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createdAt" label="注册时间" width="160" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="lastLoginAt" label="最后登录" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.lastLoginAt ? formatDate(row.lastLoginAt) : '从未登录' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" @click="viewUserDetail(row)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button size="small" type="primary" @click="editUser(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="deleteUser(row)"
|
||||||
|
:disabled="row.role === 'ROLE_ADMIN'"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑用户对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="userDialogVisible"
|
||||||
|
:title="isEdit ? '编辑用户' : '添加用户'"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="userFormRef"
|
||||||
|
:model="userForm"
|
||||||
|
:rules="userRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
:disabled="isEdit"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password" v-if="!isEdit">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="角色" prop="role">
|
||||||
|
<el-radio-group v-model="userForm.role">
|
||||||
|
<el-radio value="ROLE_USER">普通用户</el-radio>
|
||||||
|
<el-radio value="ROLE_ADMIN">管理员</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="userDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmitUser" :loading="submitLoading">
|
||||||
|
{{ isEdit ? '更新' : '创建' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="用户详情"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<div v-if="currentUser">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="用户ID">{{ currentUser.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户名">{{ currentUser.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">{{ currentUser.email }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="角色">
|
||||||
|
<el-tag :type="getRoleType(currentUser.role)">
|
||||||
|
{{ getRoleText(currentUser.role) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="注册时间">{{ formatDate(currentUser.createdAt) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后登录" v-if="currentUser.lastLoginAt">
|
||||||
|
{{ formatDate(currentUser.lastLoginAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const users = ref([])
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = ref({
|
||||||
|
totalUsers: 0,
|
||||||
|
adminUsers: 0,
|
||||||
|
normalUsers: 0,
|
||||||
|
todayUsers: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const filters = reactive({
|
||||||
|
role: '',
|
||||||
|
search: '',
|
||||||
|
todayOnly: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
const sortBy = ref('createdAt')
|
||||||
|
const sortDir = ref('desc')
|
||||||
|
|
||||||
|
// 用户对话框
|
||||||
|
const userDialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const userFormRef = ref()
|
||||||
|
const userForm = reactive({
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
role: 'ROLE_USER'
|
||||||
|
})
|
||||||
|
|
||||||
|
const userRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
role: [
|
||||||
|
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户详情对话框
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentUser = ref(null)
|
||||||
|
|
||||||
|
// 获取角色类型
|
||||||
|
const getRoleType = (role) => {
|
||||||
|
return role === 'ROLE_ADMIN' ? 'danger' : 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色文本
|
||||||
|
const getRoleText = (role) => {
|
||||||
|
return role === 'ROLE_ADMIN' ? '管理员' : '普通用户'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
const mockUsers = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
role: 'ROLE_ADMIN',
|
||||||
|
createdAt: '2024-01-01T10:00:00Z',
|
||||||
|
lastLoginAt: '2024-01-01T15:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: 'user1',
|
||||||
|
email: 'user1@example.com',
|
||||||
|
role: 'ROLE_USER',
|
||||||
|
createdAt: '2024-01-01T11:00:00Z',
|
||||||
|
lastLoginAt: '2024-01-01T14:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
username: 'user2',
|
||||||
|
email: 'user2@example.com',
|
||||||
|
role: 'ROLE_USER',
|
||||||
|
createdAt: '2024-01-01T12:00:00Z',
|
||||||
|
lastLoginAt: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
username: 'admin2',
|
||||||
|
email: 'admin2@example.com',
|
||||||
|
role: 'ROLE_ADMIN',
|
||||||
|
createdAt: '2024-01-01T13:00:00Z',
|
||||||
|
lastLoginAt: '2024-01-01T16:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
username: 'user3',
|
||||||
|
email: 'user3@example.com',
|
||||||
|
role: 'ROLE_USER',
|
||||||
|
createdAt: '2024-01-01T14:00:00Z',
|
||||||
|
lastLoginAt: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
username: 'newuser1',
|
||||||
|
email: 'newuser1@example.com',
|
||||||
|
role: 'ROLE_USER',
|
||||||
|
createdAt: `${today}T10:00:00Z`,
|
||||||
|
lastLoginAt: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
username: 'newuser2',
|
||||||
|
email: 'newuser2@example.com',
|
||||||
|
role: 'ROLE_USER',
|
||||||
|
createdAt: `${today}T11:00:00Z`,
|
||||||
|
lastLoginAt: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
username: 'newadmin',
|
||||||
|
email: 'newadmin@example.com',
|
||||||
|
role: 'ROLE_ADMIN',
|
||||||
|
createdAt: `${today}T12:00:00Z`,
|
||||||
|
lastLoginAt: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 根据筛选条件过滤用户
|
||||||
|
let filteredUsers = mockUsers
|
||||||
|
|
||||||
|
// 按角色筛选
|
||||||
|
if (filters.role) {
|
||||||
|
filteredUsers = filteredUsers.filter(user => user.role === filters.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按搜索关键词筛选
|
||||||
|
if (filters.search) {
|
||||||
|
const searchLower = filters.search.toLowerCase()
|
||||||
|
filteredUsers = filteredUsers.filter(user =>
|
||||||
|
user.username.toLowerCase().includes(searchLower) ||
|
||||||
|
user.email.toLowerCase().includes(searchLower)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按今日注册筛选(模拟)
|
||||||
|
if (filters.todayOnly) {
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
filteredUsers = filteredUsers.filter(user =>
|
||||||
|
user.createdAt.startsWith(today)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
users.value = filteredUsers
|
||||||
|
pagination.total = filteredUsers.length
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
stats.value = {
|
||||||
|
totalUsers: mockUsers.length,
|
||||||
|
adminUsers: mockUsers.filter(user => user.role === 'ROLE_ADMIN').length,
|
||||||
|
normalUsers: mockUsers.filter(user => user.role === 'ROLE_USER').length,
|
||||||
|
todayUsers: mockUsers.filter(user => {
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
return user.createdAt.startsWith(today)
|
||||||
|
}).length
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch users error:', error)
|
||||||
|
ElMessage.error('获取用户列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选变化
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilters = () => {
|
||||||
|
filters.role = ''
|
||||||
|
filters.search = ''
|
||||||
|
pagination.page = 1
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序变化
|
||||||
|
const handleSortChange = ({ prop, order }) => {
|
||||||
|
if (prop) {
|
||||||
|
sortBy.value = prop
|
||||||
|
sortDir.value = order === 'ascending' ? 'asc' : 'desc'
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
pagination.page = 1
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建用户对话框
|
||||||
|
const showCreateUserDialog = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
resetUserForm()
|
||||||
|
userDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
const editUser = (user) => {
|
||||||
|
isEdit.value = true
|
||||||
|
userForm.username = user.username
|
||||||
|
userForm.email = user.email
|
||||||
|
userForm.role = user.role
|
||||||
|
userForm.password = ''
|
||||||
|
userDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置用户表单
|
||||||
|
const resetUserForm = () => {
|
||||||
|
userForm.username = ''
|
||||||
|
userForm.email = ''
|
||||||
|
userForm.password = ''
|
||||||
|
userForm.role = 'ROLE_USER'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交用户表单
|
||||||
|
const handleSubmitUser = async () => {
|
||||||
|
if (!userFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await userFormRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
|
||||||
|
// 模拟提交
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
ElMessage.success(isEdit.value ? '用户更新成功' : '用户创建成功')
|
||||||
|
userDialogVisible.value = false
|
||||||
|
fetchUsers()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Submit user error:', error)
|
||||||
|
ElMessage.error(isEdit.value ? '用户更新失败' : '用户创建失败')
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看用户详情
|
||||||
|
const viewUserDetail = (user) => {
|
||||||
|
currentUser.value = user
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const deleteUser = async (user) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除用户 "${user.username}" 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('用户删除成功')
|
||||||
|
fetchUsers()
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理统计卡片点击事件
|
||||||
|
const handleStatClick = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'all':
|
||||||
|
// 显示所有用户
|
||||||
|
filters.role = ''
|
||||||
|
filters.search = ''
|
||||||
|
filters.todayOnly = false
|
||||||
|
ElMessage.info('显示所有用户')
|
||||||
|
break
|
||||||
|
case 'admin':
|
||||||
|
// 筛选管理员用户
|
||||||
|
filters.role = 'ROLE_ADMIN'
|
||||||
|
filters.search = ''
|
||||||
|
filters.todayOnly = false
|
||||||
|
ElMessage.info('筛选管理员用户')
|
||||||
|
break
|
||||||
|
case 'user':
|
||||||
|
// 筛选普通用户
|
||||||
|
filters.role = 'ROLE_USER'
|
||||||
|
filters.search = ''
|
||||||
|
filters.todayOnly = false
|
||||||
|
ElMessage.info('筛选普通用户')
|
||||||
|
break
|
||||||
|
case 'today':
|
||||||
|
// 筛选今日注册用户
|
||||||
|
filters.role = ''
|
||||||
|
filters.search = ''
|
||||||
|
filters.todayOnly = true
|
||||||
|
ElMessage.info('筛选今日注册用户')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新获取用户列表
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-users {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.clickable:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
621
demo/frontend/src/views/Dashboard.vue
Normal file
621
demo/frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h1>数据仪表盘</h1>
|
||||||
|
<p class="dashboard-subtitle">系统数据概览与关键指标监控</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 概览卡片 -->
|
||||||
|
<div class="overview-cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon users">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>{{ overviewData.totalUsers || 0 }}</h3>
|
||||||
|
<p>用户总数</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon paying">
|
||||||
|
<i class="fas fa-credit-card"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>{{ overviewData.payingUsers || 0 }}</h3>
|
||||||
|
<p>付费用户数</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon revenue">
|
||||||
|
<i class="fas fa-dollar-sign"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>¥{{ formatNumber(overviewData.todayRevenue || 0) }}</h3>
|
||||||
|
<p>今日收入</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon conversion">
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>{{ overviewData.conversionRate || 0 }}%</h3>
|
||||||
|
<p>转化率</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="charts-section">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>日活用户趋势</h3>
|
||||||
|
<div class="chart" ref="dailyActiveChart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>收入趋势</h3>
|
||||||
|
<div class="chart" ref="revenueChart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分布图表 -->
|
||||||
|
<div class="distribution-section">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>订单状态分布</h3>
|
||||||
|
<div class="chart" ref="orderStatusChart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>支付方式分布</h3>
|
||||||
|
<div class="chart" ref="paymentMethodChart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近订单 -->
|
||||||
|
<div class="recent-orders">
|
||||||
|
<h3>最近订单</h3>
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>订单号</th>
|
||||||
|
<th>用户</th>
|
||||||
|
<th>金额</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="order in recentOrders" :key="order.id">
|
||||||
|
<td>{{ order.orderNumber }}</td>
|
||||||
|
<td>{{ order.username }}</td>
|
||||||
|
<td>¥{{ formatNumber(order.totalAmount) }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge" :class="getStatusClass(order.status)">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ formatDate(order.createdAt) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { dashboardApi } from '@/api/dashboard'
|
||||||
|
|
||||||
|
// 动态加载ECharts
|
||||||
|
const loadECharts = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (window.echarts) {
|
||||||
|
resolve(window.echarts)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'
|
||||||
|
script.onload = () => resolve(window.echarts)
|
||||||
|
script.onerror = reject
|
||||||
|
document.head.appendChild(script)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Dashboard',
|
||||||
|
setup() {
|
||||||
|
const overviewData = ref({})
|
||||||
|
const dailyActiveData = ref([])
|
||||||
|
const revenueData = ref([])
|
||||||
|
const orderStatusData = ref([])
|
||||||
|
const paymentMethodData = ref([])
|
||||||
|
const recentOrders = ref([])
|
||||||
|
|
||||||
|
const dailyActiveChart = ref(null)
|
||||||
|
const revenueChart = ref(null)
|
||||||
|
const orderStatusChart = ref(null)
|
||||||
|
const paymentMethodChart = ref(null)
|
||||||
|
|
||||||
|
// 加载仪表盘数据
|
||||||
|
const loadDashboardData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await dashboardApi.getAllData()
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
overviewData.value = data.overview
|
||||||
|
dailyActiveData.value = data.dailyActiveUsers.dailyData || []
|
||||||
|
revenueData.value = data.revenueTrend.revenueData || []
|
||||||
|
orderStatusData.value = data.orderStatusDistribution.statusData || []
|
||||||
|
paymentMethodData.value = data.paymentMethodDistribution.methodData || []
|
||||||
|
recentOrders.value = data.recentOrders.orders || []
|
||||||
|
|
||||||
|
// 等待DOM更新后初始化图表
|
||||||
|
await nextTick()
|
||||||
|
await initCharts()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载仪表盘数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
const initCharts = async () => {
|
||||||
|
try {
|
||||||
|
const echarts = await loadECharts()
|
||||||
|
await initDailyActiveChart(echarts)
|
||||||
|
await initRevenueChart(echarts)
|
||||||
|
await initOrderStatusChart(echarts)
|
||||||
|
await initPaymentMethodChart(echarts)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载ECharts失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日活用户图表
|
||||||
|
const initDailyActiveChart = async (echarts) => {
|
||||||
|
if (!dailyActiveChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(dailyActiveChart.value)
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: dailyActiveData.value.map(item => item.date)
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
data: dailyActiveData.value.map(item => item.activeUsers),
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 0.3
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: '#4CAF50'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#4CAF50'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收入趋势图表
|
||||||
|
const initRevenueChart = async (echarts) => {
|
||||||
|
if (!revenueChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(revenueChart.value)
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: function(params) {
|
||||||
|
return `${params[0].axisValue}<br/>收入: ¥${params[0].value}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: revenueData.value.map(item => item.date)
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
data: revenueData.value.map(item => item.revenue),
|
||||||
|
type: 'bar',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#2196F3'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单状态分布图表
|
||||||
|
const initOrderStatusChart = async (echarts) => {
|
||||||
|
if (!orderStatusChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(orderStatusChart.value)
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'pie',
|
||||||
|
radius: '50%',
|
||||||
|
data: orderStatusData.value.map(item => ({
|
||||||
|
value: item.count,
|
||||||
|
name: item.status
|
||||||
|
})),
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付方式分布图表
|
||||||
|
const initPaymentMethodChart = async (echarts) => {
|
||||||
|
if (!paymentMethodChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(paymentMethodChart.value)
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'pie',
|
||||||
|
radius: '50%',
|
||||||
|
data: paymentMethodData.value.map(item => ({
|
||||||
|
value: item.count,
|
||||||
|
name: item.method
|
||||||
|
})),
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (typeof num === 'string') {
|
||||||
|
num = parseFloat(num)
|
||||||
|
}
|
||||||
|
return num.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态样式类
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'status-pending',
|
||||||
|
'CONFIRMED': 'status-confirmed',
|
||||||
|
'PAID': 'status-paid',
|
||||||
|
'PROCESSING': 'status-processing',
|
||||||
|
'SHIPPED': 'status-shipped',
|
||||||
|
'DELIVERED': 'status-delivered',
|
||||||
|
'COMPLETED': 'status-completed',
|
||||||
|
'CANCELLED': 'status-cancelled',
|
||||||
|
'REFUNDED': 'status-refunded'
|
||||||
|
}
|
||||||
|
return statusMap[status] || 'status-default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'CONFIRMED': '已确认',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'PROCESSING': '处理中',
|
||||||
|
'SHIPPED': '已发货',
|
||||||
|
'DELIVERED': '已送达',
|
||||||
|
'COMPLETED': '已完成',
|
||||||
|
'CANCELLED': '已取消',
|
||||||
|
'REFUNDED': '已退款'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDashboardData()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
overviewData,
|
||||||
|
dailyActiveData,
|
||||||
|
revenueData,
|
||||||
|
orderStatusData,
|
||||||
|
paymentMethodData,
|
||||||
|
recentOrders,
|
||||||
|
dailyActiveChart,
|
||||||
|
revenueChart,
|
||||||
|
orderStatusChart,
|
||||||
|
paymentMethodChart,
|
||||||
|
formatNumber,
|
||||||
|
formatDate,
|
||||||
|
getStatusClass,
|
||||||
|
getStatusText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面特殊效果 */
|
||||||
|
.dashboard::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||||
|
animation: dashboardGlow 8s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dashboardGlow {
|
||||||
|
0% { opacity: 0.3; }
|
||||||
|
100% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容层级 */
|
||||||
|
.dashboard > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-subtitle {
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon.users {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon.paying {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon.revenue {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon.conversion {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h3 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.distribution-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-orders {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-orders h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-confirmed {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-paid {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-processing {
|
||||||
|
background-color: #cce5ff;
|
||||||
|
color: #004085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-shipped {
|
||||||
|
background-color: #e2e3e5;
|
||||||
|
color: #383d41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-delivered {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-refunded {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-section,
|
||||||
|
.distribution-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
436
demo/frontend/src/views/Home.vue
Normal file
436
demo/frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<!-- 欢迎横幅 -->
|
||||||
|
<el-row class="welcome-banner">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="banner-content">
|
||||||
|
<h1>欢迎使用 AIGC Demo</h1>
|
||||||
|
<p>现代化的订单管理和支付系统</p>
|
||||||
|
<div class="banner-actions">
|
||||||
|
<el-button v-if="!userStore.isAuthenticated" type="primary" size="large" @click="$router.push('/login')">
|
||||||
|
立即登录
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="!userStore.isAuthenticated" type="success" size="large" @click="$router.push('/register')">
|
||||||
|
免费注册
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="userStore.isAuthenticated" type="primary" size="large" @click="$router.push('/orders/create')">
|
||||||
|
创建订单
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 功能特色 -->
|
||||||
|
<el-row :gutter="20" class="features">
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-card class="feature-card" @click="goToOrders" style="cursor: pointer;">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<el-icon size="48" color="#409EFF"><ShoppingCart /></el-icon>
|
||||||
|
</div>
|
||||||
|
<h3>订单管理</h3>
|
||||||
|
<p>完整的订单生命周期管理,从创建到完成的全流程跟踪</p>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-card class="feature-card" @click="goToPayments" style="cursor: pointer;">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<el-icon size="48" color="#67C23A"><CreditCard /></el-icon>
|
||||||
|
</div>
|
||||||
|
<h3>支付</h3>
|
||||||
|
<p>支持支付宝、PayPal等多种支付方式,安全便捷</p>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-card class="feature-card" @click="goToAdmin" style="cursor: pointer;">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<el-icon size="48" color="#E6A23C"><Management /></el-icon>
|
||||||
|
</div>
|
||||||
|
<h3>管理后台</h3>
|
||||||
|
<p>强大的管理功能,支持用户管理、订单统计等</p>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 统计数据 -->
|
||||||
|
<el-row v-if="userStore.isAuthenticated" :gutter="20" class="stats">
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.totalOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">总订单数</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#409EFF"><List /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.pendingOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">待支付</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#E6A23C"><Clock /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.completedOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">已完成</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#67C23A"><Check /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="12" :sm="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number">{{ stats.totalAmount || 0 }}</div>
|
||||||
|
<div class="stat-label">总金额</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon" color="#F56C6C"><Money /></el-icon>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 最近订单 -->
|
||||||
|
<el-row v-if="userStore.isAuthenticated" class="recent-orders">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>最近订单</span>
|
||||||
|
<el-button type="primary" @click="$router.push('/orders')">查看全部</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="recentOrders" v-loading="loading" empty-text="暂无订单">
|
||||||
|
<el-table-column prop="orderNumber" label="订单号" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<router-link :to="`/orders/${row.id}`" class="order-link">
|
||||||
|
{{ row.orderNumber }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="totalAmount" label="金额" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.currency }} {{ row.totalAmount }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="$router.push(`/orders/${row.id}`)">
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useOrderStore } from '@/stores/orders'
|
||||||
|
import { getOrderStats } from '@/api/orders'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
|
||||||
|
const stats = ref({})
|
||||||
|
const recentOrders = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 功能卡片点击事件
|
||||||
|
const goToOrders = () => {
|
||||||
|
if (userStore.isAuthenticated) {
|
||||||
|
router.push('/orders')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToPayments = () => {
|
||||||
|
router.push('/payments')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToAdmin = () => {
|
||||||
|
if (userStore.isAuthenticated) {
|
||||||
|
if (userStore.isAdmin) {
|
||||||
|
router.push('/admin/orders')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('需要管理员权限')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计数据
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getOrderStats()
|
||||||
|
if (response.success) {
|
||||||
|
stats.value = response.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch stats error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最近订单
|
||||||
|
const fetchRecentOrders = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await orderStore.fetchOrders({ page: 0, size: 5 })
|
||||||
|
if (response.success) {
|
||||||
|
recentOrders.value = orderStore.orders
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch recent orders error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'CONFIRMED': 'info',
|
||||||
|
'PAID': 'primary',
|
||||||
|
'PROCESSING': '',
|
||||||
|
'SHIPPED': 'success',
|
||||||
|
'DELIVERED': 'success',
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'CANCELLED': 'danger',
|
||||||
|
'REFUNDED': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'CONFIRMED': '已确认',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'PROCESSING': '处理中',
|
||||||
|
'SHIPPED': '已发货',
|
||||||
|
'DELIVERED': '已送达',
|
||||||
|
'COMPLETED': '已完成',
|
||||||
|
'CANCELLED': '已取消',
|
||||||
|
'REFUNDED': '已退款'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.isAuthenticated) {
|
||||||
|
fetchStats()
|
||||||
|
fetchRecentOrders()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面特殊效果 */
|
||||||
|
.home::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%);
|
||||||
|
animation: shimmer 3s infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容层级 */
|
||||||
|
.home > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-banner {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 60px 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-actions .el-button {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
text-align: center;
|
||||||
|
height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-orders {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.banner-content {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
749
demo/frontend/src/views/ImageToVideo.vue
Normal file
749
demo/frontend/src/views/ImageToVideo.vue
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-to-video-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">logo</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToProfile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人主页</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSubscription">
|
||||||
|
<el-icon><Compass /></el-icon>
|
||||||
|
<span>会员订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToMyWorks">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>我的作品</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-divider"></div>
|
||||||
|
<div class="nav-item" @click="goToTextToVideo">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>文生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item storyboard-item" @click="goToStoryboard">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>分镜视频</span>
|
||||||
|
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部用户信息卡片 -->
|
||||||
|
<div class="user-info-card">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<div class="avatar-placeholder">||</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||||
|
<div class="profile-prompt">还没有设置个人简介,点击填写</div>
|
||||||
|
<div class="user-id">ID 2994509784706419</div>
|
||||||
|
</div>
|
||||||
|
<div class="edit-profile-btn">
|
||||||
|
<el-button type="primary">编辑资料</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已发布作品区域 -->
|
||||||
|
<div class="published-works">
|
||||||
|
<div class="works-tabs">
|
||||||
|
<div class="tab active">已发布</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="works-grid">
|
||||||
|
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
|
||||||
|
<div class="work-thumbnail">
|
||||||
|
<img :src="work.cover" :alt="work.title" />
|
||||||
|
<div class="work-overlay">
|
||||||
|
<div class="overlay-text">{{ work.text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-info">
|
||||||
|
<div class="work-title">{{ work.title }}</div>
|
||||||
|
<div class="work-meta">{{ work.id }} · {{ work.size }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-actions" v-if="index === 0">
|
||||||
|
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="work-director" v-else>
|
||||||
|
<span>DIRECTED BY VANNOCENT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 作品详情模态框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
:title="selectedItem?.title"
|
||||||
|
width="60%"
|
||||||
|
class="detail-dialog"
|
||||||
|
:modal="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="detail-left">
|
||||||
|
<div class="video-player">
|
||||||
|
<img :src="selectedItem?.cover" :alt="selectedItem?.title" class="video-thumbnail" />
|
||||||
|
<div class="play-overlay">
|
||||||
|
<div class="play-button">▶</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-right">
|
||||||
|
<div class="metadata-section">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">作品 ID</span>
|
||||||
|
<span class="value">{{ selectedItem?.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">文件大小</span>
|
||||||
|
<span class="value">{{ selectedItem?.size }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ selectedItem?.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">分类</span>
|
||||||
|
<span class="value">{{ selectedItem?.category }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-section">
|
||||||
|
<button class="create-similar-btn" @click="createSimilar">
|
||||||
|
做同款
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElIcon, ElButton, ElTag, ElMessage, ElDialog } from 'element-plus'
|
||||||
|
import { User, Compass, Document, VideoPlay, Picture } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
|
||||||
|
// 已发布作品数据
|
||||||
|
const publishedWorks = ref([
|
||||||
|
{
|
||||||
|
id: '2995000000001',
|
||||||
|
title: '图生视频作品 #1',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '图生视频',
|
||||||
|
createTime: '2025/01/15 14:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2995000000002',
|
||||||
|
title: '图生视频作品 #2',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '图生视频',
|
||||||
|
createTime: '2025/01/14 16:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2995000000003',
|
||||||
|
title: '图生视频作品 #3',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '图生视频',
|
||||||
|
createTime: '2025/01/13 09:20'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToProfile = () => {
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToSubscription = () => {
|
||||||
|
router.push('/subscription')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToMyWorks = () => {
|
||||||
|
router.push('/works')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTextToVideo = () => {
|
||||||
|
router.push('/text-to-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToStoryboard = () => {
|
||||||
|
router.push('/storyboard-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToCreate = (work) => {
|
||||||
|
// 跳转到图生视频创作页面
|
||||||
|
router.push('/image-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模态框相关函数
|
||||||
|
const openDetail = (work) => {
|
||||||
|
selectedItem.value = work
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
selectedItem.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDescription = (item) => {
|
||||||
|
if (!item) return ''
|
||||||
|
return `这是一个${item.category}作品,展现了"What Does it Mean To You"的主题。作品通过AI技术生成,具有独特的视觉风格和创意表达。`
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSimilar = () => {
|
||||||
|
// 关闭模态框并跳转到创作页面
|
||||||
|
handleClose()
|
||||||
|
router.push('/image-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 页面初始化
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-to-video-page {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #333;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sora-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜视频特殊样式 */
|
||||||
|
.storyboard-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-item .sora-tag {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3) !important;
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息卡片 */
|
||||||
|
.user-info-card {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-prompt {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-profile-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已发布作品区域 */
|
||||||
|
.published-works {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-info {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-actions {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover .work-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.image-to-video-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框样式 */
|
||||||
|
:deep(.detail-dialog .el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__title) {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__headerbtn) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局覆盖Element Plus默认样式 */
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
height: 50vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-left {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player:hover .play-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d1d5db;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
781
demo/frontend/src/views/ImageToVideoCreate.vue
Normal file
781
demo/frontend/src/views/ImageToVideoCreate.vue
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-to-video-create-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="back-btn" @click="goBack">
|
||||||
|
← 首页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="credits-info">
|
||||||
|
<div class="credits-circle">25</div>
|
||||||
|
<span>| 首购优惠</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-icon">
|
||||||
|
🔔
|
||||||
|
<div class="notification-badge">5</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-avatar">
|
||||||
|
👤
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 左侧设置面板 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<!-- 创作模式标签 -->
|
||||||
|
<div class="creation-tabs">
|
||||||
|
<div class="tab" @click="goToTextToVideo">文生视频</div>
|
||||||
|
<div class="tab active">图生视频</div>
|
||||||
|
<div class="tab" @click="goToStoryboard">分镜视频</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片输入区域 -->
|
||||||
|
<div class="image-input-section">
|
||||||
|
<div class="image-upload-area">
|
||||||
|
<div class="upload-box" @click="uploadFirstFrame">
|
||||||
|
<div class="upload-icon">+</div>
|
||||||
|
<div class="upload-text">首帧</div>
|
||||||
|
</div>
|
||||||
|
<div class="arrow-icon">↔</div>
|
||||||
|
<div class="upload-box optional" @click="uploadLastFrame">
|
||||||
|
<div class="upload-icon">+</div>
|
||||||
|
<div class="upload-text">尾帧 (可选)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已上传的图片预览 -->
|
||||||
|
<div class="image-preview" v-if="firstFrameImage || lastFrameImage">
|
||||||
|
<div class="preview-item" v-if="firstFrameImage">
|
||||||
|
<img :src="firstFrameImage" alt="首帧" />
|
||||||
|
<button class="remove-btn" @click="removeFirstFrame">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-item" v-if="lastFrameImage">
|
||||||
|
<img :src="lastFrameImage" alt="尾帧" />
|
||||||
|
<button class="remove-btn" @click="removeLastFrame">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文本输入区域 -->
|
||||||
|
<div class="text-input-section">
|
||||||
|
<textarea
|
||||||
|
v-model="inputText"
|
||||||
|
placeholder="结合图片,描述想要生成的内容"
|
||||||
|
class="text-input"
|
||||||
|
rows="6"
|
||||||
|
></textarea>
|
||||||
|
<div class="optimize-btn">
|
||||||
|
<button class="optimize-button">
|
||||||
|
✨ 一键优化
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频设置 -->
|
||||||
|
<div class="video-settings">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>比例</label>
|
||||||
|
<select v-model="aspectRatio" class="setting-select">
|
||||||
|
<option value="16:9">16:9</option>
|
||||||
|
<option value="4:3">4:3</option>
|
||||||
|
<option value="1:1">1:1</option>
|
||||||
|
<option value="3:4">3:4</option>
|
||||||
|
<option value="9:16">9:16</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>时长</label>
|
||||||
|
<select v-model="duration" class="setting-select">
|
||||||
|
<option value="5">5s</option>
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="15">15s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>高清模式 (1080P)</label>
|
||||||
|
<div class="hd-setting">
|
||||||
|
<input type="checkbox" v-model="hdMode" class="hd-switch">
|
||||||
|
<span class="cost-text">开启消耗20积分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成按钮 -->
|
||||||
|
<div class="generate-section">
|
||||||
|
<button class="generate-btn" @click="startGenerate">
|
||||||
|
开始生成
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧预览区域 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="preview-area">
|
||||||
|
<div class="status-checkbox">
|
||||||
|
<input type="checkbox" v-model="inProgress" id="progress-checkbox">
|
||||||
|
<label for="progress-checkbox">进行中</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-content">
|
||||||
|
<div class="preview-placeholder">
|
||||||
|
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const inputText = ref('')
|
||||||
|
const aspectRatio = ref('16:9')
|
||||||
|
const duration = ref('5')
|
||||||
|
const hdMode = ref(false)
|
||||||
|
const inProgress = ref(false)
|
||||||
|
|
||||||
|
// 图片上传
|
||||||
|
const firstFrameImage = ref('')
|
||||||
|
const lastFrameImage = ref('')
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTextToVideo = () => {
|
||||||
|
router.push('/text-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToStoryboard = () => {
|
||||||
|
alert('分镜视频功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传处理
|
||||||
|
const uploadFirstFrame = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = 'image/*'
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
firstFrameImage.value = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadLastFrame = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = 'image/*'
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
lastFrameImage.value = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFirstFrame = () => {
|
||||||
|
firstFrameImage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeLastFrame = () => {
|
||||||
|
lastFrameImage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const startGenerate = () => {
|
||||||
|
if (!firstFrameImage.value) {
|
||||||
|
alert('请上传首帧图片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputText.value.trim()) {
|
||||||
|
alert('请输入描述文字')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress.value = true
|
||||||
|
alert('开始生成视频...')
|
||||||
|
|
||||||
|
// 模拟生成过程
|
||||||
|
setTimeout(() => {
|
||||||
|
inProgress.value = false
|
||||||
|
alert('视频生成完成!')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-to-video-create-page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航栏 */
|
||||||
|
.top-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 32px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border-bottom: 1px solid #1f1f1f;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credits-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credits-circle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #374151, #1f2937);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 400px 1fr;
|
||||||
|
gap: 0;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧面板 */
|
||||||
|
.left-panel {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #2a2a2a;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 创作模式标签 */
|
||||||
|
.creation-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片输入区域 */
|
||||||
|
.image-input-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-box {
|
||||||
|
flex: 1;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px dashed #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-box:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-box.optional {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本输入区域 */
|
||||||
|
.text-input-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input::placeholder {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-button {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频设置 */
|
||||||
|
.video-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select:hover {
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd-setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd-switch {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成按钮 */
|
||||||
|
.generate-section {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:disabled {
|
||||||
|
background: #6b7280;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧面板 */
|
||||||
|
.right-panel {
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
flex: 1;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content:hover {
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 350px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
padding: 16px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-tabs {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
587
demo/frontend/src/views/ImageToVideoDetail.vue
Normal file
587
demo/frontend/src/views/ImageToVideoDetail.vue
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
<template>
|
||||||
|
<div class="video-detail-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="logo">logo</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<el-icon class="action-icon"><User /></el-icon>
|
||||||
|
<el-icon class="action-icon"><Setting /></el-icon>
|
||||||
|
<el-icon class="action-icon"><Bell /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>文件</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图片</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>视频</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 视频播放器区域 -->
|
||||||
|
<div class="video-section">
|
||||||
|
<div class="video-player">
|
||||||
|
<video
|
||||||
|
ref="videoRef"
|
||||||
|
:src="videoData.videoUrl"
|
||||||
|
@click="togglePlay"
|
||||||
|
@timeupdate="updateTime"
|
||||||
|
@loadedmetadata="onLoadedMetadata"
|
||||||
|
>
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<!-- 视频控制栏 -->
|
||||||
|
<div class="video-controls" v-show="showControls">
|
||||||
|
<div class="controls-left">
|
||||||
|
<el-button circle size="small" @click="togglePlay">
|
||||||
|
<el-icon><VideoPlay v-if="!isPlaying" /><VideoPause v-else /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls-right">
|
||||||
|
<el-button circle size="small" @click="toggleFullscreen">
|
||||||
|
<el-icon><FullScreen /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频操作按钮 -->
|
||||||
|
<div class="video-actions">
|
||||||
|
<el-tooltip content="分享" placement="bottom">
|
||||||
|
<el-button circle size="small" @click="shareVideo">
|
||||||
|
<el-icon><Share /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="下载" placement="bottom">
|
||||||
|
<el-button circle size="small" @click="downloadVideo">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="bottom">
|
||||||
|
<el-button circle size="small" @click="deleteVideo">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧详情区域 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="detail-header">
|
||||||
|
<h3>图片详情</h3>
|
||||||
|
<p class="subtitle">参考生图</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="input-section">
|
||||||
|
<el-input
|
||||||
|
v-model="detailInput"
|
||||||
|
placeholder="输入详情"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="thumbnails">
|
||||||
|
<div class="thumbnail" v-for="(thumb, index) in thumbnails" :key="index">
|
||||||
|
<img :src="thumb" :alt="`缩略图${index + 1}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
<h4>描述</h4>
|
||||||
|
<p>{{ videoData.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ videoData.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">视频 ID</span>
|
||||||
|
<span class="value">{{ videoData.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">时长</span>
|
||||||
|
<span class="value">{{ videoData.duration }}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">清晰度</span>
|
||||||
|
<span class="value">{{ videoData.resolution }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">宽高比</span>
|
||||||
|
<span class="value">{{ videoData.aspectRatio }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-button">
|
||||||
|
<el-button type="primary" size="large" @click="makeSimilar">
|
||||||
|
做同款
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 滚动指示器 -->
|
||||||
|
<div class="scroll-indicators">
|
||||||
|
<el-icon class="scroll-arrow up"><ArrowUp /></el-icon>
|
||||||
|
<el-icon class="scroll-arrow down"><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
User, Setting, Bell, Document, Picture, VideoPlay, VideoPause,
|
||||||
|
FullScreen, Share, Download, Delete, ArrowUp, ArrowDown
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const videoRef = ref(null)
|
||||||
|
|
||||||
|
// 视频播放状态
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const showControls = ref(true)
|
||||||
|
|
||||||
|
// 详情数据
|
||||||
|
const detailInput = ref('')
|
||||||
|
const videoData = ref({
|
||||||
|
id: '2995697841305810',
|
||||||
|
videoUrl: '/images/backgrounds/welcome.jpg', // 临时使用图片,实际应该是视频URL
|
||||||
|
description: '图1在图2中奔跑视频',
|
||||||
|
createTime: '2025/10/17 13:41',
|
||||||
|
duration: 5,
|
||||||
|
resolution: '1080p',
|
||||||
|
aspectRatio: '16:9'
|
||||||
|
})
|
||||||
|
|
||||||
|
const thumbnails = ref([
|
||||||
|
'/images/backgrounds/welcome.jpg',
|
||||||
|
'/images/backgrounds/welcome.jpg'
|
||||||
|
])
|
||||||
|
|
||||||
|
// 视频控制方法
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (!videoRef.value) return
|
||||||
|
|
||||||
|
if (isPlaying.value) {
|
||||||
|
videoRef.value.pause()
|
||||||
|
} else {
|
||||||
|
videoRef.value.play()
|
||||||
|
}
|
||||||
|
isPlaying.value = !isPlaying.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTime = () => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
currentTime.value = videoRef.value.currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoadedMetadata = () => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
duration.value = videoRef.value.duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
const minutes = Math.floor(time / 60)
|
||||||
|
const seconds = Math.floor(time % 60)
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (!videoRef.value) return
|
||||||
|
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
} else {
|
||||||
|
videoRef.value.requestFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作按钮方法
|
||||||
|
const shareVideo = () => {
|
||||||
|
ElMessage.info('分享功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadVideo = () => {
|
||||||
|
ElMessage.success('开始下载视频')
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteVideo = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除这个视频吗?', '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
ElMessage.success('视频已删除')
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeSimilar = () => {
|
||||||
|
ElMessage.info('做同款功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动隐藏控制栏
|
||||||
|
let controlsTimer = null
|
||||||
|
const resetControlsTimer = () => {
|
||||||
|
clearTimeout(controlsTimer)
|
||||||
|
showControls.value = true
|
||||||
|
controlsTimer = setTimeout(() => {
|
||||||
|
showControls.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 监听鼠标移动来显示/隐藏控制栏
|
||||||
|
document.addEventListener('mousemove', resetControlsTimer)
|
||||||
|
resetControlsTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimeout(controlsTimer)
|
||||||
|
document.removeEventListener('mousemove', resetControlsTimer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-detail-page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航栏 */
|
||||||
|
.top-bar {
|
||||||
|
height: 60px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 60px;
|
||||||
|
width: 200px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
padding: 20px 0;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
margin-left: 200px;
|
||||||
|
margin-top: 60px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频播放器区域 */
|
||||||
|
.video-section {
|
||||||
|
flex: 2;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions .el-button {
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions .el-button:hover {
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
border-color: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧详情区域 */
|
||||||
|
.detail-section {
|
||||||
|
flex: 1;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-left: 1px solid #333;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnails {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description p {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button .el-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-indicators {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-arrow:hover {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section {
|
||||||
|
flex: none;
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
flex: none;
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
504
demo/frontend/src/views/Login.vue
Normal file
504
demo/frontend/src/views/Login.vue
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="logo">Logo</div>
|
||||||
|
|
||||||
|
<!-- 登录卡片 -->
|
||||||
|
<div class="login-card">
|
||||||
|
<!-- Logo图标 -->
|
||||||
|
<div class="card-logo">
|
||||||
|
<div class="logo-icon">Logo</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 欢迎文字 -->
|
||||||
|
<div class="welcome-text">
|
||||||
|
<h1>欢迎来到 Logo</h1>
|
||||||
|
<p>智创无限,灵感变现</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<div class="login-form">
|
||||||
|
<!-- 手机号输入 -->
|
||||||
|
<div class="phone-input-group">
|
||||||
|
<div class="country-code">
|
||||||
|
<span>+86</span>
|
||||||
|
<el-icon><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
class="phone-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 验证码输入 -->
|
||||||
|
<div class="code-input-group">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.code"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
class="code-input"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
class="get-code-btn"
|
||||||
|
:disabled="countdown > 0"
|
||||||
|
@click="getCode"
|
||||||
|
>
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="login-button"
|
||||||
|
:loading="userStore.loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
{{ userStore.loading ? '登录中...' : '登陆/注册' }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<!-- 协议文字 -->
|
||||||
|
<p class="agreement-text">
|
||||||
|
登录即表示您同意遵守用户协议和隐私政策
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 测试账号提示 -->
|
||||||
|
<div class="test-accounts">
|
||||||
|
<el-divider>测试账号</el-divider>
|
||||||
|
<div class="account-list">
|
||||||
|
<div class="account-item" @click="fillTestAccount('15538239326', '0627')">
|
||||||
|
<strong>普通用户:</strong> 15538239326 / 0627
|
||||||
|
</div>
|
||||||
|
<div class="account-item" @click="fillTestAccount('15538239327', 'admin123')">
|
||||||
|
<strong>管理员:</strong> 15538239327 / admin123
|
||||||
|
</div>
|
||||||
|
<div class="account-item" @click="fillTestAccount('15538239328', 'test123')">
|
||||||
|
<strong>测试用户:</strong> 15538239328 / test123
|
||||||
|
</div>
|
||||||
|
<div class="account-item" @click="fillTestAccount('15538239329', '123456')">
|
||||||
|
<strong>个人主页:</strong> 15538239329 / 123456
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const countdown = ref(0)
|
||||||
|
let countdownTimer = null
|
||||||
|
|
||||||
|
const loginForm = reactive({
|
||||||
|
phone: '',
|
||||||
|
code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清除可能的缓存数据
|
||||||
|
const clearForm = () => {
|
||||||
|
loginForm.phone = ''
|
||||||
|
loginForm.code = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速填充测试账号
|
||||||
|
const fillTestAccount = (username, password) => {
|
||||||
|
loginForm.phone = username
|
||||||
|
loginForm.code = password
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时清除表单
|
||||||
|
onMounted(() => {
|
||||||
|
clearForm()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取验证码
|
||||||
|
const getCode = () => {
|
||||||
|
if (!loginForm.phone) {
|
||||||
|
ElMessage.warning('请先输入手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
|
||||||
|
ElMessage.warning('请输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟发送验证码
|
||||||
|
ElMessage.success('验证码已发送')
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
countdown.value = 60
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!loginForm.phone) {
|
||||||
|
ElMessage.warning('请输入手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loginForm.code) {
|
||||||
|
ElMessage.warning('请输入验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
|
||||||
|
ElMessage.warning('请输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('开始登录...')
|
||||||
|
|
||||||
|
// 模拟验证码登录,这里可以调用实际的API
|
||||||
|
// 为了演示,我们使用手机号作为用户名,验证码作为密码
|
||||||
|
const mockForm = {
|
||||||
|
username: loginForm.phone,
|
||||||
|
password: loginForm.code
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await userStore.loginUser(mockForm)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('登录成功,用户信息:', userStore.user)
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
|
// 等待一下确保状态更新
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200))
|
||||||
|
|
||||||
|
// 跳转到原始路径或个人主页
|
||||||
|
const redirectPath = route.query.redirect || '/profile'
|
||||||
|
console.log('准备跳转到:', redirectPath)
|
||||||
|
|
||||||
|
// 使用replace而不是push,避免浏览器历史记录问题
|
||||||
|
await router.replace(redirectPath)
|
||||||
|
|
||||||
|
console.log('路由跳转完成')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '登录失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
ElMessage.error('登录失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: url('/images/backgrounds/login.png') center/cover no-repeat;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景效果已移除,使用图片背景 */
|
||||||
|
|
||||||
|
/* 左上角Logo */
|
||||||
|
.logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
left: 30px;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录卡片 */
|
||||||
|
.login-card {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 8%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 500px;
|
||||||
|
background: rgba(26, 26, 46, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 50px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片内Logo */
|
||||||
|
.card-logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 欢迎文字 */
|
||||||
|
.welcome-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text h1 {
|
||||||
|
color: white;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text p {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录表单 */
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机号输入组 */
|
||||||
|
.phone-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-code {
|
||||||
|
width: 100px;
|
||||||
|
height: 55px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-code:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-code .el-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input :deep(.el-input__inner) {
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input :deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 验证码输入组 */
|
||||||
|
.code-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__inner) {
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-code-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #409EFF;
|
||||||
|
color: #409EFF;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 55px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-code-btn:hover {
|
||||||
|
background: #409EFF;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-code-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
background: #409EFF;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
background: #337ecc;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 协议文字 */
|
||||||
|
.agreement-text {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 25px 0 0 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试账号提示 */
|
||||||
|
.test-accounts {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-accounts :deep(.el-divider__text) {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item strong {
|
||||||
|
color: #409EFF;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.login-card {
|
||||||
|
right: 5%;
|
||||||
|
width: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-card {
|
||||||
|
position: relative;
|
||||||
|
top: auto;
|
||||||
|
right: auto;
|
||||||
|
transform: none;
|
||||||
|
margin: 50px auto;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
position: relative;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-card {
|
||||||
|
padding: 40px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input-group,
|
||||||
|
.code-input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-code {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
794
demo/frontend/src/views/MyWorks.vue
Normal file
794
demo/frontend/src/views/MyWorks.vue
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
<template>
|
||||||
|
<div class="works-page">
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-radio-group v-model="activeTab" size="small" class="seg-control">
|
||||||
|
<el-radio-button label="all">全部</el-radio-button>
|
||||||
|
<el-radio-button label="video">视频</el-radio-button>
|
||||||
|
<el-radio-button label="image">图片</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters-bar">
|
||||||
|
<el-space wrap size="small" class="filters">
|
||||||
|
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" size="small" />
|
||||||
|
<el-select v-model="category" placeholder="任务类型" size="small" style="width: 120px" @change="onFilterChange">
|
||||||
|
<el-option label="全部" value="all" />
|
||||||
|
<el-option label="文生视频" value="text2video" />
|
||||||
|
<el-option label="图生视频" value="image2video" />
|
||||||
|
<el-option label="分镜视频" value="storyboard" />
|
||||||
|
<el-option label="参考图" value="reference" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="resolution" placeholder="清晰度" clearable size="small" style="width: 120px">
|
||||||
|
<el-option label="标清" value="sd" />
|
||||||
|
<el-option label="高清" value="hd" />
|
||||||
|
<el-option label="超清" value="uhd" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="sortBy" size="small" style="width: 120px">
|
||||||
|
<el-option label="比例" value="ratio" />
|
||||||
|
<el-option label="时间" value="date" />
|
||||||
|
<el-option label="热门" value="hot" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="order" size="small" style="width: 100px">
|
||||||
|
<el-option label="升序" value="asc" />
|
||||||
|
<el-option label="降序" value="desc" />
|
||||||
|
</el-select>
|
||||||
|
</el-space>
|
||||||
|
<div class="right">
|
||||||
|
<el-input v-model="keyword" placeholder="名字/提示词/ID" size="small" clearable style="width: 220px" @keyup.enter.native="reload" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="select-row">
|
||||||
|
<el-checkbox v-model="multiSelect" size="small">选择多个</el-checkbox>
|
||||||
|
<template v-if="multiSelect && selectedIds.size">
|
||||||
|
<el-tag type="success" size="small">已选 {{ selectedIds.size }} 个项目</el-tag>
|
||||||
|
<el-button size="small" type="primary" @click="bulkDownload" plain>下载</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="bulkDelete" plain>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="works-grid">
|
||||||
|
<el-col v-for="item in filteredItems" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6">
|
||||||
|
<el-card class="work-card" :class="{ selected: selectedIds.has(item.id) }" shadow="hover">
|
||||||
|
<div class="thumb" @click="multiSelect ? toggleSelect(item.id) : openDetail(item)">
|
||||||
|
<img :src="item.cover" :alt="item.title" />
|
||||||
|
|
||||||
|
<div class="checker" v-if="multiSelect">
|
||||||
|
<el-checkbox :model-value="selectedIds.has(item.id)" @change="() => toggleSelect(item.id)" />
|
||||||
|
</div>
|
||||||
|
<div class="actions" @click.stop>
|
||||||
|
<el-tooltip content="收藏" placement="top"><el-button circle size="small" text><el-icon><Star /></el-icon></el-button></el-tooltip>
|
||||||
|
<el-dropdown @command="(cmd)=>moreCommand(cmd,item)">
|
||||||
|
<el-button circle size="small" text><el-icon><MoreFilled /></el-icon></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="download_with_watermark">带水印下载</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="download_without_watermark">
|
||||||
|
不带水印下载
|
||||||
|
<el-tag type="primary" size="small" style="margin-left: 8px;">会员</el-tag>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="rename" divided>重命名</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="delete">删除</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<div class="title" :title="item.title">{{ item.title }}</div>
|
||||||
|
<div class="sub">{{ item.id }} · {{ item.sizeText }}</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-space size="small">
|
||||||
|
<el-button text size="small" @click.stop="download(item)">下载</el-button>
|
||||||
|
<el-button text size="small" @click.stop="share(item)">分享</el-button>
|
||||||
|
</el-space>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 作品详情模态框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
:title="selectedItem?.title"
|
||||||
|
width="60%"
|
||||||
|
:before-close="handleClose"
|
||||||
|
class="detail-dialog"
|
||||||
|
:modal="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
>
|
||||||
|
<div class="detail-content" v-if="selectedItem">
|
||||||
|
<div class="detail-left">
|
||||||
|
<div class="video-container">
|
||||||
|
<video
|
||||||
|
v-if="selectedItem.type === 'video'"
|
||||||
|
class="detail-video"
|
||||||
|
:src="selectedItem.cover"
|
||||||
|
:poster="selectedItem.cover"
|
||||||
|
controls
|
||||||
|
>
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="detail-image"
|
||||||
|
:src="selectedItem.cover"
|
||||||
|
:alt="selectedItem.title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 视频文字叠加 -->
|
||||||
|
<div class="video-overlay" v-if="selectedItem.type === 'video' && selectedItem.overlayText">
|
||||||
|
<div class="overlay-text">{{ selectedItem.overlayText }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-right">
|
||||||
|
<!-- 用户信息头部 -->
|
||||||
|
<div class="detail-header">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="avatar">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="username">mingzi_FBx7foZYDS7inL</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签页 -->
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab" :class="{ active: activeDetailTab === 'detail' }" @click="activeDetailTab = 'detail'">作品详情</div>
|
||||||
|
<div class="tab" :class="{ active: activeDetailTab === 'category' }" @click="activeDetailTab = 'category'">{{ selectedItem.category }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述区域 -->
|
||||||
|
<div class="description-section" v-if="activeDetailTab === 'detail'">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 参考图特殊内容 -->
|
||||||
|
<div class="reference-content" v-if="activeDetailTab === 'category' && selectedItem.category === '参考图'">
|
||||||
|
<div class="input-details-section">
|
||||||
|
<h3 class="section-title">输入详情</h3>
|
||||||
|
<div class="input-images">
|
||||||
|
<div class="input-image-item">
|
||||||
|
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
|
||||||
|
</div>
|
||||||
|
<div class="input-image-item">
|
||||||
|
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">图1在图2中奔跑视频</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 其他分类的内容 -->
|
||||||
|
<div class="description-section" v-if="activeDetailTab === 'category' && selectedItem.category !== '参考图'">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 元数据区域 -->
|
||||||
|
<div class="metadata-section">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ selectedItem.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">作品 ID</span>
|
||||||
|
<span class="value">{{ selectedItem.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">日期</span>
|
||||||
|
<span class="value">{{ selectedItem.date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||||
|
<span class="label">时长</span>
|
||||||
|
<span class="value">5s</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||||
|
<span class="label">清晰度</span>
|
||||||
|
<span class="value">1080p</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">分类</span>
|
||||||
|
<span class="value">{{ selectedItem.category }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||||
|
<span class="label">宽高比</span>
|
||||||
|
<span class="value">16:9</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-section">
|
||||||
|
<button class="create-similar-btn" @click="createSimilar">
|
||||||
|
做同款
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<div class="finished" v-if="!hasMore && filteredItems.length>0">已加载全部内容</div>
|
||||||
|
<el-empty v-if="!loading && filteredItems.length===0" description="没有找到相关内容" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Star, MoreFilled, User } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const activeTab = ref('all')
|
||||||
|
const dateRange = ref([])
|
||||||
|
const category = ref('all')
|
||||||
|
const resolution = ref('')
|
||||||
|
const sortBy = ref('date')
|
||||||
|
const order = ref('desc')
|
||||||
|
const keyword = ref('')
|
||||||
|
const multiSelect = ref(false)
|
||||||
|
const selectedIds = ref(new Set())
|
||||||
|
|
||||||
|
// 模态框相关状态
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
const activeDetailTab = ref('detail')
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(4)
|
||||||
|
const loading = ref(false)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const items = ref([])
|
||||||
|
|
||||||
|
const mockData = (count, startId = 1) => Array.from({ length: count }).map((_, i) => {
|
||||||
|
const id = startId + i
|
||||||
|
|
||||||
|
// 定义不同的分类和类型
|
||||||
|
const categories = [
|
||||||
|
{ type: 'image', category: '参考图', title: '图片作品' },
|
||||||
|
{ type: 'image', category: '参考图', title: '图片作品' },
|
||||||
|
{ type: 'video', category: '文生视频', title: '视频作品' },
|
||||||
|
{ type: 'video', category: '图生视频', title: '视频作品' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const itemConfig = categories[i] || categories[0]
|
||||||
|
|
||||||
|
// 生成不同的日期
|
||||||
|
const dates = ['2025/01/15', '2025/01/14', '2025/01/13', '2025/01/12']
|
||||||
|
const createTimes = ['2025/01/15 14:30', '2025/01/14 16:45', '2025/01/13 09:20', '2025/01/12 11:15']
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `2995${id.toString().padStart(9,'0')}`,
|
||||||
|
title: `${itemConfig.title} #${id}`,
|
||||||
|
type: itemConfig.type,
|
||||||
|
category: itemConfig.category,
|
||||||
|
sizeText: itemConfig.type === 'video' ? '9 MB' : '6 MB',
|
||||||
|
cover: itemConfig.type === 'video'
|
||||||
|
? '/images/backgrounds/welcome.jpg'
|
||||||
|
: '/images/backgrounds/login.png',
|
||||||
|
createTime: createTimes[i] || createTimes[0],
|
||||||
|
date: dates[i] || dates[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
// TODO: 替换为真实接口
|
||||||
|
await new Promise(r => setTimeout(r, 400))
|
||||||
|
const data = mockData(pageSize.value, (page.value - 1) * pageSize.value + 1)
|
||||||
|
if (page.value === 1) items.value = []
|
||||||
|
items.value = items.value.concat(data)
|
||||||
|
hasMore.value = false
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选后的作品列表
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
let filtered = [...items.value]
|
||||||
|
|
||||||
|
// 按类型筛选(全部/视频/图片)
|
||||||
|
if (activeTab.value === 'video') {
|
||||||
|
filtered = filtered.filter(item => item.type === 'video')
|
||||||
|
} else if (activeTab.value === 'image') {
|
||||||
|
filtered = filtered.filter(item => item.type === 'image')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按分类筛选
|
||||||
|
if (category.value !== 'all') {
|
||||||
|
const categoryMap = {
|
||||||
|
'text2video': '文生视频',
|
||||||
|
'image2video': '图生视频',
|
||||||
|
'storyboard': '分镜视频',
|
||||||
|
'reference': '参考图'
|
||||||
|
}
|
||||||
|
const targetCategory = categoryMap[category.value]
|
||||||
|
if (targetCategory) {
|
||||||
|
filtered = filtered.filter(item => item.category === targetCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按关键词筛选
|
||||||
|
if (keyword.value) {
|
||||||
|
const keywordLower = keyword.value.toLowerCase()
|
||||||
|
filtered = filtered.filter(item =>
|
||||||
|
item.title.toLowerCase().includes(keywordLower) ||
|
||||||
|
item.id.includes(keywordLower)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
page.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选变化时的处理
|
||||||
|
const onFilterChange = () => {
|
||||||
|
// 筛选是响应式的,不需要额外处理
|
||||||
|
console.log('筛选条件变化:', { category: category.value, activeTab: activeTab.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
if (loading.value || !hasMore.value) return
|
||||||
|
page.value += 1
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetail = (item) => {
|
||||||
|
selectedItem.value = item
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作品描述
|
||||||
|
const getDescription = (item) => {
|
||||||
|
if (item.type === 'video') {
|
||||||
|
return '影片捕捉了暴风雪中的午夜时分,坐落在积雪覆盖的悬崖顶上的孤立灯塔。相机逐渐放大灯塔的灯光,穿透飞舞的雪花,投射出幽幽的光芒。在白茫茫的环境中,灯塔的黑色轮廓显得格外醒目,呼啸的风声和远处海浪的撞击声增强了孤独的氛围。这一场景展示了灯塔的孤独力量。'
|
||||||
|
} else {
|
||||||
|
return '这是一张精美的参考图片,展现了独特的艺术风格和创意构思。图片构图优美,色彩搭配和谐,具有很高的艺术价值和参考意义。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
const handleClose = () => {
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
selectedItem.value = null
|
||||||
|
activeDetailTab.value = 'detail'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建同款
|
||||||
|
const createSimilar = () => {
|
||||||
|
ElMessage.info('跳转到创作页面')
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = (item) => {
|
||||||
|
ElMessage.success(`开始下载:${item.title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const share = (item) => {
|
||||||
|
ElMessageBox.alert('分享链接功能即将上线', '提示')
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreCommand = async (cmd, item) => {
|
||||||
|
if (cmd === 'download_with_watermark') {
|
||||||
|
ElMessage.success('开始下载带水印版本')
|
||||||
|
} else if (cmd === 'download_without_watermark') {
|
||||||
|
ElMessage.success('开始下载不带水印版本(会员专享)')
|
||||||
|
} else if (cmd === 'rename') {
|
||||||
|
ElMessage.info('重命名功能开发中')
|
||||||
|
} else if (cmd === 'delete') {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该作品吗?', '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
ElMessage.success('已删除')
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelect = (id) => {
|
||||||
|
const next = new Set(selectedIds.value)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
selectedIds.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
const bulkDownload = () => {
|
||||||
|
ElMessage.success(`开始下载 ${selectedIds.value.size} 个文件`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bulkDelete = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除选中的 ${selectedIds.value.size} 个项目吗?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
ElMessage.success('已删除选中项目')
|
||||||
|
selectedIds.value = new Set()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.works-page {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0 4px;
|
||||||
|
}
|
||||||
|
.seg-control { margin-left: 2px; }
|
||||||
|
:deep(.seg-control .el-radio-button__inner) {
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
padding: 0 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #0a0a0a; /* 与页面背景相同 */
|
||||||
|
color: #cbd5e1;
|
||||||
|
border-color: #2a2a2a;
|
||||||
|
}
|
||||||
|
:deep(.seg-control .el-radio-button:first-child .el-radio-button__inner) { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
|
||||||
|
:deep(.seg-control .el-radio-button:last-child .el-radio-button__inner) { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }
|
||||||
|
:deep(.seg-control .el-radio-button.is-active .el-radio-button__inner) {
|
||||||
|
background-color: #23262b; /* 比背景略亮,保持可区分 */
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
.filters { margin-left: 10px; }
|
||||||
|
.filters-bar {
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 0 2px;
|
||||||
|
/* 覆盖 Element Plus 变量,确保与页面背景一致 */
|
||||||
|
--el-input-bg-color: #0a0a0a;
|
||||||
|
--el-fill-color-blank: #0a0a0a;
|
||||||
|
--el-border-color: #2a2a2a;
|
||||||
|
--el-text-color-regular: #cbd5e1;
|
||||||
|
}
|
||||||
|
:deep(.filters .el-select .el-input__wrapper),
|
||||||
|
:deep(.filters .el-date-editor.el-input__wrapper),
|
||||||
|
:deep(.filters .el-input__wrapper) {
|
||||||
|
background-color: #0a0a0a; /* 与页面背景相同 */
|
||||||
|
border-color: #2a2a2a;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
:deep(.filters .el-input__wrapper.is-focus) { border-color: #3a3a3a; box-shadow: none; }
|
||||||
|
:deep(.filters .el-input__inner) { color: #cbd5e1; }
|
||||||
|
:deep(.filters .el-input__suffix) { color: #cbd5e1; }
|
||||||
|
.select-row { padding: 4px 0 8px; }
|
||||||
|
.works-grid { margin-top: 12px; }
|
||||||
|
.work-card { margin-bottom: 14px; }
|
||||||
|
.thumb { position: relative; width: 100%; padding-top: 56.25%; overflow: hidden; border-radius: 6px; cursor: pointer; }
|
||||||
|
.thumb img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.checker { position: absolute; left: 6px; top: 6px; }
|
||||||
|
.actions { position: absolute; right: 6px; top: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity .2s ease; }
|
||||||
|
.thumb:hover .actions { opacity: 1; }
|
||||||
|
.work-card.selected .thumb::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 2px solid #409eff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 0 2px rgba(64,158,255,0.15) inset;
|
||||||
|
}
|
||||||
|
.meta { margin-top: 10px; }
|
||||||
|
.title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.sub { color: #909399; font-size: 12px; margin-top: 4px; }
|
||||||
|
.finished { text-align: center; color: #909399; margin: 14px 0 4px; font-size: 12px; }
|
||||||
|
|
||||||
|
/* 让卡片与页面背景一致 */
|
||||||
|
:deep(.work-card.el-card) {
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
border-color: #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
:deep(.work-card .el-card__body) {
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
}
|
||||||
|
:deep(.work-card .el-card__footer) {
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
border-top: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框样式 */
|
||||||
|
:deep(.detail-dialog .el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__title) {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__headerbtn) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强制覆盖Element Plus默认样式 */
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
height: 50vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-left {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-video, .detail-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-text {
|
||||||
|
font-family: 'Brush Script MT', cursive;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #8b5cf6;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right {
|
||||||
|
flex: 1;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #409eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d1d5db;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 参考图特殊内容样式 */
|
||||||
|
.reference-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-details-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-images {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-image-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:hover {
|
||||||
|
background: #337ecc;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更强制性的样式覆盖 */
|
||||||
|
:deep(.detail-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-overlay-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局模态框样式覆盖 */
|
||||||
|
:deep(.el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
381
demo/frontend/src/views/OrderCreate.vue
Normal file
381
demo/frontend/src/views/OrderCreate.vue
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-create">
|
||||||
|
<el-page-header @back="$router.go(-1)" content="创建订单">
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
创建订单
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
|
||||||
|
<el-card class="form-card">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-form-item label="订单类型" prop="orderType">
|
||||||
|
<el-select v-model="form.orderType" placeholder="请选择订单类型">
|
||||||
|
<el-option label="AI服务" value="SERVICE" />
|
||||||
|
<el-option label="AI订阅" value="SUBSCRIPTION" />
|
||||||
|
<el-option label="数字商品" value="DIGITAL" />
|
||||||
|
<el-option label="虚拟商品" value="VIRTUAL" />
|
||||||
|
</el-select>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>选择您要购买的虚拟商品类型:AI服务(如AI绘画、AI写作)、AI订阅(按月/年付费)、数字商品(如软件、电子书)、虚拟商品(如游戏道具、虚拟货币)</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="货币" prop="currency">
|
||||||
|
<el-select v-model="form.currency" placeholder="请选择货币">
|
||||||
|
<el-option label="人民币 (CNY)" value="CNY" />
|
||||||
|
<el-option label="美元 (USD)" value="USD" />
|
||||||
|
<el-option label="欧元 (EUR)" value="EUR" />
|
||||||
|
</el-select>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>选择支付货币类型,系统会根据您选择的货币进行计费和结算</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="订单描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请详细描述您的订单需求,如:需要AI绘画服务,风格为动漫风格,尺寸为1024x1024像素"
|
||||||
|
/>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>详细描述您的订单需求,包括服务要求、特殊需求等,这将帮助服务提供方更好地理解您的需求</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="联系邮箱" prop="contactEmail">
|
||||||
|
<el-input
|
||||||
|
v-model="form.contactEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="请输入联系邮箱(用于接收虚拟商品)"
|
||||||
|
/>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>必填项!虚拟商品将通过此邮箱发送给您,请确保邮箱地址正确且可正常接收邮件</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="联系电话" prop="contactPhone">
|
||||||
|
<el-input
|
||||||
|
v-model="form.contactPhone"
|
||||||
|
placeholder="请输入联系电话(可选)"
|
||||||
|
/>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>可选填写,用于紧急情况联系或重要通知,建议填写以便服务提供方在需要时联系您</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 虚拟商品不需要收货地址 -->
|
||||||
|
<template v-if="isPhysicalOrder">
|
||||||
|
<el-form-item label="收货地址" prop="shippingAddress">
|
||||||
|
<el-input
|
||||||
|
v-model="form.shippingAddress"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入收货地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="账单地址" prop="billingAddress">
|
||||||
|
<el-input
|
||||||
|
v-model="form.billingAddress"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入账单地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 订单项 -->
|
||||||
|
<el-form-item label="虚拟商品">
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>添加您要购买的虚拟商品,包括商品名称、单价和数量。支持添加多个商品。</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-items">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in form.orderItems"
|
||||||
|
:key="index"
|
||||||
|
class="order-item"
|
||||||
|
>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-input
|
||||||
|
v-model="item.productName"
|
||||||
|
placeholder="商品名称(如:AI绘画服务、AI写作助手、AI翻译服务等)"
|
||||||
|
@input="calculateSubtotal(index)"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-input-number
|
||||||
|
v-model="item.unitPrice"
|
||||||
|
:precision="2"
|
||||||
|
:min="0"
|
||||||
|
placeholder="单价"
|
||||||
|
@change="calculateSubtotal(index)"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-input-number
|
||||||
|
v-model="item.quantity"
|
||||||
|
:min="1"
|
||||||
|
placeholder="数量"
|
||||||
|
@change="calculateSubtotal(index)"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-input
|
||||||
|
v-model="item.subtotal"
|
||||||
|
readonly
|
||||||
|
placeholder="小计"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
@click="removeItem(index)"
|
||||||
|
v-if="form.orderItems.length > 1"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="Plus"
|
||||||
|
@click="addItem"
|
||||||
|
class="add-item-btn"
|
||||||
|
>
|
||||||
|
添加虚拟商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<div class="total-amount">
|
||||||
|
<span class="total-label">订单总计:</span>
|
||||||
|
<span class="total-value">{{ form.currency }} {{ totalAmount }}</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useOrderStore } from '@/stores/orders'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Plus, Delete, Check, InfoFilled } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
orderType: 'SERVICE',
|
||||||
|
currency: 'CNY',
|
||||||
|
description: '',
|
||||||
|
contactEmail: '',
|
||||||
|
contactPhone: '',
|
||||||
|
shippingAddress: '',
|
||||||
|
billingAddress: '',
|
||||||
|
orderItems: [
|
||||||
|
{
|
||||||
|
productName: '',
|
||||||
|
unitPrice: 0,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
orderType: [
|
||||||
|
{ required: true, message: '请选择订单类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
currency: [
|
||||||
|
{ required: true, message: '请选择货币', trigger: 'change' }
|
||||||
|
],
|
||||||
|
contactEmail: [
|
||||||
|
{ required: true, message: '请输入联系邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为实体商品订单
|
||||||
|
const isPhysicalOrder = computed(() => {
|
||||||
|
return form.orderType === 'PHYSICAL'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
const totalAmount = computed(() => {
|
||||||
|
return form.orderItems.reduce((total, item) => {
|
||||||
|
return total + parseFloat(item.subtotal || 0)
|
||||||
|
}, 0).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算小计
|
||||||
|
const calculateSubtotal = (index) => {
|
||||||
|
const item = form.orderItems[index]
|
||||||
|
if (item.unitPrice && item.quantity) {
|
||||||
|
item.subtotal = parseFloat((item.unitPrice * item.quantity).toFixed(2))
|
||||||
|
} else {
|
||||||
|
item.subtotal = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加商品项
|
||||||
|
const addItem = () => {
|
||||||
|
form.orderItems.push({
|
||||||
|
productName: '',
|
||||||
|
unitPrice: 0,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除商品项
|
||||||
|
const removeItem = (index) => {
|
||||||
|
if (form.orderItems.length > 1) {
|
||||||
|
form.orderItems.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
// 验证订单项
|
||||||
|
const validItems = form.orderItems.filter(item =>
|
||||||
|
item.productName && item.unitPrice > 0 && item.quantity > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (validItems.length === 0) {
|
||||||
|
ElMessage.error('请至少添加一个有效虚拟商品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 准备提交数据
|
||||||
|
const orderData = {
|
||||||
|
orderType: form.orderType,
|
||||||
|
currency: form.currency,
|
||||||
|
description: form.description,
|
||||||
|
contactEmail: form.contactEmail,
|
||||||
|
contactPhone: form.contactPhone,
|
||||||
|
shippingAddress: form.shippingAddress,
|
||||||
|
billingAddress: form.billingAddress,
|
||||||
|
totalAmount: parseFloat(totalAmount.value),
|
||||||
|
status: 'PENDING', // 新创建的订单状态为待支付
|
||||||
|
orderItems: validItems.map(item => ({
|
||||||
|
productName: item.productName,
|
||||||
|
unitPrice: parseFloat(item.unitPrice),
|
||||||
|
quantity: parseInt(item.quantity),
|
||||||
|
subtotal: parseFloat(item.subtotal)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await orderStore.createNewOrder(orderData)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success('虚拟商品订单创建成功!商品将发送到您的邮箱')
|
||||||
|
router.push('/orders')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '创建订单失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create order error:', error)
|
||||||
|
ElMessage.error('创建订单失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-create {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-items {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-btn {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-amount {
|
||||||
|
text-align: right;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border: 1px solid #b3d8ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #409eff;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-description .el-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-top: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-description span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
201
demo/frontend/src/views/OrderDetail.vue
Normal file
201
demo/frontend/src/views/OrderDetail.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-detail">
|
||||||
|
<el-page-header @back="$router.go(-1)" content="订单详情">
|
||||||
|
<template #extra>
|
||||||
|
<el-button-group>
|
||||||
|
<el-button v-if="order?.canPay()" type="success" @click="handlePayment">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
立即支付
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="order?.canCancel()" type="danger" @click="handleCancel">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
取消订单
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
|
||||||
|
<el-card v-if="order" class="order-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="order-header">
|
||||||
|
<h3>订单信息</h3>
|
||||||
|
<el-tag :type="getStatusType(order.status)">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单号">{{ order.orderNumber }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单类型">{{ getOrderTypeText(order.orderType) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单金额">
|
||||||
|
<span class="amount">{{ order.currency }} {{ order.totalAmount }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(order.createdAt) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系邮箱" v-if="order.contactEmail">{{ order.contactEmail }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话" v-if="order.contactPhone">{{ order.contactPhone }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="order.description" class="order-description">
|
||||||
|
<h4>订单描述</h4>
|
||||||
|
<p>{{ order.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="order.orderItems && order.orderItems.length > 0" class="order-items">
|
||||||
|
<h4>订单商品</h4>
|
||||||
|
<el-table :data="order.orderItems" border>
|
||||||
|
<el-table-column prop="productName" label="商品名称" />
|
||||||
|
<el-table-column prop="unitPrice" label="单价" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ order.currency }} {{ row.unitPrice }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quantity" label="数量" width="80" />
|
||||||
|
<el-table-column prop="subtotal" label="小计" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ order.currency }} {{ row.subtotal }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-empty v-else description="订单不存在" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useOrderStore } from '@/stores/orders'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
|
||||||
|
const order = ref(null)
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'CONFIRMED': 'info',
|
||||||
|
'PAID': 'primary',
|
||||||
|
'PROCESSING': '',
|
||||||
|
'SHIPPED': 'success',
|
||||||
|
'DELIVERED': 'success',
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'CANCELLED': 'danger',
|
||||||
|
'REFUNDED': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'CONFIRMED': '已确认',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'PROCESSING': '处理中',
|
||||||
|
'SHIPPED': '已发货',
|
||||||
|
'DELIVERED': '已送达',
|
||||||
|
'COMPLETED': '已完成',
|
||||||
|
'CANCELLED': '已取消',
|
||||||
|
'REFUNDED': '已退款'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单类型文本
|
||||||
|
const getOrderTypeText = (orderType) => {
|
||||||
|
const typeMap = {
|
||||||
|
'PRODUCT': '商品订单',
|
||||||
|
'SERVICE': '服务订单',
|
||||||
|
'SUBSCRIPTION': '订阅订单',
|
||||||
|
'DIGITAL': '数字商品',
|
||||||
|
'PHYSICAL': '实体商品'
|
||||||
|
}
|
||||||
|
return typeMap[orderType] || orderType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理支付
|
||||||
|
const handlePayment = () => {
|
||||||
|
ElMessage.info('支付功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
ElMessage.info('取消订单功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const orderId = route.params.id
|
||||||
|
if (orderId) {
|
||||||
|
const response = await orderStore.fetchOrderById(orderId)
|
||||||
|
if (response.success) {
|
||||||
|
order.value = orderStore.currentOrder
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '获取订单详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-detail {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-description,
|
||||||
|
.order-items {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-description h4,
|
||||||
|
.order-items h4 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
525
demo/frontend/src/views/Orders.vue
Normal file
525
demo/frontend/src/views/Orders.vue
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
<template>
|
||||||
|
<div class="orders">
|
||||||
|
<!-- 页面标题和操作 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<h2>
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
订单管理
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="page-actions">
|
||||||
|
<el-button type="primary" @click="$router.push('/orders/create')">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
创建订单
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.status"
|
||||||
|
placeholder="选择订单状态"
|
||||||
|
clearable
|
||||||
|
@change="handleFilterChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="status in orderStatuses"
|
||||||
|
:key="status.value"
|
||||||
|
:label="status.label"
|
||||||
|
:value="status.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.search"
|
||||||
|
placeholder="搜索订单号"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-button @click="resetFilters">重置筛选</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<el-card class="orders-card">
|
||||||
|
<el-table
|
||||||
|
:data="orderStore.orders"
|
||||||
|
v-loading="orderStore.loading"
|
||||||
|
empty-text="暂无订单"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
|
>
|
||||||
|
<el-table-column prop="orderNumber" label="订单号" width="150" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<router-link :to="`/orders/${row.id}`" class="order-link">
|
||||||
|
{{ row.orderNumber }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="totalAmount" label="金额" width="120" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="amount">{{ row.currency }} {{ row.totalAmount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="orderType" label="类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getOrderTypeText(row.orderType) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" width="160" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" @click="$router.push(`/orders/${row.id}`)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-dropdown v-if="canPay(row)" trigger="click" :teleported="true" popper-class="table-dropdown" @command="(command) => handlePayment(row, command)">
|
||||||
|
<el-button size="small" type="success">
|
||||||
|
支付<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="ALIPAY">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
支付宝支付
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="PAYPAL">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
PayPal支付
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="canCancel(row)"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="handleCancel(row)"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 取消订单对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="cancelDialogVisible"
|
||||||
|
title="取消订单"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
<el-form :model="cancelForm" label-width="80px">
|
||||||
|
<el-form-item label="取消原因">
|
||||||
|
<el-input
|
||||||
|
v-model="cancelForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入取消原因(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="cancelDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="danger" @click="confirmCancel">确认取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useOrderStore } from '@/stores/orders'
|
||||||
|
import { createOrderPayment } from '@/api/orders'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const filters = reactive({
|
||||||
|
status: '',
|
||||||
|
search: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
const sortBy = ref('createdAt')
|
||||||
|
const sortDir = ref('desc')
|
||||||
|
|
||||||
|
// 取消订单对话框
|
||||||
|
const cancelDialogVisible = ref(false)
|
||||||
|
const cancelForm = reactive({
|
||||||
|
reason: ''
|
||||||
|
})
|
||||||
|
const currentCancelOrder = ref(null)
|
||||||
|
|
||||||
|
// 订单状态选项
|
||||||
|
const orderStatuses = [
|
||||||
|
{ value: '', label: '全部状态' },
|
||||||
|
{ value: 'PENDING', label: '待支付' },
|
||||||
|
{ value: 'CONFIRMED', label: '已确认' },
|
||||||
|
{ value: 'PAID', label: '已支付' },
|
||||||
|
{ value: 'PROCESSING', label: '处理中' },
|
||||||
|
{ value: 'SHIPPED', label: '已发货' },
|
||||||
|
{ value: 'DELIVERED', label: '已送达' },
|
||||||
|
{ value: 'COMPLETED', label: '已完成' },
|
||||||
|
{ value: 'CANCELLED', label: '已取消' },
|
||||||
|
{ value: 'REFUNDED', label: '已退款' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'CONFIRMED': 'info',
|
||||||
|
'PAID': 'primary',
|
||||||
|
'PROCESSING': '',
|
||||||
|
'SHIPPED': 'success',
|
||||||
|
'DELIVERED': 'success',
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'CANCELLED': 'danger',
|
||||||
|
'REFUNDED': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'CONFIRMED': '已确认',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'PROCESSING': '处理中',
|
||||||
|
'SHIPPED': '已发货',
|
||||||
|
'DELIVERED': '已送达',
|
||||||
|
'COMPLETED': '已完成',
|
||||||
|
'CANCELLED': '已取消',
|
||||||
|
'REFUNDED': '已退款'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单类型文本
|
||||||
|
const getOrderTypeText = (orderType) => {
|
||||||
|
const typeMap = {
|
||||||
|
'PRODUCT': '商品订单',
|
||||||
|
'SERVICE': '服务订单',
|
||||||
|
'SUBSCRIPTION': '订阅订单',
|
||||||
|
'DIGITAL': '数字商品',
|
||||||
|
'PHYSICAL': '实体商品'
|
||||||
|
}
|
||||||
|
return typeMap[orderType] || orderType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以支付
|
||||||
|
const canPay = (order) => {
|
||||||
|
return order.status === 'PENDING' || order.status === 'CONFIRMED'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以取消
|
||||||
|
const canCancel = (order) => {
|
||||||
|
return order.status === 'PENDING' || order.status === 'CONFIRMED'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
console.log('=== 开始获取订单列表 ===')
|
||||||
|
console.log('当前用户:', userStore.user)
|
||||||
|
console.log('认证状态:', userStore.isAuthenticated)
|
||||||
|
console.log('Token:', sessionStorage.getItem('token'))
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
page: pagination.page - 1,
|
||||||
|
size: pagination.size,
|
||||||
|
sortBy: sortBy.value,
|
||||||
|
sortDir: sortDir.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.status) {
|
||||||
|
params.status = filters.status
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.search) {
|
||||||
|
params.search = filters.search
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('请求参数:', params)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await orderStore.fetchOrders(params)
|
||||||
|
console.log('API响应:', response)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
pagination.total = orderStore.pagination.total
|
||||||
|
console.log('订单数据:', orderStore.orders)
|
||||||
|
console.log('分页信息:', orderStore.pagination)
|
||||||
|
} else {
|
||||||
|
console.error('获取订单失败:', response.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单异常:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选变化
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilters = () => {
|
||||||
|
filters.status = ''
|
||||||
|
filters.search = ''
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序变化
|
||||||
|
const handleSortChange = ({ prop, order }) => {
|
||||||
|
if (prop) {
|
||||||
|
sortBy.value = prop
|
||||||
|
sortDir.value = order === 'ascending' ? 'asc' : 'desc'
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
pagination.page = 1
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理支付
|
||||||
|
const handlePayment = async (order, paymentMethod) => {
|
||||||
|
try {
|
||||||
|
const response = await createOrderPayment(order.id, paymentMethod)
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success('正在跳转到支付页面...')
|
||||||
|
// 这里应该跳转到支付页面
|
||||||
|
window.open(response.data.paymentUrl, '_blank')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '创建支付失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Payment error:', error)
|
||||||
|
ElMessage.error('创建支付失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleCancel = (order) => {
|
||||||
|
currentCancelOrder.value = order
|
||||||
|
cancelForm.reason = ''
|
||||||
|
cancelDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认取消
|
||||||
|
const confirmCancel = async () => {
|
||||||
|
if (!currentCancelOrder.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await orderStore.cancelOrderById(
|
||||||
|
currentCancelOrder.value.id,
|
||||||
|
cancelForm.reason
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success('订单取消成功')
|
||||||
|
cancelDialogVisible.value = false
|
||||||
|
fetchOrders()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '取消订单失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Cancel order error:', error)
|
||||||
|
ElMessage.error('取消订单失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.orders {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面特殊效果 */
|
||||||
|
.orders::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
|
||||||
|
animation: ordersPulse 5s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ordersPulse {
|
||||||
|
0% { opacity: 0.3; transform: scale(1); }
|
||||||
|
100% { opacity: 0.6; transform: scale(1.02); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容层级 */
|
||||||
|
.orders > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保表格内下拉菜单不被裁剪/遮挡 */
|
||||||
|
:deep(.table-dropdown) {
|
||||||
|
z-index: 3000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
254
demo/frontend/src/views/PaymentCreate.vue
Normal file
254
demo/frontend/src/views/PaymentCreate.vue
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payment-create">
|
||||||
|
<el-page-header @back="$router.go(-1)" content="创建支付">
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
创建支付
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
|
||||||
|
<el-card class="form-card">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-form-item label="订单号" prop="orderId">
|
||||||
|
<el-input
|
||||||
|
v-model="form.orderId"
|
||||||
|
placeholder="请输入订单号"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="支付金额" prop="amount">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.amount"
|
||||||
|
:precision="2"
|
||||||
|
:min="0.01"
|
||||||
|
placeholder="请输入支付金额"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="货币" prop="currency">
|
||||||
|
<el-select v-model="form.currency" placeholder="请选择货币">
|
||||||
|
<el-option label="人民币 (CNY)" value="CNY" />
|
||||||
|
<el-option label="美元 (USD)" value="USD" />
|
||||||
|
<el-option label="欧元 (EUR)" value="EUR" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="支付方式" prop="paymentMethod">
|
||||||
|
<el-radio-group v-model="form.paymentMethod">
|
||||||
|
<el-radio value="ALIPAY">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
支付宝
|
||||||
|
</el-radio>
|
||||||
|
<el-radio value="PAYPAL">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
PayPal
|
||||||
|
</el-radio>
|
||||||
|
<el-radio value="WECHAT">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
微信支付
|
||||||
|
</el-radio>
|
||||||
|
<el-radio value="UNIONPAY">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
银联支付
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="支付描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入支付描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="回调URL" prop="callbackUrl">
|
||||||
|
<el-input
|
||||||
|
v-model="form.callbackUrl"
|
||||||
|
placeholder="请输入回调URL(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="返回URL" prop="returnUrl">
|
||||||
|
<el-input
|
||||||
|
v-model="form.returnUrl"
|
||||||
|
placeholder="请输入返回URL(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 支付方式说明 -->
|
||||||
|
<el-card class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<h4>支付方式说明</h4>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="payment-method-info">
|
||||||
|
<el-icon size="32" color="#1677FF"><CreditCard /></el-icon>
|
||||||
|
<h5>支付宝</h5>
|
||||||
|
<p>支持支付宝扫码支付和网页支付</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="payment-method-info">
|
||||||
|
<el-icon size="32" color="#0070BA"><CreditCard /></el-icon>
|
||||||
|
<h5>PayPal</h5>
|
||||||
|
<p>支持PayPal账户支付和信用卡支付</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="payment-method-info">
|
||||||
|
<el-icon size="32" color="#07C160"><CreditCard /></el-icon>
|
||||||
|
<h5>微信支付</h5>
|
||||||
|
<p>支持微信扫码支付和H5支付</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="payment-method-info">
|
||||||
|
<el-icon size="32" color="#E6A23C"><CreditCard /></el-icon>
|
||||||
|
<h5>银联支付</h5>
|
||||||
|
<p>支持银联卡支付和网银支付</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
orderId: '',
|
||||||
|
amount: 0,
|
||||||
|
currency: 'CNY',
|
||||||
|
paymentMethod: 'ALIPAY',
|
||||||
|
description: '',
|
||||||
|
callbackUrl: '',
|
||||||
|
returnUrl: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
orderId: [
|
||||||
|
{ required: true, message: '请输入订单号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: '请输入支付金额', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 0.01, message: '支付金额必须大于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
currency: [
|
||||||
|
{ required: true, message: '请选择货币', trigger: 'change' }
|
||||||
|
],
|
||||||
|
paymentMethod: [
|
||||||
|
{ required: true, message: '请选择支付方式', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 模拟创建支付
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
ElMessage.success('支付创建成功')
|
||||||
|
router.push('/payments')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create payment error:', error)
|
||||||
|
ElMessage.error('创建支付失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-create {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info h5 {
|
||||||
|
margin: 12px 0 8px 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.payment-method-info {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
693
demo/frontend/src/views/Payments.vue
Normal file
693
demo/frontend/src/views/Payments.vue
Normal file
@@ -0,0 +1,693 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payments">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
支付记录
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.status"
|
||||||
|
placeholder="选择支付状态"
|
||||||
|
clearable
|
||||||
|
@change="handleFilterChange"
|
||||||
|
>
|
||||||
|
<el-option label="全部状态" value="" />
|
||||||
|
<el-option label="待支付" value="PENDING" />
|
||||||
|
<el-option label="支付成功" value="SUCCESS" />
|
||||||
|
<el-option label="支付失败" value="FAILED" />
|
||||||
|
<el-option label="已取消" value="CANCELLED" />
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.search"
|
||||||
|
placeholder="搜索订单号"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-button @click="resetFilters">重置筛选</el-button>
|
||||||
|
<el-button type="success" @click="showSubscriptionDialog('standard')">标准版订阅</el-button>
|
||||||
|
<el-button type="warning" @click="showSubscriptionDialog('professional')">专业版订阅</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 支付记录列表 -->
|
||||||
|
<el-card class="payments-card">
|
||||||
|
<el-table
|
||||||
|
:data="payments"
|
||||||
|
v-loading="loading"
|
||||||
|
empty-text="暂无支付记录"
|
||||||
|
>
|
||||||
|
<el-table-column prop="orderId" label="订单号" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<router-link :to="`/orders/${row.orderId}`" class="order-link">
|
||||||
|
{{ row.orderId }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="amount" label="金额" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="amount">{{ row.currency }} {{ row.amount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="paymentMethod" label="支付方式" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getPaymentMethodType(row.paymentMethod)">
|
||||||
|
{{ getPaymentMethodText(row.paymentMethod) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="description" label="描述" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="description">{{ row.description }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="paidAt" label="支付时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.paidAt ? formatDate(row.paidAt) : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
@click="viewPaymentDetail(row)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'PENDING'"
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
@click="testPaymentComplete(row)"
|
||||||
|
>
|
||||||
|
测试完成
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 支付详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="支付详情"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<div v-if="currentPayment">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单号">{{ currentPayment.orderId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付方式">
|
||||||
|
<el-tag :type="getPaymentMethodType(currentPayment.paymentMethod)">
|
||||||
|
{{ getPaymentMethodText(currentPayment.paymentMethod) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付金额">
|
||||||
|
<span class="amount">{{ currentPayment.currency }} {{ currentPayment.amount }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付状态">
|
||||||
|
<el-tag :type="getStatusType(currentPayment.status)">
|
||||||
|
{{ getStatusText(currentPayment.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="外部交易ID" v-if="currentPayment.externalTransactionId">
|
||||||
|
{{ currentPayment.externalTransactionId }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentPayment.createdAt) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付时间" v-if="currentPayment.paidAt">
|
||||||
|
{{ formatDate(currentPayment.paidAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ formatDate(currentPayment.updatedAt) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="currentPayment.description" class="payment-description">
|
||||||
|
<h4>支付描述</h4>
|
||||||
|
<p>{{ currentPayment.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 订阅对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="subscriptionDialogVisible"
|
||||||
|
:title="subscriptionDialogTitle"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<div class="subscription-info">
|
||||||
|
<h3>{{ subscriptionInfo.title }}</h3>
|
||||||
|
<p class="price">${{ subscriptionInfo.price }}</p>
|
||||||
|
<p class="description">{{ subscriptionInfo.description }}</p>
|
||||||
|
<div class="benefits">
|
||||||
|
<h4>包含功能:</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="benefit in subscriptionInfo.benefits" :key="benefit">
|
||||||
|
{{ benefit }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="points-info">
|
||||||
|
<el-tag type="success">支付完成后可获得 {{ subscriptionInfo.points }} 积分</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="payment-method">
|
||||||
|
<h4>选择支付方式:</h4>
|
||||||
|
<el-radio-group v-model="selectedPaymentMethod" @change="updatePrice">
|
||||||
|
<el-radio label="ALIPAY">支付宝</el-radio>
|
||||||
|
<el-radio label="PAYPAL">PayPal</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<div class="converted-price" v-if="convertedPrice">
|
||||||
|
<p>支付金额:<span class="price-display">{{ convertedPrice }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="subscriptionDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="createSubscription" :loading="subscriptionLoading">
|
||||||
|
立即订阅
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getPayments, testPaymentComplete as testPaymentCompleteApi, createTestPayment } from '@/api/payments'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const loading = ref(false)
|
||||||
|
const payments = ref([])
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const filters = reactive({
|
||||||
|
status: '',
|
||||||
|
search: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 支付详情对话框
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentPayment = ref(null)
|
||||||
|
|
||||||
|
|
||||||
|
// 订阅对话框
|
||||||
|
const subscriptionDialogVisible = ref(false)
|
||||||
|
const subscriptionLoading = ref(false)
|
||||||
|
const subscriptionType = ref('')
|
||||||
|
const selectedPaymentMethod = ref('ALIPAY')
|
||||||
|
const convertedPrice = ref('')
|
||||||
|
const exchangeRate = ref(7.2) // 美元对人民币汇率,可以根据实际情况调整
|
||||||
|
const subscriptionInfo = reactive({
|
||||||
|
title: '',
|
||||||
|
price: 0,
|
||||||
|
description: '',
|
||||||
|
benefits: [],
|
||||||
|
points: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const subscriptionDialogTitle = computed(() => {
|
||||||
|
return subscriptionType.value === 'standard' ? '标准版订阅' : '专业版订阅'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取支付方式类型
|
||||||
|
const getPaymentMethodType = (method) => {
|
||||||
|
const methodMap = {
|
||||||
|
'ALIPAY': 'primary',
|
||||||
|
'PAYPAL': 'success',
|
||||||
|
'WECHAT': 'success',
|
||||||
|
'UNIONPAY': 'warning'
|
||||||
|
}
|
||||||
|
return methodMap[method] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取支付方式文本
|
||||||
|
const getPaymentMethodText = (method) => {
|
||||||
|
const methodMap = {
|
||||||
|
'ALIPAY': '支付宝',
|
||||||
|
'PAYPAL': 'PayPal',
|
||||||
|
'WECHAT': '微信支付',
|
||||||
|
'UNIONPAY': '银联支付'
|
||||||
|
}
|
||||||
|
return methodMap[method] || method
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'SUCCESS': 'success',
|
||||||
|
'FAILED': 'danger',
|
||||||
|
'CANCELLED': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'SUCCESS': '支付成功',
|
||||||
|
'FAILED': '支付失败',
|
||||||
|
'CANCELLED': '已取消'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取支付记录列表
|
||||||
|
const fetchPayments = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const response = await getPayments({
|
||||||
|
page: pagination.page - 1,
|
||||||
|
size: pagination.size,
|
||||||
|
status: filters.status,
|
||||||
|
search: filters.search
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
payments.value = response.data
|
||||||
|
pagination.total = response.total || response.data.length
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '获取支付记录失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch payments error:', error)
|
||||||
|
ElMessage.error('获取支付记录失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选变化
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilters = () => {
|
||||||
|
filters.status = ''
|
||||||
|
filters.search = ''
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看支付详情
|
||||||
|
const viewPaymentDetail = (payment) => {
|
||||||
|
currentPayment.value = payment
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新价格显示
|
||||||
|
const updatePrice = () => {
|
||||||
|
if (selectedPaymentMethod.value === 'ALIPAY') {
|
||||||
|
// 支付宝使用人民币
|
||||||
|
const cnyPrice = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
|
||||||
|
convertedPrice.value = `¥${cnyPrice}`
|
||||||
|
} else if (selectedPaymentMethod.value === 'PAYPAL') {
|
||||||
|
// PayPal使用美元
|
||||||
|
convertedPrice.value = `$${subscriptionInfo.price}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示订阅对话框
|
||||||
|
const showSubscriptionDialog = (type) => {
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
ElMessage.warning('请先登录后再订阅')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionType.value = type
|
||||||
|
|
||||||
|
if (type === 'standard') {
|
||||||
|
subscriptionInfo.title = '标准版订阅'
|
||||||
|
subscriptionInfo.price = 59
|
||||||
|
subscriptionInfo.description = '适合个人用户的基础功能订阅'
|
||||||
|
subscriptionInfo.benefits = [
|
||||||
|
'基础AI功能使用',
|
||||||
|
'每月100次API调用',
|
||||||
|
'邮件技术支持',
|
||||||
|
'基础模板库访问'
|
||||||
|
]
|
||||||
|
subscriptionInfo.points = 200
|
||||||
|
} else if (type === 'professional') {
|
||||||
|
subscriptionInfo.title = '专业版订阅'
|
||||||
|
subscriptionInfo.price = 259
|
||||||
|
subscriptionInfo.description = '适合企业用户的高级功能订阅'
|
||||||
|
subscriptionInfo.benefits = [
|
||||||
|
'高级AI功能使用',
|
||||||
|
'每月1000次API调用',
|
||||||
|
'优先技术支持',
|
||||||
|
'完整模板库访问',
|
||||||
|
'API接口集成',
|
||||||
|
'数据分析报告'
|
||||||
|
]
|
||||||
|
subscriptionInfo.points = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionDialogVisible.value = true
|
||||||
|
// 初始化价格显示
|
||||||
|
updatePrice()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订阅支付
|
||||||
|
const createSubscription = async () => {
|
||||||
|
try {
|
||||||
|
subscriptionLoading.value = true
|
||||||
|
|
||||||
|
// 根据支付方式确定实际支付金额
|
||||||
|
let actualAmount
|
||||||
|
if (selectedPaymentMethod.value === 'ALIPAY') {
|
||||||
|
// 支付宝使用人民币
|
||||||
|
actualAmount = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
|
||||||
|
} else {
|
||||||
|
// PayPal使用美元
|
||||||
|
actualAmount = subscriptionInfo.price.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await createTestPayment({
|
||||||
|
amount: actualAmount,
|
||||||
|
method: selectedPaymentMethod.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success(`${subscriptionInfo.title}支付记录创建成功`)
|
||||||
|
|
||||||
|
// 根据支付方式调用相应的支付接口
|
||||||
|
if (selectedPaymentMethod.value === 'ALIPAY') {
|
||||||
|
try {
|
||||||
|
const alipayResponse = await createAlipayPayment({
|
||||||
|
paymentId: response.data.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (alipayResponse.success) {
|
||||||
|
// 跳转到支付宝支付页面
|
||||||
|
window.open(alipayResponse.data.paymentUrl, '_blank')
|
||||||
|
ElMessage.success('正在跳转到支付宝支付页面')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(alipayResponse.message || '创建支付宝支付失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建支付宝支付失败:', error)
|
||||||
|
ElMessage.error('创建支付宝支付失败')
|
||||||
|
}
|
||||||
|
} else if (selectedPaymentMethod.value === 'PAYPAL') {
|
||||||
|
try {
|
||||||
|
const paypalResponse = await createPayPalPayment({
|
||||||
|
paymentId: response.data.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (paypalResponse.success) {
|
||||||
|
// 跳转到PayPal支付页面
|
||||||
|
window.open(paypalResponse.data.paymentUrl, '_blank')
|
||||||
|
ElMessage.success('正在跳转到PayPal支付页面')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(paypalResponse.message || '创建PayPal支付失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建PayPal支付失败:', error)
|
||||||
|
ElMessage.error('创建PayPal支付失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionDialogVisible.value = false
|
||||||
|
// 刷新支付记录列表
|
||||||
|
fetchPayments()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '创建订阅支付记录失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create subscription error:', error)
|
||||||
|
ElMessage.error('创建订阅支付记录失败')
|
||||||
|
} finally {
|
||||||
|
subscriptionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 测试支付完成
|
||||||
|
const testPaymentComplete = async (payment) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要测试完成支付 ${payment.orderId} 吗?这将自动创建订单。`,
|
||||||
|
'确认测试',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await testPaymentCompleteApi(payment.id)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success('支付完成测试成功,订单已自动创建')
|
||||||
|
// 刷新支付记录列表
|
||||||
|
fetchPayments()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '测试支付完成失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('Test payment complete error:', error)
|
||||||
|
ElMessage.error('测试支付完成失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchPayments()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payments {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payments-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-description {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-description h4 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-description p {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info h3 {
|
||||||
|
color: #409eff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .price {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f56c6c;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits {
|
||||||
|
text-align: left;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits h4 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits li {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits li:before {
|
||||||
|
content: "✓ ";
|
||||||
|
color: #67c23a;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .points-info {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .payment-method {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .payment-method h4 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .converted-price {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b3d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .price-display {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
533
demo/frontend/src/views/Profile.vue
Normal file
533
demo/frontend/src/views/Profile.vue
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="logo">logo</div>
|
||||||
|
|
||||||
|
<!-- 导航菜单 -->
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人主页</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Compass /></el-icon>
|
||||||
|
<span @click="goToSubscription">会员订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>我的作品</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 工具分隔线 -->
|
||||||
|
<div class="divider">
|
||||||
|
<span>工具</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具菜单 -->
|
||||||
|
<nav class="tools-menu">
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>文生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>分镜视频</span>
|
||||||
|
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="points">
|
||||||
|
<el-icon><Star /></el-icon>
|
||||||
|
<span>25 | 首购优惠</span>
|
||||||
|
</div>
|
||||||
|
<div class="notifications">
|
||||||
|
<el-icon><Bell /></el-icon>
|
||||||
|
<div class="notification-dot"></div>
|
||||||
|
</div>
|
||||||
|
<div class="user-status">
|
||||||
|
<div class="status-icon"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 用户资料区域 -->
|
||||||
|
<section class="profile-section">
|
||||||
|
<div class="profile-info">
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="avatar-icon"></div>
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<h2 class="username">mingzi_FBx7foZYDS7inLQb</h2>
|
||||||
|
<p class="profile-status">还没有设置个人简介,点击填写</p>
|
||||||
|
<p class="user-id">ID 2994509784706419</p>
|
||||||
|
</div>
|
||||||
|
<el-button class="edit-btn">编辑资料</el-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 已发布内容 -->
|
||||||
|
<section class="published-section">
|
||||||
|
<h3 class="section-title">已发布</h3>
|
||||||
|
<div class="video-grid">
|
||||||
|
<div class="video-item" v-for="(video, index) in videos" :key="index">
|
||||||
|
<div class="video-thumbnail">
|
||||||
|
<div class="thumbnail-image">
|
||||||
|
<div class="figure"></div>
|
||||||
|
<div class="text-overlay">What Does it Mean To You</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-action">
|
||||||
|
<el-button v-if="index === 0" type="primary" size="small">做同款</el-button>
|
||||||
|
<span v-else class="director-text">DIRECTED BY VANNOCENT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Compass,
|
||||||
|
Document,
|
||||||
|
Picture,
|
||||||
|
VideoPlay,
|
||||||
|
Star,
|
||||||
|
Bell
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 模拟视频数据
|
||||||
|
const videos = ref(Array(6).fill({}))
|
||||||
|
|
||||||
|
// 跳转到会员订阅页面
|
||||||
|
const goToSubscription = () => {
|
||||||
|
router.push('/subscription')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面特殊效果 */
|
||||||
|
.profile-page::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 20%, rgba(64, 158, 255, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 90% 80%, rgba(103, 194, 58, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(230, 162, 60, 0.05) 0%, transparent 50%);
|
||||||
|
animation: profileGlow 6s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes profileGlow {
|
||||||
|
0% { opacity: 0.3; }
|
||||||
|
100% { opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容层级 */
|
||||||
|
.profile-page > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 20px 30px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu, .tools-menu {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sora-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 30px 20px 20px;
|
||||||
|
padding: 0 16px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.top-header {
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #409EFF;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #ff4757;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-status {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 2px solid #409EFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户资料区域 */
|
||||||
|
.profile-section {
|
||||||
|
padding: 30px;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
margin: 20px 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(64, 158, 255, 0.1) 0%, transparent 70%);
|
||||||
|
border-radius: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #409EFF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-status {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
background: #3a3a3a;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已发布内容 */
|
||||||
|
.published-section {
|
||||||
|
padding: 0 30px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 3px;
|
||||||
|
background: #1e3a8a;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #333;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-image {
|
||||||
|
height: 200px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.figure {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.figure::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-action {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item:hover .video-action {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.director-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.video-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profile-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu, .tools-menu {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
502
demo/frontend/src/views/Register.vue
Normal file
502
demo/frontend/src/views/Register.vue
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
<template>
|
||||||
|
<div class="register">
|
||||||
|
<el-row justify="center" align="middle" class="register-container">
|
||||||
|
<el-col :xs="22" :sm="16" :md="12" :lg="8" :xl="6">
|
||||||
|
<el-card class="register-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="register-header">
|
||||||
|
<el-icon size="32" color="#67C23A"><UserFilled /></el-icon>
|
||||||
|
<h2>用户注册</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="registerFormRef"
|
||||||
|
:model="registerForm"
|
||||||
|
:rules="registerRules"
|
||||||
|
label-width="80px"
|
||||||
|
@submit.prevent="handleRegister"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
prefix-icon="User"
|
||||||
|
clearable
|
||||||
|
@blur="checkUsername"
|
||||||
|
/>
|
||||||
|
<div v-if="usernameChecking" class="checking-text">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
检查中...
|
||||||
|
</div>
|
||||||
|
<div v-if="usernameExists" class="error-text">
|
||||||
|
<el-icon><CircleCloseFilled /></el-icon>
|
||||||
|
用户名已存在
|
||||||
|
</div>
|
||||||
|
<div v-if="usernameAvailable" class="success-text">
|
||||||
|
<el-icon><CircleCheckFilled /></el-icon>
|
||||||
|
用户名可用
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.email"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
prefix-icon="Message"
|
||||||
|
clearable
|
||||||
|
@blur="checkEmail"
|
||||||
|
/>
|
||||||
|
<div v-if="emailChecking" class="checking-text">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
检查中...
|
||||||
|
</div>
|
||||||
|
<div v-if="emailExists" class="error-text">
|
||||||
|
<el-icon><CircleCloseFilled /></el-icon>
|
||||||
|
邮箱已存在
|
||||||
|
</div>
|
||||||
|
<div v-if="emailAvailable" class="success-text">
|
||||||
|
<el-icon><CircleCheckFilled /></el-icon>
|
||||||
|
邮箱可用
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
prefix-icon="Lock"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
@input="checkPasswordStrength"
|
||||||
|
/>
|
||||||
|
<div v-if="passwordStrength" class="password-strength">
|
||||||
|
<div class="strength-bar">
|
||||||
|
<div
|
||||||
|
class="strength-fill"
|
||||||
|
:class="strengthClass"
|
||||||
|
:style="{ width: strengthWidth }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="strength-text">{{ strengthText }}</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
prefix-icon="Lock"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="agreeTerms">
|
||||||
|
我已阅读并同意
|
||||||
|
<a href="#" class="terms-link">《用户协议》</a>
|
||||||
|
和
|
||||||
|
<a href="#" class="terms-link">《隐私政策》</a>
|
||||||
|
</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
size="large"
|
||||||
|
:loading="userStore.loading"
|
||||||
|
:disabled="!canRegister"
|
||||||
|
@click="handleRegister"
|
||||||
|
class="register-button"
|
||||||
|
>
|
||||||
|
{{ userStore.loading ? '注册中...' : '注册' }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="register-footer">
|
||||||
|
<p>已有账号?<router-link to="/login" class="login-link">立即登录</router-link></p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { checkUsernameExists, checkEmailExists } from '@/api/auth'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const registerFormRef = ref()
|
||||||
|
const agreeTerms = ref(false)
|
||||||
|
|
||||||
|
// 用户名检查状态
|
||||||
|
const usernameChecking = ref(false)
|
||||||
|
const usernameExists = ref(false)
|
||||||
|
const usernameAvailable = ref(false)
|
||||||
|
|
||||||
|
// 邮箱检查状态
|
||||||
|
const emailChecking = ref(false)
|
||||||
|
const emailExists = ref(false)
|
||||||
|
const emailAvailable = ref(false)
|
||||||
|
|
||||||
|
// 密码强度
|
||||||
|
const passwordStrength = ref(false)
|
||||||
|
const strengthLevel = ref(0)
|
||||||
|
|
||||||
|
const registerForm = reactive({
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const registerRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
|
||||||
|
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value !== registerForm.password) {
|
||||||
|
callback(new Error('两次输入密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否存在
|
||||||
|
const checkUsername = async () => {
|
||||||
|
if (!registerForm.username || registerForm.username.length < 3) {
|
||||||
|
resetUsernameCheck()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
usernameChecking.value = true
|
||||||
|
usernameExists.value = false
|
||||||
|
usernameAvailable.value = false
|
||||||
|
|
||||||
|
const response = await checkUsernameExists(registerForm.username)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
usernameExists.value = response.data.exists
|
||||||
|
usernameAvailable.value = !response.data.exists
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Check username error:', error)
|
||||||
|
} finally {
|
||||||
|
usernameChecking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否存在
|
||||||
|
const checkEmail = async () => {
|
||||||
|
if (!registerForm.email || !isValidEmail(registerForm.email)) {
|
||||||
|
resetEmailCheck()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
emailChecking.value = true
|
||||||
|
emailExists.value = false
|
||||||
|
emailAvailable.value = false
|
||||||
|
|
||||||
|
const response = await checkEmailExists(registerForm.email)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
emailExists.value = response.data.exists
|
||||||
|
emailAvailable.value = !response.data.exists
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Check email error:', error)
|
||||||
|
} finally {
|
||||||
|
emailChecking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查密码强度
|
||||||
|
const checkPasswordStrength = () => {
|
||||||
|
const password = registerForm.password
|
||||||
|
if (!password) {
|
||||||
|
passwordStrength.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordStrength.value = true
|
||||||
|
|
||||||
|
let score = 0
|
||||||
|
if (password.length >= 6) score++
|
||||||
|
if (password.length >= 8) score++
|
||||||
|
if (/[a-z]/.test(password)) score++
|
||||||
|
if (/[A-Z]/.test(password)) score++
|
||||||
|
if (/[0-9]/.test(password)) score++
|
||||||
|
if (/[^A-Za-z0-9]/.test(password)) score++
|
||||||
|
|
||||||
|
strengthLevel.value = Math.min(score, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置用户名检查状态
|
||||||
|
const resetUsernameCheck = () => {
|
||||||
|
usernameChecking.value = false
|
||||||
|
usernameExists.value = false
|
||||||
|
usernameAvailable.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置邮箱检查状态
|
||||||
|
const resetEmailCheck = () => {
|
||||||
|
emailChecking.value = false
|
||||||
|
emailExists.value = false
|
||||||
|
emailAvailable.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证邮箱格式
|
||||||
|
const isValidEmail = (email) => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
return emailRegex.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const strengthClass = computed(() => {
|
||||||
|
const classes = ['weak', 'fair', 'good', 'strong']
|
||||||
|
return classes[strengthLevel.value - 1] || 'weak'
|
||||||
|
})
|
||||||
|
|
||||||
|
const strengthWidth = computed(() => {
|
||||||
|
return `${(strengthLevel.value / 4) * 100}%`
|
||||||
|
})
|
||||||
|
|
||||||
|
const strengthText = computed(() => {
|
||||||
|
const texts = ['弱', '一般', '良好', '强']
|
||||||
|
return texts[strengthLevel.value - 1] || '弱'
|
||||||
|
})
|
||||||
|
|
||||||
|
const canRegister = computed(() => {
|
||||||
|
return agreeTerms.value &&
|
||||||
|
usernameAvailable.value &&
|
||||||
|
emailAvailable.value &&
|
||||||
|
registerForm.password &&
|
||||||
|
registerForm.confirmPassword &&
|
||||||
|
registerForm.password === registerForm.confirmPassword
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!registerFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await registerFormRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
if (!agreeTerms.value) {
|
||||||
|
ElMessage.warning('请先同意用户协议和隐私政策')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await userStore.registerUser(registerForm)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(result.message || '注册成功')
|
||||||
|
router.push('/login')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '注册失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register error:', error)
|
||||||
|
ElMessage.error('注册失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.register {
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 40px 0;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面特殊效果 */
|
||||||
|
.register::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
|
||||||
|
animation: registerFloat 4s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes registerFloat {
|
||||||
|
0% { transform: translateY(0px) rotate(0deg); }
|
||||||
|
100% { transform: translateY(-10px) rotate(1deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容层级 */
|
||||||
|
.register > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-container {
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-card {
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-header h2 {
|
||||||
|
margin: 12px 0 0 0;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-footer p {
|
||||||
|
margin: 0;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
color: #67C23A;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms-link {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checking-text, .error-text, .success-text {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checking-text {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #F56C6C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-strength {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-bar {
|
||||||
|
height: 4px;
|
||||||
|
background-color: #EBEEF5;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-fill {
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-fill.weak {
|
||||||
|
background-color: #F56C6C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-fill.fair {
|
||||||
|
background-color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-fill.good {
|
||||||
|
background-color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-fill.strong {
|
||||||
|
background-color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.register {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-container {
|
||||||
|
min-height: calc(100vh - 160px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
13
demo/frontend/src/views/SimpleTest.vue
Normal file
13
demo/frontend/src/views/SimpleTest.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>测试页面</h1>
|
||||||
|
<p>如果您能看到这个页面,说明Vue应用正常工作。</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
console.log('测试页面加载成功')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
752
demo/frontend/src/views/StoryboardVideo.vue
Normal file
752
demo/frontend/src/views/StoryboardVideo.vue
Normal file
@@ -0,0 +1,752 @@
|
|||||||
|
<template>
|
||||||
|
<div class="storyboard-video-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">logo</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToProfile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人主页</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSubscription">
|
||||||
|
<el-icon><Compass /></el-icon>
|
||||||
|
<span>会员订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToMyWorks">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>我的作品</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-divider"></div>
|
||||||
|
<div class="nav-item" @click="goToTextToVideo">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>文生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToImageToVideo">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item active storyboard-item">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>分镜视频</span>
|
||||||
|
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部用户信息卡片 -->
|
||||||
|
<div class="user-info-card">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<div class="avatar-placeholder">👁️👃</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||||
|
<div class="profile-prompt">还没有设置个人简介,点击填写</div>
|
||||||
|
<div class="user-id">ID 2994509784706419</div>
|
||||||
|
</div>
|
||||||
|
<div class="edit-profile-btn">
|
||||||
|
<el-button type="primary">编辑资料</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已发布作品区域 -->
|
||||||
|
<div class="published-works">
|
||||||
|
<div class="works-tabs">
|
||||||
|
<div class="tab active">已发布</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="works-grid">
|
||||||
|
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
|
||||||
|
<div class="work-thumbnail">
|
||||||
|
<img :src="work.cover" :alt="work.title" />
|
||||||
|
<div class="work-overlay">
|
||||||
|
<div class="overlay-text">{{ work.text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-info">
|
||||||
|
<div class="work-title">{{ work.title }}</div>
|
||||||
|
<div class="work-meta">{{ work.id }} · {{ work.size }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-actions" v-if="index === 0">
|
||||||
|
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="work-director" v-else>
|
||||||
|
<span>DIRECTED BY VANNOCENT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 作品详情模态框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
:title="selectedItem?.title"
|
||||||
|
width="60%"
|
||||||
|
class="detail-dialog"
|
||||||
|
:modal="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="detail-left">
|
||||||
|
<div class="video-player">
|
||||||
|
<img :src="selectedItem?.cover" :alt="selectedItem?.title" class="video-thumbnail" />
|
||||||
|
<div class="play-overlay">
|
||||||
|
<div class="play-button">▶</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-right">
|
||||||
|
<div class="metadata-section">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">作品 ID</span>
|
||||||
|
<span class="value">{{ selectedItem?.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">文件大小</span>
|
||||||
|
<span class="value">{{ selectedItem?.size }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ selectedItem?.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">分类</span>
|
||||||
|
<span class="value">{{ selectedItem?.category }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-section">
|
||||||
|
<button class="create-similar-btn" @click="createSimilar">
|
||||||
|
做同款
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElIcon, ElButton, ElTag, ElMessage, ElDialog } from 'element-plus'
|
||||||
|
import { User, Compass, Document, VideoPlay, Picture } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
|
||||||
|
// 已发布作品数据
|
||||||
|
const publishedWorks = ref([
|
||||||
|
{
|
||||||
|
id: '2995000000001',
|
||||||
|
title: '分镜视频作品 #1',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '分镜视频',
|
||||||
|
createTime: '2025/01/15 14:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2995000000002',
|
||||||
|
title: '分镜视频作品 #2',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '分镜视频',
|
||||||
|
createTime: '2025/01/14 16:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2995000000003',
|
||||||
|
title: '分镜视频作品 #3',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '分镜视频',
|
||||||
|
createTime: '2025/01/13 09:20'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToProfile = () => {
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToSubscription = () => {
|
||||||
|
router.push('/subscription')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToMyWorks = () => {
|
||||||
|
router.push('/works')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTextToVideo = () => {
|
||||||
|
router.push('/text-to-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToImageToVideo = () => {
|
||||||
|
router.push('/image-to-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToCreate = (work) => {
|
||||||
|
// 跳转到分镜视频创作页面
|
||||||
|
router.push('/storyboard-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模态框相关函数
|
||||||
|
const openDetail = (work) => {
|
||||||
|
selectedItem.value = work
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
selectedItem.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDescription = (item) => {
|
||||||
|
if (!item) return ''
|
||||||
|
return `这是一个${item.category}作品,展现了"What Does it Mean To You"的主题。作品通过AI技术生成,具有独特的视觉风格和创意表达。`
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSimilar = () => {
|
||||||
|
// 关闭模态框并跳转到创作页面
|
||||||
|
handleClose()
|
||||||
|
router.push('/storyboard-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 页面初始化
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.storyboard-video-page {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #333;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sora-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜视频特殊样式 */
|
||||||
|
.storyboard-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-item .sora-tag {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3) !important;
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息卡片 */
|
||||||
|
.user-info-card {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-prompt {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-profile-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已发布作品区域 */
|
||||||
|
.published-works {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-info {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-actions {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover .work-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.storyboard-video-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框样式 */
|
||||||
|
:deep(.detail-dialog .el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__title) {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__headerbtn) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局覆盖Element Plus默认样式 */
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
height: 50vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-left {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player:hover .play-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d1d5db;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
732
demo/frontend/src/views/StoryboardVideoCreate.vue
Normal file
732
demo/frontend/src/views/StoryboardVideoCreate.vue
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
<template>
|
||||||
|
<div class="storyboard-video-create-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="back-btn" @click="goBack">
|
||||||
|
← 首页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="credits-info">
|
||||||
|
<div class="credits-circle">25</div>
|
||||||
|
<span>| 首购优惠</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-icon">
|
||||||
|
🔔
|
||||||
|
<div class="notification-badge">5</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-avatar">
|
||||||
|
👤
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 左侧设置面板 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<!-- 创作模式标签 -->
|
||||||
|
<div class="creation-tabs">
|
||||||
|
<div class="tab" @click="goToTextToVideo">文生视频</div>
|
||||||
|
<div class="tab" @click="goToImageToVideo">图生视频</div>
|
||||||
|
<div class="tab active">分镜视频</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分镜步骤标签 -->
|
||||||
|
<div class="storyboard-steps">
|
||||||
|
<div class="step active">生成分镜图</div>
|
||||||
|
<div class="step">生成视频</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成分镜图区域 -->
|
||||||
|
<div class="storyboard-section">
|
||||||
|
<div class="image-upload-btn" @click="uploadImage">
|
||||||
|
<span>+ 图片 (可选)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已上传的图片预览 -->
|
||||||
|
<div class="image-preview" v-if="uploadedImage">
|
||||||
|
<img :src="uploadedImage" alt="上传的图片" />
|
||||||
|
<button class="remove-btn" @click="removeImage">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-input-section">
|
||||||
|
<textarea
|
||||||
|
v-model="inputText"
|
||||||
|
placeholder="例如:一个咖啡的广告提示:简单描述即可,AI会自动优化成专业的12格黑白分镜图"
|
||||||
|
class="text-input"
|
||||||
|
rows="6"
|
||||||
|
></textarea>
|
||||||
|
<div class="optimize-btn">
|
||||||
|
<button class="optimize-button">
|
||||||
|
✨ 一键优化
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频设置 -->
|
||||||
|
<div class="video-settings">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>比例</label>
|
||||||
|
<select v-model="aspectRatio" class="setting-select">
|
||||||
|
<option value="16:9">16:9</option>
|
||||||
|
<option value="4:3">4:3</option>
|
||||||
|
<option value="1:1">1:1</option>
|
||||||
|
<option value="3:4">3:4</option>
|
||||||
|
<option value="9:16">9:16</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>高清模式 (1080P)</label>
|
||||||
|
<div class="hd-setting">
|
||||||
|
<input type="checkbox" v-model="hdMode" class="hd-switch">
|
||||||
|
<span class="cost-text">开启消耗20积分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成按钮 -->
|
||||||
|
<div class="generate-section">
|
||||||
|
<button class="generate-btn" @click="startGenerate">
|
||||||
|
开始生成
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧预览区域 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="preview-area">
|
||||||
|
<div class="status-checkbox">
|
||||||
|
<input type="checkbox" v-model="inProgress" id="progress-checkbox">
|
||||||
|
<label for="progress-checkbox">进行中</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-content">
|
||||||
|
<div class="preview-placeholder">
|
||||||
|
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const inputText = ref('')
|
||||||
|
const aspectRatio = ref('16:9')
|
||||||
|
const hdMode = ref(false)
|
||||||
|
const inProgress = ref(false)
|
||||||
|
|
||||||
|
// 图片上传
|
||||||
|
const uploadedImage = ref('')
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTextToVideo = () => {
|
||||||
|
router.push('/text-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToImageToVideo = () => {
|
||||||
|
router.push('/image-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传处理
|
||||||
|
const uploadImage = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = 'image/*'
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
uploadedImage.value = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeImage = () => {
|
||||||
|
uploadedImage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const startGenerate = () => {
|
||||||
|
if (!inputText.value.trim()) {
|
||||||
|
alert('请输入描述文字')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress.value = true
|
||||||
|
alert('开始生成分镜图...')
|
||||||
|
|
||||||
|
// 模拟生成过程
|
||||||
|
setTimeout(() => {
|
||||||
|
inProgress.value = false
|
||||||
|
alert('分镜图生成完成!')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.storyboard-video-create-page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航栏 */
|
||||||
|
.top-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 32px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border-bottom: 1px solid #1f1f1f;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credits-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credits-circle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #374151, #1f2937);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 400px 1fr;
|
||||||
|
gap: 0;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧面板 */
|
||||||
|
.left-panel {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #2a2a2a;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 创作模式标签 */
|
||||||
|
.creation-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜步骤标签 */
|
||||||
|
.storyboard-steps {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:hover:not(.active) {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜图区域 */
|
||||||
|
.storyboard-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px dashed #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本输入区域 */
|
||||||
|
.text-input-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input::placeholder {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-button {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频设置 */
|
||||||
|
.video-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select:hover {
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd-setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd-switch {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成按钮 */
|
||||||
|
.generate-section {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:disabled {
|
||||||
|
background: #6b7280;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧面板 */
|
||||||
|
.right-panel {
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
flex: 1;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content:hover {
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 350px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
padding: 16px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-tabs {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-steps {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
557
demo/frontend/src/views/Subscription.vue
Normal file
557
demo/frontend/src/views/Subscription.vue
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
<template>
|
||||||
|
<div class="subscription-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="logo">logo</div>
|
||||||
|
|
||||||
|
<!-- 导航菜单 -->
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToProfile" @mousedown="console.log('mousedown 个人主页')">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人主页</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" :class="{ active: currentSection === 'subscription' }" @click="setSection('subscription')" @mousedown="console.log('mousedown 会员订阅')">
|
||||||
|
<el-icon><Compass /></el-icon>
|
||||||
|
<span>会员订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" :class="{ active: currentSection === 'works' }" @click="setSection('works')" @mousedown="console.log('mousedown 我的作品')">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>我的作品</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 工具分隔线 -->
|
||||||
|
<div class="divider">
|
||||||
|
<span>工具</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具菜单 -->
|
||||||
|
<nav class="tools-menu">
|
||||||
|
<div class="nav-item" @click="goToTextToVideo">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>文生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToImageToVideo">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item storyboard-item">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>分镜视频</span>
|
||||||
|
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<template v-if="currentSection === 'works'">
|
||||||
|
<MyWorks />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- 顶部 两层合并为一个盒子 -->
|
||||||
|
<section class="top-merged-card">
|
||||||
|
<!-- 上层:用户信息 + 右侧按钮 -->
|
||||||
|
<div class="row-top">
|
||||||
|
<div class="user-left">
|
||||||
|
<div class="avatar-wrap">
|
||||||
|
<div class="avatar-circle">
|
||||||
|
<div class="pause-line"></div>
|
||||||
|
<div class="pause-line second"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-meta">
|
||||||
|
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||||
|
<div class="user-id">ID 2994509784706419</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-right">
|
||||||
|
<div class="points-pill">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
<span>50</span>
|
||||||
|
</div>
|
||||||
|
<button class="mini-btn">积分详情</button>
|
||||||
|
<button class="mini-btn" @click="goToWorks">我的订单</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 下层:三项总结 -->
|
||||||
|
<div class="row-bottom">
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">当前生效权益</div>
|
||||||
|
<div class="summary-value">免费版</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider-v"></div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">到期时间</div>
|
||||||
|
<div class="summary-value">永久</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider-v"></div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">剩余积分</div>
|
||||||
|
<div class="summary-value highlight">
|
||||||
|
<el-icon class="plus-icon"><Plus /></el-icon>
|
||||||
|
<span class="points-number">50</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 套餐选择 -->
|
||||||
|
<section class="subscription-packages">
|
||||||
|
<h3 class="section-title">套餐</h3>
|
||||||
|
|
||||||
|
<div class="packages-grid">
|
||||||
|
<!-- 免费版 -->
|
||||||
|
<div class="package-card free-card" :class="{ selected: selectedPlan === 'free' }" @click="selectPlan('free')">
|
||||||
|
<div class="package-header">
|
||||||
|
<h4 class="package-title">免费版</h4>
|
||||||
|
</div>
|
||||||
|
<div class="package-price">¥0/月</div>
|
||||||
|
<button class="package-button current">当前套餐</button>
|
||||||
|
<div class="package-features">
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>新用户首次登陆免费获得50积分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标准版 -->
|
||||||
|
<div class="package-card standard-card" :class="{ selected: selectedPlan === 'standard' }" @click="selectPlan('standard')">
|
||||||
|
<div class="package-header">
|
||||||
|
<h4 class="package-title">标准版</h4>
|
||||||
|
<div class="discount-tag">首购低至8.5折</div>
|
||||||
|
</div>
|
||||||
|
<div class="package-price">$59/月</div>
|
||||||
|
<div class="points-box">每月200积分</div>
|
||||||
|
<button class="package-button subscribe">立即订阅</button>
|
||||||
|
<div class="package-features">
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>快速通道生成</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>支持商用</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>下载去水印</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 专业版 -->
|
||||||
|
<div class="package-card premium-card" :class="{ selected: selectedPlan === 'premium' }" @click="selectPlan('premium')">
|
||||||
|
<div class="package-header">
|
||||||
|
<h4 class="package-title">专业版</h4>
|
||||||
|
<div class="value-tag">超值之选</div>
|
||||||
|
</div>
|
||||||
|
<div class="package-price">$259/月</div>
|
||||||
|
<div class="points-box">每月1000积分</div>
|
||||||
|
<button class="package-button premium">立即订阅</button>
|
||||||
|
<div class="package-features">
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>极速通道生成</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>支持商用</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>下载去水印</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<el-icon class="check-icon"><Check /></el-icon>
|
||||||
|
<span>新功能优先体验</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import MyWorks from '@/views/MyWorks.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Compass,
|
||||||
|
Document,
|
||||||
|
Picture,
|
||||||
|
VideoPlay,
|
||||||
|
Plus,
|
||||||
|
Bell,
|
||||||
|
Check
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 跳转到个人主页
|
||||||
|
const goToProfile = () => {
|
||||||
|
console.log('点击个人主页')
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTextToVideo = () => {
|
||||||
|
router.push('/text-to-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToImageToVideo = () => {
|
||||||
|
router.push('/image-to-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前主区块:subscription | works
|
||||||
|
const currentSection = ref('subscription')
|
||||||
|
const setSection = (section) => {
|
||||||
|
console.log('切换区块到:', section)
|
||||||
|
currentSection.value = section
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中套餐(紫色边框)
|
||||||
|
const selectedPlan = ref('free')
|
||||||
|
const selectPlan = (plan) => {
|
||||||
|
selectedPlan.value = plan
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.subscription-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: white;
|
||||||
|
display: flex !important;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px !important; /* 放大侧边栏 */
|
||||||
|
background: #1a1a1a !important;
|
||||||
|
padding: 24px 0 !important;
|
||||||
|
border-right: 1px solid #1a1a1a !important; /* 弱化分割线,与背景融为一体 */
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
z-index: 100 !important;
|
||||||
|
display: block !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 24px 32px;
|
||||||
|
font-size: 20px; /* 放大标题 */
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu, .tools-menu {
|
||||||
|
padding: 0 24px; /* 左右内边距同步放大 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 18px; /* 项高度放大 */
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 14px;
|
||||||
|
font-size: 20px; /* 放大图标 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 15px; /* 放大文字 */
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sora-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜视频特殊样式 */
|
||||||
|
.storyboard-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-item .sora-tag {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3) !important;
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 30px 20px 20px;
|
||||||
|
padding: 0 16px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
background: #0a0a0a;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 套餐选择 */
|
||||||
|
.subscription-packages {
|
||||||
|
padding: 0 40px 30px; /* 与顶部盒子保持一致的左右留白 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .section-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
text-align: left !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .section-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 0;
|
||||||
|
transform: none;
|
||||||
|
width: 60px;
|
||||||
|
height: 2px;
|
||||||
|
background: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .packages-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
max-width: 1440px;
|
||||||
|
margin: 0 40px 0 0; /* 右侧留白与左侧 padding(40px) 保持一致 */
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch; /* 卡片等高 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .package-card {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 28px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 700px; /* 进一步拉长 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .package-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .package-card.selected {
|
||||||
|
border: 2px solid #8b5cf6;
|
||||||
|
box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .package-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .package-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .discount-tag, .subscription-packages .value-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .discount-tag { background:#666; color:#fff; }
|
||||||
|
.subscription-packages .value-tag { background:#8b5cf6; color:#fff; }
|
||||||
|
|
||||||
|
.subscription-packages .package-price {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .points-box {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .package-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .package-button.current { background:#666; color:#fff; border:none; }
|
||||||
|
.subscription-packages .package-button.subscribe { background:#4a9eff; color:#fff; border:none; }
|
||||||
|
.subscription-packages .package-button.subscribe:hover { background:#3a8bdf; }
|
||||||
|
.subscription-packages .package-button.premium { background:#8b5cf6; color:#fff; border:none; }
|
||||||
|
.subscription-packages .package-button.premium:hover { background:#7c3aed; }
|
||||||
|
|
||||||
|
.subscription-packages .package-features {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: auto; /* 将特性列表推至卡片下部,保持视觉均衡 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .feature-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-packages .check-icon { color:#4a9eff; font-size:16px; }
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.subscription-packages .packages-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-overview {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-section {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部合并的两层盒子 */
|
||||||
|
.top-merged-card {
|
||||||
|
max-width: none; /* 取消限制,铺满内容区域 */
|
||||||
|
width: 100%;
|
||||||
|
margin: 32px 40px 10px 40px; /* 左右各 40px 留白,与套餐区对齐 */
|
||||||
|
background: #1a1a1a; /* 与卡片、页面风格保持一致 */
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-merged-card .row-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 26px;
|
||||||
|
padding: 22px 26px; /* 比例放大 */
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-merged-card .row-bottom {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr auto 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
padding: 22px 26px 24px; /* 比例放大 */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-merged-card .divider-v { width:1px; height:48px; background:#1f2937; }
|
||||||
|
.top-merged-card .summary-item { display:flex; flex-direction:column; gap:8px; }
|
||||||
|
.top-merged-card .summary-label { font-size:15px; color:#9ca3af; }
|
||||||
|
.top-merged-card .summary-value { font-size:17px; color:#e5e7eb; font-weight:600; }
|
||||||
|
.top-merged-card .summary-value.highlight { color:#60a5fa; display:flex; align-items:center; gap:8px; }
|
||||||
|
.top-merged-card .plus-icon { font-size:22px; color:#60a5fa; }
|
||||||
|
|
||||||
|
/* 顶部行内的用户信息与按钮样式(沿用原样式类) */
|
||||||
|
.user-left { display:flex; align-items:center; gap:18px; }
|
||||||
|
.avatar-wrap { width: 56px; height: 56px; }
|
||||||
|
.avatar-circle { width:56px; height:56px; border-radius:50%; background:linear-gradient(180deg,#1e3a8a,#111827); border:2px solid #4a9eff; display:flex; align-items:center; justify-content:center; position:relative; }
|
||||||
|
.pause-line { width:5px; height:20px; background:#fff; border-radius:2px; }
|
||||||
|
.pause-line.second { position:absolute; right:18px; width:5px; height:20px; background:#fff; border-radius:2px; }
|
||||||
|
.user-meta { display:flex; flex-direction:column; }
|
||||||
|
.username { font-size:19px; font-weight:600; color:#e5e7eb; }
|
||||||
|
.user-id { font-size:15px; color:#9ca3af; }
|
||||||
|
.user-right { display:flex; align-items:center; gap:12px; }
|
||||||
|
.points-pill { display:flex; align-items:center; gap:6px; padding:9px 14px; border-radius:999px; background:#0b1220; border:1px solid #1f3758; color:#60a5fa; font-weight:600; font-size:16px; }
|
||||||
|
.mini-btn { background:#0f172a; color:#e5e7eb; border:1px solid #334155; padding:9px 14px; border-radius:6px; font-size:14px; cursor:pointer; transition:.2s ease; }
|
||||||
|
.mini-btn:hover { background:#111827; border-color:#3b82f6; }
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.top-merged-card { margin: 0 16px 16px; }
|
||||||
|
.top-merged-card .row-top { flex-direction: column; align-items: flex-start; gap: 10px; }
|
||||||
|
.top-merged-card .row-bottom { grid-template-columns: 1fr; gap: 12px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
750
demo/frontend/src/views/TextToVideo.vue
Normal file
750
demo/frontend/src/views/TextToVideo.vue
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-to-video-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">logo</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToProfile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人主页</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSubscription">
|
||||||
|
<el-icon><Compass /></el-icon>
|
||||||
|
<span>会员订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToMyWorks">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>我的作品</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-divider"></div>
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>文生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToImageToVideo">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item storyboard-item" @click="goToStoryboard">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>分镜视频</span>
|
||||||
|
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部用户信息卡片 -->
|
||||||
|
<div class="user-info-card">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<div class="avatar-placeholder">||</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||||
|
<div class="profile-prompt">还没有设置个人简介,点击填写</div>
|
||||||
|
<div class="user-id">ID 2994509784706419</div>
|
||||||
|
</div>
|
||||||
|
<div class="edit-profile-btn">
|
||||||
|
<el-button type="primary">编辑资料</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已发布作品区域 -->
|
||||||
|
<div class="published-works">
|
||||||
|
<div class="works-tabs">
|
||||||
|
<div class="tab active">已发布</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="works-grid">
|
||||||
|
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
|
||||||
|
<div class="work-thumbnail">
|
||||||
|
<img :src="work.cover" :alt="work.title" />
|
||||||
|
<div class="work-overlay">
|
||||||
|
<div class="overlay-text">{{ work.text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-info">
|
||||||
|
<div class="work-title">{{ work.title }}</div>
|
||||||
|
<div class="work-meta">{{ work.id }} · {{ work.size }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-actions" v-if="index === 0">
|
||||||
|
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="work-director" v-else>
|
||||||
|
<span>DIRECTED BY VANNOCENT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 作品详情模态框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
:title="selectedItem?.title"
|
||||||
|
width="60%"
|
||||||
|
class="detail-dialog"
|
||||||
|
:modal="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="detail-left">
|
||||||
|
<div class="video-player">
|
||||||
|
<img :src="selectedItem?.cover" :alt="selectedItem?.title" class="video-thumbnail" />
|
||||||
|
<div class="play-overlay">
|
||||||
|
<div class="play-button">▶</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-right">
|
||||||
|
<div class="metadata-section">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">作品 ID</span>
|
||||||
|
<span class="value">{{ selectedItem?.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">文件大小</span>
|
||||||
|
<span class="value">{{ selectedItem?.size }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ selectedItem?.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">分类</span>
|
||||||
|
<span class="value">{{ selectedItem?.category }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-section">
|
||||||
|
<button class="create-similar-btn" @click="createSimilar">
|
||||||
|
做同款
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElIcon, ElButton, ElTag, ElMessage, ElDialog } from 'element-plus'
|
||||||
|
import { User, Compass, Document, VideoPlay, Picture } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
|
||||||
|
// 已发布作品数据
|
||||||
|
const publishedWorks = ref([
|
||||||
|
{
|
||||||
|
id: '2995000000001',
|
||||||
|
title: '文生视频作品 #1',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '文生视频',
|
||||||
|
createTime: '2025/01/15 14:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2995000000002',
|
||||||
|
title: '文生视频作品 #2',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '文生视频',
|
||||||
|
createTime: '2025/01/14 16:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2995000000003',
|
||||||
|
title: '文生视频作品 #3',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg',
|
||||||
|
text: 'What Does it Mean To You',
|
||||||
|
size: '9 MB',
|
||||||
|
category: '文生视频',
|
||||||
|
createTime: '2025/01/13 09:20'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToProfile = () => {
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToSubscription = () => {
|
||||||
|
router.push('/subscription')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToMyWorks = () => {
|
||||||
|
router.push('/works')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToImageToVideo = () => {
|
||||||
|
router.push('/image-to-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToStoryboard = () => {
|
||||||
|
router.push('/storyboard-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToCreate = (work) => {
|
||||||
|
// 跳转到文生视频创作页面
|
||||||
|
router.push('/text-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模态框相关函数
|
||||||
|
const openDetail = (work) => {
|
||||||
|
selectedItem.value = work
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
selectedItem.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDescription = (item) => {
|
||||||
|
if (!item) return ''
|
||||||
|
return `这是一个${item.category}作品,展现了"What Does it Mean To You"的主题。作品通过AI技术生成,具有独特的视觉风格和创意表达。`
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSimilar = () => {
|
||||||
|
// 关闭模态框并跳转到创作页面
|
||||||
|
handleClose()
|
||||||
|
router.push('/text-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 页面初始化
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-to-video-page {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #333;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sora-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜视频特殊样式 */
|
||||||
|
.storyboard-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-item .sora-tag {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3) !important;
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息卡片 */
|
||||||
|
.user-info-card {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-prompt {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-profile-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已发布作品区域 */
|
||||||
|
.published-works {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-info {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-actions {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover .work-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.text-to-video-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框样式 */
|
||||||
|
:deep(.detail-dialog .el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__title) {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__headerbtn) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局覆盖Element Plus默认样式 */
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
height: 50vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-left {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player:hover .play-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d1d5db;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
596
demo/frontend/src/views/TextToVideoCreate.vue
Normal file
596
demo/frontend/src/views/TextToVideoCreate.vue
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-to-video-create-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="back-btn" @click="goBack">
|
||||||
|
← 首页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="credits-info">
|
||||||
|
<div class="credits-circle">25</div>
|
||||||
|
<span>| 首购优惠</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-icon">
|
||||||
|
🔔
|
||||||
|
<div class="notification-badge">5</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-avatar">
|
||||||
|
👤
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 左侧设置面板 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<!-- 创作模式标签 -->
|
||||||
|
<div class="creation-tabs">
|
||||||
|
<div class="tab active">文生视频</div>
|
||||||
|
<div class="tab" @click="goToImageToVideo">图生视频</div>
|
||||||
|
<div class="tab" @click="goToStoryboardVideo">分镜视频</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文本输入区域 -->
|
||||||
|
<div class="text-input-section">
|
||||||
|
<textarea
|
||||||
|
v-model="inputText"
|
||||||
|
placeholder="输入文字,描述想要生成的内容"
|
||||||
|
class="text-input"
|
||||||
|
rows="8"
|
||||||
|
></textarea>
|
||||||
|
<div class="optimize-btn">
|
||||||
|
<button class="optimize-button">
|
||||||
|
✨ 一键优化
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频设置 -->
|
||||||
|
<div class="video-settings">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>比例</label>
|
||||||
|
<select v-model="aspectRatio" class="setting-select">
|
||||||
|
<option value="16:9">16:9</option>
|
||||||
|
<option value="9:16">9:16</option>
|
||||||
|
<option value="1:1">1:1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>时长</label>
|
||||||
|
<select v-model="duration" class="setting-select">
|
||||||
|
<option value="5">5s</option>
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="15">15s</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label>高清模式 (1080P)</label>
|
||||||
|
<div class="hd-setting">
|
||||||
|
<input type="checkbox" v-model="hdMode" class="hd-switch">
|
||||||
|
<span class="cost-text">开启消耗20积分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成按钮 -->
|
||||||
|
<div class="generate-section">
|
||||||
|
<button class="generate-btn" @click="startGenerate">
|
||||||
|
开始生成
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧预览区域 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="preview-area">
|
||||||
|
<div class="status-checkbox">
|
||||||
|
<input type="checkbox" v-model="inProgress" id="progress-checkbox">
|
||||||
|
<label for="progress-checkbox">进行中</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-content">
|
||||||
|
<div class="preview-placeholder">
|
||||||
|
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const inputText = ref('')
|
||||||
|
const aspectRatio = ref('16:9')
|
||||||
|
const duration = ref('5')
|
||||||
|
const hdMode = ref(false)
|
||||||
|
const inProgress = ref(false)
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToImageToVideo = () => {
|
||||||
|
router.push('/image-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToStoryboardVideo = () => {
|
||||||
|
router.push('/storyboard-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
const startGenerate = () => {
|
||||||
|
if (!inputText.value.trim()) {
|
||||||
|
alert('请输入描述文字')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress.value = true
|
||||||
|
alert('开始生成视频...')
|
||||||
|
|
||||||
|
// 模拟生成过程
|
||||||
|
setTimeout(() => {
|
||||||
|
inProgress.value = false
|
||||||
|
alert('视频生成完成!')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-to-video-create-page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航栏 */
|
||||||
|
.top-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 32px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border-bottom: 1px solid #1f1f1f;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credits-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credits-circle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #374151, #1f2937);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 400px 1fr;
|
||||||
|
gap: 0;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧面板 */
|
||||||
|
.left-panel {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #2a2a2a;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 创作模式标签 */
|
||||||
|
.creation-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本输入区域 */
|
||||||
|
.text-input-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 140px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input::placeholder {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-button {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optimize-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频设置 */
|
||||||
|
.video-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select:hover {
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd-setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd-switch {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成按钮 */
|
||||||
|
.generate-section {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:disabled {
|
||||||
|
background: #6b7280;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧面板 */
|
||||||
|
.right-panel {
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checkbox label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
flex: 1;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content:hover {
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 350px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
padding: 16px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-tabs {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
756
demo/frontend/src/views/VideoDetail.vue
Normal file
756
demo/frontend/src/views/VideoDetail.vue
Normal file
@@ -0,0 +1,756 @@
|
|||||||
|
<template>
|
||||||
|
<div class="video-detail-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="logo">logo</div>
|
||||||
|
|
||||||
|
<!-- 导航菜单 -->
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToProfile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人主页</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSubscription">
|
||||||
|
<el-icon><Compass /></el-icon>
|
||||||
|
<span>会员订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>我的作品</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 左侧视频播放器区域 -->
|
||||||
|
<div class="video-player-section">
|
||||||
|
<div class="video-container">
|
||||||
|
<video
|
||||||
|
ref="videoPlayer"
|
||||||
|
class="video-player"
|
||||||
|
:src="videoData.videoUrl"
|
||||||
|
:poster="videoData.cover"
|
||||||
|
@loadedmetadata="onVideoLoaded"
|
||||||
|
@timeupdate="onTimeUpdate"
|
||||||
|
@ended="onVideoEnded"
|
||||||
|
>
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<!-- 视频文字叠加 -->
|
||||||
|
<div class="video-overlay" v-if="videoData.overlayText">
|
||||||
|
<div class="overlay-text">{{ videoData.overlayText }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放控制栏 -->
|
||||||
|
<div class="video-controls">
|
||||||
|
<div class="control-left">
|
||||||
|
<button class="play-btn" @click="togglePlay">
|
||||||
|
<el-icon v-if="!isPlaying"><VideoPlay /></el-icon>
|
||||||
|
<el-icon v-else><Pause /></el-icon>
|
||||||
|
</button>
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar" @click="seekTo">
|
||||||
|
<div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-right">
|
||||||
|
<button class="control-btn" @click="toggleFullscreen">
|
||||||
|
<el-icon><FullScreen /></el-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右上角操作按钮 -->
|
||||||
|
<div class="video-actions">
|
||||||
|
<el-tooltip content="分享" placement="bottom">
|
||||||
|
<button class="action-btn" @click="shareVideo">
|
||||||
|
<el-icon><Share /></el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="下载" placement="bottom">
|
||||||
|
<button class="action-btn" @click="downloadVideo">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="bottom">
|
||||||
|
<button class="action-btn delete-btn" @click="deleteVideo">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧详情侧边栏 -->
|
||||||
|
<div class="detail-sidebar">
|
||||||
|
<!-- 用户信息头部 -->
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="avatar">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="username">{{ videoData.username }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="close-btn" @click="goBack">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签页 -->
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active">视频详情</div>
|
||||||
|
<div class="tab">文生视频</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述区域 -->
|
||||||
|
<div class="description-section">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">{{ videoData.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 元数据区域 -->
|
||||||
|
<div class="metadata-section">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ videoData.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">视频 ID</span>
|
||||||
|
<span class="value">{{ videoData.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">时长</span>
|
||||||
|
<span class="value">{{ videoData.duration }}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">清晰度</span>
|
||||||
|
<span class="value">{{ videoData.resolution }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">分类</span>
|
||||||
|
<span class="value">{{ videoData.category }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">宽高比</span>
|
||||||
|
<span class="value">{{ videoData.aspectRatio }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-section">
|
||||||
|
<button class="create-similar-btn" @click="createSimilar">
|
||||||
|
做同款
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
VideoPlay,
|
||||||
|
VideoPause as Pause,
|
||||||
|
FullScreen,
|
||||||
|
Share,
|
||||||
|
Download,
|
||||||
|
Delete,
|
||||||
|
User,
|
||||||
|
Compass,
|
||||||
|
Document,
|
||||||
|
Close
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 视频播放器相关
|
||||||
|
const videoPlayer = ref(null)
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const progressPercent = ref(0)
|
||||||
|
|
||||||
|
// 视频数据
|
||||||
|
const videoData = ref({
|
||||||
|
id: '2995697841305810',
|
||||||
|
username: 'mingzi_FBx7foZYDS7inL',
|
||||||
|
title: 'What Does it Mean To You',
|
||||||
|
overlayText: 'What Does it Mean To You',
|
||||||
|
description: '影片捕捉了暴风雪中的午夜时分,坐落在积雪覆盖的悬崖顶上的孤立灯塔。相机逐渐放大灯塔的灯光,穿透飞舞的雪花,投射出幽幽的光芒。在白茫茫的环境中,灯塔的黑色轮廓显得格外醒目,呼啸的风声和远处海浪的撞击声增强了孤独的氛围。这一场景展示了灯塔的孤独力量。',
|
||||||
|
createTime: '2025/10/17 13:41',
|
||||||
|
duration: 5,
|
||||||
|
resolution: '1080p',
|
||||||
|
category: '文生视频',
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
videoUrl: '/images/backgrounds/welcome.jpg', // 临时使用图片作为视频
|
||||||
|
cover: '/images/backgrounds/welcome.jpg'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据ID获取视频数据
|
||||||
|
const getVideoData = (id) => {
|
||||||
|
// 模拟不同ID对应不同的分类
|
||||||
|
const videoConfigs = {
|
||||||
|
'2995000000001': { category: '参考图', title: '图片作品 #1' },
|
||||||
|
'2995000000002': { category: '参考图', title: '图片作品 #2' },
|
||||||
|
'2995000000003': { category: '文生视频', title: '视频作品 #3' },
|
||||||
|
'2995000000004': { category: '图生视频', title: '视频作品 #4' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = videoConfigs[id] || videoConfigs['2995000000003']
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
username: 'mingzi_FBx7foZYDS7inL',
|
||||||
|
title: config.title,
|
||||||
|
overlayText: config.title,
|
||||||
|
description: '影片捕捉了暴风雪中的午夜时分,坐落在积雪覆盖的悬崖顶上的孤立灯塔。相机逐渐放大灯塔的灯光,穿透飞舞的雪花,投射出幽幽的光芒。在白茫茫的环境中,灯塔的黑色轮廓显得格外醒目,呼啸的风声和远处海浪的撞击声增强了孤独的氛围。这一场景展示了灯塔的孤独力量。',
|
||||||
|
createTime: '2025/10/17 13:41',
|
||||||
|
duration: 5,
|
||||||
|
resolution: '1080p',
|
||||||
|
category: config.category,
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
videoUrl: '/images/backgrounds/welcome.jpg',
|
||||||
|
cover: '/images/backgrounds/welcome.jpg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视频播放控制
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (!videoPlayer.value) return
|
||||||
|
|
||||||
|
if (isPlaying.value) {
|
||||||
|
videoPlayer.value.pause()
|
||||||
|
} else {
|
||||||
|
videoPlayer.value.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVideoLoaded = () => {
|
||||||
|
duration.value = videoPlayer.value.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTimeUpdate = () => {
|
||||||
|
currentTime.value = videoPlayer.value.currentTime
|
||||||
|
progressPercent.value = (currentTime.value / duration.value) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVideoEnded = () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekTo = (event) => {
|
||||||
|
if (!videoPlayer.value) return
|
||||||
|
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
|
const clickX = event.clientX - rect.left
|
||||||
|
const percentage = clickX / rect.width
|
||||||
|
const newTime = percentage * duration.value
|
||||||
|
|
||||||
|
videoPlayer.value.currentTime = newTime
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
const minutes = Math.floor(time / 60)
|
||||||
|
const seconds = Math.floor(time % 60)
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (!videoPlayer.value) return
|
||||||
|
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
} else {
|
||||||
|
videoPlayer.value.requestFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作功能
|
||||||
|
const shareVideo = () => {
|
||||||
|
ElMessage.info('分享功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadVideo = () => {
|
||||||
|
ElMessage.success('开始下载视频')
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteVideo = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除这个视频吗?', '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
ElMessage.success('视频已删除')
|
||||||
|
router.back()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSimilar = () => {
|
||||||
|
ElMessage.info('跳转到文生视频创作页面')
|
||||||
|
// router.push('/create-video')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToProfile = () => {
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToSubscription = () => {
|
||||||
|
router.push('/subscription')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听视频播放状态变化
|
||||||
|
const handlePlay = () => {
|
||||||
|
isPlaying.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePause = () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 根据路由参数更新视频数据
|
||||||
|
const videoId = route.params.id
|
||||||
|
if (videoId) {
|
||||||
|
videoData.value = getVideoData(videoId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoPlayer.value) {
|
||||||
|
videoPlayer.value.addEventListener('play', handlePlay)
|
||||||
|
videoPlayer.value.addEventListener('pause', handlePause)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (videoPlayer.value) {
|
||||||
|
videoPlayer.value.removeEventListener('play', handlePlay)
|
||||||
|
videoPlayer.value.removeEventListener('pause', handlePause)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-detail-page {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 24px 0;
|
||||||
|
border-right: 1px solid #1a1a1a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 24px 32px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 14px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 15px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
background: #0a0a0a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧视频播放器区域 */
|
||||||
|
.video-player-section {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-text {
|
||||||
|
font-family: 'Brush Script MT', cursive;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #8b5cf6;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频控制栏 */
|
||||||
|
.video-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #409eff;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右上角操作按钮 */
|
||||||
|
.video-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: rgba(220, 38, 38, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧详情侧边栏 */
|
||||||
|
.detail-sidebar {
|
||||||
|
flex: 1;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 12px 0 0 0;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #409eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d1d5db;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:hover {
|
||||||
|
background: #337ecc;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.video-detail-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player-section {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-sidebar {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.video-overlay {
|
||||||
|
bottom: 60px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-controls {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions {
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-sidebar {
|
||||||
|
padding: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
223
demo/frontend/src/views/Welcome.vue
Normal file
223
demo/frontend/src/views/Welcome.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome-page">
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<header class="navbar">
|
||||||
|
<div class="navbar-content">
|
||||||
|
<div class="logo">Logo</div>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="#" class="nav-link">文生视频</a>
|
||||||
|
<a href="#" class="nav-link">图生视频</a>
|
||||||
|
<a href="#" class="nav-link">分镜视频</a>
|
||||||
|
<a href="#" class="nav-link">订阅套餐</a>
|
||||||
|
</nav>
|
||||||
|
<button class="nav-button">开始体验</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<main class="content">
|
||||||
|
<h1 class="title">
|
||||||
|
<span class="title-line">
|
||||||
|
<span class="bright-text">智创</span><span class="fade-text">无限,</span>
|
||||||
|
</span>
|
||||||
|
<span class="title-line">
|
||||||
|
<span class="bright-text">灵感</span><span class="fade-text">变现。</span>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<button class="main-button">立即体验</button>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 背景光影效果已删除 -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.welcome-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: url('/images/backgrounds/welcome.jpg') center/cover no-repeat;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏 */
|
||||||
|
.navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 95%;
|
||||||
|
max-width: 1200px;
|
||||||
|
background: rgba(26, 26, 46, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: rgba(74, 158, 255, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover {
|
||||||
|
background: rgba(74, 158, 255, 1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主要内容 */
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 80px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 6.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-line {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bright-text {
|
||||||
|
color: white;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-text {
|
||||||
|
color: white;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button {
|
||||||
|
background: linear-gradient(90deg, rgba(74, 158, 255, 0.8) 0%, rgba(255, 255, 255, 0.9) 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 22px 60px;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 6px 25px rgba(74, 158, 255, 0.3);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 40px rgba(74, 158, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button:active {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景光影效果已删除 */
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.title {
|
||||||
|
font-size: 5.5rem;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
.main-button {
|
||||||
|
padding: 20px 50px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-links {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.main-button {
|
||||||
|
padding: 18px 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.title {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.main-button {
|
||||||
|
padding: 16px 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
45
demo/frontend/vite.config.js
Normal file
45
demo/frontend/vite.config.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
// 确保后端返回的 Set-Cookie 可被前端域接收与发送
|
||||||
|
cookieDomainRewrite: 'localhost',
|
||||||
|
cookiePathRewrite: '/',
|
||||||
|
configure: (proxy, _options) => {
|
||||||
|
proxy.on('error', (err, _req, _res) => {
|
||||||
|
console.log('proxy error', err);
|
||||||
|
});
|
||||||
|
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||||
|
console.log('Sending Request to the Target:', req.method, req.url);
|
||||||
|
});
|
||||||
|
proxy.on('proxyRes', (proxyRes, req, _res) => {
|
||||||
|
console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
|
||||||
|
const setCookie = proxyRes.headers['set-cookie'];
|
||||||
|
if (setCookie) {
|
||||||
|
console.log('Proxy Set-Cookie:', setCookie);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets'
|
||||||
|
}
|
||||||
|
})
|
||||||
53
demo/insert_test_data.ps1
Normal file
53
demo/insert_test_data.ps1
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# MySQL测试数据插入脚本
|
||||||
|
# 需要先安装MySQL客户端
|
||||||
|
|
||||||
|
Write-Host "正在连接MySQL数据库并插入测试数据..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# MySQL连接参数
|
||||||
|
$mysqlHost = "localhost"
|
||||||
|
$mysqlPort = "3306"
|
||||||
|
$mysqlUser = "root"
|
||||||
|
$mysqlPassword = "177615"
|
||||||
|
$mysqlDatabase = "aigc"
|
||||||
|
|
||||||
|
# 检查MySQL是否可用
|
||||||
|
try {
|
||||||
|
$testConnection = mysql -h $mysqlHost -P $mysqlPort -u $mysqlUser -p$mysqlPassword -e "SELECT 1;" 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "MySQL连接成功!" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "MySQL连接失败,请检查MySQL服务是否运行" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "MySQL客户端未安装或不在PATH中" -ForegroundColor Red
|
||||||
|
Write-Host "请安装MySQL客户端或使用MySQL Workbench执行 insert_test_data.sql" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行SQL脚本
|
||||||
|
Write-Host "正在执行SQL脚本..." -ForegroundColor Yellow
|
||||||
|
mysql -h $mysqlHost -P $mysqlPort -u $mysqlUser -p$mysqlPassword $mysqlDatabase -e "
|
||||||
|
DELETE FROM users WHERE username IN ('demo', 'admin', 'testuser', 'mingzi_FBx7foZYDS7inLQb', '15538239326');
|
||||||
|
|
||||||
|
INSERT INTO users (username, email, password_hash, role, points) VALUES
|
||||||
|
('demo', 'demo@example.com', 'demo', 'ROLE_USER', 100),
|
||||||
|
('admin', 'admin@example.com', 'admin123', 'ROLE_ADMIN', 200),
|
||||||
|
('testuser', 'testuser@example.com', 'test123', 'ROLE_USER', 75),
|
||||||
|
('mingzi_FBx7foZYDS7inLQb', 'mingzi@example.com', '123456', 'ROLE_USER', 25),
|
||||||
|
('15538239326', '15538239326@example.com', '0627', 'ROLE_USER', 50);
|
||||||
|
|
||||||
|
SELECT username, email, role, points FROM users WHERE username IN ('demo', 'admin', 'testuser', 'mingzi_FBx7foZYDS7inLQb', '15538239326');
|
||||||
|
"
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "测试数据插入成功!" -ForegroundColor Green
|
||||||
|
Write-Host "现在可以使用以下账号登录:" -ForegroundColor Cyan
|
||||||
|
Write-Host " - 普通用户: demo / demo" -ForegroundColor White
|
||||||
|
Write-Host " - 管理员: admin / admin123" -ForegroundColor White
|
||||||
|
Write-Host " - 测试用户: testuser / test123" -ForegroundColor White
|
||||||
|
Write-Host " - 个人主页: mingzi_FBx7foZYDS7inLQb / 123456" -ForegroundColor White
|
||||||
|
Write-Host " - 手机号测试: 15538239326 / 0627" -ForegroundColor White
|
||||||
|
} else {
|
||||||
|
Write-Host "测试数据插入失败!" -ForegroundColor Red
|
||||||
|
}
|
||||||
19
demo/insert_test_data.sql
Normal file
19
demo/insert_test_data.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- MySQL测试数据插入脚本
|
||||||
|
-- 连接到MySQL数据库: aigc
|
||||||
|
-- 用户名: root, 密码: 177615
|
||||||
|
|
||||||
|
USE aigc;
|
||||||
|
|
||||||
|
-- 删除现有测试数据(如果存在)
|
||||||
|
DELETE FROM users WHERE username IN ('demo', 'admin', 'testuser', 'mingzi_FBx7foZYDS7inLQb', '15538239326');
|
||||||
|
|
||||||
|
-- 插入测试用户数据
|
||||||
|
INSERT INTO users (username, email, password_hash, role, points) VALUES
|
||||||
|
('demo', 'demo@example.com', 'demo', 'ROLE_USER', 100),
|
||||||
|
('admin', 'admin@example.com', 'admin123', 'ROLE_ADMIN', 200),
|
||||||
|
('testuser', 'testuser@example.com', 'test123', 'ROLE_USER', 75),
|
||||||
|
('mingzi_FBx7foZYDS7inLQb', 'mingzi@example.com', '123456', 'ROLE_USER', 25),
|
||||||
|
('15538239326', '15538239326@example.com', '0627', 'ROLE_USER', 50);
|
||||||
|
|
||||||
|
-- 验证插入结果
|
||||||
|
SELECT username, email, role, points FROM users WHERE username IN ('demo', 'admin', 'testuser', 'mingzi_FBx7foZYDS7inLQb', '15538239326');
|
||||||
BIN
demo/mysql-connector-java-8.0.33.jar
Normal file
BIN
demo/mysql-connector-java-8.0.33.jar
Normal file
Binary file not shown.
150
demo/pom.xml
Normal file
150
demo/pom.xml
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.5.6</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.example</groupId>
|
||||||
|
<artifactId>demo</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>demo</name>
|
||||||
|
<description>Demo project for Spring Boot</description>
|
||||||
|
<url/>
|
||||||
|
<licenses>
|
||||||
|
<license/>
|
||||||
|
</licenses>
|
||||||
|
<developers>
|
||||||
|
<developer/>
|
||||||
|
</developers>
|
||||||
|
<scm>
|
||||||
|
<connection/>
|
||||||
|
<developerConnection/>
|
||||||
|
<tag/>
|
||||||
|
<url/>
|
||||||
|
</scm>
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Thymeleaf for server-side templates -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Security for login -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JPA for persistence -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Bean Validation -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- In-memory H2 database -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MySQL JDBC driver -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
<version>8.0.33</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 支付宝SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alipay.sdk</groupId>
|
||||||
|
<artifactId>alipay-sdk-java</artifactId>
|
||||||
|
<version>4.38.10.ALL</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JWT支持 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- PayPal SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.paypal.sdk</groupId>
|
||||||
|
<artifactId>rest-api-sdk</artifactId>
|
||||||
|
<version>1.14.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON处理 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Hibernate Jackson支持 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-hibernate5-jakarta</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- HTTP客户端 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
13
demo/src/main/java/com/example/demo/DemoApplication.java
Normal file
13
demo/src/main/java/com/example/demo/DemoApplication.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.example.demo;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class DemoApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DemoApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.example.demo.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public ObjectMapper objectMapper() {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// 注册Hibernate模块来处理代理对象
|
||||||
|
mapper.registerModule(new Hibernate5JakartaModule());
|
||||||
|
|
||||||
|
// 注册Java时间模块
|
||||||
|
mapper.registerModule(new JavaTimeModule());
|
||||||
|
|
||||||
|
// 禁用将日期写为时间戳
|
||||||
|
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
|
|
||||||
|
// 配置Hibernate模块
|
||||||
|
Hibernate5JakartaModule hibernateModule = new Hibernate5JakartaModule();
|
||||||
|
hibernateModule.disable(Hibernate5JakartaModule.Feature.USE_TRANSIENT_ANNOTATION);
|
||||||
|
mapper.registerModule(hibernateModule);
|
||||||
|
|
||||||
|
return mapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
demo/src/main/java/com/example/demo/config/SecurityConfig.java
Normal file
103
demo/src/main/java/com/example/demo/config/SecurityConfig.java
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package com.example.demo.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import com.example.demo.security.PlainTextPasswordEncoder;
|
||||||
|
import com.example.demo.security.JwtAuthenticationFilter;
|
||||||
|
import com.example.demo.util.JwtUtils;
|
||||||
|
import com.example.demo.service.UserService;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new PlainTextPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtils jwtUtils, UserService userService) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
|
.sessionManagement(session -> session
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,使用JWT
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/login", "/register", "/api/public/**", "/api/auth/**", "/css/**", "/js/**", "/h2-console/**").permitAll()
|
||||||
|
.requestMatchers("/api/orders/stats").permitAll() // 统计接口允许匿名访问
|
||||||
|
.requestMatchers("/api/orders/**").authenticated() // 订单接口需要认证
|
||||||
|
.requestMatchers("/api/payments/**").authenticated() // 支付接口需要认证
|
||||||
|
.requestMatchers("/api/dashboard/**").hasRole("ADMIN") // 仪表盘API需要管理员权限
|
||||||
|
.requestMatchers("/settings", "/settings/**").hasRole("ADMIN")
|
||||||
|
.requestMatchers("/users/**").hasRole("ADMIN")
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.formLogin(form -> form
|
||||||
|
.loginPage("/login")
|
||||||
|
.defaultSuccessUrl("/", true)
|
||||||
|
.permitAll()
|
||||||
|
)
|
||||||
|
.logout(Customizer.withDefaults());
|
||||||
|
|
||||||
|
// 添加JWT过滤器
|
||||||
|
http.addFilterBefore(jwtAuthenticationFilter(jwtUtils, userService), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
// H2 控制台需要以下设置
|
||||||
|
http.headers(headers -> headers.frameOptions(frame -> frame.disable()));
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DaoAuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) {
|
||||||
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||||
|
provider.setPasswordEncoder(passwordEncoder);
|
||||||
|
provider.setUserDetailsService(userDetailsService);
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration config, DaoAuthenticationProvider authenticationProvider) throws Exception {
|
||||||
|
return config.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
// 只允许前端开发服务器
|
||||||
|
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://127.0.0.1:3000"));
|
||||||
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||||
|
configuration.setAllowCredentials(true);
|
||||||
|
configuration.setExposedHeaders(Arrays.asList("Authorization"));
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtUtils jwtUtils, UserService userService) {
|
||||||
|
return new JwtAuthenticationFilter(jwtUtils, userService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
37
demo/src/main/java/com/example/demo/config/WebMvcConfig.java
Normal file
37
demo/src/main/java/com/example/demo/config/WebMvcConfig.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package com.example.demo.config;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||||
|
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocaleResolver localeResolver() {
|
||||||
|
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||||
|
slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
|
||||||
|
return slr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||||
|
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
||||||
|
lci.setParamName("lang");
|
||||||
|
return lci;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(localeChangeInterceptor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
import com.example.demo.service.UserService;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/users")
|
||||||
|
@Validated
|
||||||
|
public class AdminUserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public AdminUserController(UserService userService) {
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String list(Model model) {
|
||||||
|
List<User> users = userService.findAll();
|
||||||
|
model.addAttribute("users", users);
|
||||||
|
return "users/list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/new")
|
||||||
|
public String createForm(Model model) {
|
||||||
|
model.addAttribute("user", new User());
|
||||||
|
return "users/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String create(@RequestParam String username,
|
||||||
|
@RequestParam String email,
|
||||||
|
@RequestParam String password,
|
||||||
|
@RequestParam String role) {
|
||||||
|
userService.create(username, email, password);
|
||||||
|
// 创建后更新角色
|
||||||
|
User user = userService.findByUsername(username);
|
||||||
|
userService.update(user.getId(), username, email, null, role);
|
||||||
|
return "redirect:/users";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/edit")
|
||||||
|
public String editForm(@PathVariable Long id, Model model) {
|
||||||
|
User user = userService.findById(id);
|
||||||
|
model.addAttribute("user", user);
|
||||||
|
return "users/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}")
|
||||||
|
public String update(@PathVariable Long id,
|
||||||
|
@RequestParam String username,
|
||||||
|
@RequestParam String email,
|
||||||
|
@RequestParam(required = false) String password,
|
||||||
|
@RequestParam String role) {
|
||||||
|
userService.update(id, username, email, password, role);
|
||||||
|
return "redirect:/users";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/delete")
|
||||||
|
public String delete(@PathVariable Long id) {
|
||||||
|
userService.delete(id);
|
||||||
|
return "redirect:/users";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
import com.example.demo.service.UserService;
|
||||||
|
import com.example.demo.util.JwtUtils;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
public class AuthApiController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AuthApiController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtUtils jwtUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*/
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
try {
|
||||||
|
String username = credentials.get("username");
|
||||||
|
String password = credentials.get("password");
|
||||||
|
|
||||||
|
if (username == null || password == null) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("用户名和密码不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Spring Security进行认证
|
||||||
|
UsernamePasswordAuthenticationToken authToken =
|
||||||
|
new UsernamePasswordAuthenticationToken(username, password);
|
||||||
|
Authentication authentication = authenticationManager.authenticate(authToken);
|
||||||
|
|
||||||
|
User user = userService.findByUsername(username);
|
||||||
|
|
||||||
|
// 生成JWT Token
|
||||||
|
String token = jwtUtils.generateToken(username, user.getRole(), user.getId());
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("success", true);
|
||||||
|
body.put("message", "登录成功");
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("user", user);
|
||||||
|
data.put("token", token);
|
||||||
|
body.put("data", data);
|
||||||
|
|
||||||
|
logger.info("用户登录成功:{}", username);
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("登录失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("用户名或密码错误"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册
|
||||||
|
*/
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<Map<String, Object>> register(@Valid @RequestBody User user) {
|
||||||
|
try {
|
||||||
|
if (userService.findByUsername(user.getUsername()) != null) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("用户名已存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userService.findByEmail(user.getEmail()) != null) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("邮箱已存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
User savedUser = userService.save(user);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "注册成功");
|
||||||
|
response.put("data", savedUser);
|
||||||
|
|
||||||
|
logger.info("用户注册成功:{}", user.getUsername());
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("注册失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("注册失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登出
|
||||||
|
*/
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public ResponseEntity<Map<String, Object>> logout() {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "登出成功");
|
||||||
|
|
||||||
|
logger.info("用户登出");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/me")
|
||||||
|
public ResponseEntity<Map<String, Object>> getCurrentUser(Authentication authentication) {
|
||||||
|
try {
|
||||||
|
logger.info("获取当前用户信息 - authentication: {}", authentication);
|
||||||
|
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
String username = authentication.getName();
|
||||||
|
logger.info("当前用户名: {}", username);
|
||||||
|
|
||||||
|
try {
|
||||||
|
User user = userService.findByUsername(username);
|
||||||
|
logger.info("找到用户: {}", user.getUsername());
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", user);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查找用户失败: {}", username, e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("用户不存在: " + username));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("用户未认证");
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("用户未登录"));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取用户信息失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("获取用户信息失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户名是否存在
|
||||||
|
*/
|
||||||
|
@GetMapping("/public/users/exists/username")
|
||||||
|
public ResponseEntity<Map<String, Object>> checkUsernameExists(@RequestParam String value) {
|
||||||
|
try {
|
||||||
|
boolean exists = userService.findByUsername(value) != null;
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", Map.of("exists", exists));
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("检查用户名失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("检查用户名失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查邮箱是否存在
|
||||||
|
*/
|
||||||
|
@GetMapping("/public/users/exists/email")
|
||||||
|
public ResponseEntity<Map<String, Object>> checkEmailExists(@RequestParam String value) {
|
||||||
|
try {
|
||||||
|
boolean exists = userService.findByEmail(value) != null;
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", Map.of("exists", exists));
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("检查邮箱失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("检查邮箱失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> createErrorResponse(String message) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", message);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.example.demo.service.UserService;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@Validated
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public AuthController(UserService userService) {
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RegisterForm {
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 3, max = 50)
|
||||||
|
public String username;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Email
|
||||||
|
public String email;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 6, max = 100)
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 6, max = 100)
|
||||||
|
public String confirmPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/login")
|
||||||
|
public String loginPage() {
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/register")
|
||||||
|
public String registerPage(Model model) {
|
||||||
|
model.addAttribute("form", new RegisterForm());
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public String doRegister(@Valid @ModelAttribute("form") RegisterForm form, BindingResult bindingResult, Model model) {
|
||||||
|
if (!bindingResult.hasFieldErrors("password") && !bindingResult.hasFieldErrors("confirmPassword")) {
|
||||||
|
if (!form.password.equals(form.confirmPassword)) {
|
||||||
|
bindingResult.rejectValue("confirmPassword", "register.password.mismatch", "两次输入的密码不一致");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
userService.register(form.username, form.email, form.password);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
model.addAttribute("error", ex.getMessage());
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
return "redirect:/login?registered";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.service.DashboardService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dashboard")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class DashboardApiController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DashboardService dashboardService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仪表盘概览数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/overview")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Map<String, Object>> getOverview() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> overview = dashboardService.getDashboardOverview();
|
||||||
|
return ResponseEntity.ok(overview);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日活数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/daily-active-users")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Map<String, Object>> getDailyActiveUsers() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = dashboardService.getDailyActiveUsers();
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取收入趋势数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/revenue-trend")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Map<String, Object>> getRevenueTrend() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = dashboardService.getRevenueTrend();
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单状态分布
|
||||||
|
*/
|
||||||
|
@GetMapping("/order-status-distribution")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Map<String, Object>> getOrderStatusDistribution() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = dashboardService.getOrderStatusDistribution();
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付方式分布
|
||||||
|
*/
|
||||||
|
@GetMapping("/payment-method-distribution")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Map<String, Object>> getPaymentMethodDistribution() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = dashboardService.getPaymentMethodDistribution();
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近订单列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/recent-orders")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Map<String, Object>> getRecentOrders() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = dashboardService.getRecentOrders();
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有仪表盘数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/all")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Map<String, Object>> getAllDashboardData() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> allData = Map.of(
|
||||||
|
"overview", dashboardService.getDashboardOverview(),
|
||||||
|
"dailyActiveUsers", dashboardService.getDailyActiveUsers(),
|
||||||
|
"revenueTrend", dashboardService.getRevenueTrend(),
|
||||||
|
"orderStatusDistribution", dashboardService.getOrderStatusDistribution(),
|
||||||
|
"paymentMethodDistribution", dashboardService.getPaymentMethodDistribution(),
|
||||||
|
"recentOrders", dashboardService.getRecentOrders()
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(allData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class HomeController {
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
public String home() {
|
||||||
|
return "home";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.*;
|
||||||
|
import com.example.demo.service.OrderService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/orders")
|
||||||
|
public class OrderApiController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OrderApiController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderService orderService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单列表
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<Map<String, Object>> getOrders(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(defaultValue = "createdAt") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "desc") String sortDir,
|
||||||
|
@RequestParam(required = false) OrderStatus status,
|
||||||
|
@RequestParam(required = false) String search,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
// 检查认证信息
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "用户未认证,请重新登录");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
if (user == null) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "用户信息获取失败,请重新登录");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建分页和排序
|
||||||
|
Sort sort = Sort.by(Sort.Direction.fromString(sortDir), sortBy);
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
Page<Order> orderPage;
|
||||||
|
if (user.getRole().equals("ROLE_ADMIN")) {
|
||||||
|
// 管理员可以查看所有订单
|
||||||
|
orderPage = orderService.findAllOrders(pageable, status, search);
|
||||||
|
} else {
|
||||||
|
// 普通用户只能查看自己的订单
|
||||||
|
orderPage = orderService.findOrdersByUser(user, pageable, status, search);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", orderPage);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取订单列表失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("获取订单列表失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getOrderById(@PathVariable Long id,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Order order = orderService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("无权限访问此订单"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", order);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取订单详情失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("获取订单详情失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单
|
||||||
|
*/
|
||||||
|
@PostMapping("/create")
|
||||||
|
public ResponseEntity<Map<String, Object>> createOrder(@Valid @RequestBody Order order,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
// 检查认证信息
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "用户未认证,请重新登录");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
if (user == null) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "用户信息获取失败,请重新登录");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
order.setUser(user);
|
||||||
|
|
||||||
|
Order createdOrder = orderService.createOrder(order);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "订单创建成功");
|
||||||
|
response.put("data", createdOrder);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建订单失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("创建订单失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单状态
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/status")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateOrderStatus(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> request,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Order order = orderService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("无权限操作此订单"));
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderStatus status = OrderStatus.valueOf(request.get("status"));
|
||||||
|
String notes = request.get("notes");
|
||||||
|
|
||||||
|
Order updatedOrder = orderService.updateOrderStatus(id, status);
|
||||||
|
|
||||||
|
if (notes != null && !notes.trim().isEmpty()) {
|
||||||
|
updatedOrder.setNotes((updatedOrder.getNotes() != null ? updatedOrder.getNotes() + "\n" : "") + notes);
|
||||||
|
orderService.createOrder(updatedOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "订单状态更新成功");
|
||||||
|
response.put("data", updatedOrder);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("更新订单状态失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("更新订单状态失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订单
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/cancel")
|
||||||
|
public ResponseEntity<Map<String, Object>> cancelOrder(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> request,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Order order = orderService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("无权限操作此订单"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = request.get("reason");
|
||||||
|
Order cancelledOrder = orderService.cancelOrder(id, reason);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "订单取消成功");
|
||||||
|
response.put("data", cancelledOrder);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("取消订单失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("取消订单失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发货
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/ship")
|
||||||
|
public ResponseEntity<Map<String, Object>> shipOrder(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> request,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
|
||||||
|
// 只有管理员可以发货
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("无权限操作此订单"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String trackingNumber = request.get("trackingNumber");
|
||||||
|
Order shippedOrder = orderService.shipOrder(id, trackingNumber);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "订单发货成功");
|
||||||
|
response.put("data", shippedOrder);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("订单发货失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("订单发货失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成订单
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/complete")
|
||||||
|
public ResponseEntity<Map<String, Object>> completeOrder(@PathVariable Long id,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
|
||||||
|
// 只有管理员可以完成订单
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("无权限操作此订单"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Order completedOrder = orderService.completeOrder(id);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "订单完成成功");
|
||||||
|
response.put("data", completedOrder);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("完成订单失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("完成订单失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单支付
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/pay")
|
||||||
|
public ResponseEntity<Map<String, Object>> createOrderPayment(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> request,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Order order = orderService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!order.getUser().getId().equals(user.getId())) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("无权限操作此订单"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态
|
||||||
|
if (!order.canPay()) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("订单当前状态不允许支付"));
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentMethod paymentMethod = PaymentMethod.valueOf(request.get("paymentMethod"));
|
||||||
|
|
||||||
|
// 这里应该调用支付服务创建支付
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "支付创建成功");
|
||||||
|
|
||||||
|
// 模拟支付URL
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("paymentId", "payment-" + System.currentTimeMillis());
|
||||||
|
data.put("paymentUrl", "/payment/" + paymentMethod.name().toLowerCase() + "/create?orderId=" + id);
|
||||||
|
response.put("data", data);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建订单支付失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("创建订单支付失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单统计
|
||||||
|
*/
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public ResponseEntity<Map<String, Object>> getOrderStats(Authentication authentication) {
|
||||||
|
try {
|
||||||
|
// 检查认证信息
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "用户未认证,请重新登录");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
if (user == null) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "用户信息获取失败,请重新登录");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计数据
|
||||||
|
Map<String, Object> stats;
|
||||||
|
if (user.getRole().equals("ROLE_ADMIN")) {
|
||||||
|
// 管理员查看所有订单统计
|
||||||
|
stats = orderService.getOrderStats();
|
||||||
|
} else {
|
||||||
|
// 普通用户查看自己的订单统计
|
||||||
|
stats = orderService.getOrderStatsByUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", stats);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取订单统计失败:", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("获取订单统计失败:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> createErrorResponse(String message) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", message);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.*;
|
||||||
|
import com.example.demo.service.OrderService;
|
||||||
|
import com.example.demo.service.PaymentService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/orders")
|
||||||
|
public class OrderController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderService orderService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PaymentService paymentService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示订单列表
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public String orderList(@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(defaultValue = "createdAt") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "desc") String sortDir,
|
||||||
|
@RequestParam(required = false) OrderStatus status,
|
||||||
|
@RequestParam(required = false) String search,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Sort sort = sortDir.equalsIgnoreCase("desc") ?
|
||||||
|
Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
|
||||||
|
Page<Order> orders;
|
||||||
|
if (status != null) {
|
||||||
|
orders = orderService.findByStatus(status, pageable);
|
||||||
|
} else {
|
||||||
|
orders = orderService.findByUserId(user.getId(), pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("orders", orders);
|
||||||
|
model.addAttribute("currentPage", page);
|
||||||
|
model.addAttribute("totalPages", orders.getTotalPages());
|
||||||
|
model.addAttribute("totalElements", orders.getTotalElements());
|
||||||
|
model.addAttribute("status", status);
|
||||||
|
model.addAttribute("search", search);
|
||||||
|
model.addAttribute("sortBy", sortBy);
|
||||||
|
model.addAttribute("sortDir", sortDir);
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
|
||||||
|
return "orders/list";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取订单列表失败:", e);
|
||||||
|
model.addAttribute("error", "获取订单列表失败:" + e.getMessage());
|
||||||
|
return "orders/list";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示订单详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public String orderDetail(@PathVariable Long id,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Optional<Order> orderOpt = orderService.findById(id);
|
||||||
|
|
||||||
|
if (!orderOpt.isPresent()) {
|
||||||
|
model.addAttribute("error", "订单不存在");
|
||||||
|
return "orders/detail";
|
||||||
|
}
|
||||||
|
|
||||||
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
|
// 检查权限:用户只能查看自己的订单,管理员可以查看所有订单
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||||
|
model.addAttribute("error", "无权限访问此订单");
|
||||||
|
return "orders/detail";
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("order", order);
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
|
||||||
|
return "orders/detail";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取订单详情失败:", e);
|
||||||
|
model.addAttribute("error", "获取订单详情失败:" + e.getMessage());
|
||||||
|
return "orders/detail";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示创建订单表单
|
||||||
|
*/
|
||||||
|
@GetMapping("/create")
|
||||||
|
public String showCreateOrderForm(Model model) {
|
||||||
|
Order order = new Order();
|
||||||
|
order.setOrderItems(new ArrayList<>());
|
||||||
|
order.setOrderItems(new ArrayList<>());
|
||||||
|
|
||||||
|
// 添加一个空的订单项
|
||||||
|
OrderItem item = new OrderItem();
|
||||||
|
item.setQuantity(1);
|
||||||
|
item.setUnitPrice(BigDecimal.ZERO);
|
||||||
|
order.getOrderItems().add(item);
|
||||||
|
|
||||||
|
model.addAttribute("order", order);
|
||||||
|
model.addAttribute("orderTypes", OrderType.values());
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
|
||||||
|
return "orders/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理创建订单
|
||||||
|
*/
|
||||||
|
@PostMapping("/create")
|
||||||
|
public String createOrder(@Valid @ModelAttribute Order order,
|
||||||
|
BindingResult result,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
if (result.hasErrors()) {
|
||||||
|
model.addAttribute("orderTypes", OrderType.values());
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
return "orders/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
order.setUser(user);
|
||||||
|
|
||||||
|
// 验证订单项
|
||||||
|
if (order.getOrderItems() == null || order.getOrderItems().isEmpty()) {
|
||||||
|
model.addAttribute("error", "订单必须包含至少一个商品");
|
||||||
|
model.addAttribute("orderTypes", OrderType.values());
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
return "orders/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤掉空的订单项
|
||||||
|
order.getOrderItems().removeIf(item ->
|
||||||
|
item.getProductName() == null || item.getProductName().trim().isEmpty() ||
|
||||||
|
item.getQuantity() == null || item.getQuantity() <= 0 ||
|
||||||
|
item.getUnitPrice() == null || item.getUnitPrice().compareTo(BigDecimal.ZERO) <= 0);
|
||||||
|
|
||||||
|
if (order.getOrderItems().isEmpty()) {
|
||||||
|
model.addAttribute("error", "订单必须包含至少一个有效商品");
|
||||||
|
model.addAttribute("orderTypes", OrderType.values());
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
return "orders/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
Order createdOrder = orderService.createOrder(order);
|
||||||
|
model.addAttribute("success", "订单创建成功,订单号:" + createdOrder.getOrderNumber());
|
||||||
|
|
||||||
|
return "redirect:/orders/" + createdOrder.getId();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建订单失败:", e);
|
||||||
|
model.addAttribute("error", "创建订单失败:" + e.getMessage());
|
||||||
|
model.addAttribute("orderTypes", OrderType.values());
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
return "orders/form";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单状态
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/status")
|
||||||
|
public String updateOrderStatus(@PathVariable Long id,
|
||||||
|
@RequestParam OrderStatus status,
|
||||||
|
@RequestParam(required = false) String notes,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Optional<Order> orderOpt = orderService.findById(id);
|
||||||
|
|
||||||
|
if (!orderOpt.isPresent()) {
|
||||||
|
model.addAttribute("error", "订单不存在");
|
||||||
|
return "redirect:/orders";
|
||||||
|
}
|
||||||
|
|
||||||
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||||
|
model.addAttribute("error", "无权限操作此订单");
|
||||||
|
return "redirect:/orders";
|
||||||
|
}
|
||||||
|
|
||||||
|
Order updatedOrder = orderService.updateOrderStatus(id, status);
|
||||||
|
|
||||||
|
if (notes != null && !notes.trim().isEmpty()) {
|
||||||
|
updatedOrder.setNotes((updatedOrder.getNotes() != null ? updatedOrder.getNotes() + "\n" : "") + notes);
|
||||||
|
orderService.createOrder(updatedOrder); // 保存备注
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("success", "订单状态更新成功");
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("更新订单状态失败:", e);
|
||||||
|
model.addAttribute("error", "更新订单状态失败:" + e.getMessage());
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订单
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/cancel")
|
||||||
|
public String cancelOrder(@PathVariable Long id,
|
||||||
|
@RequestParam(required = false) String reason,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Optional<Order> orderOpt = orderService.findById(id);
|
||||||
|
|
||||||
|
if (!orderOpt.isPresent()) {
|
||||||
|
model.addAttribute("error", "订单不存在");
|
||||||
|
return "redirect:/orders";
|
||||||
|
}
|
||||||
|
|
||||||
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||||
|
model.addAttribute("error", "无权限操作此订单");
|
||||||
|
return "redirect:/orders";
|
||||||
|
}
|
||||||
|
|
||||||
|
Order cancelledOrder = orderService.cancelOrder(id, reason);
|
||||||
|
model.addAttribute("success", "订单取消成功");
|
||||||
|
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("取消订单失败:", e);
|
||||||
|
model.addAttribute("error", "取消订单失败:" + e.getMessage());
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发货
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/ship")
|
||||||
|
public String shipOrder(@PathVariable Long id,
|
||||||
|
@RequestParam(required = false) String trackingNumber,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
|
||||||
|
// 只有管理员可以发货
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||||
|
model.addAttribute("error", "无权限操作此订单");
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Order shippedOrder = orderService.shipOrder(id, trackingNumber);
|
||||||
|
model.addAttribute("success", "订单发货成功");
|
||||||
|
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("订单发货失败:", e);
|
||||||
|
model.addAttribute("error", "订单发货失败:" + e.getMessage());
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成订单
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/complete")
|
||||||
|
public String completeOrder(@PathVariable Long id,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
|
||||||
|
// 只有管理员可以完成订单
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||||
|
model.addAttribute("error", "无权限操作此订单");
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Order completedOrder = orderService.completeOrder(id);
|
||||||
|
model.addAttribute("success", "订单完成成功");
|
||||||
|
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("完成订单失败:", e);
|
||||||
|
model.addAttribute("error", "完成订单失败:" + e.getMessage());
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为订单创建支付
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/pay")
|
||||||
|
public String createPaymentForOrder(@PathVariable Long id,
|
||||||
|
@RequestParam PaymentMethod paymentMethod,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Optional<Order> orderOpt = orderService.findById(id);
|
||||||
|
|
||||||
|
if (!orderOpt.isPresent()) {
|
||||||
|
model.addAttribute("error", "订单不存在");
|
||||||
|
return "redirect:/orders";
|
||||||
|
}
|
||||||
|
|
||||||
|
Order order = orderOpt.get();
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!order.getUser().getId().equals(user.getId())) {
|
||||||
|
model.addAttribute("error", "无权限操作此订单");
|
||||||
|
return "redirect:/orders";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态
|
||||||
|
if (!order.canPay()) {
|
||||||
|
model.addAttribute("error", "订单当前状态不允许支付");
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建支付记录
|
||||||
|
Payment savedPayment = paymentService.createOrderPayment(order, paymentMethod);
|
||||||
|
|
||||||
|
// 根据支付方式跳转到相应的支付页面
|
||||||
|
if (paymentMethod == PaymentMethod.ALIPAY) {
|
||||||
|
return "redirect:/payment/alipay/create?paymentId=" + savedPayment.getId();
|
||||||
|
} else if (paymentMethod == PaymentMethod.PAYPAL) {
|
||||||
|
return "redirect:/payment/paypal/create?paymentId=" + savedPayment.getId();
|
||||||
|
} else {
|
||||||
|
model.addAttribute("error", "不支持的支付方式");
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建订单支付失败:", e);
|
||||||
|
model.addAttribute("error", "创建订单支付失败:" + e.getMessage());
|
||||||
|
return "redirect:/orders/" + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员订单管理页面
|
||||||
|
*/
|
||||||
|
@GetMapping("/admin")
|
||||||
|
public String adminOrderList(@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(defaultValue = "createdAt") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "desc") String sortDir,
|
||||||
|
@RequestParam(required = false) OrderStatus status,
|
||||||
|
@RequestParam(required = false) String search,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
|
||||||
|
// 只有管理员可以访问
|
||||||
|
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||||
|
model.addAttribute("error", "无权限访问");
|
||||||
|
return "redirect:/orders";
|
||||||
|
}
|
||||||
|
|
||||||
|
Sort sort = sortDir.equalsIgnoreCase("desc") ?
|
||||||
|
Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
|
||||||
|
Page<Order> orders;
|
||||||
|
if (status != null) {
|
||||||
|
orders = orderService.findByStatus(status, pageable);
|
||||||
|
} else {
|
||||||
|
orders = orderService.findAll(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("orders", orders);
|
||||||
|
model.addAttribute("currentPage", page);
|
||||||
|
model.addAttribute("totalPages", orders.getTotalPages());
|
||||||
|
model.addAttribute("totalElements", orders.getTotalElements());
|
||||||
|
model.addAttribute("status", status);
|
||||||
|
model.addAttribute("search", search);
|
||||||
|
model.addAttribute("sortBy", sortBy);
|
||||||
|
model.addAttribute("sortDir", sortDir);
|
||||||
|
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||||
|
model.addAttribute("isAdmin", true);
|
||||||
|
|
||||||
|
return "orders/admin";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取管理员订单列表失败:", e);
|
||||||
|
model.addAttribute("error", "获取订单列表失败:" + e.getMessage());
|
||||||
|
return "orders/admin";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,491 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.Payment;
|
||||||
|
import com.example.demo.model.PaymentStatus;
|
||||||
|
import com.example.demo.service.PaymentService;
|
||||||
|
import com.example.demo.service.AlipayService;
|
||||||
|
import com.example.demo.service.PayPalService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/payments")
|
||||||
|
public class PaymentApiController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PaymentApiController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PaymentService paymentService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AlipayService alipayService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PayPalService payPalService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的支付记录
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<Map<String, Object>> getUserPayments(
|
||||||
|
Authentication authentication,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) String search) {
|
||||||
|
try {
|
||||||
|
List<Payment> payments;
|
||||||
|
|
||||||
|
// 检查用户是否已登录
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
String username = authentication.getName();
|
||||||
|
payments = paymentService.findByUsername(username);
|
||||||
|
} else {
|
||||||
|
// 未登录用户返回空列表
|
||||||
|
payments = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的筛选逻辑
|
||||||
|
if (status != null && !status.isEmpty()) {
|
||||||
|
payments = payments.stream()
|
||||||
|
.filter(p -> p.getStatus().name().equals(status))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search != null && !search.isEmpty()) {
|
||||||
|
payments = payments.stream()
|
||||||
|
.filter(p -> p.getOrderId().contains(search))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "获取支付记录成功");
|
||||||
|
response.put("data", payments);
|
||||||
|
response.put("total", payments.size());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取支付记录失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("获取支付记录失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取支付详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getPaymentById(
|
||||||
|
@PathVariable Long id,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
Payment payment = paymentService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限访问此支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", payment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取支付详情失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("获取支付详情失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建支付
|
||||||
|
*/
|
||||||
|
@PostMapping("/create")
|
||||||
|
public ResponseEntity<Map<String, Object>> createPayment(
|
||||||
|
@RequestBody Map<String, Object> paymentData,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String username;
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
username = authentication.getName();
|
||||||
|
} else {
|
||||||
|
// 未登录用户使用匿名用户名
|
||||||
|
username = "anonymous_" + System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
String orderId = (String) paymentData.get("orderId");
|
||||||
|
String amountStr = (String) paymentData.get("amount");
|
||||||
|
String method = (String) paymentData.get("method");
|
||||||
|
|
||||||
|
if (orderId == null || amountStr == null || method == null) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("订单号、金额和支付方式不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Payment payment = paymentService.createPayment(username, orderId, amountStr, method);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "支付创建成功");
|
||||||
|
response.put("data", payment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建支付失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("创建支付失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新支付状态
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}/status")
|
||||||
|
public ResponseEntity<Map<String, Object>> updatePaymentStatus(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> statusData,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String status = statusData.get("status");
|
||||||
|
if (status == null) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("状态不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Payment payment = paymentService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限修改此支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.setStatus(PaymentStatus.valueOf(status));
|
||||||
|
paymentService.save(payment);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "状态更新成功");
|
||||||
|
response.put("data", payment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("更新支付状态失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("更新支付状态失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认支付成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/success")
|
||||||
|
public ResponseEntity<Map<String, Object>> confirmPaymentSuccess(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> successData,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String externalTransactionId = successData.get("externalTransactionId");
|
||||||
|
|
||||||
|
Payment payment = paymentService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限操作此支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentService.confirmPaymentSuccess(id, externalTransactionId);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "支付确认成功");
|
||||||
|
response.put("data", payment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("确认支付成功失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("确认支付成功失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认支付失败
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/failure")
|
||||||
|
public ResponseEntity<Map<String, Object>> confirmPaymentFailure(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> failureData,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String failureReason = failureData.get("failureReason");
|
||||||
|
|
||||||
|
Payment payment = paymentService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限操作此支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentService.confirmPaymentFailure(id, failureReason);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "支付失败确认成功");
|
||||||
|
response.put("data", payment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("确认支付失败失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("确认支付失败失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付统计
|
||||||
|
*/
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public ResponseEntity<Map<String, Object>> getPaymentStats(Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String username = authentication.getName();
|
||||||
|
Map<String, Object> stats = paymentService.getUserPaymentStats(username);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("data", stats);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取支付统计失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("获取支付统计失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试支付记录
|
||||||
|
*/
|
||||||
|
@PostMapping("/create-test")
|
||||||
|
public ResponseEntity<Map<String, Object>> createTestPayment(
|
||||||
|
@RequestBody Map<String, Object> paymentData,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String username;
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
username = authentication.getName();
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("请先登录后再创建支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String amountStr = (String) paymentData.get("amount");
|
||||||
|
String method = (String) paymentData.get("method");
|
||||||
|
|
||||||
|
if (amountStr == null || method == null) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("金额和支付方式不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成测试订单号
|
||||||
|
String testOrderId = "TEST_" + System.currentTimeMillis();
|
||||||
|
Payment payment = paymentService.createPayment(username, testOrderId, amountStr, method);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "测试支付记录创建成功");
|
||||||
|
response.put("data", payment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建测试支付记录失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("创建测试支付记录失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试支付完成(用于测试自动创建订单功能)
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/test-complete")
|
||||||
|
public ResponseEntity<Map<String, Object>> testPaymentComplete(
|
||||||
|
@PathVariable Long id,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
Payment payment = paymentService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限操作此支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟支付成功
|
||||||
|
String mockTransactionId = "TEST_" + System.currentTimeMillis();
|
||||||
|
paymentService.confirmPaymentSuccess(id, mockTransactionId);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "支付完成测试成功,订单已自动创建");
|
||||||
|
response.put("data", payment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("测试支付完成失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("测试支付完成失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建支付宝支付
|
||||||
|
*/
|
||||||
|
@PostMapping("/alipay/create")
|
||||||
|
public ResponseEntity<Map<String, Object>> createAlipayPayment(
|
||||||
|
@RequestBody Map<String, Object> paymentData,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String username;
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
username = authentication.getName();
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("请先登录后再创建支付"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long paymentId = Long.valueOf(paymentData.get("paymentId").toString());
|
||||||
|
Payment payment = paymentService.findById(paymentId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getUsername().equals(username)) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限操作此支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用支付宝接口创建支付
|
||||||
|
String paymentUrl = alipayService.createPayment(payment);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "支付宝支付创建成功");
|
||||||
|
response.put("data", Map.of("paymentUrl", paymentUrl));
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建支付宝支付失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("创建支付宝支付失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建PayPal支付
|
||||||
|
*/
|
||||||
|
@PostMapping("/paypal/create")
|
||||||
|
public ResponseEntity<Map<String, Object>> createPayPalPayment(
|
||||||
|
@RequestBody Map<String, Object> paymentData,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String username;
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
username = authentication.getName();
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("请先登录后再创建支付"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long paymentId = Long.valueOf(paymentData.get("paymentId").toString());
|
||||||
|
Payment payment = paymentService.findById(paymentId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getUsername().equals(username)) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限操作此支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用PayPal接口创建支付
|
||||||
|
String paymentUrl = payPalService.createPayment(payment);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "PayPal支付创建成功");
|
||||||
|
response.put("data", Map.of("paymentUrl", paymentUrl));
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建PayPal支付失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("创建PayPal支付失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝异步通知
|
||||||
|
*/
|
||||||
|
@PostMapping("/alipay/notify")
|
||||||
|
public ResponseEntity<String> handleAlipayNotify(@RequestParam Map<String, String> params) {
|
||||||
|
try {
|
||||||
|
logger.info("收到支付宝异步通知:{}", params);
|
||||||
|
|
||||||
|
boolean success = alipayService.handleNotify(params);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return ResponseEntity.ok("success");
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.ok("fail");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理支付宝异步通知失败:", e);
|
||||||
|
return ResponseEntity.ok("fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝同步返回
|
||||||
|
*/
|
||||||
|
@GetMapping("/alipay/return")
|
||||||
|
public ResponseEntity<String> handleAlipayReturn(@RequestParam Map<String, String> params) {
|
||||||
|
try {
|
||||||
|
logger.info("收到支付宝同步返回:{}", params);
|
||||||
|
|
||||||
|
boolean success = alipayService.handleReturn(params);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return ResponseEntity.ok("支付成功,正在跳转...");
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.ok("支付验证失败");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理支付宝同步返回失败:", e);
|
||||||
|
return ResponseEntity.ok("支付处理异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> createErrorResponse(String message) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", message);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.Payment;
|
||||||
|
import com.example.demo.model.PaymentMethod;
|
||||||
|
import com.example.demo.model.PaymentStatus;
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
import com.example.demo.service.AlipayService;
|
||||||
|
import com.example.demo.service.PayPalService;
|
||||||
|
import com.example.demo.service.PaymentService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/payment")
|
||||||
|
public class PaymentController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PaymentController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PaymentService paymentService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AlipayService alipayService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PayPalService payPalService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示支付页面
|
||||||
|
*/
|
||||||
|
@GetMapping("/create")
|
||||||
|
public String showPaymentForm(Model model) {
|
||||||
|
model.addAttribute("payment", new Payment());
|
||||||
|
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||||
|
return "payment/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理支付请求
|
||||||
|
*/
|
||||||
|
@PostMapping("/create")
|
||||||
|
public String createPayment(@Valid @ModelAttribute Payment payment,
|
||||||
|
BindingResult result,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
|
||||||
|
if (result.hasErrors()) {
|
||||||
|
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||||
|
return "payment/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置当前用户
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
payment.setUser(user);
|
||||||
|
|
||||||
|
// 设置默认货币
|
||||||
|
if (payment.getCurrency() == null || payment.getCurrency().isEmpty()) {
|
||||||
|
payment.setCurrency("CNY");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据支付方式创建支付
|
||||||
|
String redirectUrl;
|
||||||
|
if (payment.getPaymentMethod() == PaymentMethod.ALIPAY) {
|
||||||
|
redirectUrl = alipayService.createPayment(payment);
|
||||||
|
return "redirect:" + redirectUrl;
|
||||||
|
} else if (payment.getPaymentMethod() == PaymentMethod.PAYPAL) {
|
||||||
|
redirectUrl = payPalService.createPayment(payment);
|
||||||
|
return "redirect:" + redirectUrl;
|
||||||
|
} else {
|
||||||
|
model.addAttribute("error", "不支持的支付方式");
|
||||||
|
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||||
|
return "payment/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建支付订单失败:", e);
|
||||||
|
model.addAttribute("error", "创建支付订单失败:" + e.getMessage());
|
||||||
|
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||||
|
return "payment/form";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝异步通知
|
||||||
|
*/
|
||||||
|
@PostMapping("/alipay/notify")
|
||||||
|
@ResponseBody
|
||||||
|
public String alipayNotify(HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
Map<String, String> params = request.getParameterMap().entrySet().stream()
|
||||||
|
.collect(java.util.stream.Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
entry -> entry.getValue()[0]
|
||||||
|
));
|
||||||
|
|
||||||
|
boolean success = alipayService.handleNotify(params);
|
||||||
|
return success ? "success" : "fail";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理支付宝异步通知失败:", e);
|
||||||
|
return "fail";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝同步返回
|
||||||
|
*/
|
||||||
|
@GetMapping("/alipay/return")
|
||||||
|
public String alipayReturn(HttpServletRequest request, Model model) {
|
||||||
|
try {
|
||||||
|
Map<String, String> params = request.getParameterMap().entrySet().stream()
|
||||||
|
.collect(java.util.stream.Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
entry -> entry.getValue()[0]
|
||||||
|
));
|
||||||
|
|
||||||
|
boolean success = alipayService.handleReturn(params);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
String outTradeNo = params.get("out_trade_no");
|
||||||
|
Payment payment = paymentService.findByOrderId(outTradeNo)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
model.addAttribute("payment", payment);
|
||||||
|
model.addAttribute("success", true);
|
||||||
|
return "payment/result";
|
||||||
|
} else {
|
||||||
|
model.addAttribute("error", "支付验证失败");
|
||||||
|
return "payment/result";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理支付宝同步返回失败:", e);
|
||||||
|
model.addAttribute("error", "支付处理失败:" + e.getMessage());
|
||||||
|
return "payment/result";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayPal支付返回
|
||||||
|
*/
|
||||||
|
@GetMapping("/paypal/return")
|
||||||
|
public String paypalReturn(@RequestParam("paymentId") String paymentId,
|
||||||
|
@RequestParam("PayerID") String payerId,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
boolean success = payPalService.executePayment(paymentId, payerId);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Payment payment = paymentService.findByExternalTransactionId(paymentId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
model.addAttribute("payment", payment);
|
||||||
|
model.addAttribute("success", true);
|
||||||
|
return "payment/result";
|
||||||
|
} else {
|
||||||
|
model.addAttribute("error", "支付执行失败");
|
||||||
|
return "payment/result";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理PayPal支付返回失败:", e);
|
||||||
|
model.addAttribute("error", "支付处理失败:" + e.getMessage());
|
||||||
|
return "payment/result";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayPal支付取消
|
||||||
|
*/
|
||||||
|
@GetMapping("/paypal/cancel")
|
||||||
|
public String paypalCancel(Model model) {
|
||||||
|
model.addAttribute("error", "支付已取消");
|
||||||
|
return "payment/result";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayPal Webhook通知
|
||||||
|
*/
|
||||||
|
@PostMapping("/paypal/webhook")
|
||||||
|
@ResponseBody
|
||||||
|
public String paypalWebhook(HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
Map<String, String> params = request.getParameterMap().entrySet().stream()
|
||||||
|
.collect(java.util.stream.Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
entry -> entry.getValue()[0]
|
||||||
|
));
|
||||||
|
|
||||||
|
boolean success = payPalService.handleWebhook(params);
|
||||||
|
return success ? "success" : "fail";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理PayPal Webhook失败:", e);
|
||||||
|
return "fail";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付记录列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/history")
|
||||||
|
public String paymentHistory(Authentication authentication, Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
List<Payment> payments = paymentService.findByUserId(user.getId());
|
||||||
|
model.addAttribute("payments", payments);
|
||||||
|
return "payment/history";
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取支付记录失败:", e);
|
||||||
|
model.addAttribute("error", "获取支付记录失败");
|
||||||
|
return "payment/history";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail/{id}")
|
||||||
|
public String paymentDetail(@PathVariable Long id,
|
||||||
|
Authentication authentication,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Payment payment = paymentService.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!payment.getUser().getId().equals(user.getId())) {
|
||||||
|
model.addAttribute("error", "无权限访问此支付记录");
|
||||||
|
return "payment/detail";
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("payment", payment);
|
||||||
|
return "payment/detail";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取支付详情失败:", e);
|
||||||
|
model.addAttribute("error", "获取支付详情失败");
|
||||||
|
return "payment/detail";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.example.demo.repository.UserRepository;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/public")
|
||||||
|
public class PublicApiController {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public PublicApiController(UserRepository userRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/users/exists/username")
|
||||||
|
public Map<String, Boolean> existsUsername(@RequestParam("value") String username) {
|
||||||
|
return Map.of("exists", userRepository.existsByUsername(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/users/exists/email")
|
||||||
|
public Map<String, Boolean> existsEmail(@RequestParam("value") String email) {
|
||||||
|
return Map.of("exists", userRepository.existsByEmail(email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.SystemSettings;
|
||||||
|
import com.example.demo.service.SystemSettingsService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/settings")
|
||||||
|
public class SettingsController {
|
||||||
|
|
||||||
|
private final SystemSettingsService settingsService;
|
||||||
|
|
||||||
|
public SettingsController(SystemSettingsService settingsService) {
|
||||||
|
this.settingsService = settingsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String showForm(Model model) {
|
||||||
|
SystemSettings settings = settingsService.getOrCreate();
|
||||||
|
model.addAttribute("pageTitle", "系统设置");
|
||||||
|
model.addAttribute("settings", settings);
|
||||||
|
return "settings/form";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String save(@Valid @ModelAttribute("settings") SystemSettings form,
|
||||||
|
BindingResult bindingResult,
|
||||||
|
Model model) {
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
model.addAttribute("pageTitle", "系统设置");
|
||||||
|
return "settings/form";
|
||||||
|
}
|
||||||
|
settingsService.update(form);
|
||||||
|
model.addAttribute("success", "保存成功");
|
||||||
|
return "redirect:/settings";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
314
demo/src/main/java/com/example/demo/model/Order.java
Normal file
314
demo/src/main/java/com/example/demo/model/Order.java
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "orders")
|
||||||
|
public class Order {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 50, unique = true)
|
||||||
|
private String orderNumber;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin(value = "0.01", message = "订单金额必须大于0")
|
||||||
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Column(nullable = false, length = 3)
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private OrderStatus status;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private OrderType orderType;
|
||||||
|
|
||||||
|
@Column(length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String notes;
|
||||||
|
|
||||||
|
@Column(name = "shipping_address", length = 1000)
|
||||||
|
private String shippingAddress;
|
||||||
|
|
||||||
|
@Column(name = "billing_address", length = 1000)
|
||||||
|
private String billingAddress;
|
||||||
|
|
||||||
|
@Column(name = "contact_phone", length = 20)
|
||||||
|
private String contactPhone;
|
||||||
|
|
||||||
|
@Column(name = "contact_email", length = 100)
|
||||||
|
private String contactEmail;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Column(name = "paid_at")
|
||||||
|
private LocalDateTime paidAt;
|
||||||
|
|
||||||
|
@Column(name = "shipped_at")
|
||||||
|
private LocalDateTime shippedAt;
|
||||||
|
|
||||||
|
@Column(name = "delivered_at")
|
||||||
|
private LocalDateTime deliveredAt;
|
||||||
|
|
||||||
|
@Column(name = "cancelled_at")
|
||||||
|
private LocalDateTime cancelledAt;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
|
private List<OrderItem> orderItems = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
|
private List<Payment> payments = new ArrayList<>();
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
if (orderNumber == null) {
|
||||||
|
orderNumber = generateOrderNumber();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成订单号
|
||||||
|
*/
|
||||||
|
private String generateOrderNumber() {
|
||||||
|
return "ORD" + System.currentTimeMillis() + String.format("%03d", (int)(Math.random() * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算订单总金额
|
||||||
|
*/
|
||||||
|
public BigDecimal calculateTotalAmount() {
|
||||||
|
return orderItems.stream()
|
||||||
|
.map(OrderItem::getSubtotal)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以支付
|
||||||
|
*/
|
||||||
|
public boolean canPay() {
|
||||||
|
return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以取消
|
||||||
|
*/
|
||||||
|
public boolean canCancel() {
|
||||||
|
return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以发货
|
||||||
|
*/
|
||||||
|
public boolean canShip() {
|
||||||
|
return status == OrderStatus.PAID || status == OrderStatus.CONFIRMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以完成
|
||||||
|
*/
|
||||||
|
public boolean canComplete() {
|
||||||
|
return status == OrderStatus.SHIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrderNumber() {
|
||||||
|
return orderNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrderNumber(String orderNumber) {
|
||||||
|
this.orderNumber = orderNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTotalAmount() {
|
||||||
|
return totalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalAmount(BigDecimal totalAmount) {
|
||||||
|
this.totalAmount = totalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrency() {
|
||||||
|
return currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrency(String currency) {
|
||||||
|
this.currency = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrderStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(OrderStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrderType getOrderType() {
|
||||||
|
return orderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrderType(OrderType orderType) {
|
||||||
|
this.orderType = orderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNotes() {
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotes(String notes) {
|
||||||
|
this.notes = notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getShippingAddress() {
|
||||||
|
return shippingAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShippingAddress(String shippingAddress) {
|
||||||
|
this.shippingAddress = shippingAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBillingAddress() {
|
||||||
|
return billingAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBillingAddress(String billingAddress) {
|
||||||
|
this.billingAddress = billingAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactPhone() {
|
||||||
|
return contactPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactPhone(String contactPhone) {
|
||||||
|
this.contactPhone = contactPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactEmail() {
|
||||||
|
return contactEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactEmail(String contactEmail) {
|
||||||
|
this.contactEmail = contactEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getPaidAt() {
|
||||||
|
return paidAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaidAt(LocalDateTime paidAt) {
|
||||||
|
this.paidAt = paidAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getShippedAt() {
|
||||||
|
return shippedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShippedAt(LocalDateTime shippedAt) {
|
||||||
|
this.shippedAt = shippedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDeliveredAt() {
|
||||||
|
return deliveredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeliveredAt(LocalDateTime deliveredAt) {
|
||||||
|
this.deliveredAt = deliveredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCancelledAt() {
|
||||||
|
return cancelledAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCancelledAt(LocalDateTime cancelledAt) {
|
||||||
|
this.cancelledAt = cancelledAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OrderItem> getOrderItems() {
|
||||||
|
return orderItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrderItems(List<OrderItem> orderItems) {
|
||||||
|
this.orderItems = orderItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Payment> getPayments() {
|
||||||
|
return payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPayments(List<Payment> payments) {
|
||||||
|
this.payments = payments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
134
demo/src/main/java/com/example/demo/model/OrderItem.java
Normal file
134
demo/src/main/java/com/example/demo/model/OrderItem.java
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "order_items")
|
||||||
|
public class OrderItem {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String productName;
|
||||||
|
|
||||||
|
@Column(length = 500)
|
||||||
|
private String productDescription;
|
||||||
|
|
||||||
|
@Column(length = 200)
|
||||||
|
private String productSku;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin(value = "0.01", message = "单价必须大于0")
|
||||||
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
|
private BigDecimal unitPrice;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Min(value = 1, message = "数量必须大于0")
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer quantity;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin(value = "0.00", message = "小计不能为负数")
|
||||||
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
|
private BigDecimal subtotal;
|
||||||
|
|
||||||
|
@Column(length = 100)
|
||||||
|
private String productImage;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "order_id", nullable = false)
|
||||||
|
private Order order;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
@PreUpdate
|
||||||
|
protected void calculateSubtotal() {
|
||||||
|
if (unitPrice != null && quantity != null) {
|
||||||
|
subtotal = unitPrice.multiply(BigDecimal.valueOf(quantity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductName() {
|
||||||
|
return productName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductName(String productName) {
|
||||||
|
this.productName = productName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductDescription() {
|
||||||
|
return productDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductDescription(String productDescription) {
|
||||||
|
this.productDescription = productDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductSku() {
|
||||||
|
return productSku;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductSku(String productSku) {
|
||||||
|
this.productSku = productSku;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getUnitPrice() {
|
||||||
|
return unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnitPrice(BigDecimal unitPrice) {
|
||||||
|
this.unitPrice = unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getQuantity() {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuantity(Integer quantity) {
|
||||||
|
this.quantity = quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getSubtotal() {
|
||||||
|
return subtotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubtotal(BigDecimal subtotal) {
|
||||||
|
this.subtotal = subtotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductImage() {
|
||||||
|
return productImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductImage(String productImage) {
|
||||||
|
this.productImage = productImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public Order getOrder() {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrder(Order order) {
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
26
demo/src/main/java/com/example/demo/model/OrderStatus.java
Normal file
26
demo/src/main/java/com/example/demo/model/OrderStatus.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
public enum OrderStatus {
|
||||||
|
PENDING("待支付"),
|
||||||
|
CONFIRMED("已确认"),
|
||||||
|
PAID("已支付"),
|
||||||
|
PROCESSING("处理中"),
|
||||||
|
SHIPPED("已发货"),
|
||||||
|
DELIVERED("已送达"),
|
||||||
|
COMPLETED("已完成"),
|
||||||
|
CANCELLED("已取消"),
|
||||||
|
REFUNDED("已退款");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
OrderStatus(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
20
demo/src/main/java/com/example/demo/model/OrderType.java
Normal file
20
demo/src/main/java/com/example/demo/model/OrderType.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
public enum OrderType {
|
||||||
|
PRODUCT("商品订单"),
|
||||||
|
SERVICE("服务订单"),
|
||||||
|
SUBSCRIPTION("订阅订单"),
|
||||||
|
DIGITAL("数字商品"),
|
||||||
|
PHYSICAL("实体商品"),
|
||||||
|
PAYMENT("支付订单");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
OrderType(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
227
demo/src/main/java/com/example/demo/model/Payment.java
Normal file
227
demo/src/main/java/com/example/demo/model/Payment.java
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "payments")
|
||||||
|
public class Payment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Column(nullable = false, length = 50)
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin(value = "0.01", message = "金额必须大于0")
|
||||||
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Column(nullable = false, length = 3)
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private PaymentMethod paymentMethod;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private PaymentStatus status;
|
||||||
|
|
||||||
|
@Column(length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(length = 100)
|
||||||
|
private String externalTransactionId;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String callbackUrl;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String returnUrl;
|
||||||
|
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String paymentUrl;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Column(name = "paid_at")
|
||||||
|
private LocalDateTime paidAt;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "user_id")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "order_id_ref")
|
||||||
|
private Order order;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
if (status == null) {
|
||||||
|
status = PaymentStatus.PENDING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以支付
|
||||||
|
*/
|
||||||
|
public boolean canPay() {
|
||||||
|
return status == PaymentStatus.PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以退款
|
||||||
|
*/
|
||||||
|
public boolean canRefund() {
|
||||||
|
return status == PaymentStatus.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrderId() {
|
||||||
|
return orderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrderId(String orderId) {
|
||||||
|
this.orderId = orderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(BigDecimal amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrency() {
|
||||||
|
return currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrency(String currency) {
|
||||||
|
this.currency = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentMethod getPaymentMethod() {
|
||||||
|
return paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaymentMethod(PaymentMethod paymentMethod) {
|
||||||
|
this.paymentMethod = paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(PaymentStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExternalTransactionId() {
|
||||||
|
return externalTransactionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExternalTransactionId(String externalTransactionId) {
|
||||||
|
this.externalTransactionId = externalTransactionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCallbackUrl() {
|
||||||
|
return callbackUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCallbackUrl(String callbackUrl) {
|
||||||
|
this.callbackUrl = callbackUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReturnUrl() {
|
||||||
|
return returnUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReturnUrl(String returnUrl) {
|
||||||
|
this.returnUrl = returnUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPaymentUrl() {
|
||||||
|
return paymentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaymentUrl(String paymentUrl) {
|
||||||
|
this.paymentUrl = paymentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getPaidAt() {
|
||||||
|
return paidAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaidAt(LocalDateTime paidAt) {
|
||||||
|
this.paidAt = paidAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Order getOrder() {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrder(Order order) {
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
demo/src/main/java/com/example/demo/model/PaymentMethod.java
Normal file
17
demo/src/main/java/com/example/demo/model/PaymentMethod.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
public enum PaymentMethod {
|
||||||
|
ALIPAY("支付宝"),
|
||||||
|
PAYPAL("PayPal");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
PaymentMethod(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
21
demo/src/main/java/com/example/demo/model/PaymentStatus.java
Normal file
21
demo/src/main/java/com/example/demo/model/PaymentStatus.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
public enum PaymentStatus {
|
||||||
|
PENDING("待支付"),
|
||||||
|
PROCESSING("处理中"),
|
||||||
|
SUCCESS("支付成功"),
|
||||||
|
FAILED("支付失败"),
|
||||||
|
CANCELLED("已取消"),
|
||||||
|
REFUNDED("已退款");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
PaymentStatus(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
158
demo/src/main/java/com/example/demo/model/SystemSettings.java
Normal file
158
demo/src/main/java/com/example/demo/model/SystemSettings.java
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "system_settings")
|
||||||
|
public class SystemSettings {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 标准版价格(单位:元) */
|
||||||
|
@NotNull
|
||||||
|
@Min(0)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer standardPriceCny = 0;
|
||||||
|
|
||||||
|
/** 专业版价格(单位:元) */
|
||||||
|
@NotNull
|
||||||
|
@Min(0)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer proPriceCny = 0;
|
||||||
|
|
||||||
|
/** 每次生成消耗的资源点数量 */
|
||||||
|
@NotNull
|
||||||
|
@Min(0)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer pointsPerGeneration = 1;
|
||||||
|
|
||||||
|
/** 站点名称 */
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String siteName = "AIGC Demo";
|
||||||
|
|
||||||
|
/** 站点副标题 */
|
||||||
|
@Column(nullable = false, length = 150)
|
||||||
|
private String siteSubtitle = "现代化的Spring Boot应用演示";
|
||||||
|
|
||||||
|
/** 是否开放注册 */
|
||||||
|
@NotNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean registrationOpen = true;
|
||||||
|
|
||||||
|
/** 维护模式开关 */
|
||||||
|
@NotNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean maintenanceMode = false;
|
||||||
|
|
||||||
|
/** 支付渠道开关 */
|
||||||
|
@NotNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean enableAlipay = true;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean enablePaypal = true;
|
||||||
|
|
||||||
|
/** 联系邮箱 */
|
||||||
|
@Column(length = 120)
|
||||||
|
private String contactEmail = "support@example.com";
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStandardPriceCny() {
|
||||||
|
return standardPriceCny;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStandardPriceCny(Integer standardPriceCny) {
|
||||||
|
this.standardPriceCny = standardPriceCny;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getProPriceCny() {
|
||||||
|
return proPriceCny;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProPriceCny(Integer proPriceCny) {
|
||||||
|
this.proPriceCny = proPriceCny;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPointsPerGeneration() {
|
||||||
|
return pointsPerGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPointsPerGeneration(Integer pointsPerGeneration) {
|
||||||
|
this.pointsPerGeneration = pointsPerGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSiteName() {
|
||||||
|
return siteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSiteName(String siteName) {
|
||||||
|
this.siteName = siteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSiteSubtitle() {
|
||||||
|
return siteSubtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSiteSubtitle(String siteSubtitle) {
|
||||||
|
this.siteSubtitle = siteSubtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getRegistrationOpen() {
|
||||||
|
return registrationOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegistrationOpen(Boolean registrationOpen) {
|
||||||
|
this.registrationOpen = registrationOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getMaintenanceMode() {
|
||||||
|
return maintenanceMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaintenanceMode(Boolean maintenanceMode) {
|
||||||
|
this.maintenanceMode = maintenanceMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEnableAlipay() {
|
||||||
|
return enableAlipay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnableAlipay(Boolean enableAlipay) {
|
||||||
|
this.enableAlipay = enableAlipay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEnablePaypal() {
|
||||||
|
return enablePaypal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnablePaypal(Boolean enablePaypal) {
|
||||||
|
this.enablePaypal = enablePaypal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactEmail() {
|
||||||
|
return contactEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactEmail(String contactEmail) {
|
||||||
|
this.contactEmail = contactEmail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
112
demo/src/main/java/com/example/demo/model/User.java
Normal file
112
demo/src/main/java/com/example/demo/model/User.java
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users")
|
||||||
|
public class User {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 3, max = 50)
|
||||||
|
@Column(nullable = false, unique = true, length = 50)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Email
|
||||||
|
@Column(nullable = false, unique = true, length = 100)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 6, max = 100)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String passwordHash;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Column(nullable = false, length = 30)
|
||||||
|
private String role = "ROLE_USER";
|
||||||
|
|
||||||
|
@Min(0)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer points = 50; // 默认50积分
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPasswordHash() {
|
||||||
|
return passwordHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordHash(String passwordHash) {
|
||||||
|
this.passwordHash = passwordHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPoints() {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPoints(Integer points) {
|
||||||
|
this.points = points;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.model.OrderItem;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单ID查找订单项
|
||||||
|
*/
|
||||||
|
List<OrderItem> findByOrderId(Long orderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据产品SKU查找订单项
|
||||||
|
*/
|
||||||
|
List<OrderItem> findByProductSku(String productSku);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据产品名称模糊查询订单项
|
||||||
|
*/
|
||||||
|
@Query("SELECT oi FROM OrderItem oi WHERE oi.productName LIKE %:productName%")
|
||||||
|
List<OrderItem> findByProductNameContaining(@Param("productName") String productName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定产品的销售数量
|
||||||
|
*/
|
||||||
|
@Query("SELECT SUM(oi.quantity) FROM OrderItem oi WHERE oi.productSku = :productSku")
|
||||||
|
Long sumQuantityByProductSku(@Param("productSku") String productSku);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定产品的销售金额
|
||||||
|
*/
|
||||||
|
@Query("SELECT SUM(oi.subtotal) FROM OrderItem oi WHERE oi.productSku = :productSku")
|
||||||
|
java.math.BigDecimal sumSubtotalByProductSku(@Param("productSku") String productSku);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.model.Order;
|
||||||
|
import com.example.demo.model.OrderStatus;
|
||||||
|
import com.example.demo.model.OrderType;
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单号查找订单
|
||||||
|
*/
|
||||||
|
Optional<Order> findByOrderNumber(String orderNumber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID查找订单
|
||||||
|
*/
|
||||||
|
List<Order> findByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID分页查找订单
|
||||||
|
*/
|
||||||
|
Page<Order> findByUserId(Long userId, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单状态查找订单
|
||||||
|
*/
|
||||||
|
List<Order> findByStatus(OrderStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单状态分页查找订单
|
||||||
|
*/
|
||||||
|
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单类型查找订单
|
||||||
|
*/
|
||||||
|
List<Order> findByOrderType(OrderType orderType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和订单状态查找订单
|
||||||
|
*/
|
||||||
|
List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据创建时间范围查找订单
|
||||||
|
*/
|
||||||
|
List<Order> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和创建时间范围查找订单
|
||||||
|
*/
|
||||||
|
List<Order> findByUserIdAndCreatedAtBetween(Long userId, LocalDateTime startDate, LocalDateTime endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户订单数量
|
||||||
|
*/
|
||||||
|
long countByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定状态的订单数量
|
||||||
|
*/
|
||||||
|
long countByStatus(OrderStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户指定状态的订单数量
|
||||||
|
*/
|
||||||
|
long countByUserIdAndStatus(Long userId, OrderStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找用户的最近订单
|
||||||
|
*/
|
||||||
|
@Query("SELECT o FROM Order o WHERE o.user.id = :userId ORDER BY o.createdAt DESC")
|
||||||
|
List<Order> findRecentOrdersByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找待支付的订单
|
||||||
|
*/
|
||||||
|
@Query("SELECT o FROM Order o WHERE o.status IN ('PENDING', 'CONFIRMED') AND o.createdAt < :expireTime")
|
||||||
|
List<Order> findExpiredPendingOrders(@Param("expireTime") LocalDateTime expireTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单号模糊查询
|
||||||
|
*/
|
||||||
|
@Query("SELECT o FROM Order o WHERE o.orderNumber LIKE %:orderNumber%")
|
||||||
|
List<Order> findByOrderNumberContaining(@Param("orderNumber") String orderNumber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户邮箱查找订单
|
||||||
|
*/
|
||||||
|
@Query("SELECT o FROM Order o WHERE o.contactEmail = :email")
|
||||||
|
List<Order> findByContactEmail(@Param("email") String email);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户手机号查找订单
|
||||||
|
*/
|
||||||
|
@Query("SELECT o FROM Order o WHERE o.contactPhone = :phone")
|
||||||
|
List<Order> findByContactPhone(@Param("phone") String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定时间范围内的订单数量
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT(o) FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate")
|
||||||
|
long countByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定时间范围内的订单总金额
|
||||||
|
*/
|
||||||
|
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate AND o.status = 'COMPLETED'")
|
||||||
|
BigDecimal sumTotalAmountByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找需要自动取消的订单
|
||||||
|
*/
|
||||||
|
@Query("SELECT o FROM Order o WHERE o.status = 'PENDING' AND o.createdAt < :cancelTime")
|
||||||
|
List<Order> findOrdersToAutoCancel(@Param("cancelTime") LocalDateTime cancelTime);
|
||||||
|
|
||||||
|
// 新增的查询方法
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态和订单号模糊查询(分页)
|
||||||
|
*/
|
||||||
|
Page<Order> findByStatusAndOrderNumberContainingIgnoreCase(OrderStatus status, String orderNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单号模糊查询(分页)
|
||||||
|
*/
|
||||||
|
Page<Order> findByOrderNumberContainingIgnoreCase(String orderNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户查找订单(分页)
|
||||||
|
*/
|
||||||
|
Page<Order> findByUser(User user, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户和状态查找订单(分页)
|
||||||
|
*/
|
||||||
|
Page<Order> findByUserAndStatus(User user, OrderStatus status, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户和订单号模糊查询(分页)
|
||||||
|
*/
|
||||||
|
Page<Order> findByUserAndOrderNumberContainingIgnoreCase(User user, String orderNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户、状态和订单号模糊查询(分页)
|
||||||
|
*/
|
||||||
|
Page<Order> findByUserAndStatusAndOrderNumberContainingIgnoreCase(User user, OrderStatus status, String orderNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定时间之后的订单数量
|
||||||
|
*/
|
||||||
|
long countByCreatedAtAfter(LocalDateTime dateTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户指定时间之后的订单数量
|
||||||
|
*/
|
||||||
|
long countByUserAndCreatedAtAfter(User user, LocalDateTime dateTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户的订单数量
|
||||||
|
*/
|
||||||
|
long countByUser(User user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户指定状态的订单数量
|
||||||
|
*/
|
||||||
|
long countByUserAndStatus(User user, OrderStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定状态的订单总金额
|
||||||
|
*/
|
||||||
|
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.status = :status")
|
||||||
|
BigDecimal sumTotalAmountByStatus(@Param("status") OrderStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户指定状态的订单总金额
|
||||||
|
*/
|
||||||
|
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.user = :user AND o.status = :status")
|
||||||
|
BigDecimal sumTotalAmountByUserAndStatus(@Param("user") User user, @Param("status") OrderStatus status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.model.Payment;
|
||||||
|
import com.example.demo.model.PaymentStatus;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PaymentRepository extends JpaRepository<Payment, Long> {
|
||||||
|
|
||||||
|
Optional<Payment> findByOrderId(String orderId);
|
||||||
|
|
||||||
|
Optional<Payment> findByExternalTransactionId(String externalTransactionId);
|
||||||
|
|
||||||
|
List<Payment> findByUserId(Long userId);
|
||||||
|
|
||||||
|
List<Payment> findByStatus(PaymentStatus status);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Payment p WHERE p.user.id = :userId ORDER BY p.createdAt DESC")
|
||||||
|
List<Payment> findByUserIdOrderByCreatedAtDesc(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(p) FROM Payment p WHERE p.status = :status")
|
||||||
|
long countByStatus(@Param("status") PaymentStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户支付记录数量
|
||||||
|
*/
|
||||||
|
long countByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户指定状态的支付记录数量
|
||||||
|
*/
|
||||||
|
long countByUserIdAndStatus(Long userId, PaymentStatus status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.model.SystemSettings;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface SystemSettingsRepository extends JpaRepository<SystemSettings, Long> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
|
||||||
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
Optional<User> findByUsername(String username);
|
||||||
|
Optional<User> findByEmail(String email);
|
||||||
|
boolean existsByUsername(String username);
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user