我要开发一个h5的项目。 目标平台是PC浏览器和手机浏览器。 分为前端和后端。 后端采用python 不需要数据库,直接用文件系统,启动的时候读取到内存就可以。
功能 1、是前端能从后端拉起一组图片,分类为“男”,“女”。 这个图片是保存到oss文件系统的,后端需要提供图片的url。 2、前端可以选择一个图片,手动端可以拍摄图片或者选择图片。 3、前端发起更换发型的请求,把选择的图片和对应要更换的发型ID发给后端。 4、前端收到后端更换发型的请求后处理图片返回给前端。 5、后端实际处理更换发型请求的时候会调用另外一个远程的http接口来实现。 先帮我把项目结构,产品文档,系统框架等做出来。
This commit is contained in:
Binary file not shown.
+150
@@ -0,0 +1,150 @@
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
from flask_cors import CORS
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
from config import Config
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
CORS(app) # 启用CORS,允许跨域请求
|
||||
|
||||
# 加载发型数据
|
||||
hairstyles_data = None
|
||||
with open('data/hairstyles.json', 'r', encoding='utf-8') as f:
|
||||
hairstyles_data = json.load(f)
|
||||
|
||||
@app.route('/api/hairstyles', methods=['GET'])
|
||||
def get_hairstyles():
|
||||
"""
|
||||
获取发型列表
|
||||
返回按性别分类的发型数据
|
||||
"""
|
||||
try:
|
||||
# 按性别分类发型
|
||||
male_hairstyles = [h for h in hairstyles_data['hairstyles'] if h['gender'] == 'male']
|
||||
female_hairstyles = [h for h in hairstyles_data['hairstyles'] if h['gender'] == 'female']
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'male': male_hairstyles,
|
||||
'female': female_hairstyles
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'获取发型列表失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/change-hair', methods=['POST'])
|
||||
def change_hair():
|
||||
"""
|
||||
更换发型
|
||||
接收用户上传的图片和发型ID,调用远程API处理
|
||||
"""
|
||||
try:
|
||||
# 检查是否有文件上传
|
||||
if 'image' not in request.files:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请上传图片'
|
||||
}), 400
|
||||
|
||||
# 获取图片文件和发型ID
|
||||
image_file = request.files['image']
|
||||
hairstyle_id = request.form.get('hairstyle_id')
|
||||
|
||||
if not hairstyle_id:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请选择发型'
|
||||
}), 400
|
||||
|
||||
# 验证文件类型
|
||||
if not allowed_file(image_file.filename):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '不支持的文件类型,请上传图片文件'
|
||||
}), 400
|
||||
|
||||
# 读取图片数据
|
||||
image_data = image_file.read()
|
||||
|
||||
# 调用远程API处理图片
|
||||
# 注意:这里是模拟实现,实际项目中需要根据远程API的要求进行调整
|
||||
result_image = call_remote_api(image_data, hairstyle_id)
|
||||
|
||||
# 将处理后的图片转换为base64格式返回
|
||||
result_base64 = base64.b64encode(result_image).decode('utf-8')
|
||||
result_data_url = f'data:image/jpeg;base64,{result_base64}'
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'result_image': result_data_url
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'处理图片失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
def allowed_file(filename):
|
||||
"""
|
||||
验证文件类型是否允许
|
||||
"""
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
||||
|
||||
def call_remote_api(image_data, hairstyle_id):
|
||||
"""
|
||||
调用远程API处理图片
|
||||
注意:这里是模拟实现,实际项目中需要根据远程API的要求进行调整
|
||||
"""
|
||||
try:
|
||||
# 构建请求数据
|
||||
# 注意:这里需要根据远程API的实际要求调整请求格式
|
||||
files = {'image': ('upload.jpg', BytesIO(image_data), 'image/jpeg')}
|
||||
data = {'hairstyle_id': hairstyle_id}
|
||||
|
||||
# 发送请求到远程API
|
||||
# 注意:实际项目中需要替换为真实的远程API地址
|
||||
# response = requests.post(
|
||||
# app.config['REMOTE_API_URL'],
|
||||
# files=files,
|
||||
# data=data,
|
||||
# timeout=app.config['API_TIMEOUT']
|
||||
# )
|
||||
|
||||
# 模拟远程API返回结果
|
||||
# 这里简单返回原始图片数据作为模拟结果
|
||||
# 实际项目中应该使用远程API的返回结果
|
||||
return image_data
|
||||
except Exception as e:
|
||||
# 如果远程API调用失败,返回原始图片
|
||||
# 实际项目中应该根据需要进行错误处理
|
||||
return image_data
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""
|
||||
提供前端静态文件
|
||||
开发环境下使用,生产环境应该使用Nginx等静态文件服务器
|
||||
"""
|
||||
return send_from_directory('../frontend', 'index.html')
|
||||
|
||||
@app.route('/<path:path>')
|
||||
def send_static(path):
|
||||
"""
|
||||
提供前端静态文件
|
||||
"""
|
||||
return send_from_directory('../frontend', path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
@@ -0,0 +1,18 @@
|
||||
# 应用配置
|
||||
class Config:
|
||||
# 应用名称
|
||||
APP_NAME = "Hair Change Color"
|
||||
|
||||
# 远程发型更换API地址
|
||||
# 注意:这里需要根据实际的远程API地址进行修改
|
||||
REMOTE_API_URL = "https://api.example.com/change-hair"
|
||||
|
||||
# 远程API请求超时时间(秒)
|
||||
API_TIMEOUT = 30
|
||||
|
||||
# 文件上传配置
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
|
||||
# 开发环境配置
|
||||
DEBUG = True
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"hairstyles": [
|
||||
{
|
||||
"id": "hair_001",
|
||||
"name": "时尚短发",
|
||||
"gender": "male",
|
||||
"image_url": "https://oss.example.com/hairstyles/male/hair_001.jpg"
|
||||
},
|
||||
{
|
||||
"id": "hair_002",
|
||||
"name": "清爽平头",
|
||||
"gender": "male",
|
||||
"image_url": "https://oss.example.com/hairstyles/male/hair_002.jpg"
|
||||
},
|
||||
{
|
||||
"id": "hair_003",
|
||||
"name": "潮流纹理烫",
|
||||
"gender": "male",
|
||||
"image_url": "https://oss.example.com/hairstyles/male/hair_003.jpg"
|
||||
},
|
||||
{
|
||||
"id": "hair_004",
|
||||
"name": "优雅长发",
|
||||
"gender": "female",
|
||||
"image_url": "https://oss.example.com/hairstyles/female/hair_004.jpg"
|
||||
},
|
||||
{
|
||||
"id": "hair_005",
|
||||
"name": "俏皮短发",
|
||||
"gender": "female",
|
||||
"image_url": "https://oss.example.com/hairstyles/female/hair_005.jpg"
|
||||
},
|
||||
{
|
||||
"id": "hair_006",
|
||||
"name": "浪漫卷发",
|
||||
"gender": "female",
|
||||
"image_url": "https://oss.example.com/hairstyles/female/hair_006.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Flask
|
||||
Flask-CORS
|
||||
requests
|
||||
@@ -0,0 +1,239 @@
|
||||
# 发型更换H5项目系统架构文档
|
||||
|
||||
## 1. 技术选型
|
||||
|
||||
### 1.1 前端技术栈
|
||||
- **HTML5**:提供基础页面结构和语义化标签
|
||||
- **CSS3**:实现响应式布局和动画效果
|
||||
- **JavaScript (ES6+)**:实现前端交互逻辑
|
||||
- **Axios**:处理HTTP请求,与后端API通信
|
||||
- **FileReader API**:处理本地图片读取和预览
|
||||
- **MediaDevices API**:支持移动端拍照功能
|
||||
- **LocalStorage**:存储临时数据,如用户选择的发型ID
|
||||
|
||||
### 1.2 后端技术栈
|
||||
- **Python 3.8+**:核心后端语言
|
||||
- **Flask**:轻量级Web框架,用于构建API服务
|
||||
- **Flask-CORS**:处理跨域请求
|
||||
- **Requests**:调用远程HTTP接口进行发型更换处理
|
||||
- **JSON**:数据交换格式
|
||||
- **文件系统**:存储发型数据和配置信息
|
||||
|
||||
### 1.3 第三方服务
|
||||
- **OSS**:存储发型图片,提供CDN加速
|
||||
- **远程发型更换API**:处理实际的发型更换算法
|
||||
|
||||
## 2. 系统架构
|
||||
|
||||
### 2.1 架构概述
|
||||
本项目采用前后端分离的架构模式,前端通过HTTP请求与后端API进行通信。后端作为中间层,负责管理发型数据和调用远程API处理发型更换请求。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 前端H5
|
||||
participant Server as 后端服务
|
||||
participant OSS as OSS存储
|
||||
participant RemoteAPI as 远程发型更换API
|
||||
|
||||
Client->>Server: GET /api/hairstyles
|
||||
Server->>Server: 读取发型配置文件
|
||||
Server-->>Client: 返回发型列表(包含OSS URL)
|
||||
|
||||
Client->>Client: 用户选择图片和发型
|
||||
Client->>Server: POST /api/change-hair (图片数据, 发型ID)
|
||||
Server->>RemoteAPI: 调用远程API处理图片
|
||||
RemoteAPI-->>Server: 返回处理后的图片
|
||||
Server-->>Client: 返回发型更换结果
|
||||
```
|
||||
|
||||
### 2.2 目录结构
|
||||
|
||||
```
|
||||
hair_change_color/
|
||||
├── backend/ # 后端Python服务
|
||||
│ ├── app.py # Flask应用主文件
|
||||
│ ├── config.py # 配置文件
|
||||
│ ├── data/ # 数据目录
|
||||
│ │ └── hairstyles.json # 发型数据配置
|
||||
│ └── requirements.txt # Python依赖
|
||||
├── frontend/ # 前端H5
|
||||
│ ├── index.html # 首页
|
||||
│ ├── css/ # CSS文件
|
||||
│ │ └── style.css # 主样式文件
|
||||
│ ├── js/ # JavaScript文件
|
||||
│ │ ├── main.js # 主逻辑
|
||||
│ │ └── api.js # API调用封装
|
||||
│ └── images/ # 前端静态图片
|
||||
└── docs/ # 文档
|
||||
├── PRD.md # 产品需求文档
|
||||
└── ARCHITECTURE.md # 系统架构文档
|
||||
```
|
||||
|
||||
## 3. 核心模块设计
|
||||
|
||||
### 3.1 前端模块
|
||||
|
||||
#### 3.1.1 图片上传模块
|
||||
- **功能**:支持用户上传本地图片或拍摄照片
|
||||
- **实现**:
|
||||
- 使用`<input type="file">`实现本地图片选择
|
||||
- 使用`navigator.mediaDevices.getUserMedia()`实现摄像头拍摄
|
||||
- 使用`FileReader` API读取图片并生成预览
|
||||
|
||||
#### 3.1.2 发型选择模块
|
||||
- **功能**:展示发型分类和列表,支持用户选择发型
|
||||
- **实现**:
|
||||
- 从后端API获取发型列表
|
||||
- 按性别分类展示发型
|
||||
- 实现发型预览和选择逻辑
|
||||
|
||||
#### 3.1.3 结果展示模块
|
||||
- **功能**:展示发型更换后的效果
|
||||
- **实现**:
|
||||
- 接收后端返回的处理结果
|
||||
- 实现原图与效果图的对比展示
|
||||
- 提供保存和分享功能
|
||||
|
||||
### 3.2 后端模块
|
||||
|
||||
#### 3.2.1 发型管理模块
|
||||
- **功能**:管理发型数据,提供发型列表API
|
||||
- **实现**:
|
||||
- 从JSON配置文件读取发型数据
|
||||
- 启动时加载到内存
|
||||
- 提供GET /api/hairstyles接口返回发型列表
|
||||
|
||||
#### 3.2.2 图片处理模块
|
||||
- **功能**:接收用户上传的图片,调用远程API处理
|
||||
- **实现**:
|
||||
- 接收POST /api/change-hair请求
|
||||
- 解析图片数据和发型ID
|
||||
- 调用远程HTTP接口处理图片
|
||||
- 返回处理结果
|
||||
|
||||
#### 3.2.3 配置管理模块
|
||||
- **功能**:管理应用配置,如远程API地址、OSS配置等
|
||||
- **实现**:
|
||||
- 从config.py读取配置
|
||||
- 提供配置访问接口
|
||||
|
||||
## 4. 数据库设计
|
||||
|
||||
### 4.1 数据存储方案
|
||||
本项目不使用传统数据库,而是采用文件系统存储数据:
|
||||
- **发型数据**:存储在`backend/data/hairstyles.json`文件中
|
||||
- **配置数据**:存储在`backend/config.py`文件中
|
||||
- **临时文件**:使用内存存储,不持久化
|
||||
|
||||
### 4.2 发型数据结构
|
||||
|
||||
```json
|
||||
{
|
||||
"hairstyles": [
|
||||
{
|
||||
"id": "hair_001",
|
||||
"name": "时尚短发",
|
||||
"gender": "male",
|
||||
"image_url": "https://oss.example.com/hairstyles/male/hair_001.jpg"
|
||||
},
|
||||
{
|
||||
"id": "hair_002",
|
||||
"name": "优雅长发",
|
||||
"gender": "female",
|
||||
"image_url": "https://oss.example.com/hairstyles/female/hair_002.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. API 接口设计
|
||||
|
||||
### 5.1 前端API调用
|
||||
|
||||
#### 5.1.1 获取发型列表
|
||||
- **接口**:GET /api/hairstyles
|
||||
- **参数**:无
|
||||
- **返回**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"male": [
|
||||
{
|
||||
"id": "hair_001",
|
||||
"name": "时尚短发",
|
||||
"image_url": "https://oss.example.com/hairstyles/male/hair_001.jpg"
|
||||
}
|
||||
],
|
||||
"female": [
|
||||
{
|
||||
"id": "hair_002",
|
||||
"name": "优雅长发",
|
||||
"image_url": "https://oss.example.com/hairstyles/female/hair_002.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.1.2 更换发型
|
||||
- **接口**:POST /api/change-hair
|
||||
- **参数**:
|
||||
- image: 图片文件(FormData)
|
||||
- hairstyle_id: 发型ID(字符串)
|
||||
- **返回**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result_image": "data:image/jpeg;base64,..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 后端API实现
|
||||
|
||||
#### 5.2.1 Flask路由
|
||||
- `/api/hairstyles`:处理获取发型列表请求
|
||||
- `/api/change-hair`:处理发型更换请求
|
||||
- `/`:提供前端静态文件(开发环境)
|
||||
|
||||
## 6. 部署与集成方案
|
||||
|
||||
### 6.1 开发环境
|
||||
- **前端**:直接在浏览器中打开`frontend/index.html`
|
||||
- **后端**:使用Flask开发服务器,运行`python backend/app.py`
|
||||
|
||||
### 6.2 生产环境
|
||||
- **前端**:将前端文件部署到CDN或静态文件服务器
|
||||
- **后端**:使用Gunicorn或uWSGI部署Flask应用
|
||||
- **集成**:前端通过配置的API地址与后端通信
|
||||
|
||||
### 6.3 依赖管理
|
||||
- **前端**:通过CDN引入Axios
|
||||
- **后端**:使用pip管理依赖,依赖项记录在`requirements.txt`中
|
||||
|
||||
## 7. 性能与安全考虑
|
||||
|
||||
### 7.1 性能优化
|
||||
- **图片处理**:前端压缩上传图片,减少传输时间
|
||||
- **缓存策略**:缓存发型列表数据,减少API调用
|
||||
- **响应式设计**:针对不同设备优化加载速度
|
||||
|
||||
### 7.2 安全考虑
|
||||
- **CORS配置**:正确配置跨域资源共享,只允许指定域名访问
|
||||
- **文件验证**:后端验证上传文件类型和大小,防止恶意文件
|
||||
- **API限流**:对发型更换接口实施限流,防止滥用
|
||||
- **数据加密**:传输敏感数据时使用HTTPS
|
||||
|
||||
## 8. 扩展性设计
|
||||
|
||||
### 8.1 功能扩展
|
||||
- **支持更多发型分类**:通过修改配置文件即可添加新分类
|
||||
- **增加滤镜效果**:在发型更换基础上添加滤镜功能
|
||||
- **支持视频处理**:扩展为支持视频中的发型更换
|
||||
|
||||
### 8.2 技术扩展
|
||||
- **支持WebSocket**:实现实时预览功能
|
||||
- **添加用户系统**:支持保存用户历史记录和偏好设置
|
||||
- **接入更多AI能力**:如人脸识别、风格推荐等
|
||||
@@ -0,0 +1,285 @@
|
||||
# 发型更换H5项目启动和部署文档
|
||||
|
||||
## 1. 项目结构
|
||||
|
||||
```
|
||||
hair_change_color/
|
||||
├── backend/ # 后端Python服务
|
||||
│ ├── app.py # Flask应用主文件
|
||||
│ ├── config.py # 配置文件
|
||||
│ ├── data/ # 数据目录
|
||||
│ │ └── hairstyles.json # 发型数据配置
|
||||
│ └── requirements.txt # Python依赖
|
||||
├── frontend/ # 前端H5
|
||||
│ ├── index.html # 首页
|
||||
│ ├── css/ # CSS文件
|
||||
│ │ └── style.css # 主样式文件
|
||||
│ ├── js/ # JavaScript文件
|
||||
│ │ ├── main.js # 主逻辑
|
||||
│ │ └── api.js # API调用封装
|
||||
│ └── images/ # 前端静态图片
|
||||
└── docs/ # 文档
|
||||
├── PRD.md # 产品需求文档
|
||||
├── ARCHITECTURE.md # 系统架构文档
|
||||
└── DEPLOYMENT.md # 部署文档
|
||||
```
|
||||
|
||||
## 2. 开发环境搭建
|
||||
|
||||
### 2.1 后端环境
|
||||
|
||||
#### 2.1.1 依赖安装
|
||||
1. 确保已安装 Python 3.8+
|
||||
2. 进入后端目录
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
3. 安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 2.1.2 配置修改
|
||||
1. 编辑 `config.py` 文件,修改以下配置:
|
||||
- `REMOTE_API_URL`:远程发型更换API地址
|
||||
- `API_TIMEOUT`:API请求超时时间
|
||||
|
||||
2. 编辑 `data/hairstyles.json` 文件,添加或修改发型数据:
|
||||
- 确保每个发型对象包含 `id`、`name`、`gender` 和 `image_url` 字段
|
||||
- `gender` 字段值为 `male` 或 `female`
|
||||
- `image_url` 为发型图片的OSS地址
|
||||
|
||||
#### 2.1.3 启动服务
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
服务将在 `http://127.0.0.1:5000` 启动
|
||||
|
||||
### 2.2 前端环境
|
||||
|
||||
#### 2.2.1 开发模式
|
||||
在开发环境中,前端文件通过后端Flask服务提供,无需单独配置。
|
||||
|
||||
1. 启动后端服务后,访问 `http://127.0.0.1:5000` 即可看到前端页面
|
||||
2. 前端代码修改后,刷新浏览器即可看到变化
|
||||
|
||||
## 3. 生产环境部署
|
||||
|
||||
### 3.1 后端部署
|
||||
|
||||
#### 3.1.1 使用 Gunicorn 部署
|
||||
1. 安装 Gunicorn
|
||||
```bash
|
||||
pip install gunicorn
|
||||
```
|
||||
|
||||
2. 启动服务
|
||||
```bash
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
- `-w 4`:使用4个worker进程
|
||||
- `-b 0.0.0.0:5000`:绑定到所有网络接口的5000端口
|
||||
|
||||
#### 3.1.2 使用 Nginx 作为反向代理
|
||||
1. 安装 Nginx
|
||||
2. 配置 Nginx 反向代理
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 前端部署
|
||||
|
||||
#### 3.2.1 静态文件部署
|
||||
在生产环境中,建议将前端文件部署到 CDN 或静态文件服务器,以提高加载速度。
|
||||
|
||||
1. 复制前端文件到静态文件服务器
|
||||
2. 修改前端 `js/main.js` 中的 API 地址,指向后端服务地址
|
||||
|
||||
#### 3.2.2 配置示例
|
||||
修改前端 API 调用地址:
|
||||
```javascript
|
||||
// 修改 API 调用地址
|
||||
async function getHairstyles() {
|
||||
try {
|
||||
const response = await axios.get('https://api.example.com/api/hairstyles');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取发型列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function changeHair(imageFile, hairstyleId) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
formData.append('hairstyle_id', hairstyleId);
|
||||
|
||||
const response = await axios.post('https://api.example.com/api/change-hair', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('更换发型失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 系统配置
|
||||
|
||||
### 4.1 后端配置
|
||||
|
||||
#### 4.1.1 主要配置项
|
||||
编辑 `backend/config.py` 文件:
|
||||
|
||||
| 配置项 | 描述 | 默认值 |
|
||||
|-------|------|-------|
|
||||
| APP_NAME | 应用名称 | "Hair Change Color" |
|
||||
| REMOTE_API_URL | 远程发型更换API地址 | "https://api.example.com/change-hair" |
|
||||
| API_TIMEOUT | API请求超时时间(秒) | 30 |
|
||||
| MAX_CONTENT_LENGTH | 文件上传大小限制 | 16MB |
|
||||
| ALLOWED_EXTENSIONS | 允许的文件类型 | {'png', 'jpg', 'jpeg', 'gif'} |
|
||||
| DEBUG | 调试模式 | True |
|
||||
|
||||
#### 4.1.2 发型数据配置
|
||||
编辑 `backend/data/hairstyles.json` 文件,添加或修改发型数据:
|
||||
|
||||
```json
|
||||
{
|
||||
"hairstyles": [
|
||||
{
|
||||
"id": "hair_001",
|
||||
"name": "时尚短发",
|
||||
"gender": "male",
|
||||
"image_url": "https://oss.example.com/hairstyles/male/hair_001.jpg"
|
||||
},
|
||||
// 更多发型...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 前端配置
|
||||
|
||||
#### 4.2.1 响应式设计
|
||||
前端页面采用响应式设计,自动适配不同屏幕尺寸:
|
||||
- 移动端:< 768px
|
||||
- 平板:768px - 1024px
|
||||
- PC端:> 1024px
|
||||
|
||||
#### 4.2.2 浏览器兼容性
|
||||
支持的浏览器:
|
||||
- Chrome (最新版)
|
||||
- Firefox (最新版)
|
||||
- Safari (最新版)
|
||||
- Edge (最新版)
|
||||
- 移动端浏览器
|
||||
|
||||
## 5. 常见问题与解决方案
|
||||
|
||||
### 5.1 后端服务启动失败
|
||||
- **问题**:`ModuleNotFoundError: No module named 'Flask'`
|
||||
**解决方案**:运行 `pip install -r requirements.txt` 安装依赖
|
||||
|
||||
- **问题**:`FileNotFoundError: [Errno 2] No such file or directory: 'data/hairstyles.json'`
|
||||
**解决方案**:确保 `data` 目录存在且包含 `hairstyles.json` 文件
|
||||
|
||||
### 5.2 前端页面无法访问
|
||||
- **问题**:后端服务未启动
|
||||
**解决方案**:运行 `python app.py` 启动后端服务
|
||||
|
||||
- **问题**:API 调用失败
|
||||
**解决方案**:检查浏览器控制台的网络请求,确认 API 地址是否正确
|
||||
|
||||
### 5.3 发型更换功能不工作
|
||||
- **问题**:远程 API 调用失败
|
||||
**解决方案**:检查 `config.py` 中的 `REMOTE_API_URL` 配置是否正确
|
||||
|
||||
- **问题**:上传图片失败
|
||||
**解决方案**:检查图片大小是否超过限制(默认16MB),以及文件类型是否被允许
|
||||
|
||||
## 6. 性能优化建议
|
||||
|
||||
### 6.1 前端优化
|
||||
- **图片压缩**:上传前压缩图片,减少传输时间
|
||||
- **缓存策略**:缓存发型列表数据,减少 API 调用
|
||||
- **懒加载**:实现发型图片懒加载,提高页面加载速度
|
||||
|
||||
### 6.2 后端优化
|
||||
- **使用生产服务器**:在生产环境中使用 Gunicorn 或 uWSGI 替代开发服务器
|
||||
- **启用 Gzip 压缩**:减少响应数据大小
|
||||
- **增加 worker 数量**:根据服务器配置增加 Gunicorn worker 数量
|
||||
|
||||
## 7. 安全建议
|
||||
|
||||
### 7.1 前端安全
|
||||
- **使用 HTTPS**:在生产环境中使用 HTTPS 协议
|
||||
- **验证文件类型**:前端验证上传文件类型,防止恶意文件
|
||||
|
||||
### 7.2 后端安全
|
||||
- **配置 CORS**:正确配置跨域资源共享,只允许指定域名访问
|
||||
- **限制文件大小**:设置合理的文件上传大小限制
|
||||
- **API 限流**:对发型更换接口实施限流,防止滥用
|
||||
- **验证请求参数**:验证所有请求参数,防止注入攻击
|
||||
|
||||
## 8. 监控与维护
|
||||
|
||||
### 8.1 日志管理
|
||||
- **Flask 日志**:后端服务日志默认输出到控制台
|
||||
- **访问日志**:使用 Nginx 等反向代理时,配置访问日志记录
|
||||
|
||||
### 8.2 健康检查
|
||||
- **API 健康检查**:定期检查 `/api/hairstyles` 接口是否正常响应
|
||||
- **服务状态监控**:监控后端服务运行状态,确保服务稳定
|
||||
|
||||
## 9. 部署流程总结
|
||||
|
||||
1. **准备环境**:
|
||||
- 安装 Python 3.8+
|
||||
- 安装依赖:`pip install -r backend/requirements.txt`
|
||||
|
||||
2. **配置项目**:
|
||||
- 修改 `backend/config.py` 中的远程 API 地址
|
||||
- 编辑 `backend/data/hairstyles.json` 添加发型数据
|
||||
|
||||
3. **启动服务**:
|
||||
- 开发环境:`python backend/app.py`
|
||||
- 生产环境:使用 Gunicorn 或 uWSGI 启动
|
||||
|
||||
4. **访问应用**:
|
||||
- 开发环境:`http://127.0.0.1:5000`
|
||||
- 生产环境:配置域名后访问
|
||||
|
||||
5. **维护更新**:
|
||||
- 更新发型数据:修改 `hairstyles.json` 文件
|
||||
- 更新代码:部署新代码后重启服务
|
||||
|
||||
## 10. 故障排除
|
||||
|
||||
### 10.1 服务无法启动
|
||||
- 检查端口是否被占用
|
||||
- 检查依赖是否安装完整
|
||||
- 检查配置文件是否正确
|
||||
|
||||
### 10.2 API 调用失败
|
||||
- 检查网络连接
|
||||
- 检查远程 API 服务是否可用
|
||||
- 检查 API 地址配置是否正确
|
||||
|
||||
### 10.3 前端页面异常
|
||||
- 清除浏览器缓存
|
||||
- 检查浏览器控制台错误信息
|
||||
- 验证 API 响应数据格式
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
# 发型更换H5项目产品需求文档
|
||||
|
||||
## 1. 产品概览
|
||||
本项目是一个基于H5的发型更换应用,支持用户上传或拍摄照片,并选择不同发型进行更换预览。
|
||||
- 解决用户在剪发前无法直观预览不同发型效果的问题,帮助用户做出更合适的发型选择。
|
||||
- 产品目标是提供简单易用的发型预览功能,覆盖PC和移动端浏览器。
|
||||
|
||||
## 2. 核心功能
|
||||
|
||||
### 2.1 用户角色
|
||||
本产品为单一角色应用,无需登录注册,所有用户均可直接使用全部功能。
|
||||
|
||||
### 2.2 功能模块
|
||||
我们的H5应用包含以下主要页面:
|
||||
1. **首页**:展示发型分类和推荐发型,提供图片上传/拍摄入口。
|
||||
2. **发型选择页**:按性别分类展示发型列表,支持用户选择发型。
|
||||
3. **结果预览页**:展示发型更换后的效果,支持保存和分享。
|
||||
|
||||
### 2.3 页面详情
|
||||
|
||||
| 页面名称 | 模块名称 | 功能描述 |
|
||||
|---------|---------|---------|
|
||||
| 首页 | 导航栏 | 显示应用名称,提供返回按钮(在子页面) |
|
||||
| 首页 | 图片上传区 | 支持点击上传图片或拍摄照片,显示上传后的预览图 |
|
||||
| 首页 | 发型分类入口 | 展示"男"、"女"发型分类入口,点击进入对应分类的发型选择页 |
|
||||
| 发型选择页 | 分类导航 | 显示当前分类,支持切换男/女分类 |
|
||||
| 发型选择页 | 发型列表 | 以网格形式展示该分类下的发型图片,每张图片下方显示发型名称 |
|
||||
| 发型选择页 | 发型预览 | 鼠标悬停或点击发型时,在预览区显示该发型的详细效果 |
|
||||
| 发型选择页 | 确认按钮 | 点击后将选择的发型应用到用户上传的照片上 |
|
||||
| 结果预览页 | 效果展示 | 大图展示发型更换后的效果,支持左右滑动查看原图和效果图对比 |
|
||||
| 结果预览页 | 操作按钮 | 提供重新选择发型、重新上传照片、保存结果等功能按钮 |
|
||||
| 结果预览页 | 分享功能 | 支持将结果分享到社交媒体或生成分享链接 |
|
||||
|
||||
## 3. Core Process
|
||||
### 用户操作流程
|
||||
1. 用户访问H5应用首页
|
||||
2. 用户上传照片或拍摄照片
|
||||
3. 系统显示上传成功的照片预览
|
||||
4. 用户选择"男"或"女"发型分类
|
||||
5. 系统加载并展示对应分类的发型列表
|
||||
6. 用户浏览发型列表并选择喜欢的发型
|
||||
7. 用户点击确认按钮,系统将选择的发型应用到上传的照片上
|
||||
8. 系统显示发型更换后的效果
|
||||
9. 用户可以选择重新选择发型、重新上传照片或保存结果
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[首页] --> B[上传/拍摄照片]
|
||||
B --> C[选择发型分类]
|
||||
C --> D[浏览发型列表]
|
||||
D --> E[选择发型]
|
||||
E --> F[确认更换]
|
||||
F --> G[查看结果]
|
||||
G --> H{操作选择}
|
||||
H -->|重新选择发型| D
|
||||
H -->|重新上传照片| B
|
||||
H -->|保存结果| I[保存成功]
|
||||
```
|
||||
|
||||
## 4. 用户接口设计
|
||||
### 4.1 设计风格
|
||||
- **主色调**:#4A90E2(蓝色),#50E3C2(青色)
|
||||
- **辅助色**:#F5A623(橙色),#D0021B(红色)
|
||||
- **按钮样式**:圆角矩形,有轻微的阴影效果,点击时有反馈动画
|
||||
- **字体**:系统默认无衬线字体,标题18-24px,正文14-16px
|
||||
- **布局风格**:响应式设计,移动端单列布局,PC端多列网格布局
|
||||
- **图标风格**:线性图标,简洁现代
|
||||
|
||||
### 4.2 页面设计概览
|
||||
|
||||
| 页面名称 | 模块名称 | UI元素 |
|
||||
|---------|---------|---------|
|
||||
| 首页 | 导航栏 | 顶部固定,高度60px,背景色#4A90E2,白色文字 |
|
||||
| 首页 | 图片上传区 | 居中显示,宽高比1:1,虚线边框,中央有上传图标和文字提示 |
|
||||
| 首页 | 发型分类入口 | 底部固定,两个并排的大按钮,分别显示"男"、"女"字样和对应图标 |
|
||||
| 发型选择页 | 分类导航 | 顶部固定,两个切换按钮,当前选中分类高亮显示 |
|
||||
| 发型选择页 | 发型列表 | 网格布局,每张发型图片为正方形,间距10px,有轻微的阴影效果 |
|
||||
| 发型选择页 | 确认按钮 | 底部固定,宽度100%,高度50px,背景色#50E3C2,白色文字 |
|
||||
| 结果预览页 | 效果展示 | 居中显示,大图展示,支持手势缩放和滑动 |
|
||||
| 结果预览页 | 操作按钮 | 底部固定,三个并排的图标按钮,分别对应重新选择、重新上传、保存 |
|
||||
|
||||
### 4.3 自适应
|
||||
- **设计理念**:采用移动优先的响应式设计理念
|
||||
- **断点设置**:
|
||||
- 移动端:< 768px
|
||||
- 平板:768px - 1024px
|
||||
- PC端:> 1024px
|
||||
- **适配策略**:
|
||||
- 移动端:单列布局,触摸友好的大按钮
|
||||
- PC端:多列网格布局,鼠标悬停效果
|
||||
- 图片和元素尺寸会根据屏幕宽度自动调整
|
||||
- 字体大小使用相对单位(rem/em)确保在不同设备上的可读性
|
||||
@@ -0,0 +1,408 @@
|
||||
/* 全局样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 应用容器 */
|
||||
#app {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 2rem;
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
padding: 2rem;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
/* 页面切换 */
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 图片上传区 */
|
||||
.upload-area {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 400px;
|
||||
margin: 0 auto;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.upload-placeholder p {
|
||||
color: #999;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#imageUpload {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 发型分类入口 */
|
||||
.category-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.category-section h2 {
|
||||
margin-bottom: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
border: 2px solid #4A90E2;
|
||||
border-radius: 12px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.category-btn:hover {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(74, 144, 226, 0.3);
|
||||
}
|
||||
|
||||
.gender-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 分类导航 */
|
||||
.category-nav {
|
||||
display: flex;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
.nav-category-btn {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
|
||||
.nav-category-btn.active {
|
||||
border-bottom-color: #4A90E2;
|
||||
color: #4A90E2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 发型列表 */
|
||||
.hairstyle-list h2 {
|
||||
margin-bottom: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.hairstyle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hairstyle-item {
|
||||
border: 2px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.hairstyle-item:hover {
|
||||
border-color: #4A90E2;
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hairstyle-item.selected {
|
||||
border-color: #4A90E2;
|
||||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.hairstyle-item img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hairstyle-item p {
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 确认按钮 */
|
||||
.confirm-section {
|
||||
margin-top: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
padding: 1rem 3rem;
|
||||
background-color: #50E3C2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.confirm-btn:hover:not(:disabled) {
|
||||
background-color: #38D1B0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(80, 227, 194, 0.3);
|
||||
}
|
||||
|
||||
.confirm-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 结果预览 */
|
||||
.result-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.result-section h2 {
|
||||
margin-bottom: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-preview {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 500px;
|
||||
margin: 0 auto;
|
||||
border: 2px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.result-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
border: 2px solid #4A90E2;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
color: #4A90E2;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 1.2rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* 加载指示器 */
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #f3f3f3;
|
||||
border-top: 5px solid #4A90E2;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.navbar h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.category-buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.hairstyle-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hairstyle-item img {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.result-preview {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.image-preview {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.hairstyle-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.result-preview {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>发型更换</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 导航栏 -->
|
||||
<header class="navbar">
|
||||
<h1>发型更换</h1>
|
||||
<button id="backBtn" class="back-btn" style="display: none;">返回</button>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content">
|
||||
<!-- 首页 -->
|
||||
<section id="homePage" class="page active">
|
||||
<!-- 图片上传区 -->
|
||||
<div class="upload-area">
|
||||
<div id="imagePreview" class="image-preview">
|
||||
<div class="upload-placeholder">
|
||||
<div class="upload-icon">📷</div>
|
||||
<p>点击上传图片或拍摄照片</p>
|
||||
<input type="file" id="imageUpload" accept="image/*" capture="camera">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发型分类入口 -->
|
||||
<div class="category-section">
|
||||
<h2>选择发型分类</h2>
|
||||
<div class="category-buttons">
|
||||
<button id="maleBtn" class="category-btn">
|
||||
<span class="gender-icon">👨</span>
|
||||
<span>男士发型</span>
|
||||
</button>
|
||||
<button id="femaleBtn" class="category-btn">
|
||||
<span class="gender-icon">👩</span>
|
||||
<span>女士发型</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 发型选择页 -->
|
||||
<section id="hairstylePage" class="page">
|
||||
<!-- 分类导航 -->
|
||||
<div class="category-nav">
|
||||
<button id="navMaleBtn" class="nav-category-btn active">男士</button>
|
||||
<button id="navFemaleBtn" class="nav-category-btn">女士</button>
|
||||
</div>
|
||||
|
||||
<!-- 发型列表 -->
|
||||
<div class="hairstyle-list">
|
||||
<h2>选择发型</h2>
|
||||
<div id="hairstyleGrid" class="hairstyle-grid">
|
||||
<!-- 发型项将通过JavaScript动态添加 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<div class="confirm-section">
|
||||
<button id="confirmBtn" class="confirm-btn" disabled>确认选择</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 结果预览页 -->
|
||||
<section id="resultPage" class="page">
|
||||
<!-- 效果展示 -->
|
||||
<div class="result-section">
|
||||
<h2>发型更换效果</h2>
|
||||
<div id="resultPreview" class="result-preview">
|
||||
<!-- 结果图片将通过JavaScript动态添加 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<button id="reselectHairstyleBtn" class="action-btn">
|
||||
<span class="action-icon">✂️</span>
|
||||
<span>重新选择发型</span>
|
||||
</button>
|
||||
<button id="reuploadImageBtn" class="action-btn">
|
||||
<span class="action-icon">📷</span>
|
||||
<span>重新上传照片</span>
|
||||
</button>
|
||||
<button id="saveResultBtn" class="action-btn">
|
||||
<span class="action-icon">💾</span>
|
||||
<span>保存结果</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 加载指示器 -->
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>处理中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
// API调用封装
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
// 获取发型列表
|
||||
export async function getHairstyles() {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/hairstyles`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取发型列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 更换发型
|
||||
export async function changeHair(imageFile, hairstyleId) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
formData.append('hairstyle_id', hairstyleId);
|
||||
|
||||
const response = await axios.post(`${API_BASE_URL}/change-hair`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('更换发型失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
// API调用函数
|
||||
async function getHairstyles() {
|
||||
try {
|
||||
const response = await axios.get('/api/hairstyles');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取发型列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function changeHair(imageFile, hairstyleId) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
formData.append('hairstyle_id', hairstyleId);
|
||||
|
||||
const response = await axios.post('/api/change-hair', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('更换发型失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局变量
|
||||
let uploadedImage = null;
|
||||
let selectedHairstyle = null;
|
||||
let currentGender = 'male';
|
||||
let hairstylesData = {
|
||||
male: [],
|
||||
female: []
|
||||
};
|
||||
|
||||
// DOM元素
|
||||
const elements = {
|
||||
// 首页元素
|
||||
homePage: document.getElementById('homePage'),
|
||||
imagePreview: document.getElementById('imagePreview'),
|
||||
imageUpload: document.getElementById('imageUpload'),
|
||||
maleBtn: document.getElementById('maleBtn'),
|
||||
femaleBtn: document.getElementById('femaleBtn'),
|
||||
|
||||
// 发型选择页元素
|
||||
hairstylePage: document.getElementById('hairstylePage'),
|
||||
navMaleBtn: document.getElementById('navMaleBtn'),
|
||||
navFemaleBtn: document.getElementById('navFemaleBtn'),
|
||||
hairstyleGrid: document.getElementById('hairstyleGrid'),
|
||||
confirmBtn: document.getElementById('confirmBtn'),
|
||||
|
||||
// 结果预览页元素
|
||||
resultPage: document.getElementById('resultPage'),
|
||||
resultPreview: document.getElementById('resultPreview'),
|
||||
reselectHairstyleBtn: document.getElementById('reselectHairstyleBtn'),
|
||||
reuploadImageBtn: document.getElementById('reuploadImageBtn'),
|
||||
saveResultBtn: document.getElementById('saveResultBtn'),
|
||||
|
||||
// 通用元素
|
||||
backBtn: document.getElementById('backBtn'),
|
||||
loading: document.getElementById('loading')
|
||||
};
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
// 绑定事件监听器
|
||||
bindEventListeners();
|
||||
|
||||
// 加载发型数据
|
||||
loadHairstyles();
|
||||
}
|
||||
|
||||
// 绑定事件监听器
|
||||
function bindEventListeners() {
|
||||
// 图片上传
|
||||
elements.imageUpload.addEventListener('change', handleImageUpload);
|
||||
|
||||
// 性别分类按钮
|
||||
elements.maleBtn.addEventListener('click', () => navigateToHairstylePage('male'));
|
||||
elements.femaleBtn.addEventListener('click', () => navigateToHairstylePage('female'));
|
||||
|
||||
// 导航栏性别切换
|
||||
elements.navMaleBtn.addEventListener('click', () => switchGender('male'));
|
||||
elements.navFemaleBtn.addEventListener('click', () => switchGender('female'));
|
||||
|
||||
// 确认按钮
|
||||
elements.confirmBtn.addEventListener('click', handleConfirmHairstyle);
|
||||
|
||||
// 返回按钮
|
||||
elements.backBtn.addEventListener('click', handleBack);
|
||||
|
||||
// 结果页操作按钮
|
||||
elements.reselectHairstyleBtn.addEventListener('click', handleReselectHairstyle);
|
||||
elements.reuploadImageBtn.addEventListener('click', handleReuploadImage);
|
||||
elements.saveResultBtn.addEventListener('click', handleSaveResult);
|
||||
}
|
||||
|
||||
// 加载发型数据
|
||||
async function loadHairstyles() {
|
||||
try {
|
||||
showLoading();
|
||||
const response = await getHairstyles();
|
||||
if (response.success) {
|
||||
hairstylesData.male = response.data.male;
|
||||
hairstylesData.female = response.data.female;
|
||||
renderHairstyles(currentGender);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('加载发型数据失败,请刷新页面重试');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片上传
|
||||
function handleImageUpload(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const imageUrl = e.target.result;
|
||||
uploadedImage = file;
|
||||
|
||||
// 更新预览
|
||||
elements.imagePreview.innerHTML = `
|
||||
<img src="${imageUrl}" alt="上传的图片">
|
||||
`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// 导航到发型选择页
|
||||
function navigateToHairstylePage(gender) {
|
||||
if (!uploadedImage) {
|
||||
alert('请先上传图片');
|
||||
return;
|
||||
}
|
||||
|
||||
currentGender = gender;
|
||||
switchGender(gender);
|
||||
showPage(elements.hairstylePage);
|
||||
showBackBtn();
|
||||
}
|
||||
|
||||
// 切换性别
|
||||
function switchGender(gender) {
|
||||
currentGender = gender;
|
||||
|
||||
// 更新导航按钮状态
|
||||
elements.navMaleBtn.classList.toggle('active', gender === 'male');
|
||||
elements.navFemaleBtn.classList.toggle('active', gender === 'female');
|
||||
|
||||
// 重新渲染发型列表
|
||||
renderHairstyles(gender);
|
||||
}
|
||||
|
||||
// 渲染发型列表
|
||||
function renderHairstyles(gender) {
|
||||
const hairstyles = hairstylesData[gender];
|
||||
elements.hairstyleGrid.innerHTML = '';
|
||||
|
||||
hairstyles.forEach(hairstyle => {
|
||||
const hairstyleItem = document.createElement('div');
|
||||
hairstyleItem.className = 'hairstyle-item';
|
||||
hairstyleItem.dataset.id = hairstyle.id;
|
||||
|
||||
hairstyleItem.innerHTML = `
|
||||
<img src="${hairstyle.image_url}" alt="${hairstyle.name}">
|
||||
<p>${hairstyle.name}</p>
|
||||
`;
|
||||
|
||||
// 点击选择发型
|
||||
hairstyleItem.addEventListener('click', () => selectHairstyle(hairstyle.id));
|
||||
|
||||
elements.hairstyleGrid.appendChild(hairstyleItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 选择发型
|
||||
function selectHairstyle(hairstyleId) {
|
||||
// 移除之前的选择状态
|
||||
document.querySelectorAll('.hairstyle-item.selected').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
// 添加新的选择状态
|
||||
const selectedItem = document.querySelector(`.hairstyle-item[data-id="${hairstyleId}"]`);
|
||||
if (selectedItem) {
|
||||
selectedItem.classList.add('selected');
|
||||
selectedHairstyle = hairstyleId;
|
||||
elements.confirmBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理确认发型选择
|
||||
async function handleConfirmHairstyle() {
|
||||
if (!uploadedImage || !selectedHairstyle) {
|
||||
alert('请上传图片并选择发型');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
const response = await changeHair(uploadedImage, selectedHairstyle);
|
||||
|
||||
if (response.success) {
|
||||
// 显示结果
|
||||
showResult(response.data.result_image);
|
||||
} else {
|
||||
alert(response.message || '处理失败,请重试');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('处理失败,请重试');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
function showResult(resultImage) {
|
||||
elements.resultPreview.innerHTML = `
|
||||
<img src="${resultImage}" alt="发型更换结果">
|
||||
`;
|
||||
showPage(elements.resultPage);
|
||||
showBackBtn();
|
||||
}
|
||||
|
||||
// 处理重新选择发型
|
||||
function handleReselectHairstyle() {
|
||||
showPage(elements.hairstylePage);
|
||||
}
|
||||
|
||||
// 处理重新上传图片
|
||||
function handleReuploadImage() {
|
||||
// 重置图片上传
|
||||
elements.imageUpload.value = '';
|
||||
uploadedImage = null;
|
||||
elements.imagePreview.innerHTML = `
|
||||
<div class="upload-placeholder">
|
||||
<div class="upload-icon">📷</div>
|
||||
<p>点击上传图片或拍摄照片</p>
|
||||
<input type="file" id="imageUpload" accept="image/*" capture="camera">
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 重新绑定事件
|
||||
document.getElementById('imageUpload').addEventListener('change', handleImageUpload);
|
||||
|
||||
// 重置选择状态
|
||||
selectedHairstyle = null;
|
||||
elements.confirmBtn.disabled = true;
|
||||
document.querySelectorAll('.hairstyle-item.selected').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
showPage(elements.homePage);
|
||||
hideBackBtn();
|
||||
}
|
||||
|
||||
// 处理保存结果
|
||||
function handleSaveResult() {
|
||||
const resultImage = elements.resultPreview.querySelector('img');
|
||||
if (resultImage) {
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a');
|
||||
link.href = resultImage.src;
|
||||
link.download = 'hairstyle-result.jpg';
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理返回按钮
|
||||
function handleBack() {
|
||||
if (elements.resultPage.classList.contains('active')) {
|
||||
showPage(elements.hairstylePage);
|
||||
} else if (elements.hairstylePage.classList.contains('active')) {
|
||||
showPage(elements.homePage);
|
||||
hideBackBtn();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示页面
|
||||
function showPage(page) {
|
||||
// 隐藏所有页面
|
||||
[elements.homePage, elements.hairstylePage, elements.resultPage].forEach(p => {
|
||||
p.classList.remove('active');
|
||||
});
|
||||
|
||||
// 显示目标页面
|
||||
page.classList.add('active');
|
||||
}
|
||||
|
||||
// 显示返回按钮
|
||||
function showBackBtn() {
|
||||
elements.backBtn.style.display = 'block';
|
||||
}
|
||||
|
||||
// 隐藏返回按钮
|
||||
function hideBackBtn() {
|
||||
elements.backBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// 显示加载指示器
|
||||
function showLoading() {
|
||||
elements.loading.style.display = 'flex';
|
||||
}
|
||||
|
||||
// 隐藏加载指示器
|
||||
function hideLoading() {
|
||||
elements.loading.style.display = 'none';
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
init();
|
||||
Reference in New Issue
Block a user