初始化方言版AI对话助手
功能特点: - 支持8种方言(普通话、四川话、粤语、上海话、客家话、闽南话、东北话、河南话) - 用户注册登录系统(用户名、手机、邮箱可选、密码确认) - 对话功能(文字输入、语音识别、文件图片上传) - 多对话管理(新建、切换、删除) - 移动端适配(响应式设计、触摸友好) 技术栈: - Flask后端API - 原生HTML/CSS/JS前端 - Web Speech API语音识别 - JWT用户认证 - 大模型: qwen3.5-4b 端口: 19002
This commit is contained in:
531
frontend/chat.html
Normal file
531
frontend/chat.html
Normal file
@@ -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>
|
||||
217
frontend/index.html
Normal file
217
frontend/index.html
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user