Compare commits

...

4 Commits
main ... lxc

@ -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,381 @@
/* 攻略生成页整体样式 */
.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;
}
/* 每日行程样式 */
.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: 28px;
padding-left: 24px;
position: relative;
}
.schedule-item::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #2c83f2;
}
.schedule-time {
font-weight: 600;
color: #666;
margin-bottom: 8px;
font-size: 15px;
}
.schedule-content {
margin-bottom: 12px;
}
.schedule-location {
color: #2c83f2;
font-weight: 600;
font-size: 17px;
}
.schedule-desc {
color: #666;
margin-top: 8px;
padding: 12px;
background-color: #f9fafc;
border-radius: 4px;
border-left: 2px solid #e5e6eb;
}
/* 预算总结样式 */
.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;
}
.budget-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #e6f4ff;
}
.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;
}
/* 生成中状态样式 */
.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;
}
}

@ -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,32 @@
// ******************************
// API接口配置文件
// 后端服务器部署在另一台电脑此处配置其IP和端口
// ******************************
// 后端服务器基础地址
// 替换为实际的后端服务器IP和端口例如http://192.168.1.100:8080
const API_BASE = 'http://后端服务器IP:后端端口';
// 所有API接口路径配置
const API = {
// 认证相关接口
login: `${API_BASE}/api/user/login`, // 用户登录
register: `${API_BASE}/api/user/register`, // 用户注册
// 忘记密码相关接口
sendVerifyCode: `${API_BASE}/api/verify-code/send`, // 发送验证码(用于密码重置)
resetPassword: `${API_BASE}/api/user/reset-password`,// 重置密码
// 景点推荐相关接口
recommendSpots: `${API_BASE}/api/spots/recommend`, // 获取推荐景点列表
// 其他接口...
getUserInfo: `${API_BASE}/api/user/info`, // 获取用户信息
myOrderList: `${API_BASE}/api/orders/my`, // 我的订单列表
myStrategyList: `${API_BASE}/api/strategies/my`, // 我的攻略列表
ticketRefund: `${API_BASE}/api/ticket/refund`, // 退票
ticketChange: `${API_BASE}/api/ticket/change`, // 改签
updateUserInfo: `${API_BASE}/api/user/update`, // 更新用户信息
strategyGenerate: `${API_BASE}/api/strategy/generate`,// 生成攻略
strategySave: `${API_BASE}/api/strategy/save` // 保存攻略
};

