当前位置: 首页 > news >正文

Vue3旅游网站源码包:含首页/景点/线路/海报/关于我们/登录注册等9大功能页

本文还有配套的精品资源,点击获取

简介:直接可用的Vue3旅游类网站源码,包含首页、世界热门景点、国内爆款旅游线路、旅游海报图展、关于我们、用户登录、用户注册、404页面和返回顶部功能页共9个完整页面。所有页面风格统一,内置轮播图组件、嵌入式视频播放器、响应式表单(支持邮箱/密码校验)、多标签TAB切换、固定顶部导航栏、底部版权信息栏、图文混排列表、一键返回顶部按钮等高频交互功能。项目基于Vite构建,结构清晰:src目录下组件按功能分层,命名语义化,关键逻辑均有中文注释;public目录集中管理Banner图、景点缩略图、LOGO和基础图标,适配PC端及主流手机屏幕。无需后端即可本地运行,VS Code打开后执行npm run dev即可实时预览。代码零外部UI框架依赖,不耦合API接口,适合课程设计、毕业设计原型开发或中小型旅游机构官网快速搭建。

1. 项目概述:这不是一个“模板”,而是一套可直接交付的旅游网站前端骨架

我带过三届前端实训课,每年都有学生卡在“毕业设计做不出来”的死循环里——不是不会写代码,而是陷在“从零搭架子→选UI库→调样式→适配移动端→修兼容性”的泥潭里,最后交上去的网站首页轮播图都卡顿,景点列表排版错乱,注册表单连邮箱格式校验都没有。直到去年我把这套 Vue3 旅游源码包放进课程资料库,情况彻底变了:大四学生用它三天就跑通了本地预览,一周内替换了自家旅行社的真实景点图和线路文案,两周后直接部署到阿里云轻量服务器上,客户当场签了合同。它为什么能扛住真实场景?因为这不是网上常见的“花哨但空心”的UI模板,而是一个经过多轮真实项目反哺打磨的、功能闭环的前端骨架系统

核心关键词“Vue3旅游源码”“旅游网站模板”“景点页面组件”“旅游线路页面”“响应式旅游网页”,背后对应的是五个硬性能力:第一,开箱即用的工程化结构——Vite 构建、TypeScript 类型约束、ESLint + Prettier 规范、src 目录按views(页面)、components(复用组件)、assets(静态资源)、router(路由)、stores(状态)分层,连utils工具函数都按request.ts(请求封装)、validate.ts(校验规则)、format.ts(时间/金额格式化)做了归类;第二,9个页面不是孤立存在,而是通过统一的设计语言与交互逻辑串联——导航栏高亮状态由路由meta字段驱动,返回顶部按钮监听滚动距离而非简单window.onscroll,轮播图组件内部自动处理图片懒加载与触摸滑动阈值;第三,“响应式”不是一句口号,而是落实到每一处细节:Banner 图在 PC 端用background-size: cover拉满,在 iPad 上用background-size: 100% auto防止裁切,在 iPhone SE 上则切换为object-fit: contain并加一层深色蒙版确保文字可读;第四,“景点页面组件”和“旅游线路页面”共享同一套卡片渲染逻辑,仅通过props.type切换数据结构与展示字段,避免重复造轮子;第五,所有交互反馈都有明确状态映射——表单提交时按钮禁用+加载动画、TAB 切换时内容区淡入、视频播放器点击区域扩大至整个卡片容器。它解决的从来不是“能不能跑起来”,而是“上线后用户会不会觉得这是个正经网站”。

适合谁?如果你是计算机专业本科生做课程设计,它能让你把精力聚焦在“如何用 Vue Composition API 封装一个可复用的景点搜索过滤器”,而不是纠结flex布局怎么让三列卡片在不同屏幕下自适应;如果你是刚转行的前端新人,它是一本活的《Vue3 实战手册》——看src/views/AboutView.vue里的onMounted中如何用IntersectionObserver实现滚动触发动画,比读十遍文档更直观;如果你是小型旅行社老板想快速上线官网,替换public/images/scenic/下的 12 张景点图、修改src/router/index.ts里 3 行线路链接、调整src/stores/userStore.ts中的联系方式,就能生成一个风格统一、手机能点、电脑能看、客户愿意留资的真实网站。它不承诺“一键生成百万流量”,但绝对保证“你改完内容,明天就能发给客户看”。

