xuhao 6 months ago
commit 315a1b0d68

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

@ -0,0 +1,182 @@
/* 认证页面通用样式 */
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 130px); /* 减去导航和底部高度 */
background-color: #f5f7fa;
padding: 20px;
}
.auth-card {
width: 100%;
max-width: 420px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 16px rgba(0,0,0,0.1);
overflow: hidden;
}
.auth-header {
padding: 28px 24px;
border-bottom: 1px solid #f0f2f5;
text-align: center;
}
.auth-header h2 {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.auth-header p {
color: #666;
font-size: 14px;
}
.auth-form {
padding: 24px;
}
.form-item {
margin-bottom: 24px;
}
.form-item label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #666;
font-weight: 500;
}
.input-box {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 15px;
transition: all 0.3s;
box-sizing: border-box;
}
.input-box:focus {
outline: none;
border-color: #2c83f2;
box-shadow: 0 0 0 3px rgba(44, 131, 242, 0.1);
}
/* 验证码容器样式 */
.verify-code-container {
display: flex;
gap: 10px;
width: 100%;
}
.verify-code-container .input-box {
flex: 1;
}
.send-code-btn {
padding: 0 16px;
background-color: #f0f2f5;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
font-size: 15px;
transition: all 0.3s;
}
.send-code-btn:hover:not(:disabled) {
background-color: #e5e6eb;
}
.send-code-btn:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
color: #999;
border-color: #eee;
}
/* 按钮样式 */
.auth-btn {
width: 100%;
padding: 12px 0;
font-size: 16px;
font-weight: 500;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
border: none;
}
.btn-primary {
background-color: #2c83f2;
color: #fff;
}
.btn-primary:hover {
background-color: #1a73e8;
}
/* 链接区域 */
.auth-links {
margin-top: 16px;
text-align: center;
font-size: 14px;
}
.auth-links a {
color: #2c83f2;
text-decoration: none;
margin: 0 4px;
}
.auth-links a:hover {
text-decoration: underline;
}
.auth-links span {
color: #ccc;
}
/* 错误提示样式 */
.error-message {
color: #ff4d4f;
font-size: 13px;
margin-top: 6px;
display: none;
}
.error-message.show {
display: block;
}
.input-box.error {
border-color: #ff4d4f;
}
/* 响应式适配 */
@media (max-width: 480px) {
.auth-card {
box-shadow: none;
background-color: transparent;
padding: 0;
}
.auth-header, .auth-form {
padding: 16px;
}
.verify-code-container {
flex-direction: column;
}
.send-code-btn {
padding: 12px 16px;
width: 100%;
}
}

