fumeng 7 months ago committed by xuhao
commit 67e0103d73

@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

@ -0,0 +1,6 @@
{
"ExpandedNodes": [
""
],
"PreviewInSolutionExplorer": false
}

Binary file not shown.

@ -0,0 +1,12 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\11855\\Desktop\\\u674E\u5B66\u6210\\lvgl\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": []
}
]
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

@ -218,6 +218,17 @@
font-size: 16px;
}
/* 攻略主标题样式 */
.strategy-main-title {
text-align: center;
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #2c83f2;
}
/* 每日行程样式 */
.day-section {
margin-bottom: 36px;
@ -237,48 +248,44 @@
border-left: 3px solid #2c83f2;
}
/* 行程项样式 */
/* 行程项样式 - 修改为严格的时间-地点:描述格式 */
.schedule-item {
margin-bottom: 28px;
padding-left: 24px;
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 8px;
position: relative;
}
.schedule-item::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #2c83f2;
}
.schedule-time {
font-weight: 600;
color: #666;
margin-bottom: 8px;
color: #333;
margin-bottom: 4px;
font-size: 15px;
}
.schedule-content {
margin-bottom: 12px;
margin-bottom: 0;
display: flex;
align-items: flex-start;
}
.schedule-location {
color: #2c83f2;
font-weight: 600;
font-size: 17px;
font-weight: 500;
margin-right: 4px;
font-size: 16px;
}
.schedule-desc {
color: #666;
margin-top: 8px;
padding: 12px;
background-color: #f9fafc;
border-radius: 4px;
border-left: 2px solid #e5e6eb;
margin: 0;
padding: 0;
background: none;
border-left: none;
border-radius: 0;
font-size: 15px;
line-height: 1.6;
}
/* 预算总结样式 */
@ -295,6 +302,13 @@
margin-bottom: 16px;
color: #333;
font-size: 18px;
text-align: center;
}
.budget-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.budget-item {
@ -303,6 +317,7 @@
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #e6f4ff;
color: #666;
}
.budget-item:last-child:not(.budget-total) {
@ -316,6 +331,9 @@
padding-top: 16px;
border-top: 1px solid #e6f4ff;
font-size: 18px;
grid-column: 1 / 3;
display: flex;
justify-content: space-between;
}
/* 生成中状态样式 */
@ -378,4 +396,22 @@
.budget-summary {
padding: 16px;
}
}
.budget-list {
grid-template-columns: 1fr;
gap: 8px;
}
.budget-total {
grid-column: 1;
}
.schedule-content {
flex-direction: column;
align-items: flex-start;
}
.schedule-location {
margin-bottom: 4px;
}
}

