初始化方言版AI对话助手
功能特点: - 支持8种方言(普通话、四川话、粤语、上海话、客家话、闽南话、东北话、河南话) - 用户注册登录系统(用户名、手机、邮箱可选、密码确认) - 对话功能(文字输入、语音识别、文件图片上传) - 多对话管理(新建、切换、删除) - 移动端适配(响应式设计、触摸友好) 技术栈: - Flask后端API - 原生HTML/CSS/JS前端 - Web Speech API语音识别 - JWT用户认证 - 大模型: qwen3.5-4b 端口: 19002
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# 数据文件(保留空文件)
|
||||
data/users.json
|
||||
data/chats.json
|
||||
data/uploads/*
|
||||
!data/uploads/.gitkeep
|
||||
|
||||
# 环境
|
||||
venv/
|
||||
.env
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -0,0 +1,108 @@
|
||||
# 方言版AI对话助手
|
||||
|
||||
> 用家乡话聊天,更亲切!支持多种方言的AI对话助手
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 🗣️ 多方言支持
|
||||
- 普通话
|
||||
- 四川话
|
||||
- 粤语
|
||||
- 上海话
|
||||
- 客家话
|
||||
- 闽南话
|
||||
- 东北话
|
||||
- 河南话
|
||||
|
||||
### 💬 对话功能
|
||||
- 文字输入
|
||||
- **语音识别输入**(支持浏览器语音识别)
|
||||
- 文件上传
|
||||
- 图片上传
|
||||
- 多对话管理
|
||||
- 对话历史记录
|
||||
|
||||
### 👤 用户系统
|
||||
- 用户注册(用户名、手机、邮箱可选、密码确认)
|
||||
- 用户登录
|
||||
- Token认证
|
||||
|
||||
### 📱 移动端适配
|
||||
- 响应式设计
|
||||
- 触摸友好
|
||||
- 适配手机浏览器
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 访问地址
|
||||
|
||||
```
|
||||
http://localhost:19002
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
dialect-chat/
|
||||
├── backend/
|
||||
│ └── app.py # Flask后端API
|
||||
├── frontend/
|
||||
│ ├── index.html # 登录/注册页
|
||||
│ └── chat.html # 对话界面
|
||||
├── data/
|
||||
│ ├── users.json # 用户数据
|
||||
│ ├── chats.json # 对话数据
|
||||
│ └── uploads/ # 上传文件
|
||||
├── requirements.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| /api/dialects | GET | 获取方言列表 |
|
||||
| /api/register | POST | 用户注册 |
|
||||
| /api/login | POST | 用户登录 |
|
||||
| /api/user | GET | 获取用户信息 |
|
||||
| /api/chats | GET | 获取对话列表 |
|
||||
| /api/chats | POST | 创建新对话 |
|
||||
| /api/chats/:id | GET | 获取对话详情 |
|
||||
| /api/chats/:id | DELETE | 删除对话 |
|
||||
| /api/chats/:id/send | POST | 发送消息 |
|
||||
| /api/upload | POST | 上传文件 |
|
||||
|
||||
## 大模型配置
|
||||
|
||||
```
|
||||
API地址: http://192.168.2.5:1234/v1
|
||||
模型: qwen3.5-4b
|
||||
```
|
||||
|
||||
## 版本历史
|
||||
|
||||
### v0.1.0 (2026-04-08)
|
||||
- 初始版本
|
||||
- 支持8种方言
|
||||
- 用户注册登录
|
||||
- 对话功能
|
||||
- 语音识别输入
|
||||
- 文件图片上传
|
||||
- 移动端适配
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
"""
|
||||
方言版AI对话助手 - 后端API
|
||||
"""
|
||||
|
||||
from flask import Flask, request, jsonify, send_from_directory, send_file
|
||||
from flask_cors import CORS
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import jwt
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
import base64
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
app = Flask(__name__, static_folder='../frontend', static_url_path='')
|
||||
CORS(app)
|
||||
|
||||
# 配置
|
||||
SECRET_KEY = 'dialect-chat-secret-key-2026'
|
||||
LLM_BASE_URL = 'http://192.168.2.5:1234/v1'
|
||||
LLM_API_KEY = 'sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j'
|
||||
LLM_MODEL = 'qwen3.5-4b'
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = Path(__file__).parent.parent / 'data'
|
||||
USERS_FILE = DATA_DIR / 'users.json'
|
||||
CHATS_FILE = DATA_DIR / 'chats.json'
|
||||
UPLOAD_DIR = DATA_DIR / 'uploads'
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 方言配置
|
||||
DIALECTS = {
|
||||
'mandarin': {
|
||||
'name': '普通话',
|
||||
'prompt': '请用标准的普通话回复。',
|
||||
'greeting': '你好!有什么我可以帮助你的吗?'
|
||||
},
|
||||
'sichuan': {
|
||||
'name': '四川话',
|
||||
'prompt': '请用四川话回复,使用四川方言的特色表达,比如"巴适"、"要得"、"撒"等。',
|
||||
'greeting': '你好哇!有啥子事要得嘛?'
|
||||
},
|
||||
'cantonese': {
|
||||
'name': '粤语',
|
||||
'prompt': '请用粤语回复,使用粤语的特色表达,比如"嘅"、"系"、"唔"、"咁"等。',
|
||||
'greeting': '你好!有乜嘢可以帮你?'
|
||||
},
|
||||
'shanghai': {
|
||||
'name': '上海话',
|
||||
'prompt': '请用上海话回复,使用上海方言的特色表达。',
|
||||
'greeting': '侬好!有啥事体伐?'
|
||||
},
|
||||
'hakka': {
|
||||
'name': '客家话',
|
||||
'prompt': '请用客家话回复,使用客家方言的特色表达。',
|
||||
'greeting': '你好!有么个可以帮你?'
|
||||
},
|
||||
'minnan': {
|
||||
'name': '闽南话',
|
||||
'prompt': '请用闽南话回复,使用闽南方言的特色表达。',
|
||||
'greeting': '你好!有啥米代志?'
|
||||
},
|
||||
'northeast': {
|
||||
'name': '东北话',
|
||||
'prompt': '请用东北话回复,使用东北方言的特色表达,比如"咋整"、"嘎哈"、"整挺好"等。',
|
||||
'greeting': '哎呀妈呀,来了!有啥事儿啊?'
|
||||
},
|
||||
'henan': {
|
||||
'name': '河南话',
|
||||
'prompt': '请用河南话回复,使用河南方言的特色表达,比如"中"、"弄啥嘞"、"可得劲"等。',
|
||||
'greeting': '中!你有啥事儿说呗!'
|
||||
}
|
||||
}
|
||||
|
||||
# 初始化数据文件
|
||||
def init_data():
|
||||
if not USERS_FILE.exists():
|
||||
USERS_FILE.write_text(json.dumps({}, ensure_ascii=False))
|
||||
if not CHATS_FILE.exists():
|
||||
CHATS_FILE.write_text(json.dumps({}, ensure_ascii=False))
|
||||
|
||||
init_data()
|
||||
|
||||
# 辅助函数
|
||||
def load_users():
|
||||
return json.loads(USERS_FILE.read_text(encoding='utf-8'))
|
||||
|
||||
def save_users(users):
|
||||
USERS_FILE.write_text(json.dumps(users, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
|
||||
def load_chats():
|
||||
return json.loads(CHATS_FILE.read_text(encoding='utf-8'))
|
||||
|
||||
def save_chats(chats):
|
||||
CHATS_FILE.write_text(json.dumps(chats, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
|
||||
def generate_token(user_id):
|
||||
return jwt.encode({
|
||||
'user_id': user_id,
|
||||
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7)
|
||||
}, SECRET_KEY, algorithm='HS256')
|
||||
|
||||
def verify_token(token):
|
||||
try:
|
||||
return jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_current_user():
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
if not token:
|
||||
return None
|
||||
data = verify_token(token)
|
||||
if not data:
|
||||
return None
|
||||
users = load_users()
|
||||
return users.get(data['user_id'])
|
||||
|
||||
def call_llm(messages, dialect='mandarin'):
|
||||
"""调用大模型API"""
|
||||
dialect_info = DIALECTS.get(dialect, DIALECTS['mandarin'])
|
||||
|
||||
# 添加系统提示
|
||||
system_message = {
|
||||
'role': 'system',
|
||||
'content': f'你是一个友好的AI助手。{dialect_info["prompt"]}'
|
||||
}
|
||||
|
||||
full_messages = [system_message] + messages[-20:] # 保留最近20条消息
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{LLM_BASE_URL}/chat/completions',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {LLM_API_KEY}'
|
||||
},
|
||||
json={
|
||||
'model': LLM_MODEL,
|
||||
'messages': full_messages,
|
||||
'max_tokens': 1024,
|
||||
'temperature': 0.8
|
||||
},
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result['choices'][0]['message']['content']
|
||||
else:
|
||||
return f'抱歉,我遇到了一些问题,请稍后再试。'
|
||||
except Exception as e:
|
||||
return f'连接出现问题了,请稍后再试试。'
|
||||
|
||||
# ============ 页面路由 ============
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return send_file('../frontend/index.html')
|
||||
|
||||
@app.route('/chat')
|
||||
def chat_page():
|
||||
return send_file('../frontend/chat.html')
|
||||
|
||||
# ============ API路由 ============
|
||||
|
||||
@app.route('/api/dialects')
|
||||
def api_dialects():
|
||||
"""获取支持的方言列表"""
|
||||
return jsonify({k: {'name': v['name'], 'greeting': v['greeting']} for k, v in DIALECTS.items()})
|
||||
|
||||
@app.route('/api/register', methods=['POST'])
|
||||
def api_register():
|
||||
"""用户注册"""
|
||||
data = request.json
|
||||
|
||||
username = data.get('username', '').strip()
|
||||
password = data.get('password', '')
|
||||
confirm_password = data.get('confirm_password', '')
|
||||
phone = data.get('phone', '').strip()
|
||||
email = data.get('email', '').strip()
|
||||
|
||||
# 验证
|
||||
if not username or len(username) < 2:
|
||||
return jsonify({'error': '用户名至少2个字符'}), 400
|
||||
if not password or len(password) < 6:
|
||||
return jsonify({'error': '密码至少6个字符'}), 400
|
||||
if password != confirm_password:
|
||||
return jsonify({'error': '两次密码不一致'}), 400
|
||||
if not phone or len(phone) < 11:
|
||||
return jsonify({'error': '请输入正确的手机号'}), 400
|
||||
|
||||
users = load_users()
|
||||
|
||||
# 检查用户名是否存在
|
||||
for uid, user in users.items():
|
||||
if user['username'] == username:
|
||||
return jsonify({'error': '用户名已存在'}), 400
|
||||
if user['phone'] == phone:
|
||||
return jsonify({'error': '手机号已注册'}), 400
|
||||
|
||||
# 创建用户
|
||||
user_id = str(uuid.uuid4())
|
||||
users[user_id] = {
|
||||
'id': user_id,
|
||||
'username': username,
|
||||
'password': generate_password_hash(password),
|
||||
'phone': phone,
|
||||
'email': email,
|
||||
'created_at': datetime.datetime.now().isoformat(),
|
||||
'chats': []
|
||||
}
|
||||
|
||||
save_users(users)
|
||||
|
||||
token = generate_token(user_id)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'token': token,
|
||||
'user': {
|
||||
'id': user_id,
|
||||
'username': username,
|
||||
'phone': phone,
|
||||
'email': email
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def api_login():
|
||||
"""用户登录"""
|
||||
data = request.json
|
||||
|
||||
username = data.get('username', '').strip()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({'error': '请输入用户名和密码'}), 400
|
||||
|
||||
users = load_users()
|
||||
|
||||
for user_id, user in users.items():
|
||||
if user['username'] == username:
|
||||
if check_password_hash(user['password'], password):
|
||||
token = generate_token(user_id)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'token': token,
|
||||
'user': {
|
||||
'id': user_id,
|
||||
'username': user['username'],
|
||||
'phone': user['phone'],
|
||||
'email': user.get('email', '')
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': '密码错误'}), 400
|
||||
|
||||
return jsonify({'error': '用户不存在'}), 400
|
||||
|
||||
@app.route('/api/user')
|
||||
def api_user():
|
||||
"""获取当前用户信息"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'error': '未登录'}), 401
|
||||
|
||||
return jsonify({
|
||||
'id': user['id'],
|
||||
'username': user['username'],
|
||||
'phone': user['phone'],
|
||||
'email': user.get('email', '')
|
||||
})
|
||||
|
||||
@app.route('/api/chats', methods=['GET'])
|
||||
def api_get_chats():
|
||||
"""获取用户的对话列表"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'error': '未登录'}), 401
|
||||
|
||||
chats = load_chats()
|
||||
user_chats = []
|
||||
|
||||
for chat_id in user.get('chats', []):
|
||||
if chat_id in chats:
|
||||
chat = chats[chat_id]
|
||||
user_chats.append({
|
||||
'id': chat_id,
|
||||
'title': chat['title'],
|
||||
'dialect': chat['dialect'],
|
||||
'created_at': chat['created_at'],
|
||||
'updated_at': chat.get('updated_at', chat['created_at'])
|
||||
})
|
||||
|
||||
# 按更新时间倒序
|
||||
user_chats.sort(key=lambda x: x['updated_at'], reverse=True)
|
||||
|
||||
return jsonify(user_chats)
|
||||
|
||||
@app.route('/api/chats', methods=['POST'])
|
||||
def api_create_chat():
|
||||
"""创建新对话"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'error': '未登录'}), 401
|
||||
|
||||
data = request.json
|
||||
dialect = data.get('dialect', 'mandarin')
|
||||
|
||||
chat_id = str(uuid.uuid4())
|
||||
|
||||
chats = load_chats()
|
||||
chats[chat_id] = {
|
||||
'id': chat_id,
|
||||
'user_id': user['id'],
|
||||
'title': '新对话',
|
||||
'dialect': dialect,
|
||||
'messages': [],
|
||||
'created_at': datetime.datetime.now().isoformat(),
|
||||
'updated_at': datetime.datetime.now().isoformat()
|
||||
}
|
||||
save_chats(chats)
|
||||
|
||||
# 更新用户的对话列表
|
||||
users = load_users()
|
||||
if chat_id not in users[user['id']]['chats']:
|
||||
users[user['id']]['chats'].append(chat_id)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'id': chat_id,
|
||||
'title': '新对话',
|
||||
'dialect': dialect
|
||||
})
|
||||
|
||||
@app.route('/api/chats/<chat_id>', methods=['GET'])
|
||||
def api_get_chat(chat_id):
|
||||
"""获取对话详情"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'error': '未登录'}), 401
|
||||
|
||||
chats = load_chats()
|
||||
chat = chats.get(chat_id)
|
||||
|
||||
if not chat or chat['user_id'] != user['id']:
|
||||
return jsonify({'error': '对话不存在'}), 404
|
||||
|
||||
return jsonify(chat)
|
||||
|
||||
@app.route('/api/chats/<chat_id>', methods=['DELETE'])
|
||||
def api_delete_chat(chat_id):
|
||||
"""删除对话"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'error': '未登录'}), 401
|
||||
|
||||
chats = load_chats()
|
||||
chat = chats.get(chat_id)
|
||||
|
||||
if not chat or chat['user_id'] != user['id']:
|
||||
return jsonify({'error': '对话不存在'}), 404
|
||||
|
||||
del chats[chat_id]
|
||||
save_chats(chats)
|
||||
|
||||
# 更新用户的对话列表
|
||||
users = load_users()
|
||||
if chat_id in users[user['id']]['chats']:
|
||||
users[user['id']]['chats'].remove(chat_id)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/api/chats/<chat_id>/send', methods=['POST'])
|
||||
def api_send_message(chat_id):
|
||||
"""发送消息"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'error': '未登录'}), 401
|
||||
|
||||
data = request.json
|
||||
content = data.get('content', '').strip()
|
||||
|
||||
if not content:
|
||||
return jsonify({'error': '消息不能为空'}), 400
|
||||
|
||||
chats = load_chats()
|
||||
chat = chats.get(chat_id)
|
||||
|
||||
if not chat or chat['user_id'] != user['id']:
|
||||
return jsonify({'error': '对话不存在'}), 404
|
||||
|
||||
# 添加用户消息
|
||||
user_message = {
|
||||
'role': 'user',
|
||||
'content': content,
|
||||
'time': datetime.datetime.now().isoformat()
|
||||
}
|
||||
chat['messages'].append(user_message)
|
||||
|
||||
# 调用大模型
|
||||
messages = [{'role': m['role'], 'content': m['content']} for m in chat['messages']]
|
||||
ai_response = call_llm(messages, chat['dialect'])
|
||||
|
||||
# 添加AI回复
|
||||
ai_message = {
|
||||
'role': 'assistant',
|
||||
'content': ai_response,
|
||||
'time': datetime.datetime.now().isoformat()
|
||||
}
|
||||
chat['messages'].append(ai_message)
|
||||
|
||||
# 更新对话标题(第一条消息的前20字)
|
||||
if len(chat['messages']) <= 2:
|
||||
chat['title'] = content[:20] + ('...' if len(content) > 20 else '')
|
||||
|
||||
chat['updated_at'] = datetime.datetime.now().isoformat()
|
||||
save_chats(chats)
|
||||
|
||||
return jsonify({
|
||||
'user_message': user_message,
|
||||
'ai_message': ai_message
|
||||
})
|
||||
|
||||
@app.route('/api/upload', methods=['POST'])
|
||||
def api_upload():
|
||||
"""上传文件"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({'error': '未登录'}), 401
|
||||
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': '没有上传文件'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({'error': '没有选择文件'}), 400
|
||||
|
||||
# 保存文件
|
||||
file_id = str(uuid.uuid4())
|
||||
filename = f"{file_id}_{file.filename}"
|
||||
filepath = UPLOAD_DIR / filename
|
||||
file.save(filepath)
|
||||
|
||||
# 如果是图片,返回base64预览
|
||||
is_image = file.content_type.startswith('image/')
|
||||
preview = None
|
||||
if is_image:
|
||||
preview = f"/api/uploads/{filename}"
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'file_id': file_id,
|
||||
'filename': file.filename,
|
||||
'is_image': is_image,
|
||||
'preview': preview
|
||||
})
|
||||
|
||||
@app.route('/api/uploads/<filename>')
|
||||
def api_get_upload(filename):
|
||||
"""获取上传的文件"""
|
||||
return send_file(UPLOAD_DIR / filename)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 50)
|
||||
print("方言版AI对话助手")
|
||||
print("=" * 50)
|
||||
print(f"访问地址: http://localhost:19002")
|
||||
print("=" * 50)
|
||||
|
||||
app.run(host='0.0.0.0', port=19002, debug=True)
|
||||
@@ -0,0 +1 @@
|
||||
# 占位文件
|
||||
@@ -0,0 +1,531 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>方言AI助手</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<style>
|
||||
* { -webkit-tap-highlight-color: transparent; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||
.gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||
.chat-bubble { max-width: 85%; }
|
||||
.chat-bubble-user { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||
.chat-bubble-ai { background: #f3f4f6; }
|
||||
.sidebar { transform: translateX(-100%); transition: transform 0.3s; }
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.dialect-select { max-height: 50vh; overflow-y: auto; }
|
||||
.message-content img { max-width: 100%; border-radius: 8px; margin: 8px 0; }
|
||||
.recording { animation: pulse 1s infinite; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||
.typing-indicator span { animation: blink 1.4s infinite both; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes blink { 0%, 80%, 100% { opacity: 0; } 40% { opacity: 1; } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 h-screen overflow-hidden">
|
||||
<!-- 侧边栏遮罩 -->
|
||||
<div id="sidebarOverlay" class="fixed inset-0 bg-black/50 z-40 hidden" onclick="closeSidebar()"></div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<aside id="sidebar" class="sidebar fixed left-0 top-0 h-full w-72 bg-white z-50 shadow-xl flex flex-col">
|
||||
<div class="gradient-bg text-white p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="font-bold text-lg">🗣️ 方言AI助手</h2>
|
||||
<button onclick="closeSidebar()" class="p-1"><i class="ri-close-line text-xl"></i></button>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-white/80" id="userInfo">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建对话按钮 -->
|
||||
<div class="p-3 border-b">
|
||||
<button onclick="createNewChat()" class="w-full py-2.5 border-2 border-dashed border-purple-300 text-purple-600 rounded-lg hover:bg-purple-50 flex items-center justify-center gap-2">
|
||||
<i class="ri-add-line"></i> 新建对话
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 对话列表 -->
|
||||
<div class="flex-1 overflow-y-auto" id="chatList">
|
||||
<div class="p-4 text-center text-gray-400 text-sm">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="p-3 border-t space-y-2">
|
||||
<button onclick="showDialectModal()" class="w-full py-2 bg-gray-100 rounded-lg text-gray-700 flex items-center justify-center gap-2">
|
||||
<i class="ri-translate-2"></i> 切换方言
|
||||
</button>
|
||||
<button onclick="logout()" class="w-full py-2 bg-red-50 text-red-500 rounded-lg flex items-center justify-center gap-2">
|
||||
<i class="ri-logout-box-line"></i> 退出登录
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- 顶部栏 -->
|
||||
<header class="gradient-bg text-white p-4 flex items-center gap-3 shadow-lg">
|
||||
<button onclick="openSidebar()" class="p-1"><i class="ri-menu-line text-xl"></i></button>
|
||||
<div class="flex-1">
|
||||
<h1 class="font-medium" id="chatTitle">新对话</h1>
|
||||
<p class="text-xs text-white/70" id="currentDialect">普通话</p>
|
||||
</div>
|
||||
<button onclick="deleteCurrentChat()" class="p-1 text-white/70 hover:text-white">
|
||||
<i class="ri-delete-bin-line text-xl"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- 消息区域 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4" id="messagesContainer">
|
||||
<!-- 欢迎消息 -->
|
||||
<div class="text-center py-8">
|
||||
<div class="text-6xl mb-4">🗣️</div>
|
||||
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
|
||||
<p class="text-gray-500 mt-2">用家乡话聊天,更亲切</p>
|
||||
<div class="mt-4 text-sm text-gray-400">
|
||||
当前方言: <span id="welcomeDialect">普通话</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="bg-white border-t p-3">
|
||||
<!-- 图片预览 -->
|
||||
<div id="imagePreview" class="hidden mb-2 relative inline-block">
|
||||
<img src="" class="h-20 rounded-lg">
|
||||
<button onclick="clearImagePreview()" class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs">
|
||||
<i class="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end gap-2">
|
||||
<!-- 工具按钮 -->
|
||||
<div class="flex gap-1">
|
||||
<label class="p-2 text-gray-400 hover:text-purple-500 cursor-pointer">
|
||||
<i class="ri-image-line text-xl"></i>
|
||||
<input type="file" accept="image/*" class="hidden" onchange="handleImageUpload(event)">
|
||||
</label>
|
||||
<label class="p-2 text-gray-400 hover:text-purple-500 cursor-pointer">
|
||||
<i class="ri-attachment-line text-xl"></i>
|
||||
<input type="file" class="hidden" onchange="handleFileUpload(event)">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="flex-1 relative">
|
||||
<textarea id="messageInput" placeholder="输入消息..."
|
||||
class="w-full px-4 py-2.5 border border-gray-200 rounded-2xl resize-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
rows="1" style="max-height: 120px;"
|
||||
onkeydown="handleKeyDown(event)"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 发送/语音按钮 -->
|
||||
<button id="voiceBtn" onclick="toggleVoice()" class="p-2.5 text-gray-400 hover:text-purple-500">
|
||||
<i class="ri-mic-line text-xl"></i>
|
||||
</button>
|
||||
<button onclick="sendMessage()" class="p-2.5 gradient-bg text-white rounded-full">
|
||||
<i class="ri-send-plane-fill text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 方言选择弹窗 -->
|
||||
<div id="dialectModal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
|
||||
<div class="bg-white rounded-2xl w-full max-w-sm">
|
||||
<div class="p-4 border-b flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg">选择方言</h3>
|
||||
<button onclick="closeDialectModal()" class="text-gray-400"><i class="ri-close-line text-xl"></i></button>
|
||||
</div>
|
||||
<div class="dialect-select p-2" id="dialectList">
|
||||
<!-- 方言列表 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = '';
|
||||
let token = localStorage.getItem('token');
|
||||
let user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
let currentChatId = null;
|
||||
let dialects = {};
|
||||
let currentDialect = 'mandarin';
|
||||
let isRecording = false;
|
||||
let recognition = null;
|
||||
let uploadedImage = null;
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!token) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
loadDialects();
|
||||
loadChatList();
|
||||
initVoiceRecognition();
|
||||
});
|
||||
|
||||
// 加载方言列表
|
||||
async function loadDialects() {
|
||||
try {
|
||||
const res = await fetch('/api/dialects');
|
||||
dialects = await res.json();
|
||||
renderDialectList();
|
||||
} catch (e) {
|
||||
console.error('加载方言失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDialectList() {
|
||||
const container = document.getElementById('dialectList');
|
||||
container.innerHTML = Object.entries(dialects).map(([key, val]) => `
|
||||
<button onclick="selectDialect('${key}')"
|
||||
class="w-full p-3 text-left rounded-lg hover:bg-purple-50 flex items-center justify-between ${currentDialect === key ? 'bg-purple-50 text-purple-600' : ''}">
|
||||
<span>${val.name}</span>
|
||||
${currentDialect === key ? '<i class="ri-check-line"></i>' : ''}
|
||||
</button>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 加载对话列表
|
||||
async function loadChatList() {
|
||||
try {
|
||||
const res = await fetch('/api/chats', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const chats = await res.json();
|
||||
renderChatList(chats);
|
||||
|
||||
document.getElementById('userInfo').textContent = `欢迎, ${user.username || '用户'}`;
|
||||
|
||||
// 如果有对话,加载第一个
|
||||
if (chats.length > 0) {
|
||||
loadChat(chats[0].id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载对话列表失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderChatList(chats) {
|
||||
const container = document.getElementById('chatList');
|
||||
if (chats.length === 0) {
|
||||
container.innerHTML = '<div class="p-4 text-center text-gray-400 text-sm">暂无对话</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = chats.map(chat => `
|
||||
<div onclick="loadChat('${chat.id}')"
|
||||
class="p-3 border-b hover:bg-gray-50 cursor-pointer ${currentChatId === chat.id ? 'bg-purple-50' : ''}">
|
||||
<div class="font-medium text-gray-800 truncate">${chat.title}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">${dialects[chat.dialect]?.name || '普通话'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 创建新对话
|
||||
async function createNewChat() {
|
||||
try {
|
||||
const res = await fetch('/api/chats', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ dialect: currentDialect })
|
||||
});
|
||||
const chat = await res.json();
|
||||
|
||||
currentChatId = chat.id;
|
||||
document.getElementById('chatTitle').textContent = chat.title;
|
||||
document.getElementById('messagesContainer').innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-6xl mb-4">🗣️</div>
|
||||
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
|
||||
<p class="text-gray-500 mt-2">${dialects[currentDialect]?.greeting || '你好!有什么我可以帮助你的吗?'}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
loadChatList();
|
||||
closeSidebar();
|
||||
} catch (e) {
|
||||
alert('创建对话失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载对话
|
||||
async function loadChat(chatId) {
|
||||
try {
|
||||
const res = await fetch(`/api/chats/${chatId}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const chat = await res.json();
|
||||
|
||||
currentChatId = chatId;
|
||||
currentDialect = chat.dialect;
|
||||
|
||||
document.getElementById('chatTitle').textContent = chat.title;
|
||||
document.getElementById('currentDialect').textContent = dialects[currentDialect]?.name || '普通话';
|
||||
document.getElementById('welcomeDialect').textContent = dialects[currentDialect]?.name || '普通话';
|
||||
|
||||
renderMessages(chat.messages);
|
||||
closeSidebar();
|
||||
loadChatList();
|
||||
} catch (e) {
|
||||
console.error('加载对话失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessages(messages) {
|
||||
const container = document.getElementById('messagesContainer');
|
||||
|
||||
if (messages.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-6xl mb-4">🗣️</div>
|
||||
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
|
||||
<p class="text-gray-500 mt-2">${dialects[currentDialect]?.greeting || '你好!'}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = messages.map(msg => `
|
||||
<div class="flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}">
|
||||
<div class="chat-bubble ${msg.role === 'user' ? 'chat-bubble-user text-white' : 'chat-bubble-ai text-gray-800'} p-3 rounded-2xl">
|
||||
<div class="message-content text-sm whitespace-pre-wrap">${msg.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('messageInput');
|
||||
let content = input.value.trim();
|
||||
|
||||
if (!currentChatId) {
|
||||
await createNewChat();
|
||||
}
|
||||
|
||||
if (uploadedImage) {
|
||||
content = uploadedImage + '\n' + content;
|
||||
uploadedImage = null;
|
||||
document.getElementById('imagePreview').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (!content) return;
|
||||
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
|
||||
// 显示用户消息
|
||||
addMessage('user', content);
|
||||
|
||||
// 显示加载动画
|
||||
const loadingId = showLoading();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/chats/${currentChatId}/send`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
removeLoading(loadingId);
|
||||
|
||||
if (data.ai_message) {
|
||||
addMessage('assistant', data.ai_message.content);
|
||||
document.getElementById('chatTitle').textContent = data.ai_message.content.substring(0, 20) + '...';
|
||||
}
|
||||
} catch (e) {
|
||||
removeLoading(loadingId);
|
||||
addMessage('assistant', '抱歉,网络出现问题了,请稍后再试。');
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(role, content) {
|
||||
const container = document.getElementById('messagesContainer');
|
||||
const div = document.createElement('div');
|
||||
div.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
|
||||
div.innerHTML = `
|
||||
<div class="chat-bubble ${role === 'user' ? 'chat-bubble-user text-white' : 'chat-bubble-ai text-gray-800'} p-3 rounded-2xl">
|
||||
<div class="message-content text-sm whitespace-pre-wrap">${content}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
const container = document.getElementById('messagesContainer');
|
||||
const id = 'loading-' + Date.now();
|
||||
const div = document.createElement('div');
|
||||
div.id = id;
|
||||
div.className = 'flex justify-start';
|
||||
div.innerHTML = `
|
||||
<div class="chat-bubble chat-bubble-ai p-3 rounded-2xl">
|
||||
<div class="typing-indicator flex gap-1">
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
return id;
|
||||
}
|
||||
|
||||
function removeLoading(id) {
|
||||
document.getElementById(id)?.remove();
|
||||
}
|
||||
|
||||
// 语音识别
|
||||
function initVoiceRecognition() {
|
||||
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = 'zh-CN';
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
document.getElementById('messageInput').value = transcript;
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
isRecording = false;
|
||||
document.getElementById('voiceBtn').innerHTML = '<i class="ri-mic-line text-xl"></i>';
|
||||
document.getElementById('voiceBtn').classList.remove('text-red-500', 'recording');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVoice() {
|
||||
if (!recognition) {
|
||||
alert('您的浏览器不支持语音识别');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
recognition.stop();
|
||||
} else {
|
||||
recognition.start();
|
||||
isRecording = true;
|
||||
document.getElementById('voiceBtn').innerHTML = '<i class="ri-mic-fill text-xl"></i>';
|
||||
document.getElementById('voiceBtn').classList.add('text-red-500', 'recording');
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
function handleImageUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
uploadedImage = `[图片: ${file.name}]`;
|
||||
const preview = document.getElementById('imagePreview');
|
||||
preview.querySelector('img').src = e.target.result;
|
||||
preview.classList.remove('hidden');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function clearImagePreview() {
|
||||
uploadedImage = null;
|
||||
document.getElementById('imagePreview').classList.add('hidden');
|
||||
}
|
||||
|
||||
function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
document.getElementById('messageInput').value += `\n[文件: ${file.name}]`;
|
||||
}
|
||||
|
||||
// 方言选择
|
||||
function showDialectModal() {
|
||||
renderDialectList();
|
||||
document.getElementById('dialectModal').classList.remove('hidden');
|
||||
document.getElementById('dialectModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeDialectModal() {
|
||||
document.getElementById('dialectModal').classList.add('hidden');
|
||||
document.getElementById('dialectModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
function selectDialect(key) {
|
||||
currentDialect = key;
|
||||
document.getElementById('currentDialect').textContent = dialects[key]?.name || '普通话';
|
||||
document.getElementById('welcomeDialect').textContent = dialects[key]?.name || '普通话';
|
||||
closeDialectModal();
|
||||
}
|
||||
|
||||
// 侧边栏
|
||||
function openSidebar() {
|
||||
document.getElementById('sidebar').classList.add('open');
|
||||
document.getElementById('sidebarOverlay').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
document.getElementById('sidebar').classList.remove('open');
|
||||
document.getElementById('sidebarOverlay').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 删除对话
|
||||
async function deleteCurrentChat() {
|
||||
if (!currentChatId || !confirm('确定删除这个对话吗?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/chats/${currentChatId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
currentChatId = null;
|
||||
loadChatList();
|
||||
document.getElementById('messagesContainer').innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-6xl mb-4">🗣️</div>
|
||||
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
|
||||
<p class="text-gray-500 mt-2">点击"新建对话"开始聊天</p>
|
||||
</div>
|
||||
`;
|
||||
} catch (e) {
|
||||
alert('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// 输入框事件
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 自动调整输入框高度
|
||||
document.getElementById('messageInput').addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>方言AI助手 - 登录</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<style>
|
||||
* { -webkit-tap-highlight-color: transparent; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||
.gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen">
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<!-- 头部 -->
|
||||
<div class="gradient-bg text-white p-6 pb-12">
|
||||
<h1 class="text-2xl font-bold text-center">🗣️ 方言AI助手</h1>
|
||||
<p class="text-center text-white/80 mt-2 text-sm">用家乡话聊天,更亲切</p>
|
||||
</div>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<div class="flex-1 -mt-6">
|
||||
<div class="bg-white rounded-t-3xl shadow-lg p-6 max-w-md mx-auto">
|
||||
<!-- Tab切换 -->
|
||||
<div class="flex mb-6 bg-gray-100 rounded-lg p-1">
|
||||
<button id="loginTab" onclick="showTab('login')"
|
||||
class="flex-1 py-2 rounded-lg bg-white shadow text-gray-800 font-medium transition-all">
|
||||
登录
|
||||
</button>
|
||||
<button id="registerTab" onclick="showTab('register')"
|
||||
class="flex-1 py-2 rounded-lg text-gray-500 transition-all">
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<form id="loginForm" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">用户名</label>
|
||||
<div class="relative">
|
||||
<i class="ri-user-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="text" id="loginUsername" placeholder="请输入用户名"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">密码</label>
|
||||
<div class="relative">
|
||||
<i class="ri-lock-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="password" id="loginPassword" placeholder="请输入密码"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full py-3 gradient-bg text-white rounded-lg font-medium hover:opacity-90 transition-opacity">
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<form id="registerForm" class="space-y-4 hidden">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">用户名 <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<i class="ri-user-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="text" id="regUsername" placeholder="至少2个字符"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">手机号 <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<i class="ri-phone-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="tel" id="regPhone" placeholder="请输入手机号"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">邮箱 <span class="text-gray-400">(可选)</span></label>
|
||||
<div class="relative">
|
||||
<i class="ri-mail-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="email" id="regEmail" placeholder="选填"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">密码 <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<i class="ri-lock-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="password" id="regPassword" placeholder="至少6个字符"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">确认密码 <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<i class="ri-lock-2-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="password" id="regConfirmPassword" placeholder="再次输入密码"
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full py-3 gradient-bg text-white rounded-lg font-medium hover:opacity-90">
|
||||
注册
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div id="errorMsg" class="mt-4 p-3 bg-red-50 text-red-500 rounded-lg text-sm hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="text-center p-4 text-gray-400 text-sm">
|
||||
方言AI助手 © 2026
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = '';
|
||||
let currentTab = 'login';
|
||||
|
||||
function showTab(tab) {
|
||||
currentTab = tab;
|
||||
document.getElementById('loginForm').classList.toggle('hidden', tab !== 'login');
|
||||
document.getElementById('registerForm').classList.toggle('hidden', tab !== 'register');
|
||||
document.getElementById('loginTab').classList.toggle('bg-white', tab === 'login');
|
||||
document.getElementById('loginTab').classList.toggle('shadow', tab === 'login');
|
||||
document.getElementById('loginTab').classList.toggle('text-gray-800', tab === 'login');
|
||||
document.getElementById('loginTab').classList.toggle('text-gray-500', tab !== 'login');
|
||||
document.getElementById('registerTab').classList.toggle('bg-white', tab === 'register');
|
||||
document.getElementById('registerTab').classList.toggle('shadow', tab === 'register');
|
||||
document.getElementById('registerTab').classList.toggle('text-gray-800', tab === 'register');
|
||||
document.getElementById('registerTab').classList.toggle('text-gray-500', tab !== 'register');
|
||||
document.getElementById('errorMsg').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const el = document.getElementById('errorMsg');
|
||||
el.textContent = msg;
|
||||
el.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 登录
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('loginUsername').value.trim();
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showError('请输入用户名和密码');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
window.location.href = '/chat';
|
||||
} else {
|
||||
showError(data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
showError('网络错误,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
// 注册
|
||||
document.getElementById('registerForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('regUsername').value.trim();
|
||||
const phone = document.getElementById('regPhone').value.trim();
|
||||
const email = document.getElementById('regEmail').value.trim();
|
||||
const password = document.getElementById('regPassword').value;
|
||||
const confirm_password = document.getElementById('regConfirmPassword').value;
|
||||
|
||||
if (!username || !phone || !password || !confirm_password) {
|
||||
showError('请填写必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, phone, email, password, confirm_password })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
window.location.href = '/chat';
|
||||
} else {
|
||||
showError(data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
showError('网络错误,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否已登录
|
||||
if (localStorage.getItem('token')) {
|
||||
window.location.href = '/chat';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
flask>=2.3.0
|
||||
flask-cors>=4.0.0
|
||||
requests>=2.28.0
|
||||
pyjwt>=2.8.0
|
||||
werkzeug>=2.3.0
|
||||
Reference in New Issue
Block a user