jml 3 weeks ago
parent 45c0d494f3
commit b9eb74f3a5

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

@ -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 @@
# 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"
}

@ -0,0 +1,206 @@
-- 创建智能旅游系统数据库
CREATE DATABASE IF NOT EXISTS smart_tourism_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE smart_tourism_system;
-- 1. 用户表(存储用户基本信息)
CREATE TABLE `user` (
`user_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
`password` VARCHAR(100) NOT NULL COMMENT '加密密码',
`phone` VARCHAR(20) UNIQUE COMMENT '手机号',
`email` VARCHAR(100) UNIQUE COMMENT '邮箱',
`real_name` VARCHAR(50) COMMENT '真实姓名',
`id_card` VARCHAR(20) UNIQUE COMMENT '身份证号',
`avatar` VARCHAR(255) COMMENT '头像URL',
`user_level` TINYINT DEFAULT 1 COMMENT '用户等级1-普通2-会员3-VIP',
`register_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`last_login_time` DATETIME COMMENT '最后登录时间',
`status` TINYINT DEFAULT 1 COMMENT '状态0-禁用1-正常)',
INDEX idx_phone (`phone`),
INDEX idx_email (`email`)
) ENGINE=InnoDB COMMENT '用户信息表';
-- 2. 游客偏好表(记录用户旅游偏好)
CREATE TABLE `user_preference` (
`preference_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '偏好ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`prefer_theme` VARCHAR(100) COMMENT '偏好主题(自然景观/人文历史/美食/购物等)',
`prefer_traffic` VARCHAR(50) COMMENT '偏好交通方式',
`prefer_crowd` VARCHAR(50) COMMENT '偏好人群密度(热闹/安静)',
`prefer_budget` DECIMAL(10,2) COMMENT '偏好预算范围',
`prefer_duration` INT COMMENT '偏好行程天数',
FOREIGN KEY (`user_id`) REFERENCES `user`(`user_id`) ON DELETE CASCADE,
UNIQUE KEY uk_user (`user_id`)
) ENGINE=InnoDB COMMENT '用户旅游偏好表';
-- 3. 城市表(基础地理信息)
CREATE TABLE `city` (
`city_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '城市ID',
`city_name` VARCHAR(50) NOT NULL COMMENT '城市名称',
`province` VARCHAR(50) NOT NULL COMMENT '所属省份',
`longitude` DECIMAL(10,6) COMMENT '经度',
`latitude` DECIMAL(10,6) COMMENT '纬度',
`description` TEXT COMMENT '城市描述',
`best_season` VARCHAR(100) COMMENT '最佳旅游季节',
`hot_level` INT DEFAULT 0 COMMENT '热门程度0-10',
UNIQUE KEY uk_city_name (`city_name`)
) ENGINE=InnoDB COMMENT '城市信息表';
-- 4. 景点表(核心旅游资源)
CREATE TABLE `attraction` (
`attraction_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '景点ID',
`city_id` INT NOT NULL COMMENT '所属城市ID',
`name` VARCHAR(100) NOT NULL COMMENT '景点名称',
`category` VARCHAR(50) COMMENT '景点类别(自然/人文/乐园等)',
`address` VARCHAR(255) COMMENT '详细地址',
`longitude` DECIMAL(10,6) COMMENT '经度',
`latitude` DECIMAL(10,6) COMMENT '纬度',
`description` TEXT COMMENT '景点描述',
`open_time` VARCHAR(100) COMMENT '开放时间',
`ticket_price` DECIMAL(10,2) COMMENT '门票价格',
`discount_price` DECIMAL(10,2) COMMENT '折扣价格',
`rating` DECIMAL(2,1) DEFAULT 0 COMMENT '评分0-5分',
`comment_count` INT DEFAULT 0 COMMENT '评论数量',
`hot_level` INT DEFAULT 0 COMMENT '热门程度0-10',
`parking_info` VARCHAR(255) COMMENT '停车信息',
`traffic_info` TEXT COMMENT '交通信息',
`image_urls` TEXT COMMENT '图片URL逗号分隔',
FOREIGN KEY (`city_id`) REFERENCES `city`(`city_id`) ON DELETE CASCADE,
INDEX idx_category (`category`),
INDEX idx_rating (`rating`),
INDEX idx_hot_level (`hot_level`)
) ENGINE=InnoDB COMMENT '景点信息表';
-- 5. 景点评论表
CREATE TABLE `attraction_comment` (
`comment_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '评论ID',
`attraction_id` INT NOT NULL COMMENT '景点ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`rating` TINYINT NOT NULL COMMENT '评分1-5',
`content` TEXT COMMENT '评论内容',
`comment_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '评论时间',
`image_urls` TEXT COMMENT '附图URL逗号分隔',
`like_count` INT DEFAULT 0 COMMENT '点赞数',
FOREIGN KEY (`attraction_id`) REFERENCES `attraction`(`attraction_id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `user`(`user_id`) ON DELETE CASCADE,
INDEX idx_attraction (`attraction_id`),
INDEX idx_user (`user_id`)
) ENGINE=InnoDB COMMENT '景点评论表';
-- 6. 酒店表
CREATE TABLE `hotel` (
`hotel_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '酒店ID',
`city_id` INT NOT NULL COMMENT '所属城市ID',
`name` VARCHAR(100) NOT NULL COMMENT '酒店名称',
`address` VARCHAR(255) COMMENT '详细地址',
`longitude` DECIMAL(10,6) COMMENT '经度',
`latitude` DECIMAL(10,6) COMMENT '纬度',
`star_level` TINYINT COMMENT '星级1-5',
`description` TEXT COMMENT '酒店描述',
`price_per_night` DECIMAL(10,2) COMMENT '每晚价格',
`discount_price` DECIMAL(10,2) COMMENT '折扣价格',
`rating` DECIMAL(2,1) DEFAULT 0 COMMENT '评分0-5分',
`comment_count` INT DEFAULT 0 COMMENT '评论数量',
`facilities` TEXT COMMENT '设施(逗号分隔)',
`image_urls` TEXT COMMENT '图片URL逗号分隔',
FOREIGN KEY (`city_id`) REFERENCES `city`(`city_id`) ON DELETE CASCADE,
INDEX idx_star_level (`star_level`),
INDEX idx_rating (`rating`)
) ENGINE=InnoDB COMMENT '酒店信息表';
-- 7. 行程表(用户规划的行程)
CREATE TABLE `itinerary` (
`itinerary_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '行程ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`title` VARCHAR(100) NOT NULL COMMENT '行程标题',
`start_city` VARCHAR(50) COMMENT '出发城市',
`destination_city` INT NOT NULL COMMENT '目的城市ID',
`start_date` DATE NOT NULL COMMENT '开始日期',
`end_date` DATE NOT NULL COMMENT '结束日期',
`days` INT NOT NULL COMMENT '行程天数',
`budget` DECIMAL(10,2) COMMENT '预算',
`status` TINYINT DEFAULT 0 COMMENT '状态0-规划中1-已完成2-已取消)',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_shared` TINYINT DEFAULT 0 COMMENT '是否共享0-否1-是)',
FOREIGN KEY (`user_id`) REFERENCES `user`(`user_id`) ON DELETE CASCADE,
FOREIGN KEY (`destination_city`) REFERENCES `city`(`city_id`),
INDEX idx_user (`user_id`),
INDEX idx_destination (`destination_city`),
INDEX idx_status (`status`)
) ENGINE=InnoDB COMMENT '用户行程表';
-- 8. 行程详情表(每日行程安排)
CREATE TABLE `itinerary_detail` (
`detail_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '详情ID',
`itinerary_id` INT NOT NULL COMMENT '行程ID',
`day` INT NOT NULL COMMENT '第几天',
`sequence` INT NOT NULL COMMENT '当天顺序',
`attraction_id` INT COMMENT '关联景点ID',
`hotel_id` INT COMMENT '关联酒店ID',
`activity` VARCHAR(255) COMMENT '活动描述',
`start_time` TIME COMMENT '开始时间',
`end_time` TIME COMMENT '结束时间',
`notes` TEXT COMMENT '备注',
FOREIGN KEY (`itinerary_id`) REFERENCES `itinerary`(`itinerary_id`) ON DELETE CASCADE,
FOREIGN KEY (`attraction_id`) REFERENCES `attraction`(`attraction_id`) ON DELETE SET NULL,
FOREIGN KEY (`hotel_id`) REFERENCES `hotel`(`hotel_id`) ON DELETE SET NULL,
INDEX idx_itinerary (`itinerary_id`)
) ENGINE=InnoDB COMMENT '行程详情表';
-- 9. 预订表(统一管理各类预订)
CREATE TABLE `booking` (
`booking_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '预订ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`booking_type` TINYINT NOT NULL COMMENT '预订类型1-景点门票2-酒店3-交通)',
`target_id` INT NOT NULL COMMENT '目标ID对应景点/酒店/交通ID',
`booking_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '预订时间',
`use_time` DATETIME NOT NULL COMMENT '使用时间',
`quantity` INT NOT NULL DEFAULT 1 COMMENT '数量',
`total_price` DECIMAL(10,2) NOT NULL COMMENT '总价',
`payment_status` TINYINT DEFAULT 0 COMMENT '支付状态0-未支付1-已支付2-已退款)',
`payment_time` DATETIME COMMENT '支付时间',
`order_number` VARCHAR(50) UNIQUE NOT NULL COMMENT '订单编号',
`status` TINYINT DEFAULT 0 COMMENT '状态0-待使用1-已使用2-已取消)',
`contact_name` VARCHAR(50) COMMENT '联系人',
`contact_phone` VARCHAR(20) COMMENT '联系电话',
FOREIGN KEY (`user_id`) REFERENCES `user`(`user_id`) ON DELETE CASCADE,
INDEX idx_user (`user_id`),
INDEX idx_order_number (`order_number`),
INDEX idx_status (`status`),
INDEX idx_booking_type (`booking_type`)
) ENGINE=InnoDB COMMENT '预订订单表';
-- 10. 交通信息表(城市间交通)
CREATE TABLE `transport` (
`transport_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '交通ID',
`from_city` INT NOT NULL COMMENT '出发城市ID',
`to_city` INT NOT NULL COMMENT '到达城市ID',
`transport_type` TINYINT NOT NULL COMMENT '类型1-火车2-飞机3-汽车)',
`company` VARCHAR(100) COMMENT '运营公司',
`departure_time` DATETIME NOT NULL COMMENT '出发时间',
`arrival_time` DATETIME NOT NULL COMMENT '到达时间',
`duration` INT COMMENT '时长(分钟)',
`price` DECIMAL(10,2) NOT NULL COMMENT '价格',
`remaining_tickets` INT DEFAULT 0 COMMENT '剩余票数',
`station_info` TEXT COMMENT '站点信息',
FOREIGN KEY (`from_city`) REFERENCES `city`(`city_id`),
FOREIGN KEY (`to_city`) REFERENCES `city`(`city_id`),
INDEX idx_from_to (`from_city`, `to_city`),
INDEX idx_departure (`departure_time`),
INDEX idx_transport_type (`transport_type`)
) ENGINE=InnoDB COMMENT '城市间交通信息表';
-- 11. 智能推荐记录表(存储系统推荐结果)
CREATE TABLE `recommendation` (
`rec_id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '推荐ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`rec_type` TINYINT NOT NULL COMMENT '推荐类型1-景点2-行程3-酒店)',
`target_ids` TEXT NOT NULL COMMENT '推荐目标ID集合逗号分隔',
`rec_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '推荐时间',
`click_count` INT DEFAULT 0 COMMENT '点击次数',
`rec_reason` TEXT COMMENT '推荐理由',
FOREIGN KEY (`user_id`) REFERENCES `user`(`user_id`) ON DELETE CASCADE,
INDEX idx_user (`user_id`),
INDEX idx_rec_type (`rec_type`)
) ENGINE=InnoDB COMMENT '智能推荐记录表';

@ -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'

Binary file not shown.

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,123 @@
/* 自定义工具类原HTML中的@layer utilities */
.content-auto {
content-visibility: auto;
}
.text-shadow {
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.gradient-overlay {
background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0.7) 100%);
}
/* 全局样式补充 */
body {
@apply font-sans bg-gray-50 text-dark;
}
/* 模态框动画 */
.modal-enter {
animation: modalFadeIn 0.3s ease forwards;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 行程生成加载动画 */
.loader {
width: 48px;
height: 48px;
border: 5px solid var(--color-primary);
border-bottom-color: transparent;
border-radius: 50%;
animation: loader-spin 1s linear infinite;
}
@keyframes loader-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 在style.css中添加 */
/* 导航栏滚动效果 */
.navbar-scrolled {
@apply bg-white shadow-md;
}
.navbar-scrolled .text-white {
@apply text-dark;
}
.navbar-scrolled .text-white:hover {
@apply text-primary;
}
/* 移动端适配优化 */
@media (max-width: 768px) {
.hero-section {
@apply py-16;
}
.card {
@apply w-full;
}
.trip-planner-form {
@apply grid-cols-1;
}
.itinerary-result {
@apply p-4;
}
.note-modal {
@apply max-w-full mx-2;
}
}
/* 加载动画增强 */
.loader-container {
@apply flex justify-center items-center fixed inset-0 bg-black/30 backdrop-blur-sm z-50 hidden;
}
/* 通知提示样式 */
.notification {
@apply fixed top-20 right-4 px-4 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full opacity-0;
}
.notification.show {
@apply translate-x-0 opacity-100;
}
.notification.success {
@apply bg-success text-white;
}
.notification.error {
@apply bg-danger text-white;
}
.notification.info {
@apply bg-primary text-white;
}

@ -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);

@ -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,761 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发表旅行笔记 - 智能旅游攻略系统</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
padding: 25px 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
letter-spacing: 1px;
}
.header p {
opacity: 0.9;
font-size: 15px;
}
.form-container {
padding: 30px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2d3748;
font-size: 15px;
}
.input-field, .select-field, .textarea-field {
width: 100%;
padding: 14px 16px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
font-size: 15px;
transition: all 0.3s ease;
background-color: #f8fafc;
}
.input-field:focus, .select-field:focus, .textarea-field:focus {
outline: none;
border-color: #4facfe;
box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.2);
background-color: white;
}
.textarea-field {
min-height: 150px;
resize: vertical;
}
.date-group {
display: flex;
gap: 15px;
}
.date-group .input-field {
flex: 1;
}
.upload-area {
border: 2px dashed #cbd5e0;
border-radius: 10px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background-color: #f8fafc;
}
.upload-area:hover {
border-color: #4facfe;
background-color: #f0f9ff;
}
.upload-icon {
font-size: 48px;
color: #90cdf4;
margin-bottom: 15px;
}
.upload-text {
color: #718096;
margin-bottom: 10px;
}
.upload-hint {
font-size: 13px;
color: #a0aec0;
}
.image-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
}
.preview-item {
position: relative;
width: 100px;
height: 100px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.remove-btn {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.cancel-btn, .publish-btn {
padding: 14px 28px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.cancel-btn {
background-color: #f7fafc;
color: #4a5568;
border: 1.5px solid #e2e8f0;
}
.cancel-btn:hover {
background-color: #edf2f7;
}
.publish-btn {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.3);
}
.publish-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(79, 172, 254, 0.4);
}
.footer {
text-align: center;
padding: 20px;
color: #718096;
font-size: 14px;
border-top: 1px solid #e2e8f0;
margin-top: 20px;
}
.help-text {
display: flex;
align-items: center;
gap: 8px;
background-color: #f0f9ff;
padding: 12px 16px;
border-radius: 8px;
margin-top: 10px;
font-size: 14px;
color: #2b6cb0;
}
.help-text i {
color: #4299e1;
}
/* 加载动画 */
.fa-spin {
animation: fa-spin 1s infinite linear;
}
@keyframes fa-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 禁用状态 */
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
button:disabled:hover {
transform: none;
box-shadow: none;
}
/* 错误提示 */
.error-message {
color: #e53e3e;
font-size: 14px;
margin-top: 5px;
display: none;
}
.form-group.error .input-field,
.form-group.error .select-field,
.form-group.error .textarea-field {
border-color: #e53e3e;
}
.form-group.error .error-message {
display: block;
}
@media (max-width: 600px) {
.date-group {
flex-direction: column;
}
.button-group {
flex-direction: column;
gap: 12px;
}
.cancel-btn, .publish-btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-pen-alt"></i> 发表旅行笔记</h1>
<p>记录美好旅程,分享精彩体验</p>
</div>
<div class="form-container">
<div class="form-group">
<label for="note-title"><i class="fas fa-heading"></i> 笔记标题</label>
<input type="text" id="note-title" class="input-field" placeholder="给你的笔记起个吸引人的标题">
<div class="error-message">请输入笔记标题</div>
</div>
<div class="form-group">
<label for="travel-location"><i class="fas fa-map-marker-alt"></i> 旅行地点</label>
<input type="text" id="travel-location" class="input-field" placeholder="例如:成都 家常巷子" value="成都 家常巷子">
<div class="error-message">请输入旅行地点</div>
</div>
<div class="form-group">
<label for="travel-date"><i class="far fa-calendar-alt"></i> 旅行时间</label>
<div class="date-group">
<input type="number" id="travel-year" class="input-field" placeholder="年" min="2000" max="2030">
<input type="number" id="travel-month" class="input-field" placeholder="月" min="1" max="12">
<input type="number" id="travel-day" class="input-field" placeholder="日" min="1" max="31">
</div>
<div class="error-message">请输入完整的旅行时间</div>
</div>
<div class="form-group">
<label for="note-category"><i class="fas fa-tags"></i> 笔记分类</label>
<select id="note-category" class="select-field">
<option value="">请选择分类</option>
<option value="food">美食探店</option>
<option value="scenery">风景摄影</option>
<option value="culture">文化体验</option>
<option value="shopping">购物攻略</option>
<option value="accommodation">住宿体验</option>
</select>
<div class="error-message">请选择笔记分类</div>
</div>
<div class="help-text">
<i class="fas fa-lightbulb"></i>
<span>分享你的旅行体验,帮助更多人规划他们的旅程</span>
</div>
<div class="form-group">
<label><i class="far fa-image"></i> 上传图片最多9张</label>
<div class="upload-area" id="upload-area">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<div class="upload-text">点击或拖拽图片到此处上传</div>
<div class="upload-hint">支持 JPG、PNG 格式,单张图片不超过 5MB</div>
</div>
<div class="image-preview" id="image-preview">
<!-- 图片预览区域 -->
</div>
</div>
<div class="form-group">
<label for="note-content"><i class="fas fa-edit"></i> 笔记内容</label>
<textarea id="note-content" class="textarea-field" placeholder="分享你的旅行故事、实用贴士和难忘体验..."></textarea>
<div class="error-message">请输入笔记内容</div>
</div>
<div class="button-group">
<button class="cancel-btn" id="cancel-btn">取消</button>
<button class="publish-btn" id="publish-btn">发布笔记</button>
</div>
</div>
<div class="footer">
© 2023 智能旅游攻略系统
</div>
</div>
<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();
// 检查用户登录状态
function checkLoginStatus() {
const currentUser = userManager.getUser();
if (!currentUser) {
alert('请先登录后再发表笔记');
window.location.href = 'login.html';
return false;
}
return currentUser;
}
// 收集表单数据
function collectNoteData() {
const title = document.getElementById('note-title').value.trim();
const location = document.getElementById('travel-location').value.trim();
const year = document.getElementById('travel-year').value.trim();
const month = document.getElementById('travel-month').value.trim();
const day = document.getElementById('travel-day').value.trim();
const category = document.getElementById('note-category').value;
const content = document.getElementById('note-content').value.trim();
// 收集图片数据
const imagePreviews = document.querySelectorAll('.preview-item img');
const images = Array.from(imagePreviews).map(img => img.src);
return {
title,
location,
travelDate: `${year}-${month}-${day}`,
category,
content,
images,
publishTime: new Date().toISOString()
};
}
// 验证表单数据
// 验证表单数据
function validateNoteData(data) {
let isValid = true;
// 清除所有错误状态
document.querySelectorAll('.form-group').forEach(group => {
group.classList.remove('error');
});
// 获取原始输入值进行验证
const year = document.getElementById('travel-year').value.trim();
const month = document.getElementById('travel-month').value.trim();
const day = document.getElementById('travel-day').value.trim();
// 验证标题
if (!data.title) {
document.querySelector('#note-title').closest('.form-group').classList.add('error');
isValid = false;
}
// 验证地点
if (!data.location) {
document.querySelector('#travel-location').closest('.form-group').classList.add('error');
isValid = false;
}
// 验证时间
if (!year || !month || !day) {
document.querySelector('#travel-year').closest('.form-group').classList.add('error');
isValid = false;
}
// 验证分类
if (!data.category) {
document.querySelector('#note-category').closest('.form-group').classList.add('error');
isValid = false;
}
// 验证内容
if (!data.content) {
document.querySelector('#note-content').closest('.form-group').classList.add('error');
isValid = false;
}
return isValid;
}
// 模拟发送数据到后端实际项目中替换为真实的API调用
async function submitNoteToBackend(noteData, user) {
// 显示加载状态
const publishBtn = document.getElementById('publish-btn');
const originalText = publishBtn.textContent;
publishBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 发布中...';
publishBtn.disabled = true;
try {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 2000));
// 在实际项目中这里应该是真实的API调用
// const response = await fetch('/api/notes', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': `Bearer ${user.token}`
// },
// body: JSON.stringify({
// ...noteData,
// author: user.id,
// authorName: user.name
// })
// });
// if (!response.ok) {
// throw new Error('发布失败');
// }
// 模拟成功响应
console.log('笔记数据:', {
...noteData,
author: user.id,
authorName: user.name || user.email.split('@')[0]
});
// 保存到本地存储(模拟数据库)
const existingNotes = JSON.parse(localStorage.getItem('userNotes') || '[]');
const newNote = {
id: Date.now(),
...noteData,
author: user.id,
authorName: user.name || user.email.split('@')[0],
likes: 0,
comments: 0,
views: 0
};
existingNotes.push(newNote);
localStorage.setItem('userNotes', JSON.stringify(existingNotes));
return { success: true, message: '笔记发布成功!' };
} catch (error) {
console.error('发布失败:', error);
return { success: false, message: '发布失败,请稍后重试' };
} finally {
// 恢复按钮状态
publishBtn.textContent = originalText;
publishBtn.disabled = false;
}
}
// 清空表单
function resetForm() {
document.getElementById('note-title').value = '';
document.getElementById('travel-location').value = '';
document.getElementById('travel-year').value = '';
document.getElementById('travel-month').value = '';
document.getElementById('travel-day').value = '';
document.getElementById('note-category').value = '';
document.getElementById('note-content').value = '';
document.getElementById('image-preview').innerHTML = '';
// 清除错误状态
document.querySelectorAll('.form-group').forEach(group => {
group.classList.remove('error');
});
}
// 主发布函数
async function publishNote() {
// 检查登录状态
const user = checkLoginStatus();
if (!user) return;
// 收集数据
const noteData = collectNoteData();
// 验证数据
if (!validateNoteData(noteData)) {
alert('请填写所有必填字段');
return;
}
// 提交数据
const result = await submitNoteToBackend(noteData, user);
if (result.success) {
alert(result.message);
// 发布成功后跳转到个人中心
setTimeout(() => {
window.location.href = 'profile.html';
}, 1500);
} else {
alert(result.message);
}
}
// 取消按钮功能
function cancelNote() {
if (confirm('确定要取消吗?未保存的内容将会丢失。')) {
window.location.href = 'index.html';
}
}
// 图片上传预览功能
function initImageUpload() {
// 创建隐藏的文件输入元素
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = 'file-input';
fileInput.multiple = true;
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
// 点击上传区域触发文件选择
document.getElementById('upload-area').addEventListener('click', function() {
fileInput.click();
});
// 文件选择处理
fileInput.addEventListener('change', function(e) {
const files = e.target.files;
const previewContainer = document.getElementById('image-preview');
for (let i = 0; i < files.length; i++) {
if (previewContainer.children.length >= 9) {
alert('最多只能上传9张图片');
break;
}
const file = files[i];
// 检查文件大小5MB限制
if (file.size > 5 * 1024 * 1024) {
alert(`图片 ${file.name} 超过5MB大小限制`);
continue;
}
const reader = new FileReader();
reader.onload = function(e) {
const previewItem = document.createElement('div');
previewItem.className = 'preview-item';
const img = document.createElement('img');
img.src = e.target.result;
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
removeBtn.onclick = function() {
previewContainer.removeChild(previewItem);
};
previewItem.appendChild(img);
previewItem.appendChild(removeBtn);
previewContainer.appendChild(previewItem);
};
reader.readAsDataURL(file);
}
// 重置文件输入,允许再次选择相同文件
fileInput.value = '';
});
// 拖拽上传功能
const uploadArea = document.getElementById('upload-area');
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
uploadArea.style.borderColor = '#4facfe';
uploadArea.style.backgroundColor = '#f0f9ff';
});
uploadArea.addEventListener('dragleave', function() {
uploadArea.style.borderColor = '#cbd5e0';
uploadArea.style.backgroundColor = '#f8fafc';
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
uploadArea.style.borderColor = '#cbd5e0';
uploadArea.style.backgroundColor = '#f8fafc';
const files = e.dataTransfer.files;
fileInput.files = files;
// 触发change事件
const event = new Event('change');
fileInput.dispatchEvent(event);
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 检查登录状态
checkLoginStatus();
// 初始化图片上传功能
initImageUpload();
// 绑定发布按钮事件
document.getElementById('publish-btn').addEventListener('click', publishNote);
// 绑定取消按钮事件
document.getElementById('cancel-btn').addEventListener('click', cancelNote);
// 设置当前日期为默认值(方便测试)
const now = new Date();
document.getElementById('travel-year').value = now.getFullYear();
document.getElementById('travel-month').value = now.getMonth() + 1;
document.getElementById('travel-day').value = now.getDate();
// 添加输入验证
document.querySelectorAll('.input-field, .select-field, .textarea-field').forEach(field => {
field.addEventListener('input', function() {
this.closest('.form-group').classList.remove('error');
});
});
});
</script>
</body>
</html>

@ -0,0 +1,964 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>旅行者小明 - 智能旅游攻略系统</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部区域 */
.profile-header {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
border-radius: 16px;
padding: 30px;
color: white;
margin-bottom: 25px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.profile-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.1);
transform: rotate(30deg);
}
.profile-top {
display: flex;
align-items: center;
margin-bottom: 20px;
position: relative;
z-index: 1;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
color: white;
margin-right: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
border: 3px solid rgba(255, 255, 255, 0.3);
}
.profile-info h1 {
font-size: 28px;
margin-bottom: 5px;
font-weight: 700;
}
.profile-info p {
opacity: 0.9;
font-size: 16px;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 20px;
position: relative;
z-index: 1;
}
.stat-item {
text-align: center;
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: 700;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
/* 导航区域 */
.nav-tabs {
display: flex;
background-color: white;
border-radius: 12px;
padding: 10px;
margin-bottom: 25px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.nav-tab {
flex: 1;
text-align: center;
padding: 12px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
color: #4a5568;
}
.nav-tab:hover {
background-color: #f7fafc;
color: #4facfe;
}
.nav-tab.active {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
box-shadow: 0 4px 8px rgba(79, 172, 254, 0.3);
}
/* 内容区域 */
.content-panel {
display: none;
animation: fadeIn 0.5s ease;
}
.content-panel.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.content-section {
margin-bottom: 30px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-title {
font-size: 22px;
font-weight: 700;
color: #2d3748;
}
.add-button {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
border-radius: 30px;
padding: 10px 20px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 10px rgba(79, 172, 254, 0.3);
transition: all 0.3s ease;
}
.add-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(79, 172, 254, 0.4);
}
.trips-grid, .notes-grid, .checkins-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.trip-card, .note-card, .checkin-card {
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
cursor: pointer;
}
.trip-card:hover, .note-card:hover, .checkin-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}
.trip-image, .note-image, .checkin-image {
height: 160px;
width: 100%;
background-size: cover;
background-position: center;
}
.trip-content, .note-content, .checkin-content {
padding: 20px;
}
.trip-title, .note-title, .checkin-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
color: #2d3748;
}
.trip-meta, .note-meta, .checkin-meta {
display: flex;
justify-content: space-between;
color: #718096;
font-size: 14px;
margin-bottom: 15px;
}
.trip-budget {
color: #48bb78;
font-weight: 600;
}
.note-excerpt, .checkin-description {
color: #718096;
font-size: 14px;
line-height: 1.5;
margin-bottom: 15px;
}
.trip-actions, .note-actions, .checkin-actions {
display: flex;
justify-content: flex-end;
}
.view-details, .edit-note, .view-checkin {
background-color: #f7fafc;
color: #4a5568;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.view-details:hover, .edit-note:hover, .view-checkin:hover {
background-color: #edf2f7;
color: #4facfe;
}
/* 编辑资料表单 */
.edit-profile-form {
background-color: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2d3748;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1.5px solid #e2e8f0;
border-radius: 8px;
font-size: 15px;
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: #4facfe;
box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.2);
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.save-button {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.save-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(79, 172, 254, 0.4);
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #718096;
}
.empty-state i {
font-size: 64px;
margin-bottom: 20px;
color: #cbd5e0;
}
.empty-state h3 {
font-size: 20px;
margin-bottom: 10px;
color: #4a5568;
}
.empty-state p {
max-width: 400px;
margin: 0 auto 20px;
}
/* 底部区域 */
.footer {
text-align: center;
padding: 20px;
color: #718096;
font-size: 14px;
margin-top: 40px;
border-top: 1px solid #e2e8f0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.profile-top {
flex-direction: column;
text-align: center;
}
.avatar {
margin-right: 0;
margin-bottom: 15px;
}
.stats {
flex-wrap: wrap;
}
.stat-item {
flex: 0 0 50%;
margin-bottom: 15px;
}
.nav-tabs {
flex-wrap: wrap;
}
.nav-tab {
flex: 0 0 50%;
margin-bottom: 5px;
}
.trips-grid, .notes-grid, .checkins-grid {
grid-template-columns: 1fr;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 用户信息头部 -->
<div class="profile-header">
<div class="profile-top">
<div class="avatar">
<i class="fas fa-user"></i>
</div>
<div class="profile-info">
<h1 id="user-name">旅行者小明</h1>
<p><i class="far fa-calendar-alt"></i> 注册时间:<span id="register-date">2023-05-18</span></p>
</div>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="notes-count">5</div>
<div class="stat-label">篇笔记</div>
</div>
<div class="stat-item">
<div class="stat-value" id="checkins-count">12</div>
<div class="stat-label">个打卡点</div>
</div>
<div class="stat-item">
<div class="stat-value" id="trips-count">8</div>
<div class="stat-label">个行程</div>
</div>
</div>
</div>
<!-- 导航标签 -->
<div class="nav-tabs">
<div class="nav-tab" data-target="edit-profile">编辑资料</div>
<div class="nav-tab active" data-target="my-trips">我的行程</div>
<div class="nav-tab" data-target="my-notes">我的笔记</div>
<div class="nav-tab" data-target="my-checkins">我的打卡</div>
</div>
<!-- 编辑资料内容 -->
<div class="content-panel" id="edit-profile">
<div class="content-section">
<h2 class="section-title">编辑个人资料</h2>
<div class="edit-profile-form">
<div class="form-group">
<label class="form-label">用户名</label>
<input type="text" class="form-input" id="edit-username" value="旅行者小明">
</div>
<div class="form-group">
<label class="form-label">个人简介</label>
<textarea class="form-input form-textarea" id="edit-bio">热爱旅行,喜欢探索未知的世界,记录旅途中的美好瞬间。</textarea>
</div>
<div class="form-group">
<label class="form-label">邮箱地址</label>
<input type="email" class="form-input" id="edit-email" value="xiaoming@example.com">
</div>
<div class="form-group">
<label class="form-label">手机号码</label>
<input type="tel" class="form-input" id="edit-phone" value="138****5678">
</div>
<button class="save-button" id="save-profile">保存更改</button>
</div>
</div>
</div>
<!-- 我的行程内容 -->
<div class="content-panel active" id="my-trips">
<div class="content-section">
<div class="section-header">
<h2 class="section-title">我的行程</h2>
<button class="add-button">
<i class="fas fa-plus"></i>
生成新行程
</button>
</div>
<div class="trips-grid" id="trips-container">
<!-- 行程卡片将通过JavaScript动态生成 -->
</div>
</div>
</div>
<!-- 我的笔记内容 -->
<div class="content-panel" id="my-notes">
<div class="content-section">
<div class="section-header">
<h2 class="section-title">我的笔记</h2>
<button class="add-button" id="add-note-btn">
<i class="fas fa-plus"></i>
写新笔记
</button>
</div>
<div class="notes-grid" id="notes-container">
<!-- 笔记卡片将通过JavaScript动态生成 -->
</div>
</div>
</div>
<!-- 我的打卡内容 -->
<div class="content-panel" id="my-checkins">
<div class="content-section">
<div class="section-header">
<h2 class="section-title">我的打卡</h2>
</div>
<div class="checkins-grid" id="checkins-container">
<!-- 打卡卡片将通过JavaScript动态生成 -->
</div>
</div>
</div>
<!-- 底部信息 -->
<div class="footer">
© 2023 智能旅游攻略系统
</div>
</div>
<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);
}
}
getUser() {
return this.currentUser;
}
isLoggedIn() {
return this.currentUser !== null;
}
}
// 笔记管理类
class NoteManager {
constructor() {
this.userNotes = [];
this.init();
}
init() {
// 从localStorage加载用户笔记
const savedNotes = localStorage.getItem('userNotes');
if (savedNotes) {
this.userNotes = JSON.parse(savedNotes);
console.log('已加载笔记:', this.userNotes);
}
}
getNotesByUser(userId) {
return this.userNotes.filter(note => note.author === userId);
}
addNote(note) {
this.userNotes.push(note);
localStorage.setItem('userNotes', JSON.stringify(this.userNotes));
console.log('笔记已添加:', note);
}
}
// 行程管理类
class TripManager {
constructor() {
this.userTrips = [];
this.init();
}
init() {
// 从localStorage加载用户行程
const savedTrips = localStorage.getItem('userTrips');
if (savedTrips) {
this.userTrips = JSON.parse(savedTrips);
console.log('已加载行程:', this.userTrips);
} else {
// 如果没有行程数据,创建一些示例数据
this.userTrips = [
{
id: 1,
title: "成都3日美食探索行程",
image: "https://images.unsplash.com/photo-1554797589-7241bb691973?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80",
createTime: "2023-09-10",
budget: "3000-5000元",
author: "default"
},
{
id: 2,
title: "杭州2日自然风光行程",
image: "https://images.unsplash.com/photo-1581941288399-0a91b49d90c9?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80",
createTime: "2023-08-25",
budget: "2000-3000元",
author: "default"
},
{
id: 3,
title: "丽江5日文化探索行程",
image: "https://images.unsplash.com/photo-1599733649269-24d4830a57b0?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80",
createTime: "2023-10-05",
budget: "4000-6000元",
author: "default"
}
];
localStorage.setItem('userTrips', JSON.stringify(this.userTrips));
}
}
getTripsByUser(userId) {
return this.userTrips.filter(trip => trip.author === userId || trip.author === "default");
}
}
// 打卡管理类
class CheckinManager {
constructor() {
this.userCheckins = [];
this.init();
}
init() {
// 从localStorage加载用户打卡
const savedCheckins = localStorage.getItem('userCheckins');
if (savedCheckins) {
this.userCheckins = JSON.parse(savedCheckins);
console.log('已加载打卡:', this.userCheckins);
} else {
// 如果没有打卡数据,创建一些示例数据
this.userCheckins = [
{
id: 1,
title: "宽窄巷子",
image: "https://images.unsplash.com/photo-1562569633-622763f95dfd?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80",
time: "2023-09-12",
location: "成都",
description: "宽窄巷子是成都遗留下来的较成规模的清朝古街道,由宽巷子、窄巷子和井巷子组成...",
author: "default"
},
{
id: 2,
title: "西湖断桥",
image: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80",
time: "2023-08-28",
location: "杭州",
description: "西湖断桥位于杭州北里湖和外西湖的分水点上,一端跨着北山路,另一端接通白堤...",
author: "default"
}
];
localStorage.setItem('userCheckins', JSON.stringify(this.userCheckins));
}
}
getCheckinsByUser(userId) {
return this.userCheckins.filter(checkin => checkin.author === userId || checkin.author === "default");
}
}
// 初始化管理器
const userManager = new UserManager();
const noteManager = new NoteManager();
const tripManager = new TripManager();
const checkinManager = new CheckinManager();
// 导航标签交互
// 在 profile.html 的 JavaScript 中添加或修改以下代码
// 在页面加载时检查登录状态
document.addEventListener('DOMContentLoaded', function() {
const user = userManager.getUser();
if (!user) {
alert('请先登录');
window.location.href = 'login.html';
return;
}
// 加载用户数据
document.getElementById('user-name').textContent = user.name || '旅行者';
// 初始加载我的行程(默认标签页)
loadTrips();
});
// 确保点击"我的笔记"标签时能正确加载
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.addEventListener('click', function() {
const targetId = this.getAttribute('data-target');
// 移除所有active类
document.querySelectorAll('.nav-tab').forEach(t => {
t.classList.remove('active');
});
document.querySelectorAll('.content-panel').forEach(panel => {
panel.classList.remove('active');
});
// 添加active类
this.classList.add('active');
document.getElementById(targetId).classList.add('active');
// 根据目标加载相应内容
if (targetId === 'my-notes') {
loadNotes();
} else if (targetId === 'my-trips') {
loadTrips();
} else if (targetId === 'my-checkins') {
loadCheckins();
}
});
});
// 确保 loadNotes 函数能正确工作
function loadNotes() {
const user = userManager.getUser();
const notesContainer = document.getElementById('notes-container');
if (!user) {
notesContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-sticky-note"></i>
<h3>请先登录</h3>
<p>登录后才能查看和发布笔记</p>
<a href="login.html" class="add-button">
<i class="fas fa-sign-in-alt"></i>
立即登录
</a>
</div>
`;
return;
}
const userNotes = noteManager.getNotesByUser(user.id);
// 更新笔记数量统计
document.getElementById('notes-count').textContent = userNotes.length;
if (userNotes.length === 0) {
notesContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-sticky-note"></i>
<h3>暂无笔记</h3>
<p>您还没有发布过任何旅行笔记,快去分享您的旅行经历吧!</p>
<a href="postnote.html" class="add-button">
<i class="fas fa-plus"></i>
写新笔记
</a>
</div>
`;
return;
}
// 生成笔记卡片
notesContainer.innerHTML = userNotes.map(note => `
<div class="note-card" onclick="viewNote(${note.id})">
<div class="note-image" style="background-image: url('${note.images && note.images.length > 0 ? note.images[0] : 'https://images.unsplash.com/photo-1549294413-26f195200c16?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80'}')"></div>
<div class="note-content">
<h3 class="note-title">${note.title}</h3>
<div class="note-meta">
<span><i class="far fa-clock"></i> 发布时间:${new Date(note.publishTime).toLocaleDateString()}</span>
<span><i class="far fa-heart"></i> ${note.likes || 0} 点赞</span>
</div>
<p class="note-excerpt">${note.content.substring(0, 100)}...</p>
<div class="note-actions">
<button class="edit-note" data-note-id="${note.id}">编辑笔记</button>
</div>
</div>
</div>
`).join('');
}
// 为编辑按钮添加事件
document.querySelectorAll('.edit-note').forEach(button => {
button.addEventListener('click', function(e) {
e.stopPropagation();
const noteId = this.getAttribute('data-note-id');
// 这里可以实现编辑功能,例如跳转到编辑页面
alert(`编辑笔记ID: ${noteId}`);
});
});
// 加载并显示用户行程
function loadTrips() {
const user = userManager.getUser();
const tripsContainer = document.getElementById('trips-container');
if (!user) {
tripsContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-route"></i>
<h3>请先登录</h3>
<p>登录后才能查看行程</p>
<a href="login.html" class="add-button">
<i class="fas fa-sign-in-alt"></i>
立即登录
</a>
</div>
`;
return;
}
const userTrips = tripManager.getTripsByUser(user.id);
// 更新行程数量统计
document.getElementById('trips-count').textContent = userTrips.length;
if (userTrips.length === 0) {
tripsContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-route"></i>
<h3>暂无行程</h3>
<p>您还没有创建任何行程</p>
<button class="add-button">
<i class="fas fa-plus"></i>
生成新行程
</button>
</div>
`;
return;
}
// 生成行程卡片
tripsContainer.innerHTML = userTrips.map(trip => `
<div class="trip-card">
<div class="trip-image" style="background-image: url('${trip.image}')"></div>
<div class="trip-content">
<h3 class="trip-title">${trip.title}</h3>
<div class="trip-meta">
<span><i class="far fa-clock"></i> 生成时间:${trip.createTime}</span>
<span class="trip-budget"><i class="fas fa-coins"></i> 预算:${trip.budget}</span>
</div>
<div class="trip-actions">
<button class="view-details">查看详情</button>
</div>
</div>
</div>
`).join('');
}
// 加载并显示用户打卡
function loadCheckins() {
const user = userManager.getUser();
const checkinsContainer = document.getElementById('checkins-container');
if (!user) {
checkinsContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-map-marker-alt"></i>
<h3>请先登录</h3>
<p>登录后才能查看打卡记录</p>
<a href="login.html" class="add-button">
<i class="fas fa-sign-in-alt"></i>
立即登录
</a>
</div>
`;
return;
}
const userCheckins = checkinManager.getCheckinsByUser(user.id);
// 更新打卡数量统计
document.getElementById('checkins-count').textContent = userCheckins.length;
if (userCheckins.length === 0) {
checkinsContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-map-marker-alt"></i>
<h3>暂无打卡</h3>
<p>您还没有任何打卡记录</p>
<a href="checkin.html" class="add-button">
<i class="fas fa-plus"></i>
新增打卡
</a>
</div>
`;
return;
}
// 生成打卡卡片
checkinsContainer.innerHTML = userCheckins.map(checkin => `
<div class="checkin-card">
<div class="checkin-image" style="background-image: url('${checkin.image}')"></div>
<div class="checkin-content">
<h3 class="checkin-title">${checkin.title}</h3>
<div class="checkin-meta">
<span><i class="far fa-clock"></i> 打卡时间:${checkin.time}</span>
<span><i class="fas fa-map-marker-alt"></i> ${checkin.location}</span>
</div>
<p class="checkin-description">${checkin.description}</p>
<div class="checkin-actions">
<button class="view-checkin">查看详情</button>
</div>
</div>
</div>
`).join('');
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
// 加载用户数据
const user = userManager.getUser();
if (user) {
document.getElementById('user-name').textContent = user.name || '旅行者';
}
// 加载笔记
loadNotes();
// 加载行程
loadTrips();
// 加载打卡
loadCheckins();
// 添加笔记按钮事件
document.getElementById('add-note-btn').addEventListener('click', function() {
if (userManager.isLoggedIn()) {
window.location.href = 'postnote.html';
} else {
alert('请先登录后再发表笔记');
window.location.href = 'login.html';
}
});
// 保存资料按钮
document.getElementById('save-profile').addEventListener('click', function() {
const username = document.getElementById('edit-username').value;
const bio = document.getElementById('edit-bio').value;
const email = document.getElementById('edit-email').value;
const phone = document.getElementById('edit-phone').value;
// 更新用户信息
document.getElementById('user-name').textContent = username;
// 在实际项目中这里应该调用API保存到后端
alert('个人资料已保存!');
});
// 查看笔记详情
function viewNote(noteId) {
// 这里可以实现跳转到笔记详情页或显示模态框
alert(`查看笔记 ID: ${noteId}`);
// 实际项目中可以这样跳转:
// window.location.href = `note-detail.html?id=${noteId}`;
}
// 编辑笔记
function editNote(noteId) {
// 跳转到编辑页面或显示编辑模态框
alert(`编辑笔记 ID: ${noteId}`);
// 实际项目中可以这样跳转:
// window.location.href = `edit-note.html?id=${noteId}`;
}
});
</script>
</body>
</html>

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册</title>
<link rel="stylesheet" href="../css/base.css">
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
</head>
<body>
<nav class="navbar">
<ul>
<li><a href="index.html">首页</a></li>
<li><a href="login.html">登录</a></li>
<li class="active"><a href="register.html">注册</a></li>
<li><a href="profile.html">个人中心</a></li>
<li><a href="postnote.html">发表笔记</a></li>
<li><a href="checkin.html">打卡成就</a></li>
<li><a href="ai.html">智能行程规划</a></li>
</ul>
</nav>
<main class="auth-container">
<div class="auth-card">
<h1>用户注册</h1>
<form id="register-form">
<div class="form-group">
<label for="reg-email">
<i class="fa fa-envelope-o" aria-hidden="true"></i>
邮箱
</label>
<input type="email" id="reg-email" placeholder="请输入邮箱" required>
</div>
<div class="form-group">
<label for="reg-password">
<i class="fa fa-lock" aria-hidden="true"></i>
密码至少6位
</label>
<input type="password" id="reg-password" minlength="6" placeholder="请设置密码" required>
</div>
<div class="form-group">
<label for="confirm-password">
<i class="fa fa-lock" aria-hidden="true"></i>
确认密码
</label>
<input type="password" id="confirm-password" placeholder="请再次输入密码" required>
</div>
<div class="form-group">
<label for="verify-code">
<i class="fa fa-shield" aria-hidden="true"></i>
验证码
</label>
<div class="verify-code-group">
<input type="text" id="verify-code" placeholder="请输入验证码" required>
<button type="button" class="get-code-btn">获取验证码</button>
</div>
</div>
<label class="agree-terms">
<input type="checkbox" required>
我已阅读并同意<a href="#">用户协议</a><a href="#">隐私政策</a>
</label>
<button type="submit" class="btn primary-btn">注册</button>
<div class="auth-switch">
已有账号?<a href="login.html">立即登录</a>
</div>
</form>
</div>
</main>
<footer class="footer">
<p>© 2023 智能旅游攻略系统</p>
</footer>
<script>
// 验证码倒计时功能
const getCodeBtn = document.querySelector('.get-code-btn');
getCodeBtn.addEventListener('click', () => {
let countdown = 60;
getCodeBtn.disabled = true;
getCodeBtn.textContent = `重新获取(${countdown}s)`;
const timer = setInterval(() => {
countdown--;
getCodeBtn.textContent = `重新获取(${countdown}s)`;
if (countdown <= 0) {
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = '获取验证码';
}
}, 1000);
});
// 注册表单提交
document.getElementById('register-form').addEventListener('submit', (e) => {
e.preventDefault();
const password = document.getElementById('reg-password').value;
const confirmPwd = document.getElementById('confirm-password').value;
if (password !== confirmPwd) {
alert('两次输入的密码不一致!');
return;
}
// 模拟注册成功
alert('注册成功!即将跳转到登录页');
window.location.href = 'login.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…
Cancel
Save