2. 整体架构与设计思路:为什么选择 Vite + Vue3 + TypeScript 而非其他方案?

2.1 构建工具选型:Vite 的冷启动速度不是噱头,而是生产力杠杆

很多人问:“为什么不用 Vue CLI?” 我试过两种方案:用 Vue CLI 搭建同功能旅游站,npm run serve首次启动耗时 28 秒,热更新平均延迟 1.7 秒;而本项目用 Vite,npm run dev启动只要 420ms,热更新压测下稳定在 80ms 内。这差距不是数字游戏,而是直接影响开发节奏。举个真实例子:我在调试首页轮播图自动播放逻辑时,需要反复修改interval参数并观察效果。用 Vue CLI,改一次代码 → 等待编译 → 切换浏览器 → 查看效果 → 发现问题 → 再改……一个参数调优要 3 分钟;用 Vite,改完保存瞬间刷新,眼睛一眨就看到结果。这种“所见即所得”的流畅感,让开发者能真正沉浸在业务逻辑里,而不是被构建工具拖慢思考节奏。

Vite 的底层原理决定了它的优势:它利用浏览器原生 ES Module 加载机制,开发时只对当前模块做按需编译,而非像 Webpack 那样打包整个依赖树。比如你在ScenicListView.vue里引入了一个ScenicCard.vue组件,Vite 只会编译这两个文件,而 Vue CLI 会把node_modules里所有vuevue-routerpinia的代码全打包一遍。这也是为什么本项目package.jsondevDependencies仅保留vite@vitejs/plugin-vue@vitejs/plugin-vue-jsx这三个核心插件,没有webpack-dev-server@vue/cli-service这类重型依赖。vite.config.ts的配置也极度精简:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 3000, open: true, // 启动自动打开浏览器 host: 'localhost' // 显式声明host,避免某些网络环境报错 }, build: { sourcemap: true, // 保留source map便于调试 rollupOptions: { output: { manualChunks: { // 将vue、vue-router等基础库单独打包,提升缓存命中率 vendor: ['vue', 'vue-router', 'pinia'] } } } } })

注意manualChunks的配置——它把框架级依赖抽成独立 chunk,用户首次访问加载vendor.js,后续更新业务代码时这个文件不变,浏览器直接走缓存。实测在 3G 网络下,二次访问首屏时间从 2.1s 降至 0.8s。

2.2 核心框架选型:Vue3 Composition API 是复杂交互的“解耦手术刀”

本项目所有页面逻辑都基于 Vue3 的<script setup>语法编写,而非 Options API。这不是为了追新,而是解决旅游网站特有的状态纠缠问题。以“旅游线路页面”为例,它需要同时处理:线路分类筛选(国内/出境/定制)、价格区间滑块、出发城市多选、排序方式(销量/价格/评分)、分页加载、收藏状态同步。如果用 Options API,data里堆 15 个响应式变量,methods里塞 20 个函数,watch监听 5 个属性,代码很快变成意大利面条。而 Composition API 让我们能把相关逻辑聚合成可复用的组合式函数:

  • useLineFilter()封装所有筛选条件的状态与变更方法;
  • useLineSort()管理排序字段与升降序逻辑;
  • useLinePagination()处理当前页码、每页条数、总条数计算;
  • useLineFavorite()独立维护收藏状态,通过localStorage持久化,避免每次刷新重置。

这些函数在LineListView.vue中像乐高一样拼接:

<script setup lang="ts"> import { useLineFilter, useLineSort, useLinePagination, useLineFavorite } from '@/composables/line' const { filters, updateFilters } = useLineFilter() const { sortField, sortOrder, toggleSort } = useLineSort() const { currentPage, pageSize, total, loadMore } = useLinePagination() const { isFavorited, toggleFavorite } = useLineFavorite() // 所有业务逻辑已封装,组件内只剩纯净的模板绑定 </script>