@ -0,0 +1,249 @@
/* ticket-buy.css */
.ticket-wrapper {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.ticket-title {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
color: #333;
}
.spot-info-card {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
border-left: 4px solid #007bff;
}
.spot-info-card h3 {
margin: 0 0 10px 0;
color: #333;
}
.spot-info-card .spot-meta {
color: #666;
font-size: 14px;
line-height: 1.6;
}
.contact-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.form-group input:focus {
border-color: #007bff;
outline: none;
}
.ticket-form {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.form-section h3 {
margin-bottom: 15px;
color: #333;
}
.ticket-type-selector {
display: flex;
flex-direction: column;
gap: 10px;
}
.ticket-type {
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.ticket-type:hover {
border-color: #007bff;
}
.ticket-type.active {
border-color: #007bff;
background: #f8f9ff;
}
.ticket-type-info {
flex: 1;
}
.ticket-type-name {
font-weight: bold;
margin-bottom: 5px;
}
.ticket-type-desc {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.ticket-type-requirement {
color: #999;
font-size: 12px;
}
.ticket-type-price {
color: #007bff;
font-size: 18px;
font-weight: bold;
text-align: right;
}
.quantity-selector {
display: flex;
align-items: center;
gap: 10px;
}
.quantity-btn {
width: 40px;
height: 40px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.quantity-btn:hover {
background: #f8f9fa;
}
.quantity-btn:disabled {
background: #f8f9fa;
color: #ccc;
cursor: not-allowed;
}
#quantity {
width: 60px;
height: 40px;
text-align: center;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.quantity-label {
color: #666;
}
#visitDate {
width: 100%;
max-width: 200px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.date-tip {
color: #666;
font-size: 14px;
margin-top: 5px;
}
.price-summary {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.price-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
color: #666;
}
.price-total {
display: flex;
justify-content: space-between;
font-size: 18px;
font-weight: bold;
color: #333;
padding-top: 10px;
border-top: 1px solid #ddd;
}
.total-amount {
color: #e74c3c;
font-size: 24px;
}
.form-footer {
display: flex;
gap: 15px;
justify-content: center;
}
.cancel-btn {
background: #6c757d;
color: white;
}
.cancel-btn:hover {
background: #5a6268;
}
.submit-btn {
min-width: 120px;
}
.submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.loading {
text-align: center;
color: #666;
padding: 20px;
}

@ -1,32 +1,103 @@
// ******************************
// API接口配置文件
// 后端服务器部署在另一台电脑此处配置其IP和端口
// 修改为连接本地后端服务
// ******************************
// 后端服务器基础地址
// 替换为实际的后端服务器IP和端口例如http://192.168.1.100:8080
const API_BASE = 'http://后端服务器IP:后端端口';
// 后端服务器基础地址 - 修改为本地地址
const BACKEND_BASE_URL = 'http://localhost:8080';
// 所有API接口路径配置
const API = {
// 认证相关接口
login: `${API_BASE}/api/user/login`, // 用户登录
register: `${API_BASE}/api/user/register`, // 用户注册
// 用户相关
login: `${BACKEND_BASE_URL}/api/user/login`,
register: `${BACKEND_BASE_URL}/api/user/register`,
getCode: `${BACKEND_BASE_URL}/api/user/send-verify-code`, // 注册时发送验证码
sendVerifyCode: `${BACKEND_BASE_URL}/api/user/send-verify-code`,
getUserInfo: `${BACKEND_BASE_URL}/api/user/info`,
// spotList: `${BACKEND_BASE_URL}/api/spot/list`,
// spotSearch: `${BACKEND_BASE_URL}/api/spot/search`,
// 忘记密码相关接口
sendVerifyCode: `${API_BASE}/api/verify-code/send`, // 发送验证码(用于密码重置)
resetPassword: `${API_BASE}/api/user/reset-password`,// 重置密码
//sendVerifyCode: `${BACKEND_BASE_URL}/api/verify-code/send`,
resetPassword: `${BACKEND_BASE_URL}/api/user/reset-password`,
// 景点推荐相关接口
recommendSpots: `${API_BASE}/api/spots/recommend`, // 获取推荐景点列表
recommendSpots: `${BACKEND_BASE_URL}/api/spots/recommend`,
hotSpots: `${BACKEND_BASE_URL}/api/spots/hot`, // 新增热门景点接口
getAllSpots: `${BACKEND_BASE_URL}/api/spots`,
// 景点详情接口(用于加载景点信息)
spotDetail: `${BACKEND_BASE_URL}/api/spots`, // 对应SpotController的getSpotById接口
// 验证购票记录接口(新增)
createOrder: `${BACKEND_BASE_URL}/api/tickets/create-order`,
payOrder: `${BACKEND_BASE_URL}/api/tickets/pay`,
verifyTicket: `${BACKEND_BASE_URL}/api/tickets/verify`,
getTicketTypes: `${BACKEND_BASE_URL}/api/tickets/types`, // 获取票种接口
// 提交评论接口对应ReviewController的submitReview
reviewSubmit: `${BACKEND_BASE_URL}/api/reviews` ,
reviewList: `${BACKEND_BASE_URL}/api/reviews`, // 获取所有评论
reviewVerify: `${BACKEND_BASE_URL}/api/reviews`, // 验证评论需要拼接reviewId
// 区块链测试
fabricTest: `${BACKEND_BASE_URL}/api/reviews/test-fabric`,
// 其他接口...
getUserInfo: `${API_BASE}/api/user/info`, // 获取用户信息
myOrderList: `${API_BASE}/api/orders/my`, // 我的订单列表
myStrategyList: `${API_BASE}/api/strategies/my`, // 我的攻略列表
ticketRefund: `${API_BASE}/api/ticket/refund`, // 退票
ticketChange: `${API_BASE}/api/ticket/change`, // 改签
updateUserInfo: `${API_BASE}/api/user/update`, // 更新用户信息
strategyGenerate: `${API_BASE}/api/strategy/generate`,// 生成攻略
strategySave: `${API_BASE}/api/strategy/save` // 保存攻略
myOrderList: `${BACKEND_BASE_URL}/api/orders/my`,
myStrategyList: `${BACKEND_BASE_URL}/api/strategies/my`,
ticketRefund: `${BACKEND_BASE_URL}/api/ticket/refund`,
ticketChange: `${BACKEND_BASE_URL}/api/ticket/change`,
updateUserInfo: `${BACKEND_BASE_URL}/api/user/update`,
strategyGenerate: `${BACKEND_BASE_URL}/api/strategy/generate`,
strategySave: `${BACKEND_BASE_URL}/api/strategy/save`
};
// 通用的请求头配置
function getAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
};
}
// 响应拦截器
api.interceptors.response.use(
response => response.data,
error => {
console.error('请求错误:', error.response?.data || error.message);
// 检测JWT过期后端可能返回401状态码
if (error.response && error.response.status === 401) {
// 清除本地过期令牌
localStorage.removeItem('token');
localStorage.removeItem('userId');
// 跳转到登录页
window.location.href = '/login.html';
alert('登录已过期,请重新登录');
}
return Promise.reject(error);
}
);
// 通用的响应处理函数
function handleResponse(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
// 通用的错误处理
function handleError(error) {
console.error('API请求错误:', error);
throw error;
}

@ -34,30 +34,41 @@ function updateUserNav() {
function loadSpotList() {
const spotGrid = document.getElementById('spotGrid');
// 清空现有列表(避免重复渲染)
spotGrid.innerHTML = '';
spotGrid.innerHTML = '<div class="loading">加载中...</div>';
// 3.1 调用后端景点列表接口GET请求支持分页/搜索)
fetch(API.spotList, { // api.js中定义API.spotList = 'http://后端IP:端口/api/spot/list'
//fetch(API.spotList, { // api.js中定义API.spotList = 'http://后端IP:端口/api/spot/list'
fetch(API.hotSpots, { // 确保这里用的是 hotSpots不是 spotList
method: 'GET',
headers: {
// 若接口需要登录携带token可选根据后端要求
'Authorization': 'Bearer ' + localStorage.getItem('token')
//'Authorization': 'Bearer ' + localStorage.getItem('token')
'Content-Type': 'application/json',
// 热门景点是公开接口不需要token
},
// 若需要分页/筛选在URL后拼接参数?page=1&size=10&keyword=北京
})
.then(response => {
if (!response.ok) {
console.log('响应状态:', response.status);
throw new Error('景点数据加载失败');
}
return response.json();
})
.then(data => {
// 3.2 后端返回景点列表数据,动态渲染卡片
const spots = data.data.list; // 假设后端返回格式:{ code:200, data: { list: [景点1, 景点2...] } }
if (spots.length === 0) {
//const spots = data.data.list; // 假设后端返回格式:{ code:200, data: { list: [景点1, 景点2...] } }
const spots = data.data; // 后端返回的是 data.data 数组,不是 data.data.list
spotGrid.innerHTML = '';
// if (spots.length === 0) {
// spotGrid.innerHTML = '<div class="no-spot">暂无景点数据</div>';
// return;
// }
if (!spots || spots.length === 0) {
spotGrid.innerHTML = '<div class="no-spot">暂无景点数据</div>';
return;
}
console.log('成功获取景点数量:', spots.length);
// 3.3 遍历景点数据,生成卡片
spots.forEach(spot => {
@ -120,10 +131,17 @@ function loadSpotListWithKeyword(keyword) {
spotGrid.innerHTML = '<div class="loading">加载中...</div>';
// 调用后端搜索接口GET请求参数拼接在URL后
fetch(`${API.spotSearch}?keyword=${encodeURIComponent(keyword)}`, { // api.js中定义API.spotSearch = 'http://后端IP:端口/api/spot/search'
// fetch(`${API.spotSearch}?keyword=${encodeURIComponent(keyword)}`, { // api.js中定义API.spotSearch = 'http://后端IP:端口/api/spot/search'
// method: 'GET',
// headers: {
// 'Authorization': 'Bearer ' + localStorage.getItem('token')
// }
// })
// 调用推荐景点接口进行搜索
fetch(`${API.recommendSpots}?destination=${encodeURIComponent(keyword)}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
'Content-Type': 'application/json'
}
})
.then(response => {
@ -135,7 +153,8 @@ function loadSpotListWithKeyword(keyword) {
.then(data => {
// 渲染逻辑同loadSpotList可复用代码
spotGrid.innerHTML = '';
const spots = data.data.list;
//const spots = data.data.list;
const spots = data.data; // 注意:这里是 data.data
if (spots.length === 0) {
spotGrid.innerHTML = '<div class="no-spot">未找到与"${keyword}"相关的景点</div>';
return;

@ -1,46 +1,58 @@
// 1. 接口地址已在api.js中统一管理此处直接引用
// api.js中需定义const API = { login: 'http://你的后端IP:端口/api/user/login' };
// api.js中需定义const API = { login: 'http://172.20.10.2:端口/api/user/login' };
// 2. 监听登录表单提交事件
// 监听登录表单提交事件
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault(); // 阻止表单默认提交
e.preventDefault();
// 3. 获取用户输入的账号和密码
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
// 4. 调用后端登录接口POST请求
// 显示加载状态
const submitBtn = document.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.textContent = '登录中...';
submitBtn.disabled = true;
fetch(API.login, {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 告诉后端请求体是JSON格式
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username, // 后端接收的账号参数名(需与后端一致)
password: password // 后端接收的密码参数名(需与后端一致)
username: username,
password: password
})
})
.then(response => {
// 5. 处理后端响应(先判断响应状态)
if (!response.ok) {
throw new Error('登录失败,请检查账号密码');
}
return response.json(); // 解析后端返回的JSON数据
return response.json();
})
.then(data => {
// 6. 登录成功后的处理后端需返回token、用户ID等关键信息
console.log('登录成功', data);
// 6.1 存储用户信息到localStorage供其他页面使用
localStorage.setItem('token', data.token); // 存储身份令牌
localStorage.setItem('userId', data.userId); // 存储用户ID
localStorage.setItem('username', data.username); // 存储用户名
// 6.2 跳转到首页
window.location.href = '../pages/index.html';
if (data.code === 200) {
// 存储用户信息
localStorage.setItem('token', data.token);
localStorage.setItem('userId', data.userId.toString()); // 或者直接 data.userId
localStorage.setItem('username', data.username);
// 跳转到首页
window.location.href = '../pages/index.html';
} else {
throw new Error(data.message || '登录失败');
}
})
.catch(error => {
// 7. 登录失败处理(提示用户)
alert(error.message);
console.error('登录错误', error);
})
.finally(() => {
// 恢复按钮状态
submitBtn.textContent = originalText;
submitBtn.disabled = false;
});
});

@ -1,47 +1,103 @@
// 调试信息
console.log('register.js 已加载');
console.log('当前页面URL:', window.location.href);
// 检查API配置
console.log('API配置:', typeof API !== 'undefined' ? API : 'API未定义');
console.log('BASE_URL:', typeof BASE_URL !== 'undefined' ? BASE_URL : 'BASE_URL未定义');
// 页面加载完成后执行
window.onload = function() {
console.log('页面加载完成,开始绑定事件');
// 绑定验证码按钮点击事件
bindGetCodeEvent();
// 绑定注册表单提交事件
bindRegisterFormSubmit();
};
// 页面加载完成后执行
window.onload = function() {
// 绑定验证码按钮点击事件
bindGetCodeEvent();
// 绑定注册表单提交事件
bindRegisterFormSubmit();
};
// 1. 绑定获取验证码事件(参考文档:注册用例-验证码需求)
function bindGetCodeEvent() {
const getCodeBtn = document.getElementById('getCodeBtn');
const usernameInput = document.getElementById('username');
let countdown = 0; // 倒计时秒数
console.log('绑定获取验证码按钮:', getCodeBtn);
getCodeBtn.addEventListener('click', function() {
console.log('点击获取验证码按钮');
const username = usernameInput.value.trim();
// 验证账号格式(手机号/邮箱)
console.log('输入的账号:', username);
// 验证账号格式
if (!username) {
alert('请先输入账号(手机号或邮箱)');
return;
}
const isPhone = /^1[3-9]\d{9}$/.test(username);
const isEmail = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(username);
console.log('手机号验证:', isPhone, '邮箱验证:', isEmail);
if (!isPhone && !isEmail) {
alert('请输入正确的手机号或邮箱');
return;
}
// 检查API是否定义
if (!API || !API.getCode) {
console.error('API.getCode 未定义');
alert('系统配置错误,请刷新页面重试');
return;
}
console.log('准备发送请求到:', API.getCode);
// 调用后端获取验证码接口
fetch(API.getCode, { // 注需在api.js补充getCode接口`getCode: ${BASE_URL}/user/getCode`
fetch(API.getCode, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username, type: isPhone ? 'phone' : 'email' })
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: username,
type: isPhone ? 'phone' : 'email'
})
})
.then(response => {
if (!response.ok) throw new Error('验证码发送失败');
console.log('收到响应,状态码:', response.status);
console.log('响应头:', response.headers);
if (!response.ok) {
// 尝试获取错误信息
return response.text().then(text => {
console.error('响应内容:', text);
throw new Error(`验证码发送失败 (${response.status})`);
});
}
return response.json();
})
.then(data => {
console.log('解析后的数据:', data);
alert('验证码已发送,请注意查收');
// 启动倒计时参考文档1小时内最多获取5次验证码后端控制
countdown = 60;
// 启动倒计时
let countdown = 60;
getCodeBtn.disabled = true;
getCodeBtn.textContent = `重新获取(${countdown}s)`;
const timer = setInterval(() => {
countdown--;
getCodeBtn.textContent = `重新获取(${countdown}s)`;
@ -53,8 +109,12 @@ function bindGetCodeEvent() {
}, 1000);
})
.catch(error => {
alert(error.message);
console.error('获取验证码错误', error);
console.error('获取验证码完整错误:', error);
console.error('错误名称:', error.name);
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
alert('验证码发送失败: ' + error.message);
});
});
}
@ -75,15 +135,15 @@ function bindRegisterFormSubmit() {
const code = codeInput.value.trim();
// 密码复杂度验证(参考文档:注册用例-业务规则)
const passwordReg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/;
if (!passwordReg.test(password)) {
alert('密码需满足8-20位含大小写字母、数字、特殊符号');
return;
}
if (!code) {
alert('请输入验证码');
return;
}
// const passwordReg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/;
// if (!passwordReg.test(password)) {
// alert('密码需满足8-20位含大小写字母、数字、特殊符号');
// return;
// }
// if (!code) {
// alert('请输入验证码');
// return;
// }
// 2. 调用后端注册接口
fetch(API.register, {

@ -4,7 +4,9 @@ let currentSpotName = '';
// 页面加载完成后执行
window.onload = function() {
// 1. 验证登录态(参考文档:评论用例-前置条件:已登录)
console.log('=== 评论页面开始加载 ===');
// 1. 验证登录态
const token = localStorage.getItem('token');
if (!token) {
alert('请先登录后再发表评论');
@ -15,6 +17,8 @@ window.onload = function() {
// 2. 解析URL中的景点ID
const urlParams = new URLSearchParams(window.location.search);
currentSpotId = urlParams.get('spotId');
console.log('景点ID:', currentSpotId);
if (!currentSpotId) {
alert('未找到景点ID即将返回首页');
window.location.href = '../pages/index.html';
@ -24,16 +28,16 @@ window.onload = function() {
// 3. 切换导航栏用户状态
updateUserNav();
// 4. 加载景点信息(用于提示用户当前评论的景点)
// 4. 加载景点信息
loadSpotInfo();
// 5. 绑定评论内容字数统计
bindContentLengthCount();
// 6. 绑定评论表单提交事件(核心:提交+区块链存证)
// 6. 绑定评论表单提交事件
bindReviewFormSubmit();
// 7. 验证用户是否有该景点的购票记录(参考文档:评论用例-前置条件:有有效购票记录)
// 7. 验证用户是否有该景点的购票记录
verifyUserTicket();
};
@ -58,25 +62,30 @@ function loadSpotInfo() {
const spotTip = document.getElementById('spotTip');
spotTip.textContent = '加载景点信息中...';
fetch(`${API.spotDetail}?spotId=${currentSpotId}`, {
console.log('加载景点信息ID:', currentSpotId);
// 修正接口路径
fetch(`${API.spotDetail}/${currentSpotId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
headers: getAuthHeaders()
})
.then(response => {
console.log('景点信息响应状态:', response.status);
if (!response.ok) throw new Error('景点信息加载失败');
return response.json();
})
.then(data => {
currentSpotName = data.data.name;
.then(spot => {
console.log('景点信息:', spot);
currentSpotName = spot.name;
spotTip.textContent = `当前评论景点:${currentSpotName}(请确保分享真实游玩体验)`;
})
.catch(error => {
console.error('景点信息加载错误:', error);
spotTip.textContent = `景点信息加载失败:${error.message}`;
console.error('景点信息加载错误', error);
});
}
// 3. 绑定评论内容字数统计(参考文档:评论用例-业务规则至少20字
// 3. 绑定评论内容字数统计
function bindContentLengthCount() {
const contentInput = document.getElementById('reviewContent');
const lengthDom = document.getElementById('contentLength');
@ -98,37 +107,94 @@ function bindContentLengthCount() {
});
}
// 4. 验证用户是否有该景点的购票记录(参考文档:评论用例-前置条件)
// 4. 验证用户是否有该景点的购票记录
function verifyUserTicket() {
const userId = localStorage.getItem('userId');
// 调用后端验证购票记录接口
fetch(`${API.verifyTicket}?userId=${userId}&spotId=${currentSpotId}`, { // 注需在api.js补充verifyTicket接口
const username = localStorage.getItem('username');
console.log('验证购票记录 - 用户ID:', userId, '景点ID:', currentSpotId);
// 首先检查本地存储的快速验证
if (hasLocalPurchaseRecord(currentSpotId)) {
console.log('本地验证:存在购票记录');
return;
}
// 本地验证失败,调用后端接口验证
fetch(`${API.verifyTicket}?userId=${userId || username}&spotId=${currentSpotId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
headers: getAuthHeaders()
})
.then(response => {
// 后端返回:{ code:200, data: { hasValidTicket: true } } 或 { code:400, message:'无有效购票记录' }
console.log('购票验证响应状态:', response.status);
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message || '验证购票记录失败') });
return response.json().then(err => {
throw new Error(err.message || '验证购票记录失败')
});
}
return response.json();
})
.then(data => {
// 有有效购票记录:不做处理,允许提交
if (!data.data.hasValidTicket) {
console.log('购票验证结果:', data);
// 根据您的Result结构调整
if (data.data && data.data.hasValidTicket) {
console.log('后端验证:存在有效购票记录');
// 有有效购票记录,允许提交
} else {
throw new Error('仅支持已实际游览该景点的用户发表评论(需有有效购票记录)');
}
})
.catch(error => {
console.error('购票验证错误:', error);
// 无购票记录:禁用提交按钮,提示用户
alert(error.message);
document.querySelector('.submit-btn').disabled = true;
document.querySelector('.submit-btn').style.backgroundColor = '#ccc';
document.querySelector('.submit-btn').textContent = '无有效购票记录,无法提交';
disableSubmitButton(error.message);
});
}
// 5. 绑定评论表单提交事件(参考文档:评论用例-基本交互)
// 检查本地购票记录
function hasLocalPurchaseRecord(spotId) {
const purchaseHistory = JSON.parse(localStorage.getItem('purchaseHistory') || '[]');
const record = purchaseHistory.find(item => item.spotId === spotId);
if (record) {
// 检查记录是否在有效期内30天内
const purchaseTime = new Date(record.purchaseTime);
const now = new Date();
const daysDiff = (now - purchaseTime) / (1000 * 60 * 60 * 24);
if (daysDiff <= 30) {
console.log('本地购票记录有效,购买时间:', record.purchaseTime);
return true;
} else {
// 移除过期的记录
removePurchaseRecord(spotId);
console.log('本地购票记录已过期,已移除');
}
}
return false;
}
// 移除购票记录
function removePurchaseRecord(spotId) {
let purchaseHistory = JSON.parse(localStorage.getItem('purchaseHistory') || '[]');
purchaseHistory = purchaseHistory.filter(item => item.spotId !== spotId);
localStorage.setItem('purchaseHistory', JSON.stringify(purchaseHistory));
}
// 禁用提交按钮
function disableSubmitButton(message) {
const submitBtn = document.querySelector('.submit-btn');
submitBtn.disabled = true;
submitBtn.style.backgroundColor = '#ccc';
submitBtn.style.cursor = 'not-allowed';
submitBtn.textContent = '无有效购票记录,无法提交';
// 显示提示信息
alert(message);
}
// 5. 绑定评论表单提交事件
function bindReviewFormSubmit() {
const reviewForm = document.getElementById('reviewForm');
const contentInput = document.getElementById('reviewContent');
@ -144,48 +210,90 @@ function bindReviewFormSubmit() {
return;
}
// 2. 调用后端提交评论接口(含区块链存证)
// 2. 再次验证购票记录
if (!hasLocalPurchaseRecord(currentSpotId)) {
alert('请先购买该景点门票后再发表评论');
return;
}
// 3. 调用后端提交评论接口
const submitBtn = document.querySelector('.submit-btn');
submitBtn.disabled = true;
submitBtn.textContent = '提交中(区块链存证中...';
// 获取访问日期(使用今天或从购票记录中获取)
const visitDate = getVisitDate();
const reviewData = {
spotId: currentSpotId,
userId: localStorage.getItem('userId') || localStorage.getItem('username'),
rating: 5, // 默认评分,您可以根据需要添加评分选择
comment: content,
visitDate: visitDate
};
console.log('提交评论数据:', reviewData);
fetch(API.reviewSubmit, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
spotId: currentSpotId, // 景点ID
userId: localStorage.getItem('userId'), // 用户ID
content: content // 评论内容
})
headers: getAuthHeaders(),
body: JSON.stringify(reviewData)
})
.then(response => {
console.log('评论提交响应状态:', response.status);
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message || '评论提交失败') });
return response.json().then(err => {
throw new Error(err.message || '评论提交失败')
});
}
return response.json();
})
.then(data => {
// 评论提交成功(参考文档:评论用例-后置条件)
alert(`
评论提交成功
已通过区块链存证交易ID${data.data.transactionId}
即将返回景点详情页查看评论
`);
// 跳转回景点详情页
window.location.href = `../pages/spot-detail.html?spotId=${currentSpotId}`;
console.log('评论提交成功:', data);
// 根据您的Result结构调整
if (data.code === 200) {
const transactionId = data.data.transactionId || '未知交易ID';
alert(`评论提交成功!\n已通过区块链存证交易ID${transactionId}\n即将返回景点详情页查看评论`);
// 跳转回景点详情页
window.location.href = `../pages/spot-detail.html?spotId=${currentSpotId}`;
} else {
throw new Error(data.message || '评论提交失败');
}
})
.catch(error => {
alert(error.message);
console.error('评论提交错误:', error);
alert('评论提交失败: ' + error.message);
submitBtn.disabled = false;
submitBtn.textContent = '提交评论(区块链存证)';
console.error('评论提交错误', error);
});
});
}
// 获取访问日期
function getVisitDate() {
// 从本地购票记录中获取访问日期,如果没有则使用今天
const purchaseHistory = JSON.parse(localStorage.getItem('purchaseHistory') || '[]');
const record = purchaseHistory.find(item => item.spotId === currentSpotId);
if (record && record.visitDate) {
return record.visitDate;
}
// 默认使用今天
return new Date().toISOString().split('T')[0];
}
// 工具函数:获取认证头信息
function getAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
};
}
// 【与后端配合说明】
// 1. 购票记录验证接口(需补充):
// - 方法GET

@ -1,9 +1,73 @@
// 全局变量当前景点ID从URL参数获取
let currentSpotId = '';
// 硬编码的景点数据
const SPOTS_DATA = {
'10': {
id: '10',
name: '故宫',
imgUrls: [
'../assets/images/故宫.png'
],
rating: 4.8,
reviewCount: 12560,
location: '北京市东城区景山前街4号',
description: '明清两代的皇家宫殿,世界上现存规模最大、保存最为完整的木质结构古建筑之一。',
detailContent: '<h3>故宫详细介绍</h3><p>故宫,又称紫禁城,是明清两代的皇家宫殿,位于北京中轴线的中心。</p><p>故宫以三大殿为中心占地面积72万平方米建筑面积约15万平方米有大小宫殿七十多座房屋九千余间。</p><p><strong>开放时间:</strong>08:30-17:00旺季08:30-16:30淡季</p><p><strong>建议游玩:</strong>3-4小时</p><p><strong>景点特色:</strong>中国古代建筑艺术的典范,世界文化遗产。</p>',
price: 60,
openTime: '08:30-17:00',
contactPhone: '010-85007422'
},
'2': {
id: '2',
name: '长城',
imgUrls: [
'../assets/images/长城.jpg'
],
rating: 4.9,
reviewCount: 8934,
location: '北京市延庆区八达岭特区',
description: '中国古代的军事防御工程,世界文化遗产,中华民族的象征。',
detailContent: '<h3>长城详细介绍</h3><p>长城是中国古代的军事防御工事,是一道高大、坚固而且连绵不断的长垣,用以限隔敌骑的行动。</p><p>长城不是一道单纯孤立的城墙,而是以城墙为主体,同大量的城、障、亭、标相结合的防御体系。</p><p><strong>开放时间:</strong>06:30-19:00旺季07:00-18:00淡季</p><p><strong>建议游玩:</strong>2-3小时</p><p><strong>景点特色:</strong>世界文化遗产,中国古代军事防御工程的代表。</p>',
price: 45,
openTime: '06:30-19:00',
contactPhone: '010-69121383'
},
'3': {
id: '3',
name: '西湖',
imgUrls: [
'../assets/images/西湖.png'
],
rating: 4.7,
reviewCount: 7568,
location: '浙江省杭州市西湖区',
description: '中国著名的淡水湖泊,以其秀丽的湖光山色和众多的名胜古迹而闻名中外。',
detailContent: '<h3>西湖详细介绍</h3><p>西湖位于浙江省杭州市西湖区,是中国著名的淡水湖泊。</p><p>西湖有"西湖十景",包括苏堤春晓、断桥残雪、平湖秋月等著名景点。</p><p><strong>开放时间:</strong>全天开放</p><p><strong>建议游玩:</strong>1-2天</p><p><strong>景点特色:</strong>自然风光与人文景观的完美结合。</p>',
price: 0,
openTime: '全天开放',
contactPhone: '0571-87179617'
},
'1': {
id: '1',
name: '测试景点',
imgUrls: [
'../assets/images/default-spot.jpg'
],
rating: 4.5,
reviewCount: 1234,
location: '测试地址',
description: '这是一个测试景点的简介',
detailContent: '<h3>测试景点详细介绍</h3><p>这是一个测试景点的详细介绍内容。</p><p><strong>开放时间:</strong>08:00-18:00</p><p><strong>建议游玩:</strong>2-3小时</p><p><strong>景点特色:</strong>测试特色描述。</p>',
price: 100,
openTime: '08:00-18:00',
contactPhone: '400-123-4567'
}
};
// 页面加载完成后执行
window.onload = function() {
// 1. 解析URL中的景点IDspot-detail.html?spotId=1
// 1. 解析URL中的景点ID
const urlParams = new URLSearchParams(window.location.search);
currentSpotId = urlParams.get('spotId');
if (!currentSpotId) {
@ -12,23 +76,20 @@ window.onload = function() {
return;
}
// 2. 切换导航栏用户状态(登录/未登录)
// 2. 切换导航栏用户状态
updateUserNav();
// 3. 加载景点详情数据(核心接口
// 3. 加载景点详情数据(使用硬编码数据
loadSpotDetail();
// 4. 绑定标签页切换事件
bindTabSwitch();
// 5. 绑定购票按钮点击事件(参考文档:预约/购票用例)
// 5. 绑定购票按钮点击事件
bindBuyTicketBtn();
// 6. 加载评论列表(默认不加载,切换到评论标签时加载)
// 7. 加载相关攻略(默认不加载,切换到攻略标签时加载)
};
// 1. 切换导航栏用户状态(复用首页逻辑)
// 1. 切换导航栏用户状态
function updateUserNav() {
const username = localStorage.getItem('username');
const loginBtn = document.querySelector('.login-btn');
@ -39,48 +100,42 @@ function updateUserNav() {
const submitReviewBtn = document.getElementById('submitReviewBtn');
if (username) {
// 已登录:显示用户名+个人中心,显示评论提交入口
loginBtn.style.display = 'none';
registerBtn.style.display = 'none';
userInfo.style.display = 'flex';
usernameDom.textContent = username;
reviewSubmitEntry.style.display = 'block';
// 评论提交按钮拼接景点ID
submitReviewBtn.href = `../pages/review.html?spotId=${currentSpotId}`;
} else {
// 未登录:隐藏评论提交入口
reviewSubmitEntry.style.display = 'none';
}
}
// 2. 加载景点详情数据(参考文档:查看景点信息用例
// 2. 加载景点详情数据(使用硬编码数据
function loadSpotDetail() {
console.log('=== 开始加载景点详情(硬编码数据)===');
console.log('currentSpotId:', currentSpotId);
// 显示加载中状态
document.getElementById('spotName').textContent = '加载中...';
document.getElementById('spotDesc').textContent = '加载中...';
// 调用后端景点详情接口
fetch(`${API.spotDetail}?spotId=${currentSpotId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token') // 可选,按后端需求
// 模拟网络请求延迟
setTimeout(() => {
const spot = SPOTS_DATA[currentSpotId];
if (!spot) {
alert('景点不存在或已被删除');
window.location.href = '../pages/index.html';
return;
}
})
.then(response => {
if (!response.ok) throw new Error('景点详情加载失败');
return response.json();
})
.then(data => {
const spot = data.data; // 后端返回格式:{ code:200, data: { id, name, imgUrls, ... } }
console.log('景点详情数据:', spot);
// 渲染景点基础信息
renderSpotBasicInfo(spot);
// 渲染景点详情内容
renderSpotDetailContent(spot);
})
.catch(error => {
alert(error.message);
console.error('景点详情加载错误', error);
});
}, 500); // 模拟500ms网络延迟
}
// 2.1 渲染景点基础信息
@ -110,20 +165,19 @@ function renderSpotBasicInfo(spot) {
}
// 其他基础信息
document.getElementById('spotName').textContent = spot.name;
document.getElementById('spotName').textContent = spot.name || '未知景点';
document.getElementById('spotRating').textContent = spot.rating || 0.0;
document.getElementById('reviewCount').textContent = `${spot.reviewCount || 0}条评论)`;
document.getElementById('spotLocation').textContent = spot.location || '未知地址';
document.getElementById('spotDesc').textContent = spot.briefDesc || '暂无简介';
document.getElementById('openTime').textContent = spot.openTime || '暂无开放时间信息';
document.getElementById('ticketPrice').textContent = `¥${spot.minPrice || 0}`;
document.getElementById('contactPhone').textContent = spot.contactPhone || '暂无咨询电话';
document.getElementById('spotDesc').textContent = spot.description || '暂无简介';
document.getElementById('openTime').textContent = spot.openTime || '08:00-18:00';
document.getElementById('ticketPrice').textContent = `¥${spot.price || 0}`;
document.getElementById('contactPhone').textContent = spot.contactPhone || '400-123-4567';
}
// 2.2 渲染景点详情内容
function renderSpotDetailContent(spot) {
const detailContent = document.getElementById('detailContent');
// 后端返回的详情可能是HTML或纯文本这里按HTML处理
detailContent.innerHTML = spot.detailContent || '<p>暂无详细介绍</p>';
}
@ -148,40 +202,53 @@ function bindTabSwitch() {
// 懒加载:切换到评论/攻略标签时才加载数据
if (tabKey === 'review' && activeContent.innerHTML.includes('加载中')) {
loadReviewList(); // 加载评论列表
loadReviewList();
}
if (tabKey === 'strategy' && activeContent.innerHTML.includes('加载中')) {
loadRelatedStrategy(); // 加载相关攻略
loadRelatedStrategy();
}
});
});
}
// 4. 加载评论列表(参考文档:评论用例-区块链存证
// 4. 加载评论列表(使用模拟数据
function loadReviewList() {
const reviewList = document.getElementById('reviewList');
reviewList.innerHTML = '<div class="loading">评论加载中...</div>';
// 调用后端评论列表接口
fetch(`${API.reviewList}?spotId=${currentSpotId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('评论加载失败');
return response.json();
})
.then(data => {
const reviews = data.data.list || []; // 后端返回格式:{ code:200, data: { list: [...], total: 0 } }
// 模拟网络请求延迟
setTimeout(() => {
// 模拟评论数据
const mockReviews = [
{
id: '1',
username: '游客123',
content: '这个景点非常棒,风景优美,服务也很好!',
createTime: '2024-01-15T10:30:00'
},
{
id: '2',
username: '旅行者456',
content: '值得一游,下次还会再来,推荐给大家!',
createTime: '2024-01-14T15:20:00'
},
{
id: '3',
username: '摄影爱好者',
content: '拍照的好地方,每个角度都很美,收获了很多精彩照片。',
createTime: '2024-01-13T09:15:00'
}
];
reviewList.innerHTML = '';
if (reviews.length === 0) {
if (mockReviews.length === 0) {
reviewList.innerHTML = '<div class="no-data">暂无用户评论,快来成为第一个评论的人吧!</div>';
return;
}
// 渲染每条评论(含区块链存证标识)
reviews.forEach(review => {
// 渲染每条评论
mockReviews.forEach(review => {
const reviewItem = document.createElement('div');
reviewItem.className = 'review-item';
reviewItem.innerHTML = `
@ -197,69 +264,49 @@ function loadReviewList() {
`;
reviewList.appendChild(reviewItem);
});
})
.catch(error => {
reviewList.innerHTML = `<div class="no-data">评论加载失败:${error.message}</div>`;
console.error('评论加载错误', error);
});
}, 800); // 模拟800ms网络延迟
}
// 4.1 验证评论区块链存证(参考文档:评论用例-验证流程)
// 4.1 验证评论区块链存证
function verifyReview(reviewId) {
// 调用后端评论验证接口
fetch(`${API.reviewVerify}?reviewId=${reviewId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('验证失败');
return response.json();
})
.then(data => {
const verifyInfo = data.data; // 后端返回:{ isValid: true, transactionId: 'xxx', timestamp: 'xxx', blockHeight: 123 }
if (verifyInfo.isValid) {
alert(`
评论验证成功
交易ID${verifyInfo.transactionId}
存证时间${formatTime(verifyInfo.timestamp)}
区块高度${verifyInfo.blockHeight}
该评论自发布以来未被篡改
`);
} else {
alert('该评论未通过区块链验证,可能已被篡改!');
}
})
.catch(error => {
alert(`验证失败:${error.message}`);
console.error('评论验证错误', error);
});
// 模拟验证过程
alert('评论验证功能演示评论ID ' + reviewId + ' 的区块链验证功能正在开发中');
}
// 5. 加载相关攻略
// 5. 加载相关攻略(使用模拟数据)
function loadRelatedStrategy() {
const strategyList = document.getElementById('strategyList');
strategyList.innerHTML = '<div class="loading">攻略加载中...</div>';
// 调用后端相关攻略接口按景点ID关联
fetch(`${API.strategyRelated}?spotId=${currentSpotId}`, { // 注需在api.js补充strategyRelated接口
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('攻略加载失败');
return response.json();
})
.then(data => {
const strategies = data.data.list || [];
// 模拟网络请求延迟
setTimeout(() => {
// 模拟攻略数据
const mockStrategies = [
{
id: '1',
title: `${SPOTS_DATA[currentSpotId]?.name || '该景点'}完美一日游攻略`,
username: '资深导游',
createTime: '2024-01-10T14:00:00',
briefDesc: '详细介绍了最佳游览路线、必看景点和实用贴士,让你的旅行更加完美。'
},
{
id: '2',
title: `省钱游${SPOTS_DATA[currentSpotId]?.name || '该景点'}的秘诀`,
username: '背包客小张',
createTime: '2024-01-08T11:30:00',
briefDesc: '分享如何用最少的钱玩转景点,包括交通、餐饮和门票的省钱技巧。'
}
];
strategyList.innerHTML = '';
if (strategies.length === 0) {
if (mockStrategies.length === 0) {
strategyList.innerHTML = '<div class="no-data">暂无相关攻略,快去生成属于你的攻略吧!</div>';
return;
}
// 渲染攻略卡片
strategies.forEach(strategy => {
mockStrategies.forEach(strategy => {
const strategyCard = document.createElement('div');
strategyCard.className = 'strategy-card';
strategyCard.innerHTML = `
@ -272,14 +319,10 @@ function loadRelatedStrategy() {
`;
strategyList.appendChild(strategyCard);
});
})
.catch(error => {
strategyList.innerHTML = `<div class="no-data">攻略加载失败:${error.message}</div>`;
console.error('攻略加载错误', error);
});
}, 600); // 模拟600ms网络延迟
}
// 6. 绑定购票按钮点击事件(参考文档:预约/购票用例)
// 6. 绑定购票按钮点击事件
function bindBuyTicketBtn() {
const buyTicketBtn = document.getElementById('buyTicketBtn');
buyTicketBtn.addEventListener('click', () => {
@ -291,29 +334,30 @@ function bindBuyTicketBtn() {
return;
}
// 已登录:跳转购票页(或在当前页弹出购票弹窗,此处简化为跳转)
// 注:实际项目可在详情页集成购票表单,此处按跳转处理
window.location.href = `../pages/ticket-buy.html?spotId=${currentSpotId}`; // 需创建购票页
// 已登录:跳转购票页
window.location.href = `../pages/ticket-buy.html?spotId=${currentSpotId}`;
});
}
// 工具函数:时间格式化2025-11-01 14:30:00
// 工具函数:时间格式化
function formatTime(timeStr) {
if (!timeStr) return '';
const date = new Date(timeStr);
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
}
// 【与后端配合说明】
// 1. 景点详情接口API.spotDetail
// - 方法GET
// - 参数spotId必传
// - 返回格式:{ code:200, data: { id, name, imgUrls: [], rating, reviewCount, location, briefDesc, detailContent, openTime, minPrice, contactPhone } }
// 2. 评论列表接口API.reviewList
// - 方法GET
// - 参数spotId必传、page=1、size=10分页
// - 返回格式:{ code:200, data: { list: [{ id, username, content, createTime }], total: 0 } }
// 3. 评论验证接口API.reviewVerify
// - 方法GET
// - 参数reviewId必传
// - 返回格式:{ code:200, data: { isValid: true, transactionId: 'xxx', timestamp: 'xxx', blockHeight: 123 } }
// 辅助函数:获取认证头信息
function getAuthHeaders() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('token') || '')
};
}
// 辅助函数:处理响应
function handleResponse(response) {
if (!response.ok) {
throw new Error('网络请求失败,状态码: ' + response.status);
}
return response.json();
}

@ -1,12 +1,12 @@
// 页面加载完成后执行
window.onload = function() {
// 1. 切换导航栏用户状态
// 1. 切换导航栏用户状态(登录/未登录)
updateUserNav();
// 2. 设置默认出行日期(今天+1天
setDefaultTravelDate();
// 3. 绑定表单提交事件(生成攻略
// 3. 绑定表单提交事件(生成攻略核心逻辑
bindGenerateStrategy();
// 4. 绑定保存攻略按钮事件
@ -24,28 +24,26 @@ function updateUserNav() {
if (username) {
loginBtn.style.display = 'none';
registerBtn.style.display = 'none';
userInfo.style.display = 'flex';
userInfo.style.display = 'flex';
usernameDom.textContent = username;
}
}
// 2. 设置默认出行日期(今天+1天
// 2. 设置默认出行日期(今天+1天格式YYYY-MM-DD
function setDefaultTravelDate() {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// 格式化为YYYY-MM-DD
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const defaultDate = tomorrow.toISOString().split('T')[0];
document.getElementById('travelDate').value = defaultDate;
}
// 3. 重置表单
// 3. 重置表单(保留默认日期)
function resetForm() {
document.getElementById('strategyForm').reset();
setDefaultTravelDate(); // 重置后仍保持默认日期
setDefaultTravelDate();
}
// 4. 绑定生成攻略事件(核心:调用大模型接口
// 4. 绑定生成攻略事件(核心:调用后端接口+状态管理
function bindGenerateStrategy() {
const strategyForm = document.getElementById('strategyForm');
const generateBtn = document.querySelector('.generate-btn');
@ -54,21 +52,31 @@ function bindGenerateStrategy() {
const resultTitle = document.getElementById('resultTitle');
strategyForm.addEventListener('submit', function(e) {
e.preventDefault();
e.preventDefault();
// 1. 获取表单参数(参考文档:攻略生成用例-参数说明
// 1. 严格获取表单参数(确保每个字段都能拿到值
const destination = document.getElementById('destination').value.trim();
const travelDate = document.getElementById('travelDate').value;
const travelDays = document.getElementById('travelDays').value;
const budget = document.getElementById('budget').value;
// 获取选中的兴趣偏好
// 兴趣偏好:确保是数组,且值不为空
const preferences = Array.from(document.querySelectorAll('input[name="preference"]:checked'))
.map(checkbox => checkbox.value);
.map(checkbox => checkbox.value).filter(v => v);
const specialNeed = document.getElementById('specialNeed').value.trim();
// 2. 前端验证
// 【新增】打印请求参数到控制台,排查参数是否正确
console.log("前端准备的请求参数:", {
destination,
travelDate,
travelDays: parseInt(travelDays),
budget: parseInt(budget),
preferences,
specialNeed
});
// 2. 前端参数验证
if (!destination) {
alert('请输入目的地');
alert('请输入目的地(如:北京、三亚、故宫)');
return;
}
if (!travelDate) {
@ -79,177 +87,207 @@ function bindGenerateStrategy() {
alert('请输入合理的预算至少100元');
return;
}
if (preferences.length === 0) {
if (!confirm('未选择兴趣偏好,可能影响攻略准确性,是否继续?')) {
return;
}
if (preferences.length === 0 && !confirm('未选择兴趣偏好,可能影响攻略准确性,是否继续?')) {
return;
}
// 3. 显示生成中状态
// 3. 显示"生成中"状态
generateBtn.disabled = true;
generateBtn.textContent = '生成中...';
strategyResult.style.display = 'block';
strategyResult.style.display = 'block';
resultContent.innerHTML = `
<div class="generating">
<div class="loading-icon">🤖</div>
<p>智能大模型正在为您生成专属攻略请稍候约10-30...</p>
<div class="generating" style="text-align: center; padding: 40px 0; color: #666;">
<div class="loading-icon" style="font-size: 48px; margin-bottom: 16px;">🤖</div>
<p>智能大模型正在为您生成专属攻略</p>
<p style="margin-top: 8px; font-size: 14px;">预计耗时10-30请稍候...</p>
</div>
`;
// 4. 调用后端攻略生成接口参考文档AI接口对接
fetch(API.strategyGenerate, {
// 4. 调用后端攻略生成接口(确保地址正确)
const apiUrl = "http://127.0.0.1:3005/generate-strategy";
fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token') // 未登录也可生成,但无法保存
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('token') || '')
},
body: JSON.stringify({
destination: destination,
travelDate: travelDate,
travelDays: parseInt(travelDays),
budget: parseInt(budget),
preferences: preferences,
specialNeed: specialNeed
})
destination,
travelDate,
travelDays: parseInt(travelDays),
budget: parseInt(budget),
preferences,
specialNeed
}),
timeout: 35000
})
.then(response => {
if (!response.ok) throw new Error('攻略生成失败,请重试');
return response.json();
// 【新增】打印响应状态码
console.log("后端接口响应状态码:", response.status);
if (!response.ok) throw new Error(`接口请求失败(状态码:${response.status}),请检查后端是否启动`);
return response.json();
})
.then(data => {
// 5. 渲染攻略结果(后端返回结构化数据)
const strategy = data.data; // { id, title, days: [], budgetSummary: {} }
// 【新增】打印后端返回的攻略数据
console.log("后端返回的攻略数据:", data);
// 检查数据结构是否正确
if (!data.data || !data.data.days) {
throw new Error('后端返回的数据格式不正确');
}
// 检查每天的行程是否为空
data.data.days.forEach((day, index) => {
if (!day.schedule || day.schedule.length === 0) {
console.warn(`${index + 1}天的行程为空`);
} else {
console.log(`${index + 1}天有${day.schedule.length}个行程项:`, day.schedule);
}
});
// 5. 生成成功:渲染攻略结果
generateBtn.disabled = false;
generateBtn.textContent = '生成智能攻略';
resultTitle.textContent = strategy.title;
renderStrategyContent(strategy);
generateBtn.textContent = '生成智能攻略';
resultTitle.textContent = data.data.title;
renderStrategyContent(data.data);
})
.catch(error => {
alert(error.message);
// 【新增】打印错误详情到控制台
console.error('攻略生成失败,错误详情:', error);
alert('攻略生成失败:' + error.message);
generateBtn.disabled = false;
generateBtn.textContent = '生成智能攻略';
strategyResult.style.display = 'none';
console.error('攻略生成错误', error);
strategyResult.style.display = 'none';
});
});
}
// 4.1 渲染攻略内容结构化数据转HTML
// 4.1 渲染攻略内容(严格按照后端解析的格式显示
function renderStrategyContent(strategy) {
const resultContent = document.getElementById('resultContent');
let html = '';
// 1. 渲染每日行程
// 1. 渲染每日行程(严格按照时间-地点:描述的格式)
strategy.days.forEach((day, index) => {
html += `
<div class="day-section">
<h3 class="day-title">第${index + 1}${day.title}</h3>
${day.schedule.map(item => `
<h3 class="day-title">${day.title}</h3>
<div class="schedule-list">
`;
// 严格按照时间-地点:描述的格式显示
if (day.schedule && day.schedule.length > 0) {
day.schedule.forEach(item => {
// 确保每个行程项都有必要的字段
const time = item.time || '全天';
const location = item.location || '景点';
const description = item.description || '详细描述';
html += `
<div class="schedule-item">
<div class="schedule-time">${item.time}</div>
<div class="schedule-time">${time}</div>
<div class="schedule-content">
<span class="schedule-location">${item.location}</span>
<p class="schedule-desc">${item.description}</p>
<span class="schedule-location">${location}</span>
<span class="schedule-desc">${description}</span>
</div>
</div>
`).join('')}
`;
});
} else {
// 如果没有行程数据,显示默认内容
html += `
<div class="schedule-item">
<div class="schedule-time">09:00-17:00</div>
<div class="schedule-content">
<span class="schedule-location">${strategy.title.split('游')[0]}</span>
<span class="schedule-desc">全天游览体验当地文化和风景</span>
</div>
</div>
`;
}
html += `
</div>
</div>
`;
});
// 2. 渲染预算总结
// 2. 渲染预算总结(严格按照分类显示)
html += `
<div class="budget-summary">
<div class="budget-title">预算总结人均</div>
${Object.entries(strategy.budgetSummary).map(([key, value]) => `
<h3 class="budget-title">预算总结人均</h3>
<div class="budget-list">
<div class="budget-item">
<span>门票</span>
<span>¥${strategy.budgetSummary.门票}</span>
</div>
<div class="budget-item">
<span>${key}</span>
<span>¥${value}</span>
<span>餐饮</span>
<span>¥${strategy.budgetSummary.餐饮}</span>
</div>
<div class="budget-item">
<span>住宿</span>
<span>¥${strategy.budgetSummary.住宿}</span>
</div>
<div class="budget-total">
<span>总计</span>
<span>¥${strategy.totalBudget}</span>
</div>
`).join('')}
<div class="budget-item budget-total">
<span>总计</span>
<span>¥${strategy.totalBudget}</span>
</div>
</div>
`;
resultContent.innerHTML = html;
// 存储攻略ID到页面用于保存
resultContent.setAttribute('data-strategy-id', strategy.id);
}
// 5. 绑定保存攻略按钮事件
// 5. 绑定保存攻略按钮事件(登录后可保存)
function bindSaveStrategy() {
const saveBtn = document.getElementById('saveStrategyBtn');
saveBtn.addEventListener('click', function() {
const token = localStorage.getItem('token');
// 未登录:提示登录
if (!token) {
alert('请先登录后再保存攻略');
window.location.href = '../pages/login.html';
window.location.href = '../pages/login.html';
return;
}
// 获取攻略ID
const strategyId = document.getElementById('resultContent').getAttribute('data-strategy-id');
if (!strategyId) {
alert('请先生成攻略再保存');
return;
}
// 调用保存攻略接口
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
fetch(API.strategySave, {
// 【注意】这里的 API.strategySave 需确保是正确的后端保存接口地址,若未定义需替换为实际地址
fetch("http://127.0.0.1:3005/save-strategy", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
strategyId: strategyId,
userId: localStorage.getItem('userId')
userId: localStorage.getItem('userId')
})
})
.then(response => {
if (!response.ok) throw new Error('攻略保存失败');
if (!response.ok) throw new Error('攻略保存失败,可能是权限不足');
return response.json();
})
.then(data => {
alert('攻略保存成功!可在个人中心查看');
saveBtn.disabled = false;
saveBtn.textContent = '已保存';
saveBtn.style.backgroundColor = '#52c41a';
saveBtn.style.backgroundColor = '#52c41a';
})
.catch(error => {
alert(error.message);
alert('攻略保存失败:' + error.message);
saveBtn.disabled = false;
saveBtn.textContent = '保存攻略';
console.error('攻略保存错误', error);
console.error('攻略保存错误详情:', error);
});
});
}
// 【与后端配合说明】
// 1. 攻略生成接口API.strategyGenerate
// - 方法POST
// - 参数:{ destination, travelDate, travelDays, budget, preferences: [], specialNeed }
// - 返回格式:{
// code:200,
// data: {
// id: '攻略ID',
// title: '北京3日游攻略',
// days: [
// { title: '第一天:历史文化之旅', schedule: [{ time: '09:00-12:00', location: '故宫', description: '...' }] },
// ...
// ],
// budgetSummary: { 门票: 200, 餐饮: 300, 住宿: 500 },
// totalBudget: 1000
// }
// }
// 2. 攻略保存接口API.strategySave
// - 方法POST
// - 参数:{ strategyId, userId }
// - 返回格式:{ code:200, message:'保存成功' }
}

@ -0,0 +1,400 @@
// 全局变量
let currentSpotId = '';
let currentSpot = null;
let ticketTypes = [];
let selectedTicketType = null;
// 页面加载完成后执行
window.onload = function() {
// 1. 验证登录态
const token = localStorage.getItem('token');
if (!token) {
alert('请先登录后再购票');
window.location.href = '../pages/login.html';
return;
}
// 2. 解析URL中的景点ID
const urlParams = new URLSearchParams(window.location.search);
currentSpotId = urlParams.get('spotId');
if (!currentSpotId) {
alert('未找到景点信息');
window.history.back();
return;
}
// 3. 切换导航栏用户状态
updateUserNav();
// 4. 加载景点信息和票种
loadSpotInfo();
loadTicketTypes();
// 5. 绑定购票表单事件
bindTicketForm();
// 6. 预填联系人信息
prefillContactInfo();
};
// 切换导航栏用户状态
function updateUserNav() {
const username = localStorage.getItem('username');
const loginBtn = document.querySelector('.login-btn');
const registerBtn = document.querySelector('.register-btn');
const userInfo = document.querySelector('.user-info');
const usernameDom = document.getElementById('username');
if (username) {
loginBtn.style.display = 'none';
registerBtn.style.display = 'none';
userInfo.style.display = 'flex';
usernameDom.textContent = username;
}
}
// 预填联系人信息
function prefillContactInfo() {
const username = localStorage.getItem('username');
document.getElementById('contactName').value = username || '';
}
// 加载景点信息
function loadSpotInfo() {
console.log('正在加载景点信息ID:', currentSpotId);
console.log('请求URL:', `${API.spotDetail}/${currentSpotId}`);
fetch(`${API.spotDetail}/${currentSpotId}`, {
method: 'GET',
headers: getAuthHeaders()
})
.then(response => {
console.log('响应状态:', response.status);
if (!response.ok) {
throw new Error('景点信息加载失败,状态码: ' + response.status);
}
return response.json();
})
.then(spot => {
console.log('景点信息返回数据:', spot);
if (!spot) {
throw new Error('未找到景点信息');
}
currentSpot = spot;
renderSpotInfo(spot);
})
.catch(error => {
console.error('景点信息加载错误:', error);
alert('景点信息加载失败: ' + error.message);
// 返回上一页
window.history.back();
});
}
// 渲染景点信息
function renderSpotInfo(spot) {
const spotInfoCard = document.getElementById('spotInfoCard');
spotInfoCard.innerHTML = `
<h3>${spot.name}</h3>
<div class="spot-meta">
<div>地址${spot.location || '未知'}</div>
<div>城市${spot.city || '未知'}</div>
<div>评分${spot.rating || 0} ${spot.reviewCount || 0}条评论</div>
<div>类型${spot.type || '未知'}</div>
</div>
`;
}
// 加载票种信息
function loadTicketTypes() {
// 模拟票种数据 - 实际应该调用后端接口
const mockTicketTypes = [
{
id: 1,
name: '成人票',
price: currentSpot ? parseFloat(currentSpot.price) : 100,
description: '18-60周岁成人使用',
requirement: '需携带有效身份证件'
},
{
id: 2,
name: '儿童票',
price: currentSpot ? Math.round(parseFloat(currentSpot.price) * 0.6) : 60,
description: '6-18周岁儿童使用',
requirement: '需携带户口本或学生证'
},
{
id: 3,
name: '学生票',
price: currentSpot ? Math.round(parseFloat(currentSpot.price) * 0.8) : 80,
description: '全日制在校学生使用',
requirement: '需携带有效学生证'
}
];
ticketTypes = mockTicketTypes;
renderTicketTypes(mockTicketTypes);
// 默认选择第一个票种
if (mockTicketTypes.length > 0) {
selectTicketType(mockTicketTypes[0]);
}
}
// 渲染票种选择
function renderTicketTypes(types) {
const container = document.getElementById('ticketTypeSelector');
container.innerHTML = types.map(type => `
<div class="ticket-type" data-type-id="${type.id}">
<div class="ticket-type-info">
<div class="ticket-type-name">${type.name}</div>
<div class="ticket-type-desc">${type.description}</div>
<div class="ticket-type-requirement">${type.requirement}</div>
</div>
<div class="ticket-type-price">¥${type.price}</div>
</div>
`).join('');
// 绑定票种选择事件
container.querySelectorAll('.ticket-type').forEach(typeElement => {
typeElement.addEventListener('click', function() {
const typeId = parseInt(this.getAttribute('data-type-id'));
const type = ticketTypes.find(t => t.id === typeId);
if (type) {
selectTicketType(type);
}
});
});
}
// 选择票种
function selectTicketType(type) {
selectedTicketType = type;
// 更新UI
document.querySelectorAll('.ticket-type').forEach(element => {
element.classList.remove('active');
});
document.querySelector(`[data-type-id="${type.id}"]`).classList.add('active');
calculateTotal();
}
// 绑定购票表单事件
function bindTicketForm() {
// 数量选择
bindQuantitySelection();
// 日期选择
bindDateSelection();
// 表单提交
bindFormSubmit();
}
// 绑定数量选择
function bindQuantitySelection() {
const minusBtn = document.querySelector('.quantity-btn.minus');
const plusBtn = document.querySelector('.quantity-btn.plus');
const quantityInput = document.getElementById('quantity');
minusBtn.addEventListener('click', function() {
let quantity = parseInt(quantityInput.value);
if (quantity > 1) {
quantityInput.value = quantity - 1;
calculateTotal();
}
updateQuantityButtons();
});
plusBtn.addEventListener('click', function() {
let quantity = parseInt(quantityInput.value);
if (quantity < 5) {
quantityInput.value = quantity + 1;
calculateTotal();
}
updateQuantityButtons();
});
function updateQuantityButtons() {
const quantity = parseInt(quantityInput.value);
minusBtn.disabled = quantity <= 1;
plusBtn.disabled = quantity >= 5;
}
updateQuantityButtons();
}
// 绑定日期选择
function bindDateSelection() {
const visitDate = document.getElementById('visitDate');
// 设置最小日期为今天
const today = new Date().toISOString().split('T')[0];
visitDate.min = today;
// 设置最大日期为30天后
const maxDate = new Date();
maxDate.setDate(maxDate.getDate() + 30);
visitDate.max = maxDate.toISOString().split('T')[0];
// 默认选择明天
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
visitDate.value = tomorrow.toISOString().split('T')[0];
}
// 计算总价
function calculateTotal() {
if (!selectedTicketType) return;
const quantity = parseInt(document.getElementById('quantity').value);
const totalPrice = selectedTicketType.price * quantity;
document.getElementById('facePrice').textContent = totalPrice.toFixed(2);
document.getElementById('totalPrice').textContent = totalPrice.toFixed(2);
}
// 绑定表单提交
function bindFormSubmit() {
const form = document.getElementById('ticketForm');
form.addEventListener('submit', function(e) {
e.preventDefault();
createVirtualOrder();
});
}
// 创建订单 - 调用真实的后端接口
function createVirtualOrder() {
const submitBtn = document.querySelector('.submit-btn');
submitBtn.disabled = true;
submitBtn.textContent = '支付中...';
const contactName = document.getElementById('contactName').value;
const contactPhone = document.getElementById('contactPhone').value;
const quantity = parseInt(document.getElementById('quantity').value);
const visitDate = document.getElementById('visitDate').value;
// 验证表单
if (!contactName || !contactPhone) {
alert('请填写完整的联系人信息');
submitBtn.disabled = false;
submitBtn.textContent = '立即支付';
return;
}
if (!contactPhone.match(/^1[3-9]\d{9}$/)) {
alert('请输入正确的手机号码');
submitBtn.disabled = false;
submitBtn.textContent = '立即支付';
return;
}
// 检查景点信息是否已加载
if (!currentSpot) {
alert('景点信息未加载完成,请稍后重试');
submitBtn.disabled = false;
submitBtn.textContent = '立即支付';
return;
}
console.log('开始创建真实订单...');
// 构建订单数据
const orderData = {
spotId: parseInt(currentSpotId),
quantity: quantity,
visitDate: visitDate,
unitPrice: selectedTicketType.price,
totalAmount: selectedTicketType.price * quantity
};
console.log('订单数据:', orderData);
// 调用后端创建订单接口
fetch(API.createOrder, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(orderData)
})
.then(response => {
if (!response.ok) {
throw new Error('创建订单失败,状态码: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('订单创建响应:', data);
if (data.code === 200) {
// 订单创建成功,进行支付
return payOrder(data.data.orderNumber);
} else {
throw new Error(data.message || '创建订单失败');
}
})
.then(payResult => {
console.log('支付结果:', payResult);
if (payResult.code === 200) {
// 支付成功
alert(`🎉 购票成功!\n\n订单号:${payResult.data.orderNumber}\n景点:${currentSpot.name}\n票种:${selectedTicketType.name}\n数量:${quantity}\n总价:¥${orderData.totalAmount}\n游玩日期:${visitDate}\n\n您已获得该景点的评论权限!`);
// 保存购票记录到本地存储,用于快速验证
const purchaseRecord = {
spotId: currentSpotId,
orderNumber: payResult.data.orderNumber,
purchaseTime: new Date().toISOString(),
visitDate: visitDate
};
savePurchaseRecord(purchaseRecord);
// 跳转回景点详情页
window.location.href = `../pages/spot-detail.html?spotId=${currentSpotId}`;
} else {
throw new Error(payResult.message || '支付失败');
}
})
.catch(error => {
console.error('购票流程错误:', error);
alert('购票失败: ' + error.message);
submitBtn.disabled = false;
submitBtn.textContent = '立即支付';
});
}
// 支付订单
function payOrder(orderNumber) {
const payData = {
orderNumber: orderNumber,
payMethod: 'alipay' // 模拟支付方式
};
return fetch(API.payOrder, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(payData)
})
.then(response => {
if (!response.ok) {
throw new Error('支付失败,状态码: ' + response.status);
}
return response.json();
});
}
// 保存购票记录到本地存储
function savePurchaseRecord(record) {
let purchaseHistory = JSON.parse(localStorage.getItem('purchaseHistory') || '[]');
// 移除同景点的旧记录(如果有)
purchaseHistory = purchaseHistory.filter(item => item.spotId !== record.spotId);
// 添加新记录
purchaseHistory.push(record);
localStorage.setItem('purchaseHistory', JSON.stringify(purchaseHistory));
console.log('购票记录已保存:', record);
}

@ -8,6 +8,115 @@
<link rel="stylesheet" href="../css/common.css">
<!-- 引入攻略页样式 -->
<link rel="stylesheet" href="../css/strategy.css">
<!-- 1. 引入 Socket.IO 客户端库CDN 方式,与后端 4.x 版本兼容) -->
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
/* 新增:聊天面板样式(适配原有页面布局,放在右侧) */
.chat-container {
width: 380px;
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
height: fit-content;
max-height: 600px;
}
.chat-header {
background-color: #f8f9fa;
padding: 12px 16px;
border-bottom: 1px solid #eee;
font-weight: 600;
color: #333;
}
#loginArea {
padding: 20px;
text-align: center;
}
#usernameInput {
width: 70%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 8px;
}
#loginBtn {
padding: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
#chatArea {
display: none;
flex-direction: column;
height: 500px;
}
#messageList {
flex: 1;
padding: 16px;
overflow-y: auto;
background-color: #fafafa;
}
#messageList > div {
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 6px;
max-width: 70%;
line-height: 1.5;
}
#messageList .system-msg {
color: #666;
background-color: #e9ecef;
margin-left: auto;
margin-right: auto;
}
#messageList .ai-msg {
color: #333;
background-color: #e3f2fd;
margin-right: auto;
}
#messageList .self-msg {
color: #fff;
background-color: #2196F3;
margin-left: auto;
}
.chat-input-area {
padding: 12px;
border-top: 1px solid #eee;
background-color: white;
display: flex;
}
#messageInput {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 8px;
outline: none;
}
#messageInput:focus {
border-color: #2196F3;
}
#sendBtn {
padding: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* 调整原有攻略区布局,与聊天面板并排 */
.strategy-wrapper {
display: flex;
gap: 30px;
justify-content: center;
padding: 20px 0;
}
.strategy-form-container {
flex: 0 0 600px; /* 攻略表单固定宽度 */
}
</style>
</head>
<body>
<!-- 导航栏(公共组件) -->
@ -30,91 +139,114 @@
</div>
</div>
<!-- 攻略内容区 -->
<!-- 攻略内容区(新增:与聊天面板并排布局) -->
<div class="container">
<div class="strategy-wrapper">
<div class="strategy-title">智能旅游攻略生成</div>
<p class="strategy-desc">基于大模型技术,为您生成个性化、可调整的行程攻略(支持保存与分享)</p>
<!-- 原有攻略表单与结果区(保留所有逻辑) -->
<div class="strategy-form-container">
<div class="strategy-title">智能旅游攻略生成</div>
<p class="strategy-desc">基于大模型技术,为您生成个性化、可调整的行程攻略(支持保存与分享)</p>
<!-- 攻略参数表单(参考文档:攻略生成用例-核心参数) -->
<form id="strategyForm" class="strategy-form">
<div class="form-row">
<div class="form-item">
<label for="destination">目的地(城市/景点)</label>
<input type="text" id="destination" class="input-box" placeholder="如:北京、三亚、故宫" required>
<!-- 攻略参数表单 -->
<form id="strategyForm" class="strategy-form">
<div class="form-row">
<div class="form-item">
<label for="destination">目的地(城市/景点)</label>
<input type="text" id="destination" class="input-box" placeholder="如:北京、三亚、故宫" required>
</div>
<div class="form-item">
<label for="travelDate">出行日期</label>
<input type="date" id="travelDate" class="input-box" required>
</div>
</div>
<div class="form-item">
<label for="travelDate">出行日期</label>
<input type="date" id="travelDate" class="input-box" required>
<div class="form-row">
<div class="form-item">
<label for="travelDays">行程天数</label>
<select id="travelDays" class="input-box" required>
<option value="1">1天</option>
<option value="2">2天</option>
<option value="3" selected>3天</option>
<option value="4">4天</option>
<option value="5">5天</option>
<option value="6">6天</option>
<option value="7">7天</option>
</select>
</div>
<div class="form-item">
<label for="budget">人均预算(元)</label>
<input type="number" id="budget" class="input-box" placeholder="如2000、5000" min="100" required>
</div>
</div>
</div>
<div class="form-row">
<div class="form-item">
<label for="travelDays">行程天数</label>
<select id="travelDays" class="input-box" required>
<option value="1">1天</option>
<option value="2">2天</option>
<option value="3" selected>3天</option>
<option value="4">4天</option>
<option value="5">5天</option>
<option value="6">6天</option>
<option value="7">7天</option>
</select>
<label>兴趣偏好(可多选)</label>
<div class="preference-tags">
<label class="tag-item">
<input type="checkbox" name="preference" value="历史古迹"> 历史古迹
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="自然风光"> 自然风光
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="美食探店"> 美食探店
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="亲子游乐"> 亲子游乐
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="网红打卡"> 网红打卡
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="文化体验"> 文化体验
</label>
</div>
</div>
<div class="form-item">
<label for="budget">人均预算(元)</label>
<input type="number" id="budget" class="input-box" placeholder="如2000、5000" min="100" required>
<label for="specialNeed">特殊需求(可选</label>
<textarea id="specialNeed" class="input-box" placeholder="如:避开人流、含无障碍设施、偏好小众景点..." rows="3"></textarea>
</div>
</div>
<div class="form-item">
<label>兴趣偏好(可多选)</label>
<div class="preference-tags">
<label class="tag-item">
<input type="checkbox" name="preference" value="历史古迹"> 历史古迹
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="自然风光"> 自然风光
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="美食探店"> 美食探店
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="亲子游乐"> 亲子游乐
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="网红打卡"> 网红打卡
</label>
<label class="tag-item">
<input type="checkbox" name="preference" value="文化体验"> 文化体验
</label>
<div class="form-footer">
<button type="button" class="btn btn-default reset-btn" onclick="resetForm()">重置</button>
<button type="submit" class="btn btn-primary generate-btn">生成智能攻略</button>
</div>
</div>
</form>
<div class="form-item">
<label for="specialNeed">特殊需求(可选)</label>
<textarea id="specialNeed" class="input-box" placeholder="如:避开人流、含无障碍设施、偏好小众景点..." rows="3"></textarea>
<!-- 攻略生成结果(默认隐藏) -->
<div class="strategy-result" id="strategyResult" style="display: none;">
<div class="result-header">
<h2 class="result-title" id="resultTitle">北京3日游攻略2025-11-01出发</h2>
<div class="result-actions">
<button class="btn btn-default save-btn" id="saveStrategyBtn">保存攻略</button>
<button class="btn btn-default share-btn">分享攻略</button>
</div>
</div>
<div class="result-content" id="resultContent">
<!-- 攻略内容由JS动态渲染 -->
</div>
</div>
</div>
<div class="form-footer">
<button type="button" class="btn btn-default reset-btn" onclick="resetForm()">重置</button>
<button type="submit" class="btn btn-primary generate-btn">生成智能攻略</button>
<!-- 2. 新增AI聊天面板右侧固定布局 -->
<div class="chat-container">
<div class="chat-header">硅基流动AI助手旅游咨询</div>
<!-- 聊天登录区域 -->
<div id="loginArea">
<input type="text" id="usernameInput" placeholder="请输入用户名" />
<button id="loginBtn">登录聊天</button>
</div>
</form>
<!-- 攻略生成结果(默认隐藏,生成后显示) -->
<div class="strategy-result" id="strategyResult" style="display: none;">
<div class="result-header">
<h2 class="result-title" id="resultTitle">北京3日游攻略2025-11-01出发</h2>
<div class="result-actions">
<button class="btn btn-default save-btn" id="saveStrategyBtn">保存攻略</button>
<button class="btn btn-default share-btn">分享攻略</button>
<!-- 聊天内容区域(初始隐藏) -->
<div id="chatArea">
<!-- 消息记录列表 -->
<div id="messageList"></div>
<!-- 消息输入区域 -->
<div class="chat-input-area">
<input type="text" id="messageInput" placeholder="输入旅游相关问题,如:北京必吃美食?" />
<button id="sendBtn">发送</button>
</div>
</div>
<div class="result-content" id="resultContent">
<!-- 攻略内容由JS动态渲染大模型返回结构化数据 -->
</div>
</div>
</div>
</div>
@ -131,5 +263,102 @@
<script src="../js/api.js"></script>
<!-- 引入攻略页交互JS -->
<script src="../js/strategy.js"></script>
<!-- 3. 新增Socket聊天交互逻辑 -->
<script>
// 1. 连接后端 Socket.IO 服务后端地址http://localhost:3005
const socket = io("http://localhost:3005");
// 2. 获取聊天相关DOM元素
const loginArea = document.getElementById("loginArea");
const chatArea = document.getElementById("chatArea");
const usernameInput = document.getElementById("usernameInput");
const loginBtn = document.getElementById("loginBtn");
const messageList = document.getElementById("messageList");
const messageInput = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
// 3. 登录聊天事件(点击登录按钮)
loginBtn.addEventListener("click", () => {
const username = usernameInput.value.trim();
if (!username) {
alert("请输入用户名后登录!");
return;
}
// 向后端发送登录事件,携带用户名
socket.emit("login", username);
// 切换显示:隐藏登录区,显示聊天区
loginArea.style.display = "none";
chatArea.style.display = "flex";
// 聚焦到输入框
messageInput.focus();
});
// 4. 发送消息事件(点击发送按钮 / 按Enter键
sendBtn.addEventListener("click", sendMessage);
messageInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { // 按Enter发送Shift+Enter换行
e.preventDefault();
sendMessage();
}
});
// 封装:发送消息函数
function sendMessage() {
const message = messageInput.value.trim();
if (!message) return; // 空消息不发送
// 构造消息数据接收者固定为AI助手与后端逻辑匹配
const messageData = {
recipient: "硅基流动AI助手",
message: message
};
// 1. 向后端发送聊天消息
socket.emit("chatMessage", messageData);
// 2. 本地先显示自己的消息(提升用户体验)
addMessageToDOM("我", message, "self-msg");
// 3. 清空输入框
messageInput.value = "";
}
// 封装添加消息到页面DOM
function addMessageToDOM(sender, content, msgClass) {
const msgElement = document.createElement("div");
msgElement.className = msgClass;
// 处理消息内容中的换行(将\n转为<br>
const formattedContent = content.replace(/\n/g, "<br>");
msgElement.innerHTML = `<strong>${sender}</strong>${formattedContent}`;
messageList.appendChild(msgElement);
// 滚动到最新消息
messageList.scrollTop = messageList.scrollHeight;
}
// 5. 接收后端推送的消息(系统/AI回复
socket.on("messageReceived", (msg) => {
if (msg.sender === "系统") {
// 系统消息(如欢迎语)
addMessageToDOM("系统", msg.content, "system-msg");
} else if (msg.sender === "硅基流动AI助手") {
// AI助手回复
addMessageToDOM("AI助手", msg.content, "ai-msg");
}
});
// 6. 监听Socket连接状态调试用
socket.on("connect", () => {
console.log("✅ 已连接到AI聊天服务");
});
socket.on("disconnect", () => {
console.log("❌ 与AI聊天服务断开连接");
addMessageToDOM("系统", "聊天连接已断开,请刷新页面重试!", "system-msg");
// 断开后重置为登录状态
chatArea.style.display = "none";
loginArea.style.display = "block";
});
socket.on("connect_error", (error) => {
console.error("❌ 聊天服务连接失败:", error);
addMessageToDOM("系统", "聊天服务连接失败,请检查后端是否启动!", "system-msg");
});
</script>
</body>
</html>