@ -0,0 +1,153 @@
/* 全局重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
/* 导航栏样式 */
.nav-container {
width: 100%;
height: 60px;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
position: fixed;
top: 0;
left: 0;
z-index: 999;
}
.nav-content {
width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
.logo {
font-size: 22px;
font-weight: bold;
color: #2c83f2;
text-decoration: none;
}
.nav-menu {
display: flex;
list-style: none;
}
.nav-menu li {
margin: 0 20px;
}
.nav-menu li a {
text-decoration: none;
color: #333;
font-size: 16px;
transition: color 0.3s;
}
.nav-menu li a:hover {
color: #2c83f2;
}
.user-actions a {
margin-left: 20px;
text-decoration: none;
color: #333;
font-size: 16px;
}
.user-actions .login-btn {
color: #2c83f2;
}
.user-actions .register-btn {
background-color: #2c83f2;
color: #fff;
padding: 6px 16px;
border-radius: 4px;
}
/* 底部样式 */
.footer {
width: 100%;
height: 120px;
background-color: #333;
color: #fff;
margin-top: 50px;
}
.footer-content {
width: 1200px;
margin: 0 auto;
padding-top: 30px;
text-align: center;
}
.footer-content p {
margin: 8px 0;
font-size: 14px;
opacity: 0.8;
}
/* 容器通用样式(页面内容区) */
.container {
width: 1200px;
margin: 80px auto 0; /* 避开导航栏 */
min-height: calc(100vh - 200px);
}
/* 按钮通用样式 */
.btn {
padding: 8px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-primary {
background-color: #2c83f2;
color: #fff;
}
.btn-primary:hover {
background-color: #1a73e8;
}
.btn-default {
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
.btn-default:hover {
background-color: #f5f5f5;
}
/* 输入框通用样式 */
.input-box {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
margin-bottom: 16px;
}
.input-box:focus {
outline: none;
border-color: #2c83f2;
box-shadow: 0 0 0 2px rgba(44, 131, 242, 0.2);
}

@ -0,0 +1,174 @@
/* 搜索栏样式 */
.search-bar {
width: 100%;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
display: flex;
align-items: center;
}
.search-input {
flex: 1;
height: 48px;
padding: 0 16px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 16px;
}
.search-input:focus {
outline: none;
border-color: #2c83f2;
}
.search-btn {
height: 48px;
border-radius: 0 4px 4px 0;
padding: 0 30px;
}
/* 景点列表标题 */
.spot-list-title {
font-size: 22px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
/* 景点网格布局 */
.spot-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
/* 景点卡片样式 */
.spot-card {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.spot-card:hover {
transform: translateY(-5px);
}
/* 景点图片 */
.spot-img {
width: 100%;
height: 220px;
object-fit: cover;
}
/* 景点信息区 */
.spot-info {
padding: 16px;
}
.spot-name {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.spot-location {
font-size: 14px;
color: #666;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.spot-location::before {
content: "📍";
margin-right: 4px;
}
/* 评分样式 */
.spot-rating {
margin-bottom: 12px;
display: flex;
align-items: center;
}
.star {
color: #ffc107;
font-size: 16px;
margin-right: 4px;
}
.rating-value {
font-size: 16px;
font-weight: bold;
color: #333;
margin-right: 8px;
}
.review-count {
font-size: 14px;
color: #666;
}
/* 价格样式 */
.spot-price {
margin-bottom: 16px;
display: flex;
align-items: baseline;
}
.price-tag {
font-size: 16px;
color: #ff4d4f;
margin-right: 4px;
}
.price-value {
font-size: 22px;
font-weight: bold;
color: #ff4d4f;
margin-right: 4px;
}
.price-unit {
font-size: 14px;
color: #666;
}
/* 查看详情按钮 */
.spot-btn {
width: 100%;
}
/* 导航栏激活状态 */
.nav-menu .active {
color: #2c83f2;
font-weight: bold;
}
/* 用户信息样式 */
.user-info {
display: flex;
align-items: center;
gap: 20px;
}
.user-info span {
font-size: 16px;
color: #333;
}
.user-center {
text-decoration: none;
color: #333;
font-size: 16px;
}
.user-center:hover {
color: #2c83f2;
}

@ -0,0 +1,64 @@
/* 登录容器样式 */
.login-wrapper {
width: 420px;
margin: 50px auto;
background-color: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 16px rgba(0,0,0,0.1);
}
/* 标题样式 */
.login-title {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
color: #333;
}
/* 表单项样式 */
.form-item {
margin-bottom: 20px;
}
.form-item label {
display: block;
margin-bottom: 8px;
font-size: 16px;
color: #666;
}
/* 表单底部样式 */
.form-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.forgot-pwd {
color: #2c83f2;
text-decoration: none;
font-size: 14px;
}
.forgot-pwd:hover {
text-decoration: underline;
}
/* 注册链接样式 */
.register-link {
text-align: center;
font-size: 14px;
color: #666;
}
.register-link a {
color: #2c83f2;
text-decoration: none;
}
.register-link a:hover {
text-decoration: underline;
}

@ -0,0 +1,171 @@
/* 景点推荐页面样式 */
.recommendation-header {
text-align: center;
padding: 40px 0;
}
.recommendation-header h1 {
font-size: 32px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.recommendation-header p {
color: #666;
font-size: 16px;
}
/* 筛选栏样式 */
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 16px 0;
margin-bottom: 24px;
border-top: 1px solid #f0f2f5;
border-bottom: 1px solid #f0f2f5;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-group label {
color: #666;
font-size: 14px;
}
.filter-select {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
color: #333;
}
/* 景点网格布局 */
.spots-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
/* 景点卡片样式 */
.spot-card {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
background-color: #fff;
}
.spot-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
.spot-img {
width: 100%;
height: 180px;
object-fit: cover;
}
.spot-info {
padding: 16px;
}
.spot-name {
font-weight: bold;
font-size: 18px;
color: #333;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spot-location {
color: #666;
font-size: 14px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.spot-location::before {
content: "📍";
}
.spot-rating {
color: #faad14;
margin-bottom: 12px;
font-weight: 500;
}
.spot-desc {
color: #666;
font-size: 14px;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 16px;
}
.spot-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px dashed #f0f2f5;
}
.spot-price {
color: #ff4d4f;
font-weight: bold;
}
.view-detail-btn {
color: #2c83f2;
font-size: 14px;
cursor: pointer;
}
.view-detail-btn:hover {
text-decoration: underline;
}
/* 加载状态 */
.loading {
grid-column: 1 / -1;
text-align: center;
padding: 64px 0;
color: #666;
}
/* 无数据状态 */
.no-data {
grid-column: 1 / -1;
text-align: center;
padding: 64px 0;
color: #999;
}
/* 响应式适配 */
@media (max-width: 768px) {
.filter-bar {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.spots-grid {
grid-template-columns: 1fr;
}
}

@ -0,0 +1,84 @@
/* 注册容器样式(与登录页保持风格统一) */
.register-wrapper {
width: 480px;
margin: 50px auto;
background-color: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 16px rgba(0,0,0,0.1);
}
/* 标题样式 */
.register-title {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
color: #333;
}
/* 表单项样式 */
.form-item {
margin-bottom: 20px;
}
.form-item label {
display: block;
margin-bottom: 8px;
font-size: 16px;
color: #666;
}
/* 密码提示样式 */
.password-tip {
font-size: 12px;
color: #999;
margin-top: 6px;
}
/* 验证码项样式 */
.code-item {
display: flex;
gap: 12px;
}
.code-input {
flex: 1;
}
.code-btn {
width: 140px;
height: 42px;
padding: 0;
}
/* 提交按钮样式 */
.register-submit {
width: 100%;
height: 48px;
font-size: 18px;
}
/* 登录链接样式 */
.login-link {
text-align: center;
font-size: 14px;
color: #666;
margin-top: 30px;
}
.login-link a {
color: #2c83f2;
text-decoration: none;
}
.login-link a:hover {
text-decoration: underline;
}
/* 验证码按钮禁用样式 */
.code-btn:disabled {
background-color: #eee;
color: #999;
cursor: not-allowed;
}

@ -0,0 +1,108 @@
/* 评论容器样式 */
.review-wrapper {
width: 800px;
margin: 50px auto;
background-color: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 16px rgba(0,0,0,0.1);
}
/* 标题样式 */
.review-title {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
color: #333;
}
/* 景点提示样式 */
.spot-tip {
font-size: 16px;
color: #666;
margin-bottom: 24px;
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
}
/* 表单项样式 */
.form-item {
margin-bottom: 24px;
}
.form-item label {
display: block;
margin-bottom: 8px;
font-size: 16px;
color: #666;
font-weight: bold;
}
/* 评论内容输入框 */
.review-content-input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
resize: none;
transition: border-color 0.3s;
}
.review-content-input:focus {
outline: none;
border-color: #2c83f2;
box-shadow: 0 0 0 2px rgba(44, 131, 242, 0.2);
}
/* 内容字数提示 */
.content-tip {
font-size: 14px;
color: #999;
margin-top: 8px;
text-align: right;
}
.content-tip.warning {
color: #ff4d4f;
}
/* 表单底部样式 */
.form-footer {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-bottom: 24px;
}
.cancel-btn, .submit-btn {
padding: 8px 24px;
font-size: 16px;
}
.submit-btn {
background-color: #2c83f2;
}
.submit-btn:hover {
background-color: #1a73e8;
}
/* 评论须知样式 */
.review-notice {
padding: 16px;
background-color: #fff8e1;
border-radius: 4px;
color: #ff8f00;
font-size: 14px;
}
.review-notice p {
margin-bottom: 8px;
}
.review-notice p:first-child {
font-weight: bold;
}

@ -0,0 +1,295 @@
/* 1. 景点头部样式 */
.spot-header {
display: flex;
gap: 24px;
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 24px;
}
/* 1.1 景点图片区 */
.spot-imgs {
width: 50%;
}
.main-img {
width: 100%;
height: 400px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 12px;
}
.img-thumbnails {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 8px;
}
.img-thumbnail {
width: 80px;
height: 60px;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
}
.img-thumbnail.active {
border-color: #2c83f2;
}
/* 1.2 景点基础信息区 */
.spot-basic-info {
width: 50%;
display: flex;
flex-direction: column;
gap: 16px;
}
.spot-name {
font-size: 28px;
font-weight: bold;
color: #333;
line-height: 1.3;
}
.spot-rating-location {
display: flex;
align-items: center;
gap: 12px;
font-size: 16px;
}
.star {
color: #ffc107;
font-size: 18px;
}
.rating-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.review-count {
color: #666;
font-size: 14px;
}
.spot-location {
color: #666;
font-size: 14px;
display: flex;
align-items: center;
}
.spot-location::before {
content: "📍";
margin-right: 4px;
}
.spot-desc {
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.spot-meta {
display: flex;
flex-direction: column;
gap: 8px;
color: #666;
}
.meta-item {
display: flex;
gap: 8px;
}
.meta-label {
font-weight: bold;
color: #333;
}
.price {
color: #ff4d4f;
font-weight: bold;
font-size: 18px;
}
.buy-ticket-btn {
width: 100%;
height: 48px;
font-size: 18px;
margin-top: 16px;
}
/* 2. 标签页样式 */
.spot-tabs {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.tab-buttons {
display: flex;
border-bottom: 1px solid #eee;
}
.tab-btn {
padding: 16px 24px;
background: none;
border: none;
font-size: 16px;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.tab-btn.active {
color: #2c83f2;
border-bottom: 2px solid #2c83f2;
font-weight: bold;
}
.tab-btn:hover:not(.active) {
color: #333;
background-color: #f5f5f5;
}
.tab-content {
padding: 24px;
display: none;
}
.tab-content.active {
display: block;
}
/* 3. 景点介绍内容 */
.detail-content {
color: #666;
line-height: 1.8;
gap: 16px;
}
.detail-content p {
margin-bottom: 16px;
}
/* 4. 评论区样式 */
.review-submit-entry {
margin-bottom: 24px;
text-align: right;
}
.submit-review-btn {
padding: 8px 16px;
}
.review-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.review-item {
padding: 16px;
border-bottom: 1px solid #eee;
}
.review-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.review-username {
font-weight: bold;
color: #333;
}
.review-time {
color: #999;
font-size: 14px;
}
.review-content {
color: #666;
line-height: 1.6;
margin-bottom: 8px;
}
.review-verify {
display: flex;
align-items: center;
gap: 8px;
color: #2c83f2;
font-size: 14px;
cursor: pointer;
}
.verify-icon {
width: 16px;
height: 16px;
background-image: url("../assets/icons/verify-icon.png");
background-size: cover;
}
/* 5. 相关攻略列表 */
.strategy-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.strategy-card {
padding: 16px;
border: 1px solid #eee;
border-radius: 8px;
transition: box-shadow 0.3s;
}
.strategy-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.strategy-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.strategy-meta {
display: flex;
gap: 16px;
color: #999;
font-size: 14px;
margin-bottom: 8px;
}
.strategy-desc {
color: #666;
font-size: 14px;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 6. 无数据提示 */
.no-data {
text-align: center;
color: #999;
padding: 48px 0;
}

@ -0,0 +1,417 @@
/* 攻略生成页整体样式 */
.strategy-wrapper {
width: 100%;
background-color: #fff;
padding: 32px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin: 30px auto;
max-width: 1200px;
}
/* 标题与描述样式 */
.strategy-title {
font-size: 28px;
font-weight: bold;
text-align: center;
margin-bottom: 16px;
color: #333;
position: relative;
padding-bottom: 12px;
}
.strategy-title::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 3px;
background-color: #2c83f2;
}
.strategy-desc {
text-align: center;
color: #666;
margin-bottom: 32px;
font-size: 16px;
line-height: 1.6;
}
/* 表单容器样式 */
.strategy-form {
background-color: #f9fafc;
padding: 24px;
border-radius: 8px;
margin-bottom: 32px;
border: 1px solid #f0f2f5;
}
/* 表单行布局 */
.form-row {
display: flex;
gap: 24px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.form-row .form-item {
flex: 1;
min-width: 280px;
}
/* 表单项样式 */
.form-item {
margin-bottom: 24px;
}
.form-item label {
display: block;
margin-bottom: 8px;
font-size: 16px;
color: #666;
font-weight: 500;
}
/* 输入框通用样式 */
.input-box {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: all 0.3s ease;
box-sizing: border-box;
}
.input-box:focus {
outline: none;
border-color: #2c83f2;
box-shadow: 0 0 0 3px rgba(44, 131, 242, 0.1);
}
/* 兴趣偏好标签样式 */
.preference-tags {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 8px;
}
.tag-item {
display: flex;
align-items: center;
gap: 8px;
color: #666;
cursor: pointer;
padding: 6px 12px;
border-radius: 20px;
border: 1px solid #ddd;
transition: all 0.2s;
}
.tag-item:hover {
border-color: #2c83f2;
background-color: #f0f7ff;
}
.tag-item input:checked + span {
color: #2c83f2;
font-weight: 500;
}
.tag-item input:checked + span::before {
content: '✓ ';
}
/* 特殊需求文本框 */
#specialNeed {
resize: vertical;
}
/* 表单底部按钮区域 */
.form-footer {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f2f5;
}
/* 按钮样式 */
.btn {
padding: 10px 24px;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border: none;
}
.btn-default {
background-color: #f0f2f5;
color: #333;
}
.btn-default:hover {
background-color: #e5e6eb;
}
.btn-primary {
background-color: #2c83f2;
color: #fff;
}
.btn-primary:hover {
background-color: #1a73e8;
}
.btn-primary:disabled {
background-color: #96bfff;
cursor: not-allowed;
opacity: 0.8;
}
/* 攻略结果区域样式 */
.strategy-result {
margin-top: 32px;
border-top: 1px solid #f0f2f5;
padding-top: 24px;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 结果头部样式 */
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.result-title {
font-size: 22px;
font-weight: bold;
color: #333;
flex: 1;
min-width: 280px;
}
.result-actions {
display: flex;
gap: 12px;
}
/* 攻略内容样式 */
.result-content {
color: #333;
line-height: 1.8;
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;
padding-bottom: 24px;
border-bottom: 1px dashed #e5e6eb;
}
.day-title {
font-size: 19px;
font-weight: bold;
margin-bottom: 20px;
color: #2c83f2;
display: flex;
align-items: center;
gap: 10px;
padding-left: 8px;
border-left: 3px solid #2c83f2;
}
/* 行程项样式 - 修改为严格的时间-地点:描述格式 */
.schedule-item {
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 8px;
position: relative;
}
.schedule-time {
font-weight: 600;
color: #333;
margin-bottom: 4px;
font-size: 15px;
}
.schedule-content {
margin-bottom: 0;
display: flex;
align-items: flex-start;
}
.schedule-location {
color: #2c83f2;
font-weight: 500;
margin-right: 4px;
font-size: 16px;
}
.schedule-desc {
color: #666;
margin: 0;
padding: 0;
background: none;
border-left: none;
border-radius: 0;
font-size: 15px;
line-height: 1.6;
}
/* 预算总结样式 */
.budget-summary {
background-color: #f0f7ff;
padding: 20px;
border-radius: 8px;
margin-top: 32px;
border: 1px solid #e6f4ff;
}
.budget-title {
font-weight: 600;
margin-bottom: 16px;
color: #333;
font-size: 18px;
text-align: center;
}
.budget-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.budget-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #e6f4ff;
color: #666;
}
.budget-item:last-child:not(.budget-total) {
border-bottom: none;
}
.budget-total {
font-weight: bold;
color: #ff4d4f;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e6f4ff;
font-size: 18px;
grid-column: 1 / 3;
display: flex;
justify-content: space-between;
}
/* 生成中状态样式 */
.generating {
text-align: center;
padding: 60px 0;
color: #666;
}
.generating .loading-icon {
font-size: 56px;
margin-bottom: 20px;
animation: spin 2s linear infinite;
display: inline-block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.generating p {
font-size: 16px;
max-width: 500px;
margin: 0 auto;
line-height: 1.6;
}
/* 响应式适配 */
@media (max-width: 768px) {
.strategy-wrapper {
padding: 20px;
margin: 15px;
}
.form-row {
flex-direction: column;
gap: 16px;
}
.form-row .form-item {
min-width: auto;
}
.result-header {
flex-direction: column;
align-items: flex-start;
}
.result-actions {
width: 100%;
justify-content: space-between;
}
.day-section {
margin-bottom: 24px;
padding-bottom: 16px;
}
.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;
}

@ -0,0 +1,311 @@
/* 个人中心容器 */
.user-center-wrapper {
display: flex;
gap: 24px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
/* 左侧侧边栏 */
.user-sidebar {
width: 260px;
background-color: #f5f7fa;
padding: 24px 0;
}
/* 用户头像与名称 */
.user-profile {
text-align: center;
padding: 0 24px 24px;
border-bottom: 1px solid #eee;
margin-bottom: 16px;
}
.avatar img {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 12px;
}
.user-name {
font-size: 18px;
font-weight: bold;
color: #333;
}
/* 侧边栏菜单 */
.sidebar-menu {
list-style: none;
}
.sidebar-menu li {
padding: 12px 24px;
display: flex;
align-items: center;
gap: 12px;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.sidebar-menu li:hover:not(.active) {
background-color: #e9f0fb;
color: #2c83f2;
}
.sidebar-menu li.active {
background-color: #2c83f2;
color: #fff;
}
.menu-icon {
font-size: 18px;
}
.menu-text {
font-size: 16px;
}
/* 右侧内容区 */
.user-content {
flex: 1;
padding: 24px;
}
/* 标签页标题 */
.tab-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.tab-header h2 {
font-size: 22px;
font-weight: bold;
color: #333;
}
/* 内容标签页 */
.content-tab {
display: none;
}
.content-tab.active {
display: block;
}
/* 订单筛选按钮 */
.order-filters {
display: flex;
gap: 8px;
}
.filter-btn {
padding: 4px 12px;
background: none;
border: 1px solid #ddd;
border-radius: 16px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn.active {
background-color: #2c83f2;
color: #fff;
border-color: #2c83f2;
}
/* 订单列表样式 */
.order-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.order-item {
padding: 16px;
border: 1px solid #eee;
border-radius: 8px;
transition: box-shadow 0.3s;
}
.order-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.order-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 14px;
}
.order-id {
color: #999;
}
.order-status {
font-weight: bold;
}
.status-pending {
color: #faad14;
}
.status-used {
color: #52c41a;
}
.status-refunded {
color: #ff4d4f;
}
.order-body {
display: flex;
gap: 16px;
margin-bottom: 12px;
}
.order-img {
width: 100px;
height: 70px;
object-fit: cover;
border-radius: 4px;
}
.order-info {
flex: 1;
}
.order-spot-name {
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.order-date {
color: #666;
font-size: 14px;
margin-bottom: 4px;
}
.order-meta {
display: flex;
gap: 16px;
color: #666;
font-size: 14px;
}
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px dashed #eee;
}
.order-price {
font-weight: bold;
color: #ff4d4f;
}
.order-actions {
display: flex;
gap: 8px;
}
.order-btn {
padding: 4px 12px;
font-size: 14px;
}
/* 我的攻略列表样式 */
.strategy-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.strategy-item {
padding: 16px;
border: 1px solid #eee;
border-radius: 8px;
transition: box-shadow 0.3s;
}
.strategy-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.strategy-title {
font-weight: bold;
color: #333;
margin-bottom: 8px;
font-size: 16px;
}
.strategy-meta {
display: flex;
gap: 16px;
color: #999;
font-size: 14px;
margin-bottom: 8px;
}
.strategy-desc {
color: #666;
font-size: 14px;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 个人资料表单样式 */
.profile-form {
max-width: 600px;
}
.profile-form .form-item {
margin-bottom: 24px;
}
.profile-form label {
display: block;
margin-bottom: 8px;
font-size: 16px;
color: #666;
font-weight: bold;
}
.save-profile-btn {
width: 100%;
height: 48px;
font-size: 16px;
}
/* 无数据提示 */
.no-data {
text-align: center;
color: #999;
padding: 64px 0;
}
/* 加载状态 */
.loading {
text-align: center;
color: #666;
padding: 64px 0;
}

@ -0,0 +1,103 @@
// ******************************
// API接口配置文件
// 修改为连接本地后端服务
// ******************************
// 后端服务器基础地址 - 修改为本地地址
const BACKEND_BASE_URL = 'http://localhost:8080';
// 所有API接口路径配置
const API = {
// 用户相关
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: `${BACKEND_BASE_URL}/api/verify-code/send`,
resetPassword: `${BACKEND_BASE_URL}/api/user/reset-password`,
// 景点推荐相关接口
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`,
// 其他接口...
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;
}

@ -0,0 +1,125 @@
window.onload = function() {
// 绑定发送验证码按钮事件
bindSendCode();
// 绑定表单提交事件(密码重置)
bindResetPassword();
};
// 发送验证码
function bindSendCode() {
const sendBtn = document.getElementById('sendCodeBtn');
const emailInput = document.getElementById('email');
let countdown = 0;
sendBtn.addEventListener('click', function() {
const email = emailInput.value.trim();
if (!email) {
alert('请输入邮箱');
return;
}
// 简单邮箱格式验证
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
alert('请输入有效的邮箱地址');
return;
}
// ******************************
// 调用后端发送验证码接口
// 功能:向后端请求发送密码重置验证码
// 方法POST
// 参数:{ email: 用户邮箱, type: 'reset_password' }
// 后端地址API.sendVerifyCode配置在api.js中指向另一台电脑的后端服务
// ******************************
fetch(API.sendVerifyCode, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
type: 'reset_password' // 标识验证码用途:密码重置
})
})
.then(response => {
// 检查HTTP响应状态
if (!response.ok) throw new Error('验证码发送失败');
return response.json();
})
.then(data => {
// 处理后端返回的成功响应
alert('验证码已发送至您的邮箱,请注意查收');
// 开始倒计时,防止频繁发送验证码
countdown = 60;
sendBtn.disabled = true;
sendBtn.textContent = `重新发送(${countdown}s)`;
const timer = setInterval(() => {
countdown--;
sendBtn.textContent = `重新发送(${countdown}s)`;
if (countdown <= 0) {
clearInterval(timer);
sendBtn.disabled = false;
sendBtn.textContent = '发送验证码';
}
}, 1000);
})
.catch(error => {
// 处理请求错误或后端返回的错误
alert(error.message);
});
});
}
// 重置密码
function bindResetPassword() {
const form = document.getElementById('forgotForm');
form.addEventListener('submit', function(e) {
e.preventDefault();
const email = document.getElementById('email').value.trim();
const verifyCode = document.getElementById('verifyCode').value.trim();
const newPassword = document.getElementById('newPassword').value.trim();
// 前端验证
if (newPassword.length < 6 || newPassword.length > 20) {
alert('密码长度必须为6-20位');
return;
}
if (verifyCode.length !== 6) {
alert('请输入6位验证码');
return;
}
// ******************************
// 调用后端密码重置接口
// 功能:验证验证码并更新用户密码
// 方法POST
// 参数:{ email: 用户邮箱, verifyCode: 验证码, newPassword: 新密码 }
// 后端地址API.resetPassword配置在api.js中指向另一台电脑的后端服务
// ******************************
fetch(API.resetPassword, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
verifyCode: verifyCode, // 用户输入的验证码
newPassword: newPassword // 新密码(实际项目中建议前端先加密)
})
})
.then(response => {
if (!response.ok) throw new Error('密码重置失败');
return response.json();
})
.then(data => {
// 密码重置成功,跳转到登录页
alert('密码重置成功,请使用新密码登录');
window.location.href = '../pages/login.html';
})
.catch(error => {
// 处理错误(验证码错误、过期等)
alert(error.message);
});
});
}

@ -0,0 +1,204 @@
// 1. 页面加载完成后执行
window.onload = function() {
// 1.1 切换导航栏用户状态(登录/未登录)
updateUserNav();
// 1.2 加载景点列表(调用后端接口)
loadSpotList();
// 1.3 绑定搜索按钮点击事件
bindSearchEvent();
};
// 2. 切换导航栏用户状态
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;
} else {
// 未登录:显示登录/注册按钮
loginBtn.style.display = 'inline-block';
registerBtn.style.display = 'inline-block';
userInfo.style.display = 'none';
}
}
// 3. 加载景点列表(核心:调用后端接口)
function loadSpotList() {
const spotGrid = document.getElementById('spotGrid');
// 清空现有列表(避免重复渲染)
spotGrid.innerHTML = '<div class="loading">加载中...</div>';
// 3.1 调用后端景点列表接口GET请求支持分页/搜索)
//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')
'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...] } }
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 => {
const spotCard = document.createElement('div');
spotCard.className = 'spot-card';
// 卡片内容(使用后端返回的景点字段)
spotCard.innerHTML = `
<img src="${spot.imgUrl || '../assets/images/default-spot.jpg'}" alt="${spot.name}" class="spot-img">
<div class="spot-info">
<h3 class="spot-name">${spot.name}</h3>
<p class="spot-location">${spot.location}</p>
<div class="spot-rating">
<span class="star"></span>
<span class="rating-value">${spot.rating || 0}</span>
<span class="review-count">${spot.reviewCount || 0}条评论</span>
</div>
<div class="spot-price">
<span class="price-tag">¥</span>
<span class="price-value">${spot.minPrice || 0}</span>
<span class="price-unit">/</span>
</div>
<a href="../pages/spot-detail.html?spotId=${spot.id}" class="btn btn-primary spot-btn">查看详情</a>
</div>
`;
spotGrid.appendChild(spotCard);
});
})
.catch(error => {
alert(error.message);
console.error('景点加载错误', error);
});
}
// 4. 绑定搜索事件
function bindSearchEvent() {
const searchBtn = document.getElementById('searchBtn');
const searchInput = document.getElementById('searchInput');
searchBtn.addEventListener('click', function() {
const keyword = searchInput.value.trim();
if (keyword) {
// 搜索逻辑:调用带关键词的景点接口
loadSpotListWithKeyword(keyword);
} else {
alert('请输入搜索关键词');
}
});
// 回车触发搜索
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
searchBtn.click();
}
});
}
// 5. 带关键词的景点搜索(调用后端搜索接口)
function loadSpotListWithKeyword(keyword) {
const spotGrid = document.getElementById('spotGrid');
spotGrid.innerHTML = '<div class="loading">加载中...</div>';
// 调用后端搜索接口GET请求参数拼接在URL后
// 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: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('搜索失败');
}
return response.json();
})
.then(data => {
// 渲染逻辑同loadSpotList可复用代码
spotGrid.innerHTML = '';
//const spots = data.data.list;
const spots = data.data; // 注意:这里是 data.data
if (spots.length === 0) {
spotGrid.innerHTML = '<div class="no-spot">未找到与"${keyword}"相关的景点</div>';
return;
}
spots.forEach(spot => {
// 同loadSpotList的卡片生成逻辑
const spotCard = document.createElement('div');
spotCard.className = 'spot-card';
spotCard.innerHTML = `
<img src="${spot.imgUrl || '../assets/images/default-spot.jpg'}" alt="${spot.name}" class="spot-img">
<div class="spot-info">
<h3 class="spot-name">${spot.name}</h3>
<p class="spot-location">${spot.location}</p>
<div class="spot-rating">
<span class="star"></span>
<span class="rating-value">${spot.rating || 0}</span>
<span class="review-count">${spot.reviewCount || 0}条评论</span>
</div>
<div class="spot-price">
<span class="price-tag">¥</span>
<span class="price-value">${spot.minPrice || 0}</span>
<span class="price-unit">/</span>
</div>
<a href="../pages/spot-detail.html?spotId=${spot.id}" class="btn btn-primary spot-btn">查看详情</a>
</div>
`;
spotGrid.appendChild(spotCard);
});
})
.catch(error => {
alert(error.message);
spotGrid.innerHTML = '';
console.error('搜索错误', error);
});
}
// 【与后端配合说明】
// 1. 景点列表接口API.spotList
// - 方法GET
// - 参数可选page=1&size=10分页、location=北京(按地区筛选)
// - 返回格式:{ code:200, message:'success', data: { list: [ {id, name, location, rating, reviewCount, minPrice, imgUrl}, ... ], total: 100 } }
// 2. 景点搜索接口API.spotSearch
// - 方法GET
// - 参数keyword=故宫搜索关键词需URL编码
// - 返回格式:同景点列表接口
// 3. 图片处理后端返回的imgUrl若为相对路径需拼接后端图片服务器地址若为绝对路径直接使用
// 4. 景点详情页跳转通过URL参数spotId传递景点ID详情页需解析该参数调用详情接口

@ -0,0 +1,64 @@
// 1. 接口地址已在api.js中统一管理此处直接引用
// api.js中需定义const API = { login: 'http://172.20.10.2:端口/api/user/login' };
// 监听登录表单提交事件
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
// 显示加载状态
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',
},
body: JSON.stringify({
username: username,
password: password
})
})
.then(response => {
if (!response.ok) {
throw new Error('登录失败,请检查账号密码');
}
return response.json();
})
.then(data => {
console.log('登录成功', data);
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 => {
alert(error.message);
console.error('登录错误', error);
})
.finally(() => {
// 恢复按钮状态
submitBtn.textContent = originalText;
submitBtn.disabled = false;
});
});
// 【与后端配合说明】
// 1. 后端需提供POST类型的登录接口地址与api.js中定义的API.login一致
// 2. 后端接收参数:{ username: String, password: String }
// 3. 后端成功返回格式:{ code: 200, message: '登录成功', data: { token: 'xxx', userId: 'xxx', username: 'xxx' } }
// 4. 后端失败返回格式:{ code: 400, message: '账号或密码错误' }需在response.ok判断时处理
// 5. 后续请求需在请求头携带tokenheaders: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }

@ -0,0 +1,128 @@
window.onload = function() {
// 更新导航栏用户状态
updateUserNav();
// 加载推荐景点
loadRecommendedSpots();
// 绑定筛选器事件
bindFilters();
};
// 更新导航栏用户状态
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 loadRecommendedSpots(filters = {}) {
const spotsGrid = document.getElementById('spotsGrid');
spotsGrid.innerHTML = '<div class="loading">加载推荐景点中...</div>';
// 构建查询参数
const params = new URLSearchParams();
if (filters.destination) params.append('destination', filters.destination);
if (filters.type) params.append('type', filters.type);
if (filters.sort) params.append('sort', filters.sort);
// ******************************
// 调用后端景点推荐接口
// 功能:获取符合筛选条件的景点推荐列表
// 方法GET
// 参数通过URL查询参数传递筛选条件
// - destination: 目的地城市
// - type: 景点类型
// - sort: 排序方式
// 后端地址API.recommendSpots配置在api.js中指向另一台电脑的后端服务
// ******************************
fetch(`${API.recommendSpots}?${params.toString()}`, {
method: 'GET',
headers: {
// 携带登录令牌(可选,未登录用户也可获取推荐)
'Authorization': 'Bearer ' + (localStorage.getItem('token') || '')
}
})
.then(response => {
// 检查HTTP响应状态
if (!response.ok) throw new Error('景点加载失败');
return response.json();
})
.then(data => {
// 处理后端返回的景点数据
const spots = data.data || [];
spotsGrid.innerHTML = '';
if (spots.length === 0) {
spotsGrid.innerHTML = '<div class="no-data">暂无符合条件的景点推荐</div>';
return;
}
// 渲染景点列表
spots.forEach(spot => {
const spotCard = document.createElement('div');
spotCard.className = 'spot-card';
spotCard.innerHTML = `
<img src="${spot.imgUrl || '../assets/images/default-spot.jpg'}" alt="${spot.name}" class="spot-img">
<div class="spot-info">
<div class="spot-name">${spot.name}</div>
<div class="spot-location">${spot.location}</div>
<div class="spot-rating"> ${spot.rating.toFixed(1)} (${spot.reviewCount}条评价)</div>
<div class="spot-desc">${spot.description}</div>
<div class="spot-footer">
<div class="spot-price">¥${spot.price}/</div>
<div class="view-detail-btn" data-id="${spot.id}">查看详情</div>
</div>
</div>
`;
spotsGrid.appendChild(spotCard);
});
// 绑定查看详情事件
bindViewDetail();
})
.catch(error => {
// 处理请求错误
spotsGrid.innerHTML = `<div class="no-data">加载失败:${error.message}</div>`;
console.error('景点加载错误', error);
});
}
// 绑定筛选器事件
function bindFilters() {
const destinationFilter = document.getElementById('destinationFilter');
const typeFilter = document.getElementById('typeFilter');
const sortFilter = document.getElementById('sortFilter');
// 筛选器变化时重新加载景点
[destinationFilter, typeFilter, sortFilter].forEach(filter => {
filter.addEventListener('change', () => {
loadRecommendedSpots({
destination: destinationFilter.value,
type: typeFilter.value,
sort: sortFilter.value
});
});
});
}
// 绑定查看详情事件
function bindViewDetail() {
document.querySelectorAll('.view-detail-btn').forEach(btn => {
btn.addEventListener('click', function() {
const spotId = this.getAttribute('data-id');
// 跳转到景点详情页
window.location.href = `../pages/spot-detail.html?spotId=${spotId}`;
});
});
}

@ -0,0 +1,187 @@
// 调试信息
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');
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, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: username,
type: isPhone ? 'phone' : 'email'
})
})
.then(response => {
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('验证码已发送,请注意查收');
// 启动倒计时
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);
})
.catch(error => {
console.error('获取验证码完整错误:', error);
console.error('错误名称:', error.name);
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
alert('验证码发送失败: ' + error.message);
});
});
}
// 2. 绑定注册表单提交事件(参考文档:注册用例-基本交互)
function bindRegisterFormSubmit() {
const registerForm = document.getElementById('registerForm');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const codeInput = document.getElementById('code');
registerForm.addEventListener('submit', function(e) {
e.preventDefault(); // 阻止默认提交
// 1. 前端表单验证
const username = usernameInput.value.trim();
const password = passwordInput.value.trim();
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;
// }
// 2. 调用后端注册接口
fetch(API.register, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username, // 账号(手机号/邮箱)
password: password, // 密码(前端可加密,后端必须加密存储)
code: code // 验证码
})
})
.then(response => {
if (!response.ok) {
// 后端返回错误信息(如“账号已存在”“验证码错误”)
return response.json().then(err => { throw new Error(err.message || '注册失败') });
}
return response.json();
})
.then(data => {
// 注册成功(参考文档:注册用例-后置条件)
alert('注册成功!即将跳转登录页');
// 跳转登录页
window.location.href = '../pages/login.html';
})
.catch(error => {
alert(error.message);
console.error('注册错误', error);
});
});
}
// 【与后端配合说明】
// 1. 验证码接口(需补充):
// - 方法POST
// - 参数:{ username: String, type: 'phone'/'email' }
// - 业务规则1小时内同一账号最多获取5次验证码有效期5分钟后端控制
// 2. 注册接口:
// - 方法POST
// - 参数:{ username: String, password: String, code: String }
// - 返回格式:{ code:200, message:'注册成功' } 或 { code:400, message:'账号已存在' }
// - 密码存储后端需使用BCrypt等算法加密存储禁止明文

@ -0,0 +1,307 @@
// 全局变量当前景点ID、景点名称
let currentSpotId = '';
let currentSpotName = '';
// 页面加载完成后执行
window.onload = function() {
console.log('=== 评论页面开始加载 ===');
// 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');
console.log('景点ID:', currentSpotId);
if (!currentSpotId) {
alert('未找到景点ID即将返回首页');
window.location.href = '../pages/index.html';
return;
}
// 3. 切换导航栏用户状态
updateUserNav();
// 4. 加载景点信息
loadSpotInfo();
// 5. 绑定评论内容字数统计
bindContentLengthCount();
// 6. 绑定评论表单提交事件
bindReviewFormSubmit();
// 7. 验证用户是否有该景点的购票记录
verifyUserTicket();
};
// 1. 切换导航栏用户状态
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;
}
}
// 2. 加载景点信息
function loadSpotInfo() {
const spotTip = document.getElementById('spotTip');
spotTip.textContent = '加载景点信息中...';
console.log('加载景点信息ID:', currentSpotId);
// 修正接口路径
fetch(`${API.spotDetail}/${currentSpotId}`, {
method: 'GET',
headers: getAuthHeaders()
})
.then(response => {
console.log('景点信息响应状态:', response.status);
if (!response.ok) throw new Error('景点信息加载失败');
return response.json();
})
.then(spot => {
console.log('景点信息:', spot);
currentSpotName = spot.name;
spotTip.textContent = `当前评论景点:${currentSpotName}(请确保分享真实游玩体验)`;
})
.catch(error => {
console.error('景点信息加载错误:', error);
spotTip.textContent = `景点信息加载失败:${error.message}`;
});
}
// 3. 绑定评论内容字数统计
function bindContentLengthCount() {
const contentInput = document.getElementById('reviewContent');
const lengthDom = document.getElementById('contentLength');
const contentTip = document.querySelector('.content-tip');
contentInput.addEventListener('input', function() {
const content = this.value.trim();
const length = content.length;
lengthDom.textContent = length;
// 字数不足提示
if (length < 20) {
contentTip.classList.add('warning');
contentTip.textContent = `已输入 ${length}至少20字还差 ${20 - length} 字)`;
} else {
contentTip.classList.remove('warning');
contentTip.textContent = `已输入 ${length} 字,符合要求`;
}
});
}
// 4. 验证用户是否有该景点的购票记录
function verifyUserTicket() {
const userId = localStorage.getItem('userId');
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: getAuthHeaders()
})
.then(response => {
console.log('购票验证响应状态:', response.status);
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.message || '验证购票记录失败')
});
}
return response.json();
})
.then(data => {
console.log('购票验证结果:', data);
// 根据您的Result结构调整
if (data.data && data.data.hasValidTicket) {
console.log('后端验证:存在有效购票记录');
// 有有效购票记录,允许提交
} else {
throw new Error('仅支持已实际游览该景点的用户发表评论(需有有效购票记录)');
}
})
.catch(error => {
console.error('购票验证错误:', error);
// 无购票记录:禁用提交按钮,提示用户
disableSubmitButton(error.message);
});
}
// 检查本地购票记录
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');
reviewForm.addEventListener('submit', function(e) {
e.preventDefault();
// 1. 前端验证
const content = contentInput.value.trim();
if (content.length < 20) {
alert('评论内容至少20字请补充详细体验');
contentInput.focus();
return;
}
// 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: 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(data => {
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 => {
console.error('评论提交错误:', error);
alert('评论提交失败: ' + error.message);
submitBtn.disabled = false;
submitBtn.textContent = '提交评论(区块链存证)';
});
});
}
// 获取访问日期
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
// - 参数userId、spotId均必传
// - 返回格式:{ code:200, data: { hasValidTicket: true } } 或 { code:400, message:'无有效购票记录' }
// 2. 评论提交接口API.reviewSubmit
// - 方法POST
// - 参数:{ spotId: String, userId: String, content: String }
// - 返回格式:{ code:200, message:'评论提交成功', data: { reviewId: 'xxx', transactionId: 'xxx' } }
// - 后端逻辑先保存评论到MySQL再计算哈希上链返回交易ID
// 3. 业务规则控制同一用户30天内对同一景点最多10条评论后端通过userId+spotId+时间判断)

@ -0,0 +1,363 @@
// 全局变量当前景点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中的景点ID
const urlParams = new URLSearchParams(window.location.search);
currentSpotId = urlParams.get('spotId');
if (!currentSpotId) {
alert('未找到景点ID即将返回首页');
window.location.href = '../pages/index.html';
return;
}
// 2. 切换导航栏用户状态
updateUserNav();
// 3. 加载景点详情数据(使用硬编码数据)
loadSpotDetail();
// 4. 绑定标签页切换事件
bindTabSwitch();
// 5. 绑定购票按钮点击事件
bindBuyTicketBtn();
};
// 1. 切换导航栏用户状态
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');
const reviewSubmitEntry = document.getElementById('reviewSubmitEntry');
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';
submitReviewBtn.href = `../pages/review.html?spotId=${currentSpotId}`;
} else {
reviewSubmitEntry.style.display = 'none';
}
}
// 2. 加载景点详情数据(使用硬编码数据)
function loadSpotDetail() {
console.log('=== 开始加载景点详情(硬编码数据)===');
console.log('currentSpotId:', currentSpotId);
// 显示加载中状态
document.getElementById('spotName').textContent = '加载中...';
document.getElementById('spotDesc').textContent = '加载中...';
// 模拟网络请求延迟
setTimeout(() => {
const spot = SPOTS_DATA[currentSpotId];
if (!spot) {
alert('景点不存在或已被删除');
window.location.href = '../pages/index.html';
return;
}
console.log('景点详情数据:', spot);
// 渲染景点基础信息
renderSpotBasicInfo(spot);
// 渲染景点详情内容
renderSpotDetailContent(spot);
}, 500); // 模拟500ms网络延迟
}
// 2.1 渲染景点基础信息
function renderSpotBasicInfo(spot) {
// 主图
const mainSpotImg = document.getElementById('mainSpotImg');
mainSpotImg.src = spot.imgUrls && spot.imgUrls.length > 0 ? spot.imgUrls[0] : '../assets/images/default-spot.jpg';
mainSpotImg.alt = spot.name;
// 缩略图
const imgThumbnails = document.getElementById('imgThumbnails');
imgThumbnails.innerHTML = '';
if (spot.imgUrls && spot.imgUrls.length > 0) {
spot.imgUrls.forEach((imgUrl, index) => {
const thumbnail = document.createElement('img');
thumbnail.src = imgUrl;
thumbnail.alt = `${spot.name}${index+1}`;
thumbnail.className = `img-thumbnail ${index === 0 ? 'active' : ''}`;
// 缩略图点击切换主图
thumbnail.addEventListener('click', () => {
mainSpotImg.src = imgUrl;
document.querySelectorAll('.img-thumbnail').forEach(t => t.classList.remove('active'));
thumbnail.classList.add('active');
});
imgThumbnails.appendChild(thumbnail);
});
}
// 其他基础信息
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.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');
detailContent.innerHTML = spot.detailContent || '<p>暂无详细介绍</p>';
}
// 3. 绑定标签页切换事件
function bindTabSwitch() {
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
// 切换按钮激活状态
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// 切换内容显示
const tabKey = btn.getAttribute('data-tab');
tabContents.forEach(content => {
content.classList.remove('active');
});
const activeContent = document.getElementById(`${tabKey}Tab`);
activeContent.classList.add('active');
// 懒加载:切换到评论/攻略标签时才加载数据
if (tabKey === 'review' && activeContent.innerHTML.includes('加载中')) {
loadReviewList();
}
if (tabKey === 'strategy' && activeContent.innerHTML.includes('加载中')) {
loadRelatedStrategy();
}
});
});
}
// 4. 加载评论列表(使用模拟数据)
function loadReviewList() {
const reviewList = document.getElementById('reviewList');
reviewList.innerHTML = '<div class="loading">评论加载中...</div>';
// 模拟网络请求延迟
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 (mockReviews.length === 0) {
reviewList.innerHTML = '<div class="no-data">暂无用户评论,快来成为第一个评论的人吧!</div>';
return;
}
// 渲染每条评论
mockReviews.forEach(review => {
const reviewItem = document.createElement('div');
reviewItem.className = 'review-item';
reviewItem.innerHTML = `
<div class="review-header">
<span class="review-username">${review.username || '匿名用户'}</span>
<span class="review-time">${formatTime(review.createTime)}</span>
</div>
<div class="review-content">${review.content}</div>
<div class="review-verify" onclick="verifyReview('${review.id}')">
<div class="verify-icon"></div>
<span>已区块链存证 · 点击验证</span>
</div>
`;
reviewList.appendChild(reviewItem);
});
}, 800); // 模拟800ms网络延迟
}
// 4.1 验证评论区块链存证
function verifyReview(reviewId) {
// 模拟验证过程
alert('评论验证功能演示评论ID ' + reviewId + ' 的区块链验证功能正在开发中');
}
// 5. 加载相关攻略(使用模拟数据)
function loadRelatedStrategy() {
const strategyList = document.getElementById('strategyList');
strategyList.innerHTML = '<div class="loading">攻略加载中...</div>';
// 模拟网络请求延迟
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 (mockStrategies.length === 0) {
strategyList.innerHTML = '<div class="no-data">暂无相关攻略,快去生成属于你的攻略吧!</div>';
return;
}
// 渲染攻略卡片
mockStrategies.forEach(strategy => {
const strategyCard = document.createElement('div');
strategyCard.className = 'strategy-card';
strategyCard.innerHTML = `
<div class="strategy-title">${strategy.title}</div>
<div class="strategy-meta">
<span>作者${strategy.username}</span>
<span>发布时间${formatTime(strategy.createTime)}</span>
</div>
<div class="strategy-desc">${strategy.briefDesc}</div>
`;
strategyList.appendChild(strategyCard);
});
}, 600); // 模拟600ms网络延迟
}
// 6. 绑定购票按钮点击事件
function bindBuyTicketBtn() {
const buyTicketBtn = document.getElementById('buyTicketBtn');
buyTicketBtn.addEventListener('click', () => {
const token = localStorage.getItem('token');
// 未登录:跳转登录页
if (!token) {
alert('请先登录后再购票');
window.location.href = `../pages/login.html?redirect=spot-detail.html%3FspotId%3D${currentSpotId}`;
return;
}
// 已登录:跳转购票页
window.location.href = `../pages/ticket-buy.html?spotId=${currentSpotId}`;
});
}
// 工具函数:时间格式化
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')}`;
}
// 辅助函数:获取认证头信息
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();
}

@ -0,0 +1,293 @@
// 页面加载完成后执行
window.onload = function() {
// 1. 切换导航栏用户状态(登录/未登录)
updateUserNav();
// 2. 设置默认出行日期(今天+1天
setDefaultTravelDate();
// 3. 绑定表单提交事件(生成攻略核心逻辑)
bindGenerateStrategy();
// 4. 绑定保存攻略按钮事件
bindSaveStrategy();
};
// 1. 切换导航栏用户状态
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;
}
}
// 2. 设置默认出行日期(今天+1天格式YYYY-MM-DD
function setDefaultTravelDate() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const defaultDate = tomorrow.toISOString().split('T')[0];
document.getElementById('travelDate').value = defaultDate;
}
// 3. 重置表单(保留默认日期)
function resetForm() {
document.getElementById('strategyForm').reset();
setDefaultTravelDate();
}
// 4. 绑定生成攻略事件(核心:调用后端接口+状态管理)
function bindGenerateStrategy() {
const strategyForm = document.getElementById('strategyForm');
const generateBtn = document.querySelector('.generate-btn');
const strategyResult = document.getElementById('strategyResult');
const resultContent = document.getElementById('resultContent');
const resultTitle = document.getElementById('resultTitle');
strategyForm.addEventListener('submit', function(e) {
e.preventDefault();
// 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).filter(v => v);
const specialNeed = document.getElementById('specialNeed').value.trim();
// 【新增】打印请求参数到控制台,排查参数是否正确
console.log("前端准备的请求参数:", {
destination,
travelDate,
travelDays: parseInt(travelDays),
budget: parseInt(budget),
preferences,
specialNeed
});
// 2. 前端参数验证
if (!destination) {
alert('请输入目的地(如:北京、三亚、故宫)');
return;
}
if (!travelDate) {
alert('请选择出行日期');
return;
}
if (!budget || budget < 100) {
alert('请输入合理的预算至少100元');
return;
}
if (preferences.length === 0 && !confirm('未选择兴趣偏好,可能影响攻略准确性,是否继续?')) {
return;
}
// 3. 显示"生成中"状态
generateBtn.disabled = true;
generateBtn.textContent = '生成中...';
strategyResult.style.display = 'block';
resultContent.innerHTML = `
<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. 调用后端攻略生成接口(确保地址正确)
const apiUrl = "http://127.0.0.1:3005/generate-strategy";
fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('token') || '')
},
body: JSON.stringify({
destination,
travelDate,
travelDays: parseInt(travelDays),
budget: parseInt(budget),
preferences,
specialNeed
}),
timeout: 35000
})
.then(response => {
// 【新增】打印响应状态码
console.log("后端接口响应状态码:", response.status);
if (!response.ok) throw new Error(`接口请求失败(状态码:${response.status}),请检查后端是否启动`);
return response.json();
})
.then(data => {
// 【新增】打印后端返回的攻略数据
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 = data.data.title;
renderStrategyContent(data.data);
})
.catch(error => {
// 【新增】打印错误详情到控制台
console.error('攻略生成失败,错误详情:', error);
alert('攻略生成失败:' + error.message);
generateBtn.disabled = false;
generateBtn.textContent = '生成智能攻略';
strategyResult.style.display = 'none';
});
});
}
// 4.1 渲染攻略内容(严格按照后端解析的格式显示)
function renderStrategyContent(strategy) {
const resultContent = document.getElementById('resultContent');
let html = '';
// 1. 渲染每日行程(严格按照时间-地点:描述的格式)
strategy.days.forEach((day, index) => {
html += `
<div class="day-section">
<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">${time}</div>
<div class="schedule-content">
<span class="schedule-location">${location}</span>
<span class="schedule-desc">${description}</span>
</div>
</div>
`;
});
} 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. 渲染预算总结(严格按照分类显示)
html += `
<div class="budget-summary">
<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>餐饮</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>
</div>
</div>
`;
resultContent.innerHTML = html;
resultContent.setAttribute('data-strategy-id', strategy.id);
}
// 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';
return;
}
const strategyId = document.getElementById('resultContent').getAttribute('data-strategy-id');
if (!strategyId) {
alert('请先生成攻略再保存');
return;
}
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
// 【注意】这里的 API.strategySave 需确保是正确的后端保存接口地址,若未定义需替换为实际地址
fetch("http://127.0.0.1:3005/save-strategy", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
strategyId: strategyId,
userId: localStorage.getItem('userId')
})
})
.then(response => {
if (!response.ok) throw new Error('攻略保存失败,可能是权限不足');
return response.json();
})
.then(data => {
alert('攻略保存成功!可在个人中心查看');
saveBtn.disabled = false;
saveBtn.textContent = '已保存';
saveBtn.style.backgroundColor = '#52c41a';
})
.catch(error => {
alert('攻略保存失败:' + error.message);
saveBtn.disabled = false;
saveBtn.textContent = '保存攻略';
console.error('攻略保存错误详情:', error);
});
});
}

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

@ -0,0 +1,402 @@
// 页面加载完成后执行
window.onload = function() {
// 1. 验证登录态(个人中心必须登录)
const token = localStorage.getItem('token');
if (!token) {
alert('请先登录后再访问个人中心');
window.location.href = '../pages/login.html';
return;
}
// 2. 初始化页面
updateUserNav(); // 更新导航栏
loadUserProfile(); // 加载个人资料
loadMyOrders(); // 加载我的订单(默认标签页)
// 3. 绑定侧边栏切换事件
bindSidebarSwitch();
// 4. 绑定订单筛选事件
bindOrderFilter();
// 5. 绑定订单操作事件(退票/改签)
bindOrderActions();
// 6. 绑定个人资料保存事件
bindSaveProfile();
// 7. 绑定退出登录事件
bindLogout();
};
// 1. 更新导航栏用户状态
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;
document.getElementById('userName').textContent = username; // 侧边栏用户名
}
}
// 2. 加载个人资料
function loadUserProfile() {
fetch(API.getUserInfo, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('个人资料加载失败');
return response.json();
})
.then(data => {
const user = data.data; // { nickname, phone, email, avatar }
// 填充表单
document.getElementById('nickname').value = user.nickname || '';
document.getElementById('phone').value = user.phone || '';
document.getElementById('email').value = user.email || '';
// 更新头像
if (user.avatar) {
document.querySelector('.avatar img').src = user.avatar;
}
})
.catch(error => {
console.error('个人资料加载错误', error);
});
}
// 3. 加载我的订单
function loadMyOrders(status = 'all') {
const orderList = document.getElementById('orderList');
orderList.innerHTML = '<div class="loading">加载订单中...</div>';
fetch(`${API.myOrderList}?userId=${localStorage.getItem('userId')}&status=${status}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('订单加载失败');
return response.json();
})
.then(data => {
const orders = data.data.list || [];
orderList.innerHTML = '';
if (orders.length === 0) {
orderList.innerHTML = '<div class="no-data">暂无相关订单</div>';
return;
}
// 渲染订单列表
orders.forEach(order => {
const statusText = {
'pending': '待使用',
'used': '已使用',
'refunded': '已退款'
}[order.status] || '未知状态';
const statusClass = `status-${order.status}`;
const orderItem = document.createElement('div');
orderItem.className = 'order-item';
orderItem.innerHTML = `
<div class="order-header">
<span class="order-id">订单编号${order.id}</span>
<span class="order-status ${statusClass}">${statusText}</span>
</div>
<div class="order-body">
<img src="${order.spotImg || '../assets/images/default-spot.jpg'}" alt="${order.spotName}" class="order-img">
<div class="order-info">
<div class="order-spot-name">${order.spotName}</div>
<div class="order-date">使用日期${formatDate(order.useDate)}</div>
<div class="order-meta">
<span>数量${order.ticketCount}</span>
<span>下单时间${formatTime(order.createTime)}</span>
</div>
</div>
</div>
<div class="order-footer">
<div class="order-price">总价¥${order.totalPrice}</div>
<div class="order-actions">
${order.status === 'pending' ? `
<button class="btn btn-default order-btn change-btn" data-order-id="${order.id}">改签</button>
<button class="btn btn-default order-btn refund-btn" data-order-id="${order.id}">退票</button>
` : ''}
</div>
</div>
`;
orderList.appendChild(orderItem);
});
// 重新绑定订单操作按钮事件(因为是动态生成)
bindOrderActions();
})
.catch(error => {
orderList.innerHTML = `<div class="no-data">订单加载失败:${error.message}</div>`;
console.error('订单加载错误', error);
});
}
// 4. 加载我的攻略
function loadMyStrategies() {
const strategyList = document.getElementById('myStrategyList');
strategyList.innerHTML = '<div class="loading">加载攻略中...</div>';
fetch(`${API.myStrategyList}?userId=${localStorage.getItem('userId')}`, {
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 || [];
strategyList.innerHTML = '';
if (strategies.length === 0) {
strategyList.innerHTML = '<div class="no-data">暂无保存的攻略,快去生成并保存吧!</div>';
return;
}
// 渲染攻略列表
strategies.forEach(strategy => {
const strategyItem = document.createElement('div');
strategyItem.className = 'strategy-item';
strategyItem.innerHTML = `
<div class="strategy-title">${strategy.title}</div>
<div class="strategy-meta">
<span>生成时间${formatDate(strategy.createTime)}</span>
<span>天数${strategy.days}</span>
</div>
<div class="strategy-desc">${strategy.briefDesc || '无描述'}</div>
`;
strategyList.appendChild(strategyItem);
});
})
.catch(error => {
strategyList.innerHTML = `<div class="no-data">攻略加载失败:${error.message}</div>`;
console.error('攻略加载错误', error);
});
}
// 5. 绑定侧边栏切换事件
function bindSidebarSwitch() {
const menuItems = document.querySelectorAll('.sidebar-menu li');
const contentTabs = document.querySelectorAll('.content-tab');
menuItems.forEach(item => {
item.addEventListener('click', () => {
const tabKey = item.getAttribute('data-tab');
if (!tabKey) return; // 退出登录按钮无tabKey
// 切换菜单激活状态
menuItems.forEach(menu => menu.classList.remove('active'));
item.classList.add('active');
// 切换内容标签页
contentTabs.forEach(tab => tab.classList.remove('active'));
document.getElementById(`${tabKey}Tab`).classList.add('active');
// 懒加载数据
if (tabKey === 'strategies' && document.getElementById('myStrategyList').innerHTML.includes('加载中')) {
loadMyStrategies();
}
});
});
}
// 6. 绑定订单筛选事件
function bindOrderFilter() {
const filterBtns = document.querySelectorAll('.filter-btn');
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
const status = btn.getAttribute('data-status');
// 切换筛选按钮激活状态
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// 重新加载订单
loadMyOrders(status);
});
});
}
// 7. 绑定订单操作事件(退票/改签)
function bindOrderActions() {
// 退票按钮
document.querySelectorAll('.refund-btn').forEach(btn => {
btn.addEventListener('click', function() {
const orderId = this.getAttribute('data-order-id');
if (confirm('确定要退票吗?退票可能会产生手续费')) {
refundTicket(orderId, this);
}
});
});
// 改签按钮
document.querySelectorAll('.change-btn').forEach(btn => {
btn.addEventListener('click', function() {
const orderId = this.getAttribute('data-order-id');
const newDate = prompt('请选择新的使用日期', formatDate(new Date()));
if (newDate) {
changeTicket(orderId, newDate, this);
}
});
});
}
// 7.1 退票接口调用
function refundTicket(orderId, btn) {
btn.disabled = true;
btn.textContent = '处理中...';
fetch(API.ticketRefund, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({ orderId: orderId })
})
.then(response => {
if (!response.ok) throw new Error('退票失败');
return response.json();
})
.then(data => {
alert('退票成功退款将在1-3个工作日内退回原支付账户');
// 重新加载订单列表
loadMyOrders(document.querySelector('.filter-btn.active').getAttribute('data-status'));
})
.catch(error => {
alert(error.message);
btn.disabled = false;
btn.textContent = '退票';
console.error('退票错误', error);
});
}
// 7.2 改签接口调用
function changeTicket(orderId, newDate, btn) {
btn.disabled = true;
btn.textContent = '处理中...';
fetch(API.ticketChange, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
orderId: orderId,
newUseDate: newDate
})
})
.then(response => {
if (!response.ok) throw new Error('改签失败');
return response.json();
})
.then(data => {
alert('改签成功!新的使用日期为:' + newDate);
// 重新加载订单列表
loadMyOrders(document.querySelector('.filter-btn.active').getAttribute('data-status'));
})
.catch(error => {
alert(error.message);
btn.disabled = false;
btn.textContent = '改签';
console.error('改签错误', error);
});
}
// 8. 绑定个人资料保存事件
function bindSaveProfile() {
const profileForm = document.getElementById('profileForm');
profileForm.addEventListener('submit', function(e) {
e.preventDefault();
const nickname = document.getElementById('nickname').value.trim();
const phone = document.getElementById('phone').value.trim();
const email = document.getElementById('email').value.trim();
// 调用更新个人资料接口
fetch(API.updateUserInfo, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
userId: localStorage.getItem('userId'),
nickname: nickname,
phone: phone,
email: email
})
})
.then(response => {
if (!response.ok) throw new Error('资料更新失败');
return response.json();
})
.then(data => {
alert('个人资料更新成功');
// 更新侧边栏用户名
if (nickname) {
document.getElementById('userName').textContent = nickname;
localStorage.setItem('username', nickname); // 更新localStorage
updateUserNav(); // 同步更新导航栏
}
})
.catch(error => {
alert(error.message);
console.error('资料更新错误', error);
});
});
}
// 9. 绑定退出登录事件
function bindLogout() {
document.getElementById('logoutBtn').addEventListener('click', function() {
if (confirm('确定要退出登录吗?')) {
// 清除localStorage中的用户信息
localStorage.removeItem('token');
localStorage.removeItem('userId');
localStorage.removeItem('username');
// 跳转登录页
window.location.href = '../pages/login.html';
}
});
}
// 工具函数格式化日期YYYY-MM-DD
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
}
// 【与后端配合说明】
// 1. 我的订单接口API.myOrderList
// - 方法GET
// - 参数userId、statusall/pending/used/refunded可选
// - 返回格式:{ code:200, data: { list: [{ id, spotName, spotImg, useDate, ticketCount, totalPrice, status, createTime }] } }
// 2. 我的攻略接口API.myStrategyList
// - 方法GET
// - 参数userId
// - 返回格式:{ code:200, data: { list: [{ id, title, days, briefDesc, createTime }] } }
// 3. 退票接口API.ticketRefund
// - 方法POST
// - 参数:{ orderId }
// - 返回格式:{ code:200, message:'退票成功' }
// 4. 改签接口API.ticketChange
// - 方法POST
// - 参数:{ orderId, newUseDate }
// - 返回格式:{ code:200, message:'改签成功' }
// 5. 个人资料接口API.getUserInfo / updateUserInfo
// - GET返回{ code:200, data: { nickname, phone, email, avatar } }
// - POST参数{ userId, nickname, phone, email }