@ -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,185 @@
// 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 = '';
// 3.1 调用后端景点列表接口GET请求支持分页/搜索)
fetch(API.spotList, { // api.js中定义API.spotList = 'http://后端IP:端口/api/spot/list'
method: 'GET',
headers: {
// 若接口需要登录携带token可选根据后端要求
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
// 若需要分页/筛选在URL后拼接参数?page=1&size=10&keyword=北京
})
.then(response => {
if (!response.ok) {
throw new Error('景点数据加载失败');
}
return response.json();
})
.then(data => {
// 3.2 后端返回景点列表数据,动态渲染卡片
const spots = data.data.list; // 假设后端返回格式:{ code:200, data: { list: [景点1, 景点2...] } }
if (spots.length === 0) {
spotGrid.innerHTML = '<div class="no-spot">暂无景点数据</div>';
return;
}
// 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')
}
})
.then(response => {
if (!response.ok) {
throw new Error('搜索失败');
}
return response.json();
})
.then(data => {
// 渲染逻辑同loadSpotList可复用代码
spotGrid.innerHTML = '';
const spots = data.data.list;
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,52 @@
// 1. 接口地址已在api.js中统一管理此处直接引用
// api.js中需定义const API = { login: 'http://你的后端IP:端口/api/user/login' };
// 2. 监听登录表单提交事件
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault(); // 阻止表单默认提交
// 3. 获取用户输入的账号和密码
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
// 4. 调用后端登录接口POST请求
fetch(API.login, {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 告诉后端请求体是JSON格式
},
body: JSON.stringify({
username: username, // 后端接收的账号参数名(需与后端一致)
password: password // 后端接收的密码参数名(需与后端一致)
})
})
.then(response => {
// 5. 处理后端响应(先判断响应状态)
if (!response.ok) {
throw new Error('登录失败,请检查账号密码');
}
return response.json(); // 解析后端返回的JSON数据
})
.then(data => {
// 6. 登录成功后的处理后端需返回token、用户ID等关键信息
console.log('登录成功', data);
// 6.1 存储用户信息到localStorage供其他页面使用
localStorage.setItem('token', data.token); // 存储身份令牌
localStorage.setItem('userId', data.userId); // 存储用户ID
localStorage.setItem('username', data.username); // 存储用户名
// 6.2 跳转到首页
window.location.href = '../pages/index.html';
})
.catch(error => {
// 7. 登录失败处理(提示用户)
alert(error.message);
console.error('登录错误', error);
});
});
// 【与后端配合说明】
// 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,127 @@
// 页面加载完成后执行
window.onload = function() {
// 绑定验证码按钮点击事件
bindGetCodeEvent();
// 绑定注册表单提交事件
bindRegisterFormSubmit();
};
// 1. 绑定获取验证码事件(参考文档:注册用例-验证码需求)
function bindGetCodeEvent() {
const getCodeBtn = document.getElementById('getCodeBtn');
const usernameInput = document.getElementById('username');
let countdown = 0; // 倒计时秒数
getCodeBtn.addEventListener('click', function() {
const username = usernameInput.value.trim();
// 验证账号格式(手机号/邮箱)
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);
if (!isPhone && !isEmail) {
alert('请输入正确的手机号或邮箱');
return;
}
// 调用后端获取验证码接口
fetch(API.getCode, { // 注需在api.js补充getCode接口`getCode: ${BASE_URL}/user/getCode`
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username, type: isPhone ? 'phone' : 'email' })
})
.then(response => {
if (!response.ok) throw new Error('验证码发送失败');
return response.json();
})
.then(data => {
alert('验证码已发送,请注意查收');
// 启动倒计时参考文档1小时内最多获取5次验证码后端控制
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 => {
alert(error.message);
console.error('获取验证码错误', error);
});
});
}
// 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,199 @@
// 全局变量当前景点ID、景点名称
let currentSpotId = '';
let currentSpotName = '';
// 页面加载完成后执行
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('未找到景点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 = '加载景点信息中...';
fetch(`${API.spotDetail}?spotId=${currentSpotId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('景点信息加载失败');
return response.json();
})
.then(data => {
currentSpotName = data.data.name;
spotTip.textContent = `当前评论景点:${currentSpotName}(请确保分享真实游玩体验)`;
})
.catch(error => {
spotTip.textContent = `景点信息加载失败:${error.message}`;
console.error('景点信息加载错误', error);
});
}
// 3. 绑定评论内容字数统计(参考文档:评论用例-业务规则至少20字
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');
// 调用后端验证购票记录接口
fetch(`${API.verifyTicket}?userId=${userId}&spotId=${currentSpotId}`, { // 注需在api.js补充verifyTicket接口
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
// 后端返回:{ code:200, data: { hasValidTicket: true } } 或 { code:400, message:'无有效购票记录' }
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message || '验证购票记录失败') });
}
return response.json();
})
.then(data => {
// 有有效购票记录:不做处理,允许提交
if (!data.data.hasValidTicket) {
throw new Error('仅支持已实际游览该景点的用户发表评论(需有有效购票记录)');
}
})
.catch(error => {
// 无购票记录:禁用提交按钮,提示用户
alert(error.message);
document.querySelector('.submit-btn').disabled = true;
document.querySelector('.submit-btn').style.backgroundColor = '#ccc';
document.querySelector('.submit-btn').textContent = '无有效购票记录,无法提交';
});
}
// 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. 调用后端提交评论接口(含区块链存证)
const submitBtn = document.querySelector('.submit-btn');
submitBtn.disabled = true;
submitBtn.textContent = '提交中(区块链存证中...';
fetch(API.reviewSubmit, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
spotId: currentSpotId, // 景点ID
userId: localStorage.getItem('userId'), // 用户ID
content: content // 评论内容
})
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message || '评论提交失败') });
}
return response.json();
})
.then(data => {
// 评论提交成功(参考文档:评论用例-后置条件)
alert(`
评论提交成功
已通过区块链存证交易ID${data.data.transactionId}
即将返回景点详情页查看评论
`);
// 跳转回景点详情页
window.location.href = `../pages/spot-detail.html?spotId=${currentSpotId}`;
})
.catch(error => {
alert(error.message);
submitBtn.disabled = false;
submitBtn.textContent = '提交评论(区块链存证)';
console.error('评论提交错误', error);
});
});
}
// 【与后端配合说明】
// 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,319 @@
// 全局变量当前景点ID从URL参数获取
let currentSpotId = '';
// 页面加载完成后执行
window.onload = function() {
// 1. 解析URL中的景点IDspot-detail.html?spotId=1
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();
// 6. 加载评论列表(默认不加载,切换到评论标签时加载)
// 7. 加载相关攻略(默认不加载,切换到攻略标签时加载)
};
// 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';
// 评论提交按钮拼接景点ID
submitReviewBtn.href = `../pages/review.html?spotId=${currentSpotId}`;
} else {
// 未登录:隐藏评论提交入口
reviewSubmitEntry.style.display = 'none';
}
}
// 2. 加载景点详情数据(参考文档:查看景点信息用例)
function loadSpotDetail() {
// 显示加载中状态
document.getElementById('spotName').textContent = '加载中...';
document.getElementById('spotDesc').textContent = '加载中...';
// 调用后端景点详情接口
fetch(`${API.spotDetail}?spotId=${currentSpotId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token') // 可选,按后端需求
}
})
.then(response => {
if (!response.ok) throw new Error('景点详情加载失败');
return response.json();
})
.then(data => {
const spot = data.data; // 后端返回格式:{ code:200, data: { id, name, imgUrls, ... } }
// 渲染景点基础信息
renderSpotBasicInfo(spot);
// 渲染景点详情内容
renderSpotDetailContent(spot);
})
.catch(error => {
alert(error.message);
console.error('景点详情加载错误', error);
});
}
// 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.briefDesc || '暂无简介';
document.getElementById('openTime').textContent = spot.openTime || '暂无开放时间信息';
document.getElementById('ticketPrice').textContent = `¥${spot.minPrice || 0}`;
document.getElementById('contactPhone').textContent = spot.contactPhone || '暂无咨询电话';
}
// 2.2 渲染景点详情内容
function renderSpotDetailContent(spot) {
const detailContent = document.getElementById('detailContent');
// 后端返回的详情可能是HTML或纯文本这里按HTML处理
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>';
// 调用后端评论列表接口
fetch(`${API.reviewList}?spotId=${currentSpotId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('评论加载失败');
return response.json();
})
.then(data => {
const reviews = data.data.list || []; // 后端返回格式:{ code:200, data: { list: [...], total: 0 } }
reviewList.innerHTML = '';
if (reviews.length === 0) {
reviewList.innerHTML = '<div class="no-data">暂无用户评论,快来成为第一个评论的人吧!</div>';
return;
}
// 渲染每条评论(含区块链存证标识)
reviews.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);
});
})
.catch(error => {
reviewList.innerHTML = `<div class="no-data">评论加载失败:${error.message}</div>`;
console.error('评论加载错误', error);
});
}
// 4.1 验证评论区块链存证(参考文档:评论用例-验证流程)
function verifyReview(reviewId) {
// 调用后端评论验证接口
fetch(`${API.reviewVerify}?reviewId=${reviewId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('验证失败');
return response.json();
})
.then(data => {
const verifyInfo = data.data; // 后端返回:{ isValid: true, transactionId: 'xxx', timestamp: 'xxx', blockHeight: 123 }
if (verifyInfo.isValid) {
alert(`
评论验证成功
交易ID${verifyInfo.transactionId}
存证时间${formatTime(verifyInfo.timestamp)}
区块高度${verifyInfo.blockHeight}
该评论自发布以来未被篡改
`);
} else {
alert('该评论未通过区块链验证,可能已被篡改!');
}
})
.catch(error => {
alert(`验证失败:${error.message}`);
console.error('评论验证错误', error);
});
}
// 5. 加载相关攻略
function loadRelatedStrategy() {
const strategyList = document.getElementById('strategyList');
strategyList.innerHTML = '<div class="loading">攻略加载中...</div>';
// 调用后端相关攻略接口按景点ID关联
fetch(`${API.strategyRelated}?spotId=${currentSpotId}`, { // 注需在api.js补充strategyRelated接口
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (!response.ok) throw new Error('攻略加载失败');
return response.json();
})
.then(data => {
const strategies = data.data.list || [];
strategyList.innerHTML = '';
if (strategies.length === 0) {
strategyList.innerHTML = '<div class="no-data">暂无相关攻略,快去生成属于你的攻略吧!</div>';
return;
}
// 渲染攻略卡片
strategies.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);
});
})
.catch(error => {
strategyList.innerHTML = `<div class="no-data">攻略加载失败:${error.message}</div>`;
console.error('攻略加载错误', error);
});
}
// 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}`; // 需创建购票页
});
}
// 工具函数时间格式化2025-11-01 14:30:00
function formatTime(timeStr) {
if (!timeStr) return '';
const date = new Date(timeStr);
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
}
// 【与后端配合说明】
// 1. 景点详情接口API.spotDetail
// - 方法GET
// - 参数spotId必传
// - 返回格式:{ code:200, data: { id, name, imgUrls: [], rating, reviewCount, location, briefDesc, detailContent, openTime, minPrice, contactPhone } }
// 2. 评论列表接口API.reviewList
// - 方法GET
// - 参数spotId必传、page=1、size=10分页
// - 返回格式:{ code:200, data: { list: [{ id, username, content, createTime }], total: 0 } }
// 3. 评论验证接口API.reviewVerify
// - 方法GET
// - 参数reviewId必传
// - 返回格式:{ code:200, data: { isValid: true, transactionId: 'xxx', timestamp: 'xxx', blockHeight: 123 } }