@ -0,0 +1,108 @@
<!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/common.css">
<link rel="stylesheet" href="../css/ticket-buy.css">
</head>
<body>
<div class="nav-container">
<div class="nav-content">
<a href="../pages/index.html" class="logo">旅游攻略</a>
<ul class="nav-menu">
<li><a href="../pages/index.html">首页</a></li>
<li><a href="#" class="active">景点推荐</a></li>
<li><a href="../pages/strategy.html">攻略生成</a></li>
</ul>
<div class="user-actions" id="userActions">
<a href="../pages/login.html" class="login-btn">登录</a>
<a href="../pages/register.html" class="register-btn">注册</a>
<div class="user-info" style="display: none;">
<span id="username"></span>
<a href="../pages/user-center.html" class="user-center">个人中心</a>
</div>
</div>
</div>
</div>
<div class="container">
<div class="ticket-wrapper">
<div class="ticket-title">景点购票</div>
<!-- 景点信息 -->
<div class="spot-info-card" id="spotInfoCard">
<div class="loading">加载景点信息中...</div>
</div>
<!-- 联系人信息 -->
<div class="form-section">
<h3>联系人信息</h3>
<div class="contact-form">
<div class="form-group">
<label for="contactName">联系人姓名</label>
<input type="text" id="contactName" name="contactName" placeholder="请输入真实姓名" required>
</div>
<div class="form-group">
<label for="contactPhone">联系人手机</label>
<input type="tel" id="contactPhone" name="contactPhone" placeholder="请输入手机号码" required>
</div>
</div>
</div>
<!-- 购票表单 -->
<form id="ticketForm" class="ticket-form">
<div class="form-section">
<h3>选择票种</h3>
<div class="ticket-type-selector" id="ticketTypeSelector">
<!-- 票种由JS动态加载 -->
</div>
</div>
<div class="form-section">
<h3>选择数量</h3>
<div class="quantity-selector">
<button type="button" class="quantity-btn minus">-</button>
<input type="number" id="quantity" name="quantity" value="1" min="1" max="5" readonly>
<button type="button" class="quantity-btn plus">+</button>
<span class="quantity-label"></span>
</div>
</div>
<div class="form-section">
<h3>选择游玩日期</h3>
<input type="date" id="visitDate" name="visitDate" required>
<p class="date-tip">请选择未来30天内的日期</p>
</div>
<div class="price-summary">
<div class="price-item">
<span>票面价</span>
<span>¥<span id="facePrice">0</span></span>
</div>
<div class="price-total">
<span>总计</span>
<span class="total-amount">¥<span id="totalPrice">0</span></span>
</div>
</div>
<div class="form-footer">
<button type="button" class="btn btn-default cancel-btn" onclick="window.history.back()">取消</button>
<button type="submit" class="btn btn-primary submit-btn">立即支付</button>
</div>
</form>
</div>
</div>
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<script src="../js/api.js"></script>
<script src="../js/ticket-buy.js"></script>
</body>
</html>
Loading…
Cancel
Save