@ -0,0 +1,179 @@
<!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">
<link rel="stylesheet" href="../css/common.css">
<link rel="stylesheet" href="../css/auth.css">
<style>
/* 基础重置 */
body {
margin: 0;
padding: 0;
min-height: 100vh;
}
/* 居中容器样式 */
.auth-container {
/* 使用flex确保完全居中 */
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
min-height: 100vh; /* 占满整个视口高度 */
padding: 15px;
background: #f5f7fa;
box-sizing: border-box; /* 确保padding不影响整体尺寸 */
}
.auth-card {
width: 100%;
max-width: 360px; /* 保持小巧尺寸 */
padding: 24px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
box-sizing: border-box;
}
.auth-header {
padding: 0 0 16px;
margin-bottom: 16px;
text-align: center;
border-bottom: 1px solid #f0f2f5;
}
.auth-header h2 {
font-size: 20px;
margin-bottom: 6px;
color: #333;
}
.auth-header p {
font-size: 13px;
color: #666;
margin: 0;
}
.auth-form {
padding: 0;
}
.form-item {
margin-bottom: 16px;
position: relative;
}
.form-item label {
font-size: 13px;
margin-bottom: 6px;
display: block;
color: #666;
}
.input-box {
height: 40px;
width: 100%;
padding: 8px 12px 8px 36px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-item i {
position: absolute;
left: 12px;
top: 32px;
color: #999;
font-size: 14px;
}
.verify-code-container {
display: flex;
gap: 8px;
}
.verify-code-container .input-box {
flex: 1;
}
.send-code-btn {
height: 40px;
padding: 0 12px;
background-color: #f0f2f5;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
}
.btn-primary.auth-btn {
width: 100%;
height: 42px;
background-color: #2c83f2;
color: #fff;
border: none;
border-radius: 4px;
font-size: 15px;
cursor: pointer;
margin-top: 8px;
}
.auth-links {
margin-top: 16px;
text-align: center;
font-size: 13px;
}
.auth-links a {
color: #2c83f2;
text-decoration: none;
}
</style>
</head>
<body>
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<h2>忘记密码</h2>
<p>输入邮箱获取验证码重置密码</p>
</div>
<form id="forgotForm" class="auth-form">
<div class="form-item">
<label for="email">邮箱</label>
<input type="email" id="email" class="input-box" placeholder="注册邮箱" required>
<i class="fas fa-envelope"></i>
</div>
<div class="form-item">
<label for="verifyCode">验证码</label>
<div class="verify-code-container">
<input type="text" id="verifyCode" class="input-box" placeholder="6位验证码" required>
<i class="fas fa-shield-alt"></i>
<button type="button" id="sendCodeBtn" class="send-code-btn">发送验证码</button>
</div>
</div>
<div class="form-item">
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" class="input-box" placeholder="6-20位密码" required>
<i class="fas fa-lock"></i>
</div>
<button type="submit" class="btn btn-primary auth-btn">重置密码</button>
<div class="auth-links">
<a href="../pages/login.html">返回登录</a>
</div>
</form>
</div>
</div>
<script src="../js/api.js"></script>
<script src="../js/forgot-password.js"></script>
</body>
</html>

@ -0,0 +1,82 @@
<!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/index.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" class="active">首页</a></li>
<li><a href="../pages/recommendation.html">景点推荐</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>
<!-- 登录后显示:用户名/个人中心由JS动态切换 -->
<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="search-bar">
<input type="text" id="searchInput" class="search-input" placeholder="搜索景点名称或目的地...">
<button id="searchBtn" class="btn btn-primary search-btn">搜索</button>
</div>
<!-- 景点列表 -->
<div class="spot-list-title">热门景点推荐</div>
<div class="spot-grid" id="spotGrid">
<!-- 景点卡片由JS动态渲染调用后端景点列表接口 -->
<!-- 示例卡片(静态,实际由接口数据替换) -->
<div class="spot-card">
<img src="../assets/images/spot1.jpg" alt="景点图片" class="spot-img">
<div class="spot-info">
<h3 class="spot-name">故宫博物院</h3>
<p class="spot-location">北京市东城区</p>
<div class="spot-rating">
<span class="star"></span>
<span class="rating-value">4.8</span>
<span class="review-count">1234条评论</span>
</div>
<div class="spot-price">
<span class="price-tag">¥</span>
<span class="price-value">60</span>
<span class="price-unit">起/人</span>
</div>
<a href="../pages/spot-detail.html?spotId=1" class="btn btn-primary spot-btn">查看详情</a>
</div>
</div>
</div>
</div>
<!-- 底部(公共组件) -->
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<!-- 引入接口管理JS -->
<script src="../js/api.js"></script>
<!-- 引入首页交互JS -->
<script src="../js/index.js"></script>
</body>
</html>

@ -0,0 +1,68 @@
<!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/login.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="../pages/recommendation.html">景点推荐</a></li>
<li><a href="../pages/strategy.html">攻略生成</a></li>
</ul>
<div class="user-actions">
<a href="../pages/login.html" class="login-btn">登录</a>
<a href="../pages/register.html" class="register-btn">注册</a>
</div>
</div>
</div>
<!-- 登录内容区 -->
<div class="container">
<div class="login-wrapper">
<div class="login-title">账号登录</div>
<form id="loginForm" class="login-form">
<div class="form-item">
<label for="username">账号(手机号/邮箱)</label>
<input type="text" id="username" class="input-box" placeholder="请输入账号" required>
</div>
<div class="form-item">
<label for="password">密码</label>
<input type="password" id="password" class="input-box" placeholder="请输入密码" required>
</div>
<div class="auth-links">
<a href="../pages/forgot-password.html">忘记密码?</a>
<span>|</span>
<a href="../pages/register.html">注册账号</a>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">登录</button>
</div>
</form>
</div>
</div>
<!-- 底部(公共组件) -->
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<!-- 引入接口管理JS -->
<script src="../js/api.js"></script>
<!-- 引入登录页交互JS -->
<script src="../js/login.js"></script>
</body>
</html>

@ -0,0 +1,91 @@
<!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/recommendation.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="../pages/recommendation.html" 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="recommendation-header">
<h1>热门景点推荐</h1>
<p>根据季节、热度和用户评价为您精选优质景点</p>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-group">
<label>目的地:</label>
<select id="destinationFilter" class="filter-select">
<option value="">全部</option>
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="广州">广州</option>
<option value="深圳">深圳</option>
<option value="成都">成都</option>
</select>
</div>
<div class="filter-group">
<label>类型:</label>
<select id="typeFilter" class="filter-select">
<option value="">全部</option>
<option value="自然景观">自然景观</option>
<option value="人文古迹">人文古迹</option>
<option value="主题乐园">主题乐园</option>
<option value="城市观光">城市观光</option>
</select>
</div>
<div class="filter-group">
<label>排序:</label>
<select id="sortFilter" class="filter-select">
<option value="popular">按热度</option>
<option value="rating">按评分</option>
<option value="distance">按距离</option>
</select>
</div>
</div>
<!-- 景点列表 -->
<div class="spots-grid" id="spotsGrid">
<!-- 景点卡片将通过JS动态生成 -->
<div class="loading">加载推荐景点中...</div>
</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/recommendation.js"></script>
</body>
</html>

@ -0,0 +1,71 @@
<!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/register.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="#">景点推荐</a></li>
<li><a href="../pages/strategy.html">攻略生成</a></li>
</ul>
<div class="user-actions">
<a href="../pages/login.html" class="login-btn">登录</a>
<a href="../pages/register.html" class="register-btn">注册</a>
</div>
</div>
</div>
<!-- 注册内容区 -->
<div class="container">
<div class="register-wrapper">
<div class="register-title">账号注册</div>
<form id="registerForm" class="register-form">
<div class="form-item">
<label for="username">账号(手机号/邮箱)</label>
<input type="text" id="username" class="input-box" placeholder="请输入手机号或邮箱" required>
</div>
<div class="form-item">
<label for="password">密码</label>
<input type="password" id="password" class="input-box" placeholder="8-20位含大小写+数字+特殊符号" required>
<p class="password-tip">密码需满足8-20位包含至少1个大写字母、1个小写字母、1个数字、1个特殊符号!@#$%^&*</p>
</div>
<div class="form-item code-item">
<label for="code">验证码</label>
<input type="text" id="code" class="input-box code-input" placeholder="请输入验证码" required>
<button type="button" id="getCodeBtn" class="btn btn-default code-btn">获取验证码</button>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary register-submit">注册</button>
</div>
<div class="login-link">
已有账号?<a href="../pages/login.html">立即登录</a>
</div>
</form>
</div>
</div>
<!-- 底部(公共组件) -->
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<!-- 引入接口管理JS -->
<script src="../js/api.js"></script>
<!-- 引入注册页交互JS -->
<script src="../js/register.js"></script>
</body>
</html>

@ -0,0 +1,74 @@
<!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/review.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="review-wrapper">
<div class="review-title">发表景点评论</div>
<!-- 景点信息提示 -->
<div class="spot-tip" id="spotTip">加载中...</div>
<!-- 评论表单(参考文档:评论用例-前置条件:需有购票记录) -->
<form id="reviewForm" class="review-form">
<div class="form-item">
<label for="reviewContent">评论内容</label>
<textarea id="reviewContent" class="review-content-input" placeholder="请分享你的游玩体验至少20字..." rows="6" required></textarea>
<p class="content-tip">已输入 <span id="contentLength">0</span>至少20字</p>
</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>
<div class="review-notice">
<p>⚠️ 温馨提示:</p>
<p>1. 评论提交后将通过区块链存证,不可篡改</p>
<p>2. 同一用户30天内对同一景点最多发表10条评论</p>
<p>3. 评论内容需合规,违规内容将被屏蔽</p>
</div>
</form>
</div>
</div>
<!-- 底部(公共组件) -->
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<!-- 引入接口管理JS -->
<script src="../js/api.js"></script>
<!-- 引入评论页交互JS -->
<script src="../js/review.js"></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/common.css">
<!-- 引入详情页样式 -->
<link rel="stylesheet" href="../css/spot-detail.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">
<!-- 1. 景点基础信息 -->
<div class="spot-header">
<div class="spot-imgs">
<img src="../assets/images/default-spot.jpg" alt="景点主图" class="main-img" id="mainSpotImg">
<div class="img-thumbnails" id="imgThumbnails">
<!-- 缩略图由JS动态渲染 -->
</div>
</div>
<div class="spot-basic-info">
<h1 class="spot-name" id="spotName">加载中...</h1>
<div class="spot-rating-location">
<span class="star"></span>
<span class="rating-value" id="spotRating">0.0</span>
<span class="review-count" id="reviewCount">0条评论</span>
<span class="spot-location" id="spotLocation">加载中...</span>
</div>
<div class="spot-desc" id="spotDesc">加载中...</div>
<div class="spot-meta">
<div class="meta-item">
<span class="meta-label">开放时间:</span>
<span class="meta-value" id="openTime">加载中...</span>
</div>
<div class="meta-item">
<span class="meta-label">门票价格:</span>
<span class="meta-value price" id="ticketPrice">¥0 起</span>
</div>
<div class="meta-item">
<span class="meta-label">咨询电话:</span>
<span class="meta-value" id="contactPhone">加载中...</span>
</div>
</div>
<button class="btn btn-primary buy-ticket-btn" id="buyTicketBtn">立即购票</button>
</div>
</div>
<!-- 2. 景点详情标签页 -->
<div class="spot-tabs">
<div class="tab-buttons">
<button class="tab-btn active" data-tab="detail">景点介绍</button>
<button class="tab-btn" data-tab="review">用户评论</button>
<button class="tab-btn" data-tab="strategy">相关攻略</button>
</div>
<!-- 2.1 景点介绍 -->
<div class="tab-content active" id="detailTab">
<div class="detail-content" id="detailContent">加载中...</div>
</div>
<!-- 2.2 用户评论(含区块链存证) -->
<div class="tab-content" id="reviewTab">
<!-- 评论提交入口(登录后显示) -->
<div class="review-submit-entry" id="reviewSubmitEntry" style="display: none;">
<a href="../pages/review.html?spotId=" class="btn btn-default submit-review-btn" id="submitReviewBtn">发表评论</a>
</div>
<!-- 评论列表 -->
<div class="review-list" id="reviewList">加载中...</div>
</div>
<!-- 2.3 相关攻略 -->
<div class="tab-content" id="strategyTab">
<div class="strategy-list" id="strategyList">加载中...</div>
</div>
</div>
</div>
<!-- 底部(公共组件) -->
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<!-- 引入接口管理JS -->
<script src="../js/api.js"></script>
<!-- 引入详情页交互JS -->
<script src="../js/spot-detail.js"></script>
</body>
</html>

@ -0,0 +1,364 @@
<!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/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>
<!-- 导航栏(公共组件) -->
<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="../pages/recommendation.html">景点推荐</a></li>
<li><a href="../pages/strategy.html" class="active">攻略生成</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="strategy-wrapper">
<!-- 原有攻略表单与结果区(保留所有逻辑) -->
<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>
</div>
<div class="form-item">
<label for="travelDate">出行日期</label>
<input type="date" id="travelDate" class="input-box" required>
</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>
</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 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>
</div>
<div class="form-item">
<label for="specialNeed">特殊需求(可选)</label>
<textarea id="specialNeed" class="input-box" placeholder="如:避开人流、含无障碍设施、偏好小众景点..." rows="3"></textarea>
</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>
</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>
</div>
<div class="result-content" id="resultContent">
<!-- 攻略内容由JS动态渲染 -->
</div>
</div>
</div>
<!-- 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>
<!-- 聊天内容区域(初始隐藏) -->
<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>
</div>
</div>
<!-- 底部(公共组件) -->
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<!-- 引入接口管理JS -->
<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>

@ -0,0 +1,134 @@
<!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/user-center.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="#">景点推荐</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 active">个人中心</a>
</div>
</div>
</div>
</div>
<!-- 个人中心内容区 -->
<div class="container">
<div class="user-center-wrapper">
<!-- 左侧导航 -->
<div class="user-sidebar">
<div class="user-profile">
<div class="avatar" id="userAvatar">
<img src="../assets/images/default-avatar.png" alt="用户头像">
</div>
<div class="user-name" id="userName">加载中...</div>
</div>
<ul class="sidebar-menu">
<li class="active" data-tab="orders">
<span class="menu-icon">🎫</span>
<span class="menu-text">我的订单</span>
</li>
<li data-tab="strategies">
<span class="menu-icon">📝</span>
<span class="menu-text">我的攻略</span>
</li>
<li data-tab="profile">
<span class="menu-icon">👤</span>
<span class="menu-text">个人资料</span>
</li>
<li id="logoutBtn">
<span class="menu-icon">🚪</span>
<span class="menu-text">退出登录</span>
</li>
</ul>
</div>
<!-- 右侧内容区 -->
<div class="user-content">
<!-- 6.1 我的订单 -->
<div class="content-tab active" id="ordersTab">
<div class="tab-header">
<h2>我的订单</h2>
<div class="order-filters">
<button class="filter-btn active" data-status="all">全部</button>
<button class="filter-btn" data-status="pending">待使用</button>
<button class="filter-btn" data-status="used">已使用</button>
<button class="filter-btn" data-status="refunded">已退款</button>
</div>
</div>
<div class="order-list" id="orderList">
<!-- 订单列表由JS动态渲染 -->
<div class="loading">加载订单中...</div>
</div>
</div>
<!-- 6.2 我的攻略 -->
<div class="content-tab" id="strategiesTab">
<div class="tab-header">
<h2>我的攻略</h2>
</div>
<div class="strategy-list" id="myStrategyList">
<!-- 攻略列表由JS动态渲染 -->
<div class="loading">加载攻略中...</div>
</div>
</div>
<!-- 6.3 个人资料 -->
<div class="content-tab" id="profileTab">
<div class="tab-header">
<h2>个人资料</h2>
</div>
<form id="profileForm" class="profile-form">
<div class="form-item">
<label for="nickname">昵称</label>
<input type="text" id="nickname" class="input-box" placeholder="请输入昵称">
</div>
<div class="form-item">
<label for="phone">手机号</label>
<input type="tel" id="phone" class="input-box" placeholder="请输入手机号">
</div>
<div class="form-item">
<label for="email">邮箱</label>
<input type="email" id="email" class="input-box" placeholder="请输入邮箱">
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary save-profile-btn">保存修改</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 底部(公共组件) -->
<div class="footer">
<div class="footer-content">
<p>旅游攻略系统 © 2025 版权所有</p>
<p>联系我们support@travelguide.com</p>
</div>
</div>
<!-- 引入接口管理JS -->
<script src="../js/api.js"></script>
<!-- 引入个人中心交互JS -->
<script src="../js/user-center.js"></script>
</body>
</html>
Loading…
Cancel
Save