这种模式让代码具备极强的可测试性——你可以单独为useLineFilter写单元测试,验证当用户选择“三亚”出发地时,filters.departureCity是否正确更新,而不必启动整个 Vue 应用。src/composables/目录下目前有 7 个此类函数,覆盖轮播图控制、表单校验、滚动监听、视频播放状态管理等高频场景,它们就是本项目的“能力原子”。

2.3 类型系统设计:TypeScript 不是装饰,而是防止低级错误的护栏

旅游网站最常出错的地方,往往藏在数据流转的缝隙里。比如“景点页面”从 API 获取数据时,后端可能返回scenicList: [](空数组)或scenicList: null(字段缺失),如果前端不做类型断言,直接.map()就会报错。本项目用 TypeScript 建立了三层防护:

第一层是API 响应类型定义。在src/api/types.ts中,明确定义景点接口返回结构:

export interface ScenicItem { id: number name: string location: string price: number rating: number coverImage: string briefIntro: string tags: string[] } export interface ScenicResponse { code: number message: string data: { list: ScenicItem[] total: number } }

第二层是组件 Props 类型约束ScenicCard.vue的 props 强制要求传入scenic: ScenicItem,如果父组件传了any类型对象,VS Code 会立刻标红提示:

<script setup lang="ts"> import type { ScenicItem } from '@/api/types' const props = defineProps<{ scenic: ScenicItem }>() </script>

第三层是运行时类型守卫。在src/utils/validate.ts中,提供isScenicItem(obj)函数,对从 localStorage 读取的缓存数据做二次校验:

export function isScenicItem(obj: any): obj is ScenicItem { return obj && typeof obj.id === 'number' && typeof obj.name === 'string' && typeof obj.price === 'number' && Array.isArray(obj.tags) }

这三层防护让“undefined is not an object”这类错误在开发阶段就被拦截。我曾让学生故意删掉ScenicItem中的rating字段,然后运行npm run build,TypeScript 编译器直接报错:“Property ‘rating’ does not exist on type ‘ScenicItem’”,根本无法打包成功。这种“编译即测试”的体验,远胜于上线后靠用户反馈才发现价格显示 NaN。

3. 核心功能模块深度解析:从轮播图到返回顶部,每个组件都是经验沉淀

3.1 首页 Banner 轮播图:不只是切换,更是性能与体验的平衡术

首页 Banner 是用户第一眼看到的内容,但它也是移动端性能杀手。本项目轮播图组件BannerSlider.vue解决了三个关键痛点:

痛点一:图片体积过大导致首屏白屏
public/images/banner/下的 5 张 Banner 图,全部经过双重压缩:先用 Photoshop “导出为 Web 所用格式”将 PNG 转为 WebP(质量 75%),再用sharpCLI 工具进行无损优化:

npx sharp public/images/banner/*.webp --quality 75 --mozjpeg true --progressive true

实测单张图从 1.2MB 降至 280KB,加载时间从 3.2s 缩短至 0.7s。组件内还实现图片懒加载:只有进入视口的图片才开始加载,未进入区域的img标签src属性为空,用data-src存储真实路径,通过IntersectionObserver监听触发:

const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target as HTMLImageElement img.src = img.dataset.src! observer.unobserve(img) // 加载完成后取消监听,避免重复触发 } }) })

痛点二:触摸滑动不跟手,PC 端鼠标拖拽失效
很多轮播图只处理touchstart/touchmove,忽略 PC 端mousedown/mousemove。本组件用统一的useDrag组合式函数封装:

// src/composables/useDrag.ts export function useDrag(onStart: (pos: number) => void, onMove: (delta: number) => void, onEnd: () => void) { let startX = 0 let isDragging = false const handleStart = (e: MouseEvent | TouchEvent) => { const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX startX = clientX isDragging = true onStart(clientX) } const handleMove = (e: MouseEvent | TouchEvent) => { if (!isDragging) return const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX onMove(clientX - startX) } const handleEnd = () => { isDragging = false onEnd() } return { handleStart, handleMove, handleEnd } }

这样在BannerSlider.vue中,只需绑定@mousedown="drag.handleStart"@mousemove="drag.handleMove"@mouseup="drag.handleEnd",一套逻辑同时支持触屏与鼠标。

痛点三:自动播放与用户交互冲突
用户手动滑动后,轮播应暂停 5 秒再继续,避免干扰操作。组件内用ref管理定时器 ID,并在handleEnd中清除旧定时器、启动新定时器:

let autoPlayTimer: NodeJS.Timeout | null = null const startAutoPlay = () => { autoPlayTimer = setInterval(() => { nextSlide() }, 5000) } const stopAutoPlay = () => { if (autoPlayTimer) { clearInterval(autoPlayTimer) autoPlayTimer = null } } // 在 handleEnd 中调用 stopAutoPlay(),并在 nextSlide() 最后调用 startAutoPlay()

3.2 景点与线路页面:复用式卡片设计与动态数据映射

“景点页面”和“旅游线路页面”看似不同,但核心展示逻辑高度一致:都是“图文卡片列表”,只是数据字段和操作按钮不同。本项目通过ScenicCard.vueLineCard.vue的抽象,实现了 90% 代码复用。

数据结构映射是关键ScenicItemLineItem接口虽不同,但都包含idnamecoverImagebriefIntroprice这些共性字段。组件内用defineProps的泛型约束,让同一个卡片组件能接收不同类型的数据:

<!-- ScenicCard.vue --> <script setup lang="ts"> import type { ScenicItem } from '@/api/types' const props = defineProps<{ item: ScenicItem }>() </script> <!-- LineCard.vue --> <script setup lang="ts"> import type { LineItem } from '@/api/types' const props = defineProps<{ item: LineItem }>() </script>

模板中所有共性渲染逻辑(如封面图、标题、简介、价格)完全一致,差异仅在于操作按钮:
- 景点卡片显示“查看详情”按钮,跳转/scenic/detail/:id
- 线路卡片显示“立即预订”和“收藏”按钮,后者调用useLineFavorite

这种设计让新增一个“酒店页面”变得极其简单:只需定义HotelItem接口,创建HotelCard.vue继承相同结构,再在HotelListView.vue中循环渲染即可。src/components/Cards/目录下已预留HotelCard.vue的空文件,注释写着:“按此模式扩展,5 分钟完成”。

图文混排列表的响应式断点设计更体现细节。PC 端(≥1200px)显示 4 列卡片,使用grid-template-columns: repeat(4, minmax(280px, 1fr)));平板(768px-1199px)降为 2 列;手机(<768px)强制 1 列。但这里有个陷阱:单纯用@media会导致卡片宽度突变,用户滚动时看到卡片“跳动”。解决方案是在src/assets/styles/variables.scss中定义 CSS 变量:

:root { --card-columns: 4; } @media (max-width: 1199px) { :root { --card-columns: 2; } } @media (max-width: 767px) { :root { --card-columns: 1; } } .card-grid { display: grid; grid-template-columns: repeat(var(--card-columns), minmax(280px, 1fr))); gap: 24px; }

CSS 变量的过渡是平滑的,浏览器会自动插值计算中间状态,避免视觉跳跃。

3.3 登录与注册表单:响应式校验不是“弹窗提醒”,而是实时反馈闭环

旅游网站的注册转化率,很大程度取决于表单体验。本项目LoginView.vueRegisterView.vue的表单设计,贯彻了“实时、精准、无感”的校验原则。

邮箱校验不再依赖正则模糊匹配。传统/^[^\s@]+@[^\s@]+\.[^\s@]+$/会放过user@domain.c这种无效域名。本项目采用validator.js库的isEmail()方法,并增加 DNS 预检(开发环境模拟):

import { isEmail } from 'validator' const validateEmail = (email: string): boolean => { if (!isEmail(email)) return false // 开发环境模拟DNS检查:若邮箱域名是 gmail.com / qq.com / 163.com 则通过 const domain = email.split('@')[1] const validDomains = ['gmail.com', 'qq.com', '163.com'] return validDomains.includes(domain) }

校验时机也不是“失焦后才检查”,而是输入时实时触发(防抖 300ms),并在输入框右侧显示图标反馈:
- ✅ 绿色对勾:格式正确且域名有效;
- ⚠️ 黄色感叹号:格式正确但域名不在白名单(提示“建议使用常用邮箱”);
- ❌ 红色叉号:格式错误,下方显示具体错误文案(如“邮箱格式不正确”)。

密码强度校验采用多维度评分。不是简单判断“是否含大小写字母+数字”,而是计算熵值:

const calculatePasswordEntropy = (password: string): number => { let entropy = 0 if (/[a-z]/.test(password)) entropy += 26 // 小写字母 if (/[A-Z]/.test(password)) entropy += 26 // 大写字母 if (/\d/.test(password)) entropy += 10 // 数字 if (/[^a-zA-Z\d]/.test(password)) entropy += 32 // 特殊字符 return password.length * Math.log2(entropy) } // 评分标准:> 50 强,30-50 中,< 30 弱

用户输入时,进度条实时显示强度等级,并给出改进建议:“添加一个特殊符号可提升强度”。

响应式布局的精髓在于“内容优先”。PC 端表单左右分栏(左侧文案说明,右侧输入框),手机端则变为垂直流式布局,但关键点在于:左侧文案区域在手机上并非简单隐藏,而是折叠为可展开的“帮助信息”按钮,点击后以slide-down动画展开详细说明。这样既节省空间,又不丢失信息。

3.4 返回顶部按钮:不是“固定定位+scrollTo”,而是智能触发与动效融合

“返回顶部”按钮常被当作鸡肋功能,但本项目赋予它真正的用户体验价值。

触发逻辑智能化。不是简单监听window.scrollY > 300就显示,而是结合页面内容高度与用户滚动意图:

const showBackTop = ref(false) let lastScrollTop = 0 const handleScroll = () => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop const scrollHeight = document.documentElement.scrollHeight const windowHeight = window.innerHeight // 当滚动到底部 200px 内,或向上滚动时,才显示按钮 const nearBottom = scrollHeight - scrollTop - windowHeight < 200 const scrollingUp = scrollTop < lastScrollTop showBackTop.value = nearBottom || scrollingUp lastScrollTop = scrollTop }

这样用户快速向下滚动时按钮不干扰视线,只有当他们想返回或已接近底部时才出现,符合直觉。

动效设计降低操作成本。点击后不是生硬跳转,而是scroll-behavior: smooth+ 自定义滚动曲线:

html { scroll-behavior: smooth; } .back-top-button { transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1); } .back-top-button:hover { transform: scale(1.1); }

cubic-bezier(0.22, 0.61, 0.36, 1)是经过多次 A/B 测试选定的缓动函数,比默认ease更柔和,比ease-in-out更有节奏感。

4. 实操部署与定制指南:从本地运行到上线交付的完整链路

4.1 本地开发环境搭建:三步走,拒绝“npm install 报错”

很多学生第一次运行项目卡在依赖安装,根源在于 Node.js 版本与包管理器不匹配。本项目严格锁定环境:

  • Node.js 版本:18.17.0(LTS 版本,package.jsonengines.node字段明确声明)
  • 包管理器:pnpm 8.6.12(比 npm 快 2 倍,比 yarn 节省 50% 磁盘空间)

执行步骤必须严格按顺序:

  1. 卸载旧版本 Node.js:控制面板 → 卸载程序 → 删除所有 Node.js 相关条目,清空C:\Users\用户名\AppData\Roaming\npm目录(Windows)或~/.npm(Mac)。
  2. 安装 Node.js 18.17.0:从官网下载.msi安装包(Windows)或.pkg(Mac),安装时勾选“Automatically install the necessary tools”(自动安装构建工具)。
  3. 全局安装 pnpm
    bash npm install -g pnpm@8.6.12
    验证:pnpm -v输出8.6.12

此时再进入项目根目录,执行:

pnpm install # 注意!不是 npm install pnpm run dev

pnpm install会创建pnpm-lock.yaml,精确锁定所有依赖版本,避免node_modules因安装顺序不同产生差异。pnpm run dev启动后,浏览器自动打开http://localhost:3000,首页即刻呈现。

提示:若 VS Code 中 TypeScript 报错“Cannot find module ‘vue’”,右键node_modules→ “重新加载窗口”,或执行pnpm exec tsc --noEmit手动触发类型检查。

4.2 内容替换全流程:旅行社老板也能 30 分钟上线

假设你是某海南旅行社,想用此源码上线官网。以下是零技术背景人员可操作的替换清单:

文件路径替换内容操作说明注意事项
public/images/logo.png公司 LOGO替换为 200×60px 的 PNG 透明底图片尺寸必须严格匹配,否则导航栏高度错乱
public/images/banner/首页 Banner 图放 5 张 1920×600px 的 WebP 图,命名为banner1.webpbanner5.webp用 Photoshop 导出时勾选“转换为 sRGB”
public/images/scenic/景点缩略图放 12 张 320×240px 的 WebP 图,命名与src/data/scenicData.tsid对应(如scenic_1.webp图片命名必须与数据 ID 一致,否则显示空白
src/data/scenicData.ts景点文案数据修改namelocationbriefIntro字段,price单位为“元/人”tags数组最多 3 个,超出部分会被截断
src/data/lineData.ts线路文案数据修改titledepartureCityduration(天数)、price(起价)itinerary字段是字符串数组,每项为一天行程描述
src/router/index.ts页面链接修改routes数组中pathname,如将/about改为/company必须保持meta.title与页面标题一致,否则 SEO 失效

替换完成后,无需任何代码修改,直接pnpm run build生成dist目录,将整个dist文件夹上传至服务器即可。实测某客户替换全部内容耗时 22 分钟,包括图片压缩和文案编辑。

4.3 生产环境部署:Nginx 配置与缓存策略

部署到 Linux 服务器(如腾讯云轻量应用服务器)只需三步:

  1. 上传文件:用 WinSCP 或scpdist目录上传至/var/www/tour/
  2. 配置 Nginx:编辑/etc/nginx/conf.d/tour.conf
    ```nginx
    server {
    listen 80;
    server_name your-domain.com;
    root /var/www/tour;
    index index.html;

    # 关键:解决 Vue Router history 模式 404 问题
    location / {
    try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存 1 年
    location ~* .(js|css|png|jpg|jpeg|gif|ico|svg|webp)$ {
    expires 1y;
    add_header Cache-Control “public, immutable”;
    }

    # 禁止访问敏感文件
    location ~ /. {
    deny all;
    }
    }
    3. **重启服务**:bash
    sudo nginx -t # 测试配置
    sudo systemctl reload nginx
    ```

注意:try_files $uri $uri/ /index.html;这行是核心。它告诉 Nginx,当用户直接访问/scenic/123这样的 URL 时,先尝试找对应文件,找不到就返回index.html,由 Vue Router 处理路由,避免 404。

5. 常见问题与避坑指南:那些文档里不会写的实战血泪

5.1 “轮播图不自动播放”——90% 的原因是图片路径没配对

现象:首页 Banner 区域空白,控制台报错GET http://localhost:3000/images/banner/banner1.webp 404

原因分析:public/images/banner/目录下图片名为banner1.jpg,但src/data/bannerData.tsimagePath字段写的是'banner1.webp'。Vite 开发服务器严格区分大小写与后缀名。

解决方案:
1. 统一图片格式:全部转为.webp(推荐用 Squoosh 在线工具,拖拽上传自动压缩);
2. 检查bannerData.ts中路径:确保imagePath: 'banner1.webp'与实际文件名完全一致;
3. 清除浏览器缓存:Ctrl+F5强制刷新,或 Chrome 开发者工具 → Network → 勾选 “Disable cache”。

实操心得:我在实训课上让学生每人随机改一张 Banner 图的后缀名,然后集体排查。这个练习让他们深刻理解“路径即契约”,比讲十遍原理都管用。

5.2 “手机端页面错位,文字挤在一起”——viewport 设置被覆盖

现象:iPhone 上打开,页面宽度异常,文字重叠,导航栏按钮无法点击。

根本原因:public/index.html<meta name="viewport" content="width=device-width, initial-scale=1.0">被某些 CSS 框架的重置样式覆盖,或src/assets/styles/reset.scsshtml { font-size: 16px; }导致 rem 计算错误。

排查步骤:
1. Chrome 开发者工具 → Device Toolbar → 选 iPhone 12,查看 Elements 面板中<html>标签的 computed styles,确认font-size是否为16px
2. 检查src/assets/styles/main.scss是否误写了html { width: 100vw; }
3. 在src/assets/styles/variables.scss中强制重置:
scss @media (max-width: 767px) { html { font-size: 16px !important; } }

5.3 “登录后状态不持久,刷新页面回到未登录”——Pinia Store 持久化配置遗漏

现象:用户登录成功,跳转到个人中心,但 F5 刷新后又回到登录页。

原因:src/stores/userStore.ts中的state默认不持久化,刷新后isLogin重置为false

修复方案:安装pinia-plugin-persistedstate插件:

pnpm add pinia-plugin-persistedstate

src/stores/index.ts中启用:

import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) export default pinia

并在userStore.ts中指定持久化字段:

export const useUserStore = defineStore('user', { state: (): UserState => ({ isLogin: false, userInfo: null, token: '' }), persist: true // 或更精细控制:persist: { key: 'user-store', paths: ['isLogin', 'userInfo'] } })

5.4 “部署后图片全显示为小方块”——Nginx MIME 类型未识别 WebP

现象:服务器部署后,所有.webp图片显示为破损图标,Chrome 控制台报Failed to load resource: the server responded with a status of 404 ()

原因:Nginx 默认不识别.webp文件类型,返回Content-Type: text/plain,浏览器拒绝渲染。

解决方案:编辑/etc/nginx/mime.types,在types { ... }块内添加:

image/webp webp;

然后重启 Nginx:

sudo nginx -t && sudo systemctl reload nginx

5.5 “VS Code 中组件标签红色波浪线”——TypeScript 路径别名未生效

现象:<ScenicCard />组件在模板中报错“Cannot find name ‘ScenicCard’”,但实际能正常运行。

原因:tsconfig.jsoncompilerOptions.paths配置未被 VS Code 识别。

解决步骤:
1. 确认tsconfig.json中有:
json "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } }
2. VS Code 中Ctrl+Shift+P→ 输入 “TypeScript: Restart TS Server”;
3. 若仍无效,在项目根目录创建jsconfig.json(供 JS 项目用):
json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src/**/*"], "exclude": ["node_modules"] }

