资讯动态

基于Vue的济南旅游网站开发:数据驱动、组件复用与部署实践

发布时间:2026/9/15 19:09:38 来源:尧图企业网站定制
简介面向毕业设计与期末大作业场景的济南旅游网站完整项目基于HTML5与Vue.js构建前端页面配合Java后端整合数据接口形成从页面展示到业务处理均可运行的Web应用闭环。压缩包共807个文件、约18.12MB其中以156个JavaScript脚本、44个Vue组件、46个CSS样式表搭建前端交互界面111个Java类文件承载后端业务逻辑另含38个HTML页面、50张JPG图片、79张GIF动效、2份SQL数据库脚本与若干配置说明并附有论文、开发文档、数据文档方便从需求分析到代码实现逐层对照阅读。包内还提供1-install.bat、2-run.bat、3-build.bat等部署命令脚本配合已经本地编译并通过严格调试的源码下载后即可快速搭建运行环境减少起步配置成本。资源难度定位适中覆盖旅游网站常见的景点展示、线路推荐、留言交互等模块适合计算机相关专业学生借鉴课题划分、页面布局、接口设计与前后端整合方式。目前已有41人浏览学习是毕业设计或期末大作业可用的完整参考范本。1. 从 HTML 静态页到 Vue 数据驱动的济南旅游网站开发顺序是什么一个纯静态 HTML 旅游网站在做到第 10 个景点时会很难继续维护新增一条景点要复制一整段卡片结构筛选“只看历下区”只能靠页面跳转地图和视频播放器又要单独引一套脚本。标题里的“基于 html 的济南旅游网站vue.zip”就是这一类最常见的前端交付物——HTML 负责页面骨架Vue 负责把景点列表、筛选条件、路由跳转和地图标点变成可维护的数据逻辑而不是一堆复制粘贴的 DOM。这种压缩包通常没有后端接口景点数据来自本地 JS 或 JSON 文件所以开发重点不在接口联调而在数据建模、组件通信和打包部署。适合有 HTML/CSS 基础、正准备接触 Vue 的开发者也适合做城市旅游站作品集或前端课设的人。下面直接从工程骨架开始把整个项目的可复现路径讲清楚。2. 项目骨架从纯 HTML 改造到 Vue 单页应用的最小项目划分2.1 Vue 的介入点不是页面美化而是数据与视图分离在原生 HTML 里更新一个景点价格要先用getElementById找到对应元素再手动替换文本筛选逻辑稍微复杂一点就需要维护多个隐藏和显示状态。Vue 把景点卡片拆成独立组件组件只接收一个景点对象至于对象来自本地数组还是网络请求组件内部完全不关心。济南旅游网站的数据特征是景点多、区域分布明确、季节推荐差异大常见做法是先建一份本地模拟数据等后端接口就绪后再替换数据源。原生 HTML 多页面与 Vue 单页应用的差异在改造时会直接影响目录结构和打包方式对比项原生 HTML 多页面Vue 单页应用页面数量一个景点一个 html一套路由对应多个视图组件数据更新手写 getElementById 操作 DOM修改 data 后视图自动更新导航跳转a 标签整页刷新vue-router 局部渲染体验更接近 App静态资源手写相对路径打包阶段统一处理路径与压缩如果只是给现有 HTML 加几个特效没必要引入 Vue但“筛选 详情页 地图 视频播放”这套组合正好是 Vue 的舒适区。2.2 用 Vue CLI 初始化一个可运行的济南旅游项目拿到压缩包后第一件事是确认它能不能本地跑起来。常见交付方式是工程目录里含package.json但node_modules被删除所以先执行依赖安装。下面是一套最小命令npm install -g vue/cli vue --version vue create jinan-travel cd jinan-travel npm install vue-router4 npm run servevue create进入交互模式后选择Manually select features勾选Router和CSS Pre-processors其余保持默认即可。勾选 Router 是为了拿到路由目录CSS 预处理器按团队习惯选 Sass 或 Less。vue-router4对应 Vue 3如果你本地环境还是 Vue 2就要安装vue-router3版本配错时控制台会直接提示inject() can only be used inside setup()一类错误。npm run serve默认监听 8080 端口端口被占用时 CLI 会询问是否换另一个端口浏览器访问时看到 Vue 默认首页说明工程骨架已经通了。2.3 目录规划views、components、data、router 分开静态 HTML 项目通常是一个 index.html 带多个 css/js 文件改造成 Vue 工程时我一般会先固定目录结构jinan-travel/ ├── index.html ├── src/ │ ├── main.js │ ├── App.vue │ ├── router/ │ │ └── index.js │ ├── data/ │ │ └── attractions.js │ ├── views/ │ │ ├── Home.vue │ │ └── Detail.vue │ └── components/ │ ├── AttractionCard.vue │ ├── MapBox.vue │ └── VideoPlayer.vuedata目录专门放静态模拟数据后续后端接口开发完只需要把attractions.js里的导出换成 axios 请求结果页面组件不需要大改。components放可复用卡片、地图、播放器views放路由页面。静态页面里的轮播图和宣传视频初期可以放到public/images下public目录里的文件会被原样拷贝到dist根部。2.4 main.js 挂载与路由注册的骨架代码最小入口src/main.js只需要创建 Vue 实例、注册路由、挂载到#appimport { createApp } from vue import App from ./App.vue import router from ./router createApp(App).use(router).mount(#app)路由配置文件src/router/index.js里按视图划分路由import { createRouter, createWebHistory } from vue-router import Home from ../views/Home.vue const routes [ { path: /, name: home, component: Home }, { path: /detail/:id, name: detail, component: () import(../views/Detail.vue) } ] const router createRouter({ history: createWebHistory(), routes }) export default routercreateWebHistory生成的是 history 模式的 URL比如/detail/1没有#符号视觉上更干净但线上部署时必须让服务器把未匹配到文件的请求转回index.html否则刷新详情页会 404这一点会在最后一章详细处理。src/App.vue里只放一个RouterView /它负责按当前 URL 渲染对应视图组件。不要在RouterView /外面包一层固定宽度的容器否则页面切换时很容易出现滚动条错乱这个坑在打包后尤其难排查。3. 济南旅游数据建模景点卡片、筛选器与组件通信3.1 数据字段先定义再写组件景点卡片看起来只是图片加文字但筛选器、详情页、地图标点都要用同一份数据。先看一份常见的景点数据模型export const attractions [ { id: 1, name: 趵突泉, district: 历下区, type: natural, price: 40, score: 4.8, seasons: [spring, summer, autumn, winter], tags: [泉水, 5A景区, 市区], coordinates: { lat: 36.6627, lng: 117.0137 }, image: /images/baotuquan.jpg, description: 七十二名泉之首泉水常年恒温。 } ]seasons一定要设计成数组而不是存四季皆宜这种文案。筛选器判断“春季推荐”时只需要attraction.seasons.includes(spring)如果存的是中文句子筛选逻辑就要拆字符串代码会变得很脆。coordinates为地图标点预留price和score用来做排序。各字段的价值对应关系如下字段示例值对应交互id1路由跳转/detail/1district历下区区域下拉筛选seasons[spring]季节推荐筛选coordinates{ lat, lng }腾讯地图标点price40门票排序3.2 用 V-for 渲染景点卡片字段与视图解耦景点卡片组件接收一个attraction对象内部只负责展示不直接操作列表数据template div classattraction-card click$emit(select, attraction) img :srcattraction.image :altattraction.name loadinglazy / h3{{ attraction.name }}/h3 p{{ attraction.district }} · {{ attraction.price }}元/p div classtags span v-fortag in attraction.tags :keytag{{ tag }}/span /div /div /template script export default { name: AttractionCard, props: { attraction: { type: Object, required: true } }, emits: [select] } /script:src是v-bind:src的缩写attraction.image以/images/开头构建后会指向dist/images下对应文件。loadinglazy让视口外的图片延迟加载首页景点一多这个属性比任何懒加载库都省事。父组件views/Home.vue里的列表部分template section classattraction-list AttractionCard v-foritem in filteredAttractions :keyitem.id :attractionitem selectgoDetail / /section /template script setup import { useRouter } from vue-router import AttractionCard from ../components/AttractionCard.vue const router useRouter() function goDetail(item) { router.push({ path: /detail/${item.id} }) } /script这里用path而不是name params跳转好处是详情页 URL 是完整的/detail/1用户手动复制链接分享后依然能打开。select监听子组件的自定义事件事件名和emits声明要保持完全一致否则 Vue 会在控制台输出警告。3.3 筛选器和排序computed 把过滤逻辑收起来筛选器本质是修改几个响应式变量然后由一个计算属性统一产出最终列表。下拉框与数据字段对齐select v-modelfilterSeason option value全部季节/option option valuespring春季/option option valuesummer夏季/option option valueautumn秋季/option option valuewinter冬季/option /select select v-modelsortBy option valuescore评分优先/option option valueprice门票从低到高/option /select对应的计算属性import { ref, computed } from vue import { attractions } from ../data/attractions const filterSeason ref() const sortBy ref(score) const filteredAttractions computed(() { let list attractions if (filterSeason.value) { list list.filter((item) item.seasons.includes(filterSeason.value)) } if (sortBy.value price) { list [...list].sort((a, b) a.price - b.price) } else if (sortBy.value score) { list [...list].sort((a, b) b.score - a.score) } return list })关键点是排序前必须[...list]复制一份数组不能直接attractions.sort(...)否则会修改原始数据源导致其他组件在读取时拿到乱序列表。filterSeason的值与seasons数组里的英文枚举完全一致筛选才不需要额外转换。如果拿到的压缩包里已经有现成 HTML 页面筛选逻辑可以直接复用这套 computed不需要重写页面结构。后端接口返回的数据通常是{ list: [...], total: 10 }这种嵌套结构只要在 computed 入口处先解构出list即可。3.4 组件之间通信的参数约定景点卡片是纯展示组件父组件负责数据请求和筛选地图组件接收坐标播放器接收视频地址。组件通信的参数越少越好认方向通信方式内容触发时机父 → 子propsattraction 对象列表渲染父 → 子propscenter 坐标对象详情页地图初始化子 → 父emit select被点击的景点对象点击卡片子 → 父emit error视频播放失败原因视频加载失败有人习惯在子组件里直接引入attractions.js这样省去 props 传参但卡片组件一旦依赖全局数据源就无法在另一个城市旅游项目里复用。把数据源放在父组件或路由视图里子组件只接收参数以后换城市只需要换数据文件和页面布局。4. 地图、详情页与视频播放旅游站里最容易被问到的三个交互4.1 详情页路由传参query 与 params 的取舍景点卡片跳转到详情页最常见的写法是把景点 id 放进路由参数。路由定义是/detail/:id跳转代码router.push({ path: /detail/${item.id} })详情页读取参数import { useRoute } from vue-router import { computed } from vue import { attractions } from ../data/attractions const route useRoute() const id computed(() Number(route.params.id)) const detail computed(() attractions.find((item) item.id id.value))route.params.id永远是字符串所以传给find之前要Number()转换否则1 ! 1会匹配不到数据。query方式则是/detail?id1读取route.query.id适合带多个查询条件但 URL 里没有层级语义详情页之间跳转不太直观。如果详情页内部有“上一篇 / 下一篇”切换必须监听路由参数变化watch( () route.params.id, (newId) { detail.value attractions.find((item) item.id Number(newId)) }, { immediate: true } )不加watch时点击下一篇只会修改 URL详情页组件因为复用了同一个实例而不会重新执行mounted页面内容保持不变这是 Vue Router 面试里经常被问到的坑。用了immediate: true之后组件初次创建也会执行一次加载逻辑就不需要在onMounted里再写一遍。4.2 在 Vue 中接入腾讯地图JS SDK 的初始化顺序详情页通常要展示景点位置。腾讯地图 JavaScript SDK 的引入方式是在index.html中加载脚本script srchttps://map.qq.com/api/gljs?v1.expkey你的腾讯位置服务Key/script然后封装一个MapBox.vue组件template div refmapContainer classmap-container/div /template script export default { name: MapBox, props: { center: { type: Object, required: true } }, mounted() { if (!window.TMap) { console.error(腾讯地图 SDK 未加载成功) return } this.map new TMap.Map(this.$refs.mapContainer, { center: new TMap.LatLng(this.center.lat, this.center.lng), zoom: 15 }) new TMap.Marker({ map: this.map, position: new TMap.LatLng(this.center.lat, this.center.lng) }) }, watch: { center: { handler(val) { if (this.map) { this.map.setCenter(new TMap.LatLng(val.lat, val.lng)) } }, deep: false } } } /scriptmounted阶段执行地图初始化是因为此时$refs.mapContainer对应的 DOM 已经渲染完成。SDK 是同步加载的只要脚本顺序在业务代码之前window.TMap就会存在。如果加载失败常见原因是 Key 的域名白名单没配本地开发用localhost线上要单独加域名。地图容器的 CSS 不能省略.map-container { width: 100%; height: 300px; }不给高度时地图容器高度为 0控制台不报错但页面一片空白这是接入地图最常见的“有代码无显示”问题。4.3 播放景区宣传视频mp4 与 m3u8 的兼容方案景区宣传视频大多直接放 mp4但有些交付项目里会出现 m3u8 流地址。m3u8 本质是一个分片播放列表Chrome 桌面端不原生支持需要 hls.js 转封装import Hls from hls.js export default { props: { src: { type: String, required: true } }, mounted() { const video this.$refs.video if (this.src.endsWith(.m3u8)) { if (Hls.isSupported()) { const hls new Hls({ maxBufferLength: 30 }) hls.loadSource(this.src) hls.attachMedia(video) this.hls hls } } else { video.src this.src } }, beforeUnmount() { if (this.hls) { this.hls.destroy() } } }模板部分video refvideo controls preloadmetadata playsinline/videosafari原生支持 m3u8不需要 hls.js其他浏览器需要 。maxBufferLength: 30表示最多缓冲 30 秒数据数值太小容易在弱网下频繁卡顿太大又会增加带宽占用。如果是几分钟的景区宣传片直接用 mp4 更稳hls.js 更适合直播或长视频场景。m3u8 播放失败的排查重点在 CORS视频服务器没有返回跨域响应头时hls.js 能加载到播放列表但分片请求会被浏览器拦截。常见做法是让资源服务器开启 CORS 权限或者把视频文件放到与页面同域的位置。5. 打包部署与布局异常排查dist 目录如何让设计稿不变形5.1 publicPath 与 assetsDir子目录部署白屏的根源开发环境一切正常npm run build后把dist上传到服务器打开页面只有文字没有样式通常不是 CSS 写错而是静态资源路径不对。根因在vue.config.jsconst { defineConfig } require(vue/cli-service) module.exports defineConfig({ publicPath: ./, outputDir: dist, assetsDir: static, productionSourceMap: false })publicPath: ./让构建产物里的 JS/CSS 引用变成相对路径适合部署在域名子目录下的场景比如https://example.com/jinan/。如果publicPath保持默认的/构建后资源路径是/js/app.js子目录部署时浏览器会去域名根目录找文件结果 404整页样式丢失。assetsDir: static把 JS、CSS 统一放static目录productionSourceMap: false关闭 sourceMap减少上传体积。改完配置要重新 build只刷新浏览器不会生效。5.2 history 路由刷新 404服务器需要把请求交给 index.htmlcreateWebHistory模式下的/detail/1是虚拟路径服务器上根本没有这个文件直接刷新会返回 404。Nginx 静态部署时加一段try_files规则server { listen 80; server_name jinan.local; root /usr/share/nginx/html/jinan-travel/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html的含义先找实际文件找不到就回退到index.html再由 Vue Router 根据当前 URL 渲染对应页面。如果部署环境不允许改 Nginx 配置可以把createWebHistory改成createWebHashURL 会变成/detail/1#/虽然不够美观但任何静态服务器都能直接打开不需要额外配置。5.3 路由懒加载与图片懒加载详情页组件不应该和首页打包在同一个 JS 文件里改为动态 import 后站点的首屏只剩首页代码{ path: /detail/:id, name: detail, component: () import(../views/Detail.vue) }打包后dist/static/js下会多出按页面拆分的 chunk 文件用户访问首页时不会加载详情页代码。图片层面景点卡片已经加了loadinglazy但 CSS 背景图不吃这个属性如果轮播图用背景图实现需要配合 IntersectionObserver 自行控制加载时机。5.4 打包后布局异常的三步排查打包后出现布局错乱先不要改样式按下面顺序排查打开 Chrome DevTools 的 Network 面板刷新页面看 CSS 和 JS 文件状态码。出现 404 时定位publicPath重新构建。检查dist/index.html里是否保留了 viewport 标签meta nameviewport contentwidthdevice-width, initial-scale1 /没有这个标签时手机端会按 980px 宽度渲染页面导航栏和卡片全部挤在一起。检查 Flex 容器的子项是否溢出。景点名带上英文标点时容易撑破卡片给子项加min-width: 0通常能解决。排查路径类问题时第一步永远先看 Network 面板里 CSS/JS 的响应状态。如果只有页面刷新后才异常而首页内点击跳转正常那就不是 CSS 的问题回到 5.2 的路由配置上处理。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价