@ -0,0 +1,255 @@
// 页面加载完成后执行
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天
function setDefaultTravelDate() {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// 格式化为YYYY-MM-DD
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);
const specialNeed = document.getElementById('specialNeed').value.trim();
// 2. 前端验证
if (!destination) {
alert('请输入目的地');
return;
}
if (!travelDate) {
alert('请选择出行日期');
return;
}
if (!budget || budget < 100) {
alert('请输入合理的预算至少100元');
return;
}
if (preferences.length === 0) {
if (!confirm('未选择兴趣偏好,可能影响攻略准确性,是否继续?')) {
return;
}
}
// 3. 显示生成中状态
generateBtn.disabled = true;
generateBtn.textContent = '生成中...';
strategyResult.style.display = 'block';
resultContent.innerHTML = `
<div class="generating">
<div class="loading-icon">🤖</div>
<p>智能大模型正在为您生成专属攻略请稍候约10-30...</p>
</div>
`;
// 4. 调用后端攻略生成接口参考文档AI接口对接
fetch(API.strategyGenerate, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token') // 未登录也可生成,但无法保存
},
body: JSON.stringify({
destination: destination,
travelDate: travelDate,
travelDays: parseInt(travelDays),
budget: parseInt(budget),
preferences: preferences,
specialNeed: specialNeed
})
})
.then(response => {
if (!response.ok) throw new Error('攻略生成失败,请重试');
return response.json();
})
.then(data => {
// 5. 渲染攻略结果(后端返回结构化数据)
const strategy = data.data; // { id, title, days: [], budgetSummary: {} }
generateBtn.disabled = false;
generateBtn.textContent = '生成智能攻略';
resultTitle.textContent = strategy.title;
renderStrategyContent(strategy);
})
.catch(error => {
alert(error.message);
generateBtn.disabled = false;
generateBtn.textContent = '生成智能攻略';
strategyResult.style.display = 'none';
console.error('攻略生成错误', error);
});
});
}
// 4.1 渲染攻略内容结构化数据转HTML
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">第${index + 1}${day.title}</h3>
${day.schedule.map(item => `
<div class="schedule-item">
<div class="schedule-time">${item.time}</div>
<div class="schedule-content">
<span class="schedule-location">${item.location}</span>
<p class="schedule-desc">${item.description}</p>
</div>
</div>
`).join('')}
</div>
`;
});
// 2. 渲染预算总结
html += `
<div class="budget-summary">
<div class="budget-title">预算总结人均</div>
${Object.entries(strategy.budgetSummary).map(([key, value]) => `
<div class="budget-item">
<span>${key}</span>
<span>¥${value}</span>
</div>
`).join('')}
<div class="budget-item budget-total">
<span>总计</span>
<span>¥${strategy.totalBudget}</span>
</div>
</div>
`;
resultContent.innerHTML = html;
// 存储攻略ID到页面用于保存
resultContent.setAttribute('data-strategy-id', strategy.id);
}
// 5. 绑定保存攻略按钮事件
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;
}
// 获取攻略ID
const strategyId = document.getElementById('resultContent').getAttribute('data-strategy-id');
if (!strategyId) {
alert('请先生成攻略再保存');
return;
}
// 调用保存攻略接口
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
fetch(API.strategySave, {
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);
});
});
}
// 【与后端配合说明】
// 1. 攻略生成接口API.strategyGenerate
// - 方法POST
// - 参数:{ destination, travelDate, travelDays, budget, preferences: [], specialNeed }
// - 返回格式:{
// code:200,
// data: {
// id: '攻略ID',
// title: '北京3日游攻略',
// days: [
// { title: '第一天:历史文化之旅', schedule: [{ time: '09:00-12:00', location: '故宫', description: '...' }] },
// ...
// ],
// budgetSummary: { 门票: 200, 餐饮: 300, 住宿: 500 },
// totalBudget: 1000
// }
// }
// 2. 攻略保存接口API.strategySave
// - 方法POST
// - 参数:{ strategyId, userId }
// - 返回格式:{ code:200, message:'保存成功' }

@ -0,0 +1,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,135 @@
<!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">
</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-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>
</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>
</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