6. 扩展可能性与进阶建议:让这个骨架长出你的业务肌肉

这个 Vue3 旅游源码包的价值,不仅在于“能用”,更在于“好扩展”。它像一辆改装潜力巨大的底盘,你可以根据需求加装不同“部件”。

第一层扩展:接入真实后端 API
目前所有数据来自src/data/xxxData.ts的静态 JSON。要对接真实接口,只需修改src/api/目录下的scenicApi.tslineApi.ts

// src/api/scenicApi.ts import { request } from '@/utils/request' export const getScenicList = (params: ScenicFilterParams) => { return request.get<ScenicResponse>('/api/scenic/list', { params }) } // src/utils/request.ts 已封装 axios,自动添加 token、错误拦截、loading 状态

request.ts中的拦截器会自动读取useUserStore中的token,添加到请求头,无需在每个 API 调用处手动写。

第二层扩展:集成地图与定位
旅游网站离不开地理位置。在ScenicDetailView.vue中,可以轻松嵌入高德地图 SDK:

<script setup lang="ts"> import { onMounted } from 'vue' onMounted(() => { // 动态加载高德地图 JS const script = document.createElement('script') script.src = 'https://webapi.amap.com/maps?v=2.0&key=YOUR_KEY' script.onload = () => { // 初始化地图实例 const map = new AMap.Map('map-container', { center: [props.scenic.lng, props.scenic.lat], zoom: 12 }) } document.head.appendChild(script) }) </script>

props.scenic中的lng/lat字段已在ScenicItem接口中预留,只需后端返回即可。

第三层扩展:SEO 优化增强
当前是纯前端渲染,搜索引擎抓取困难。可引入vue-meta插件,在每个页面setup中设置 meta:

import { useMeta } from 'vue-meta' useMeta({ title: `【${props.scenic.name}】${props.scenic.briefIntro}`, meta: [ { name: 'description', content: props.scenic.briefIntro }, { property: 'og:title', content: props.scenic.name } ] })

配合 Nginx 的try_files配置,即可实现服务端渲染(SSR)的 SEO 效果,而无需重构整个项目。

最后分享一个小技巧:我在给客户交付时,总会把src/data/目录下的所有.ts文件,用 Excel 表格整理成一份《内容填充说明书》,列明每个字段含义、长度限制、示例值。客户市场部同事照着表格填,30 分钟搞定全部文案,再也不用担心“技术黑箱”。这个源码包真正的价值,从来不是代码本身,而是它帮你把“技术实现”和“业务落地”之间的鸿沟,填得足够平、足够宽、足够稳。

本文还有配套的精品资源,点击获取

简介:直接可用的Vue3旅游类网站源码,包含首页、世界热门景点、国内爆款旅游线路、旅游海报图展、关于我们、用户登录、用户注册、404页面和返回顶部功能页共9个完整页面。所有页面风格统一,内置轮播图组件、嵌入式视频播放器、响应式表单(支持邮箱/密码校验)、多标签TAB切换、固定顶部导航栏、底部版权信息栏、图文混排列表、一键返回顶部按钮等高频交互功能。项目基于Vite构建,结构清晰:src目录下组件按功能分层,命名语义化,关键逻辑均有中文注释;public目录集中管理Banner图、景点缩略图、LOGO和基础图标,适配PC端及主流手机屏幕。无需后端即可本地运行,VS Code打开后执行npm run dev即可实时预览。代码零外部UI框架依赖,不耦合API接口,适合课程设计、毕业设计原型开发或中小型旅游机构官网快速搭建。


本文还有配套的精品资源,点击获取

http://www.gsyq.cn/news/1424803.html

相关文章:

  • Claude合同条款审查实操手册:5步精准定位AI服务隐性风险,90%企业已踩坑
  • 2026年卫生避光瓶top10推荐:江苏瓶盖/江苏精油盖/江苏胶头滴管盖/江苏螺口瓶/合规性与性能双维度盘点 - 优质品牌商家
  • Airy光束自由传播光强仿真:Matlab一键运行生成2D/3D分布图
  • Claude Code相关最新问题解决API Error: 400 Failed to deserialize the JSON body into the target type:
  • 【AI时代PRD新范式】:为什么你的Claude需求文档总被研发拒收?3个权威验证指标揭晓
  • 2026腾讯广告算法大赛的反思
  • 2026年至今杭州植物饮料提取生产线厂商选择与行业深度观察 - 2026年企业资讯
  • 终极HS2游戏增强补丁完整解决方案:从零到精通的安装配置指南
  • ncmdump终极指南:3分钟快速解密网易云音乐NCM文件
  • 定了!创想三维明日上市,12周年新品齐发
  • MATLAB多目标航迹起始仿真工具|5个动态目标同步建模+噪声与检测概率可调
  • 第15章:AI辅助安全监控与应急响应——链上异常实时告警
  • 【LangGraph】LangGraph 协调者-工作者模式完全解析:从零构建一个智能报告生成系统
  • vue3 + ts reactive方式清空表单对象
  • 从“增程之王”到“纯电标杆”,理想汽车击碎偏见
  • 别再死记硬背了!用这3个方法,让你的Mac快捷键记忆效率翻倍(附实用工具推荐)
  • 2026最新华为OD机试新系统 机考真题考点分类 + 备考策略
  • FreeRTOS 队列深度解析:队列的读写
  • 书匠策AI到底是个啥?一个论文科普博主的深度拆解,看完你会回来谢我
  • “摸鱼神器”来袭!系统故障模拟器,让你的摸鱼更有借口
  • 数学建模竞赛党必备的MATLAB算法工具箱:十大高频算法+详细注释+真题参考解法
  • 055、运动模糊图片如何复原?DeblurGAN 推理加速与退化模拟方案
  • 从“激活弹窗“到“永久安心“:一个普通用户的KMS激活故事
  • 从手工录入到实时BI看板:一家TOP5商管公司用Lindy实现租务处理时效提升300%的完整链路(含真实ROI测算模型)
  • Windows下可直接运行的Android全版本API离线查询工具包(CHM/CHW双格式)
  • 2026年Q2 UV快干胶权威厂家排行 实测维度解析 - 优质品牌商家
  • 国产电容咪头新标杆:汇普声超低失真ECM
  • 微信小程序汽车服务预约系统源码,支持保养维修美容检测全流程线上管理
  • Ethos-U NPU的MAC与内存配置优化指南
  • 线程池版流水线模式 技术笔记