main
parent
45c0d494f3
commit
b9eb74f3a5
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
智能旅游系统/
|
||||
├── app.py # Flask主应用
|
||||
├── requirements.txt # 依赖包列表
|
||||
├── config.py # 配置文件
|
||||
├── .env # 环境变量
|
||||
├── .gitignore # Git忽略文件
|
||||
├── database.db # 数据库文件
|
||||
├── static/ # 静态文件
|
||||
│ ├── css/
|
||||
│ │ ├── base.css
|
||||
│ │ └── style.css
|
||||
│ ├── js/
|
||||
│ │ ├── voice.js
|
||||
│ │ └── itinerary.js
|
||||
│ └── images/
|
||||
└── templates/ # HTML模板
|
||||
├── index.html
|
||||
├── ai.html
|
||||
├── login.html
|
||||
├── register.html
|
||||
├── profile.html
|
||||
├── postnote.html
|
||||
├── checkin.html
|
||||
└── base.html
|
||||
@ -0,0 +1 @@
|
||||
1.3.0
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1 @@
|
||||
36067
|
||||
@ -0,0 +1 @@
|
||||
# this functions like a .gitignore placed at the root of the currently open workspace, but for the bevel Analyze command
|
||||
@ -0,0 +1,75 @@
|
||||
{
|
||||
"supportedFileExtensions": {
|
||||
"COBOL": [
|
||||
".cbl",
|
||||
".cob",
|
||||
".ccp",
|
||||
".cobol",
|
||||
".cpy",
|
||||
".cpb",
|
||||
".cblcpy",
|
||||
".mf"
|
||||
],
|
||||
"CSharp": [
|
||||
".cs"
|
||||
],
|
||||
"Kotlin": [
|
||||
".kt",
|
||||
".kts"
|
||||
],
|
||||
"TypeScript": [
|
||||
".ts",
|
||||
".tsx"
|
||||
],
|
||||
"JavaScript": [
|
||||
".js",
|
||||
".jsx"
|
||||
],
|
||||
"Python": [
|
||||
".py"
|
||||
],
|
||||
"Ruby": [
|
||||
".rb"
|
||||
],
|
||||
"Java": [
|
||||
".java"
|
||||
],
|
||||
"Go": [
|
||||
".go"
|
||||
],
|
||||
"Rust": [
|
||||
".rs"
|
||||
],
|
||||
"Scala": [
|
||||
".scala"
|
||||
],
|
||||
"Swift": [
|
||||
".swift"
|
||||
],
|
||||
"C": [
|
||||
".c",
|
||||
".h"
|
||||
],
|
||||
"Cpp": [
|
||||
".cpp",
|
||||
".h",
|
||||
".hpp"
|
||||
],
|
||||
"Perl": [
|
||||
".pl",
|
||||
".pm",
|
||||
".pod"
|
||||
],
|
||||
"PHP": [
|
||||
".php"
|
||||
],
|
||||
"Pascal": [
|
||||
".pas",
|
||||
".dfm",
|
||||
".inc"
|
||||
],
|
||||
"Dart": [
|
||||
".dart"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"selectedLLM": "AZURE_OPENAI",
|
||||
"endpoint": "https://[CUSTOM ORGANIZATION LINK].cognitiveservices.azure.com",
|
||||
"deploymentName": "gpt-4o-mini"
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"bevel.java": "c:\\Users\\lzt\\.vscode\\extensions\\bevel-software.bevel-1.3.0\\backend\\jdk17.0.16_8\\bin\\java.exe"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
# Virtual environment
|
||||
venv/
|
||||
env/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@ -0,0 +1,100 @@
|
||||
"""
|
||||
This use case teaches you how to use local plugin.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import List
|
||||
# Our official coze sdk for Python [cozepy](https://github.com/coze-dev/coze-py)
|
||||
from cozepy import COZE_CN_BASE_URL, ChatEvent, Stream, ToolOutput
|
||||
|
||||
# Get an access_token through personal access token or oauth.
|
||||
coze_api_token = 'cztei_ha83OqiqELsD7clOVl42byYbWUQwQhyN4jPSbCt1I6Tzev2x1OBl7ZmkJM46rlT5f'
|
||||
# The default access is api.coze.com, but if you need to access api.coze.cn,
|
||||
# please use base_url to configure the api endpoint to access
|
||||
coze_api_base = COZE_CN_BASE_URL
|
||||
|
||||
from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageContentType, ChatEventType # noqa
|
||||
|
||||
# Init the Coze client through the access_token.
|
||||
coze = Coze(auth=TokenAuth(token=coze_api_token), base_url=coze_api_base)
|
||||
|
||||
# Create a bot instance in Coze, copy the last number from the web link as the bot's ID.
|
||||
bot_id = '7560964057697222695'
|
||||
# The user id identifies the identity of a user. Developers can use a custom business ID
|
||||
# or a random string.
|
||||
user_id = '123456789'
|
||||
|
||||
|
||||
# These two functions are mock implementations.
|
||||
class LocalPluginMocker(object):
|
||||
@staticmethod
|
||||
def get_schedule():
|
||||
return "I have two interviews in the afternoon."
|
||||
|
||||
@staticmethod
|
||||
def screenshot():
|
||||
return "The background of my screen is a little dog running on the beach."
|
||||
|
||||
@staticmethod
|
||||
def get_function(name: str):
|
||||
return {
|
||||
"get_schedule": LocalPluginMocker.get_schedule,
|
||||
"screenshot": LocalPluginMocker.screenshot,
|
||||
}[name]
|
||||
|
||||
|
||||
# `handle_stream` is used to handle events. When the `CONVERSATION_CHAT_REQUIRES_ACTION` event is received,
|
||||
# the `submit_tool_outputs` method needs to be called to submit the running result.
|
||||
def handle_stream(stream: Stream[ChatEvent]):
|
||||
for event in stream:
|
||||
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
|
||||
print(event.message.content, end="", flush=True)
|
||||
|
||||
if event.event == ChatEventType.CONVERSATION_CHAT_REQUIRES_ACTION:
|
||||
if not event.chat.required_action or not event.chat.required_action.submit_tool_outputs:
|
||||
continue
|
||||
tool_calls = event.chat.required_action.submit_tool_outputs.tool_calls
|
||||
tool_outputs: List[ToolOutput] = []
|
||||
for tool_call in tool_calls:
|
||||
print(f"function call: {tool_call.function.name} {tool_call.function.arguments}")
|
||||
local_function = LocalPluginMocker.get_function(tool_call.function.name)
|
||||
output = json.dumps({"output": local_function()})
|
||||
tool_outputs.append(ToolOutput(tool_call_id=tool_call.id, output=output))
|
||||
|
||||
handle_stream(
|
||||
coze.chat.submit_tool_outputs(
|
||||
conversation_id=event.chat.conversation_id,
|
||||
chat_id=event.chat.id,
|
||||
tool_outputs=tool_outputs,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
|
||||
if event.event == ChatEventType.CONVERSATION_CHAT_COMPLETED:
|
||||
print()
|
||||
print("token usage:", event.chat.usage.token_count)
|
||||
|
||||
|
||||
# The intelligent entity will call LocalPluginMocker.get_schedule to obtain the schedule.
|
||||
# get_schedule is just a mock method.
|
||||
handle_stream(
|
||||
coze.chat.stream(
|
||||
bot_id=bot_id,
|
||||
user_id=user_id,
|
||||
additional_messages=[
|
||||
Message.build_user_question_text("What do I have to do in the afternoon?"),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# The intelligent entity will obtain a screenshot through LocalPluginMocker.screenshot.
|
||||
handle_stream(
|
||||
coze.chat.stream(
|
||||
bot_id=bot_id,
|
||||
user_id=user_id,
|
||||
additional_messages=[
|
||||
Message.build_user_question_text("What's on my screen?"),
|
||||
],
|
||||
)
|
||||
)
|
||||
@ -0,0 +1,18 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
|
||||
# 数据库配置
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///travel.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# 大模型API配置
|
||||
DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY')
|
||||
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
|
||||
|
||||
# 文件上传配置
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
||||
UPLOAD_FOLDER = 'static/uploads'
|
||||
|
After Width: | Height: | Size: 269 KiB |
@ -0,0 +1,88 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
class User(db.Model):
|
||||
"""用户模型"""
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
username = db.Column(db.String(64), nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
avatar = db.Column(db.String(200), default='https://picsum.photos/id/1005/200/200')
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
notes = db.relationship('Note', backref='author', lazy='dynamic')
|
||||
checkins = db.relationship('Checkin', backref='author', lazy='dynamic')
|
||||
itineraries = db.relationship('Itinerary', backref='author', lazy='dynamic')
|
||||
comments = db.relationship('Comment', backref='author', lazy='dynamic')
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
|
||||
class Note(db.Model):
|
||||
"""旅行笔记模型"""
|
||||
__tablename__ = 'notes'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
destination = db.Column(db.String(100))
|
||||
images = db.Column(db.JSON) # 存储图片URL列表
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
comments = db.relationship('Comment', backref='note', lazy='dynamic')
|
||||
|
||||
|
||||
class Checkin(db.Model):
|
||||
"""打卡记录模型"""
|
||||
__tablename__ = 'checkins'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
location = db.Column(db.String(100), nullable=False)
|
||||
location_type = db.Column(db.String(50))
|
||||
notes = db.Column(db.Text)
|
||||
rating = db.Column(db.Integer) # 1-5分
|
||||
longitude = db.Column(db.Float)
|
||||
latitude = db.Column(db.Float)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
|
||||
|
||||
class Comment(db.Model):
|
||||
"""评论模型"""
|
||||
__tablename__ = 'comments'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
note_id = db.Column(db.Integer, db.ForeignKey('notes.id'), nullable=False)
|
||||
|
||||
|
||||
class Itinerary(db.Model):
|
||||
"""行程规划模型"""
|
||||
__tablename__ = 'itineraries'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
destination = db.Column(db.String(100), nullable=False)
|
||||
days = db.Column(db.Integer, nullable=False)
|
||||
trip_type = db.Column(db.String(50))
|
||||
budget = db.Column(db.String(50))
|
||||
content = db.Column(db.JSON) # 行程详情
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
@ -0,0 +1,16 @@
|
||||
beautifulsoup4==4.14.2
|
||||
blinker==1.9.0
|
||||
certifi==2025.10.5
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.0
|
||||
colorama==0.4.6
|
||||
Flask==3.1.2
|
||||
idna==3.11
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.3
|
||||
requests==2.32.5
|
||||
soupsieve==2.8
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
Werkzeug==3.1.3
|
||||
@ -0,0 +1,27 @@
|
||||
/* Tailwind基础样式 + 自定义主题配置 */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 自定义主题变量(全局可复用) */
|
||||
@layer base {
|
||||
:root {
|
||||
--color-primary: #165DFF;
|
||||
--color-secondary: #FF7D00;
|
||||
--color-dark: #1D2129;
|
||||
--color-light: #F2F3F5;
|
||||
--color-success: #00B42A;
|
||||
--color-warning: #FF7D00;
|
||||
--color-danger: #F53F3F;
|
||||
--color-gray-light: #C9CDD4;
|
||||
--color-gray-medium: #86909C;
|
||||
--color-gray-dark: #4E5969;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义字体配置 */
|
||||
@layer theme {
|
||||
.font-sans {
|
||||
font-family: 'Inter', 'system-ui', 'sans-serif';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
// voice.js - 语音交互功能
|
||||
class VoiceAssistant {
|
||||
constructor() {
|
||||
this.recognition = null;
|
||||
this.isListening = false;
|
||||
this.initSpeechRecognition();
|
||||
}
|
||||
|
||||
initSpeechRecognition() {
|
||||
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
this.recognition = new SpeechRecognition();
|
||||
|
||||
this.recognition.continuous = false;
|
||||
this.recognition.interimResults = false;
|
||||
this.recognition.lang = 'zh-CN';
|
||||
|
||||
this.recognition.onstart = () => {
|
||||
this.isListening = true;
|
||||
this.updateUIListeningState(true);
|
||||
};
|
||||
|
||||
this.recognition.onend = () => {
|
||||
this.isListening = false;
|
||||
this.updateUIListeningState(false);
|
||||
};
|
||||
|
||||
this.recognition.onresult = (event) => {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
this.processVoiceCommand(transcript);
|
||||
};
|
||||
|
||||
this.recognition.onerror = (event) => {
|
||||
console.error('语音识别错误:', event.error);
|
||||
this.isListening = false;
|
||||
this.updateUIListeningState(false);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
startListening() {
|
||||
if (this.recognition && !this.isListening) {
|
||||
try {
|
||||
this.recognition.start();
|
||||
} catch (error) {
|
||||
console.error('启动语音识别失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopListening() {
|
||||
if (this.recognition && this.isListening) {
|
||||
this.recognition.stop();
|
||||
}
|
||||
}
|
||||
|
||||
updateUIListeningState(listening) {
|
||||
const voiceBtn = document.getElementById('voice-btn');
|
||||
if (voiceBtn) {
|
||||
if (listening) {
|
||||
voiceBtn.innerHTML = '<i class="fa fa-microphone-slash"></i> 停止';
|
||||
voiceBtn.classList.add('listening');
|
||||
} else {
|
||||
voiceBtn.innerHTML = '<i class="fa fa-microphone"></i> 语音输入';
|
||||
voiceBtn.classList.remove('listening');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async processVoiceCommand(transcript) {
|
||||
console.log('语音输入:', transcript);
|
||||
|
||||
// 显示用户语音输入
|
||||
this.showVoiceInput(transcript);
|
||||
|
||||
try {
|
||||
// 调用大模型处理语音指令
|
||||
const response = await this.sendToAI(transcript);
|
||||
this.showAIResponse(response);
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理语音指令失败:', error);
|
||||
this.showAIResponse('抱歉,我暂时无法处理您的请求,请稍后重试。');
|
||||
}
|
||||
}
|
||||
|
||||
async sendToAI(message) {
|
||||
const response = await fetch('/api/voice_chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
history: [] // 可以维护对话历史
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
return result.response;
|
||||
}
|
||||
|
||||
showVoiceInput(transcript) {
|
||||
// 在页面上显示语音输入内容
|
||||
const voiceInputDiv = document.getElementById('voice-input') || this.createVoiceInputElement();
|
||||
voiceInputDiv.innerHTML = `
|
||||
<div class="voice-message user-message">
|
||||
<div class="message-content">
|
||||
<i class="fa fa-user mr-2"></i>${transcript}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showAIResponse(response) {
|
||||
const voiceOutputDiv = document.getElementById('voice-output') || this.createVoiceOutputElement();
|
||||
voiceOutputDiv.innerHTML = `
|
||||
<div class="voice-message ai-message">
|
||||
<div class="message-content">
|
||||
<i class="fa fa-robot mr-2"></i>${response}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 语音合成回复
|
||||
this.speakResponse(response);
|
||||
}
|
||||
|
||||
speakResponse(text) {
|
||||
if ('speechSynthesis' in window) {
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = 'zh-CN';
|
||||
utterance.rate = 1.0;
|
||||
utterance.pitch = 1.0;
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
|
||||
createVoiceInputElement() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'voice-input';
|
||||
div.className = 'voice-input-container';
|
||||
document.body.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
|
||||
createVoiceOutputElement() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'voice-output';
|
||||
div.className = 'voice-output-container';
|
||||
document.body.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化语音助手
|
||||
const voiceAssistant = new VoiceAssistant();
|
||||
|
||||
// 在ai.html中添加语音按钮
|
||||
function addVoiceButton() {
|
||||
const form = document.getElementById('itinerary-form');
|
||||
const voiceBtn = document.createElement('button');
|
||||
voiceBtn.type = 'button';
|
||||
voiceBtn.id = 'voice-btn';
|
||||
voiceBtn.className = 'w-full bg-secondary hover:bg-secondary/90 text-white font-medium py-3 px-4 rounded-lg transition-colors mt-2 flex items-center justify-center';
|
||||
voiceBtn.innerHTML = '<i class="fa fa-microphone mr-2"></i> 语音输入需求';
|
||||
|
||||
voiceBtn.addEventListener('click', () => {
|
||||
if (voiceAssistant.isListening) {
|
||||
voiceAssistant.stopListening();
|
||||
} else {
|
||||
voiceAssistant.startListening();
|
||||
}
|
||||
});
|
||||
|
||||
form.appendChild(voiceBtn);
|
||||
}
|
||||
|
||||
// 页面加载完成后添加语音按钮
|
||||
document.addEventListener('DOMContentLoaded', addVoiceButton);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户登录 - 智能旅游系统</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.card-shadow {
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 font-sans">
|
||||
<!-- 导航栏 -->
|
||||
<header class="bg-white shadow-md">
|
||||
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<a href="index.html" class="flex items-center space-x-2">
|
||||
<i class="fa fa-paper-plane text-blue-600 text-2xl"></i>
|
||||
<span class="text-xl font-bold text-gray-800">智慧游</span>
|
||||
</a>
|
||||
<nav class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="text-gray-600 hover:text-blue-600 transition-colors">首页</a>
|
||||
<a href="ai.html" class="text-gray-600 hover:text-blue-600 transition-colors">智能规划</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="min-h-screen flex items-center justify-center py-12">
|
||||
<div class="bg-white rounded-2xl card-shadow p-8 w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">用户登录</h1>
|
||||
<p class="text-gray-600">欢迎回到智能旅游系统</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<form id="login-form">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fa fa-envelope-o mr-2"></i>邮箱
|
||||
</label>
|
||||
<input type="email" id="email" name="email" placeholder="请输入邮箱" required
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between mb-2">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">
|
||||
<i class="fa fa-lock mr-2"></i>密码
|
||||
</label>
|
||||
<a href="#" class="text-sm text-blue-600 hover:text-blue-800">忘记密码?</a>
|
||||
</div>
|
||||
<input type="password" id="password" name="password" placeholder="请输入密码" required
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="remember-me" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-600">记住我</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors flex items-center justify-center">
|
||||
<span id="login-text">登录</span>
|
||||
<div id="login-spinner" class="hidden ml-2">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-gray-600">
|
||||
还没有账号?
|
||||
<a href="register.html" class="text-blue-600 font-medium hover:text-blue-800">立即注册</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relative my-6">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">或使用以下方式登录</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<button type="button" class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fa fa-weixin text-green-500 text-xl mr-2"></i>
|
||||
<span class="text-sm">微信登录</span>
|
||||
</button>
|
||||
<button type="button" class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fa fa-qq text-blue-500 text-xl mr-2"></i>
|
||||
<span class="text-sm">QQ登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-gray-800 text-white py-8 mt-12">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<p>© 2025 智能旅游系统 | 基于大模型的个性化行程规划</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// 用户管理类
|
||||
class UserManager {
|
||||
constructor() {
|
||||
this.currentUser = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 检查localStorage中是否有保存的用户信息
|
||||
const savedUser = localStorage.getItem('currentUser');
|
||||
if (savedUser) {
|
||||
this.currentUser = JSON.parse(savedUser);
|
||||
console.log('已加载用户:', this.currentUser);
|
||||
}
|
||||
}
|
||||
|
||||
login(email, password, rememberMe = false) {
|
||||
// 模拟API调用
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
// 简单验证 - 实际项目中应该调用后端API
|
||||
if (email && password) {
|
||||
this.currentUser = {
|
||||
id: Date.now(),
|
||||
email: email,
|
||||
name: email.split('@')[0],
|
||||
loginTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('currentUser', JSON.stringify(this.currentUser));
|
||||
|
||||
// 如果选择"记住我",设置过期时间
|
||||
if (rememberMe) {
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + 30); // 30天后过期
|
||||
localStorage.setItem('userExpiry', expiryDate.toISOString());
|
||||
}
|
||||
|
||||
resolve(this.currentUser);
|
||||
} else {
|
||||
reject(new Error('邮箱和密码不能为空'));
|
||||
}
|
||||
}, 1500); // 模拟网络延迟
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.currentUser = null;
|
||||
localStorage.removeItem('currentUser');
|
||||
localStorage.removeItem('userExpiry');
|
||||
console.log('用户已登出');
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
// 检查用户是否登录且有有效的会话
|
||||
if (!this.currentUser) return false;
|
||||
|
||||
// 检查是否过期
|
||||
const expiry = localStorage.getItem('userExpiry');
|
||||
if (expiry && new Date() > new Date(expiry)) {
|
||||
this.logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.currentUser;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化用户管理器
|
||||
const userManager = new UserManager();
|
||||
|
||||
// 登录表单处理
|
||||
document.getElementById('login-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const rememberMe = document.getElementById('remember-me').checked;
|
||||
|
||||
// 显示加载状态
|
||||
const submitBtn = document.querySelector('button[type="submit"]');
|
||||
const loginText = document.getElementById('login-text');
|
||||
const loginSpinner = document.getElementById('login-spinner');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
loginText.textContent = '登录中...';
|
||||
loginSpinner.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
// 调用登录方法
|
||||
const user = await userManager.login(email, password, rememberMe);
|
||||
|
||||
// 登录成功
|
||||
loginText.textContent = '登录成功!';
|
||||
|
||||
// 延迟跳转,让用户看到成功消息
|
||||
setTimeout(() => {
|
||||
// 重定向到首页
|
||||
window.location.href = 'index.html';
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
// 登录失败
|
||||
loginText.textContent = '登录';
|
||||
loginSpinner.classList.add('hidden');
|
||||
submitBtn.disabled = false;
|
||||
|
||||
alert('登录失败: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (userManager.isLoggedIn()) {
|
||||
console.log('用户已登录:', userManager.getUser());
|
||||
// 如果已登录,直接跳转到首页
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.10/libs/cn/index.js"></script>
|
||||
<script>
|
||||
new CozeWebSDK.WebChatClient({
|
||||
config: {
|
||||
bot_id: '7560957909019492367',
|
||||
},
|
||||
componentProps: {
|
||||
title: 'Coze',
|
||||
},
|
||||
auth: {
|
||||
type: 'token',
|
||||
token: 'pat_********',
|
||||
onRefreshToken: function () {
|
||||
return 'pat_********'
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in new issue