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

SaaS场快订首页的前端搭建【持续更新】

文章目录

  • 一、创建页面
  • 二、配置路由
  • 三、写接口文件(api)
    • 1.定位的接口函数(腾讯地图api)
      • 实现代码:
    • 2.获取场馆分类的数据
    • 3.获取附近场馆列表的数据
  • 四、开发首页页面
    • 1.顶部区域
    • 2.搜索框
    • 3.场馆分类
    • 4.附近场馆列表
  • 五、难点介绍
    • 1.实时定位功能的实现
      • 思路:
      • 核心逻辑:
        • 1)优先获取精准定位:
        • 2)缓存机制:
        • 3)降级策略:
        • 4)交互反馈:
      • 实现代码:
    • 2.场馆分类组件的实现
      • 思路:
      • 实现代码:
    • 3.附近场馆列表组件的实现
      • 思路:
      • 实现代码:
    • 4.分页加载
      • 思路:
        • 1)初始化状态
        • 2)首屏数据加载
        • 3)滚动监听触发
        • 4)分页请求处理
        • 5)边界状态管理
      • 核心逻辑:
        • 1)数据结构设计:
        • 2)核心触发机制:
        • 3)细节:
        • 4)分页加载流程图
      • 实现代码:
        • 监听用户滑动到底部
        • 请求下一页的数据:
    • 5.提升用户体验
        • 1)骨架屏​:
        • 2)回到顶部​:
        • 3)错误提示​:
    • 6.样式与交互设计
  • 六、思路和建议
        • 项目说明和其他介绍:

一、创建页面

在pages文件夹下创建index文件夹,下面添加index.vue页面。

二、配置路由

在pages.json中配置首页的信息

{"path": "pages/index/index","style": {// "navigationBarTitleText": "","navigationBarTitleText": "体育馆预约系统","enablePullDownRefresh": false,// 网站类型"navigationStyle": "custom"}},

三、写接口文件(api)

本项目的首页需要写关于以下几个方面的接口函数

1.定位的接口函数(腾讯地图api)

这里我根据腾讯位置服务中提供的一些接口,编写地址的请求函数,主要是IP定位和逆地址解析。

官方文档:
IP定位API文档:

https://lbs.qq.com/service/webService/webServiceGuide/position/webServiceIp

逆地址解析API文档:

https://lbs.qq.com/service/webService/webServiceGuide/address/Gcoder

实现代码:

//IP定位
const IP = '111.206.145.41';
const API_KEY = '你的key';export function getLocationByIP(a) {a.$jsonp("https://apis.map.qq.com/ws/geocoder/v1/ip", {key: API_KEY,output: 'jsonp',// ip: IP, //要把这个ip这一行注释掉// location: '31.973929,119.756208',//可以通过uni.getLocation获取,谷歌浏览器会对定位请求清除,有时候定位准,有时候定位不准会出现初始地址甘肃省,但项目发布上https就行了,不准的时候用其他浏览器测试// location: '33.67,119.28',get_poi: '0'}).then(resp => {let res = resp;console.log(JSON.stringify(resp));let a = resp.result.ad_info;console.log(JSON.stringify(a));})
}
//逆地址解析
export async function reverseGeocoding(that, latitude, longitude) {try {const resp = await that.$jsonp("https://apis.map.qq.com/ws/geocoder/v1", {key: API_KEY,output: 'jsonp',location: `${latitude},${longitude}`,get_poi: '0'});return resp.result.formatted_addresses.recommend; // 明确返回 recommend} catch (error) {console.log("报错啦");console.error('根据经纬度逆地址解析失败:', error);throw error; // 重新抛出错误}
}

2.获取场馆分类的数据

export function getVenueTypes(keyword) {return httpRequest.request({url: '接口地址',method: 'GET',params: keyword})
}

3.获取附近场馆列表的数据

// 获取场馆列表
export function getVenueList(venueListReqDTO) {return httpRequest.request({url: '接口地址',method: 'post', data: venueListReqDTO})
}

四、开发首页页面

1.顶部区域

实时定位,icon小图标

在这里插入图片描述

2.搜索框

在这里插入图片描述

3.场馆分类

场馆分类的组件(基础实现和改进版本)

基础版(使用u-scroll-list横向滚动列表):

在这里插入图片描述

改进版(使用swiper实现滑动翻页):

在这里插入图片描述

4.附近场馆列表

场馆列表的组件(该组件也可以在查询页面的场馆列表渲染时复用)

在这里插入图片描述

五、难点介绍

1.实时定位功能的实现

思路:

开发者需要在腾讯位置服务先注册一个账号,然后选择你想要的地图相关功能,为这个功能分配一定的额度,个人开发者每天都有一定量的免费的额度,自己使用是足够的了。下面是腾讯位置服务官网:

https://lbs.qq.com/location/

在这里插入图片描述

在这里插入图片描述

核心逻辑:

1)优先获取精准定位:

这个项目主要使用了IP定位和逆地址解析两个服务,或者为了更快获取经纬度信息,还可以使用uni.getLocation获取经纬度,这是uniapp的内置方法。成功获取经纬度后,通过腾讯位置服务提供的逆地址解析功能,把经纬度信息解析为具体的地址,并显示在页面顶部的定位栏中。

2)缓存机制:

定位信息这里,还采用了缓存机制,将定位结果(经纬度)

在哪里查看缓存呢?如下图所示,点击应用程序,再展开本地存储,就可以看到你的位置信息已经缓存起来了,这样可以在你接下来再来访问这个页面的时候不用重新定位了,毕竟定位也需要重复请求花费一定的时间和额度。

代码中还实现了基于用户名的隔离缓存策略(避免多账号冲突)

在这里插入图片描述

3)降级策略:

若用户拒绝定位权限,尝试通过 IP 定位获取大致位置。

4)交互反馈:

定位过程中显示“定位中…”,成功/失败后更新地址栏,点击地址栏可清空缓存重新定位。

实现代码:

async getLocation() {this.isLocating = true; // 开始定位,设置状态为定位中try {const res = await new Promise((resolve, reject) => {uni.getLocation({type: 'wgs84',success: (res) => {resolve(res);},fail: (err) => {reject(err);}});});this.locationInfo = {latitude: res.latitude,longitude: res.longitude,};console.log('当前位置的纬度:', res.latitude);console.log('当前位置的经度:', res.longitude);// 调用逆地址解析函数try {const recommend = await reverseGeocoding(this, res.latitude, res.longitude);// 更新推荐地址this.recommend = recommend;// 存储到缓存const userName = uni.getStorageSync('curUser').userName;// console.log("userName:" + JSON.stringify(userName));const cacheKey = `location_${userName}`;let location = {latitude: res.latitude,longitude: res.longitude,recommend: recommend};console.log("location:" + JSON.stringify(location));uni.setStorageSync(cacheKey, location);console.log("逆地址解析成功,缓存键:", cacheKey);} catch (error) {console.error('逆地址解析失败:', error);uni.showToast({title: '逆地址解析失败',icon: 'none'});}} catch (err) {console.error('获取位置失败,尝试通过 IP 获取', err);try {const location = await getLocation();if (location) {this.locationInfo = {latitude: location.lat,longitude: location.lng};console.log('通过 IP 获取的位置 - 纬度:', location.lat);console.log('通过 IP 获取的位置 - 经度:', location.lng);} else {uni.showToast({title: '通过 IP 获取位置失败',icon: 'none'});}} catch (ipErr) {console.error('通过 IP 获取位置失败', ipErr);// uni.showToast({//         title: '获取位置失败',//         icon: 'none'// });}} finally {this.isLocating = false; // 定位结束,无论成功与否,都设置状态为定位结束}},

2.场馆分类组件的实现

思路:

可以使用u-scroll-list横向滚动列表:

https://uviewui.com/components/scrollList.html#api

改进版使用swiper:

https://uniapp.dcloud.net.cn/component/swiper.html

实现代码:

<!-- 设置 u-scroll-list 宽度为屏幕宽度 --><u-scroll-list direction="horizontal" :show-scrollbar="false" :enhanced="false" style="width: 100vw"><!-- 按每页 10 个元素分组渲染 --><view class="page" v-for="(page, pageIndex) in groupedPages" :key="pageIndex"><view class="type-row" v-for="(row, rowIndex) in splitIntoRows(page)" :key="rowIndex"><view class="type-item" v-for="(item, index) in row" :key="index"><view class="icon-container"><text class="iconfont" v-html="item.icon"></text></view><text class="type-name">{{item.value}}</text></view></view></view></u-scroll-list>
<swiper class="swiper-container" :current="currentPage" :circular="false":display-multiple-items="1" :indicator-dots="false"><swiper-item v-for="(page, pageIndex) in groupedPages" :key="pageIndex"><view class="page"><view class="type-row" v-for="(row, rowIndex) in splitIntoRows(page)" :key="rowIndex"><view class="type-item" v-for="(item, index) in row" :key="index"><view class="icon-container"><text class="iconfont" v-html="item.icon"></text></view><text class="type-name":style="{ color: selectedType === item.value ? 'blue' : 'inherit' }">{{item.value}}</text></view></view></view></swiper-item></swiper>

3.附近场馆列表组件的实现

思路:

1)将场馆列表单独封装成组件,通过props接收数据。

2)用户体验:通过图片懒加载、文字截断处理(省略号)、开放时间分开显示等美化组件的布局,提升用户体验。

实现代码:

<!-- 场馆列表 --><view class="venue-list"><view class="venue-row"><view class="venue-item" v-for="(item,index) in venueList" :key="index" @click="goToVenueDetail(item.id)"><!-- 图片容器,添加加载效果 --><view class="image-container"><image class="venue-image":src="item.pictureList && item.pictureList.length > 0 ? urlConstruct(item.pictureList[0].url) : '{{item.url}}'"lazy-load="true" mode="aspectFill" @error="onImageError(index)"@load="onImageLoad(index)"></image><!-- 加载动画 --><view class="image-loading" v-if="!imageLoaded[index]"><u-loading-icon mode="circle" color="#2979ff" size="24"></u-loading-icon></view></view><view class="venue-info"><view class="venue-name-tag"><view class="venue-name">{{truncateName(item.name)}}</view><view class="venue-tags"><text class="tag">{{item.typeName}}</text></view></view><view class="venue-meta"><view class="map-distance"><u-icon name="map" color="#666" size="13"></u-icon><text>{{item.distance ? parseFloat(item.distance).toFixed(1) : '0.0'}}km</text><text class="text-ellipsis">{{item.address}}</text></view></view><view class="venue-contact"></view><view class="venue-hours"><view class="icon-text-container"><u-icon name="clock" color="#666" size="12"></u-icon><span style="margin-left: 3px;">{{truncateOpenTimeFirstLine(item.openTime)}}</span></view><span class="remaining-open-time">{{truncateOpenTimeRemaining(item.openTime)}}</span></view></view></view></view></view>

4.分页加载

思路:

1)初始化状态
2)首屏数据加载
3)滚动监听触发
4)分页请求处理
5)边界状态管理

数据加载完毕的判定和异常错误处理

核心逻辑:

1)数据结构设计:
  • venueListData.data 存储分页数据(包含 current/size/total/records 字段)

  • page 对象维护当前页码(pageNum)和分页大小(pageSize)

  • loadmoreStatus 控制加载状态(loadmore/loading/nomore/error)

2)核心触发机制:
  • 通过onReachBottom生命周期监听滚动到底部事件

  • 滚动位置通过onPageScroll实时更新,用于控制返回顶部按钮

3)细节:
  • 页码计算采用 current = pageNum - 1 的转换逻辑(适配后端0-based分页)

  • 使用数组合并策略:records = [...oldRecords, ...newRecords]

  • 双重状态判断(records.length >= total 和 API响应空数据)

4)分页加载流程图

在这里插入图片描述

实现代码:

监听用户滑动到底部
// 监听用户滑动到底部onReachBottom() {this.getMoreVenueList();console.log('页面滚动到底部,触发分页加载');},watch: {loadmoreStatus(newStatus) {console.log('loadmoreStatus 发生变化,新值为:', newStatus);if (newStatus === 'loadmore') {console.log('分页加载成功');} else if (newStatus === 'nomore') {console.log('分页加载无新数据');} else if (newStatus === 'error') {console.log('分页加载失败');}}},
请求下一页的数据:
/*** 发起场馆列表请求*/async fetchVenueList() {try {return await getVenueList({current: this.page.pageNum - 1,size: this.page.pageSize,latitude: this.locationInfo.latitude,longitude: this.locationInfo.longitude,km: 10,});if (!response.data || !response.data.records || response.data.records.length === 0) {console.error('获取场馆列表数据为空');this.dataLoadError = true;this.loading = false;throw new Error('获取场馆列表数据为空');}return response;} catch (error) {console.error('获取场馆列表数据失败:', error);this.loading = false; // 隐藏骨架屏throw error;}},
/*** 获取下一页的场馆信息*/async getMoreVenueList() {if (this.venueListData.data.records.length >= this.total) {// 没有更多数据了this.loadmoreStatus = "nomore";} else {if (!this.loading) {this.page.pageNum++;// 显示正在加载this.loadmoreStatus = "loading";// 修改后try {const newData = await this.fetchVenueList();this.venueListData.data.records = this.venueListData.data.records.concat(newData.data.records);this.loadmoreStatus = newData.data.records.length > 0 ? "loadmore" : "nomore";} catch (error) {console.error('获取下一页场馆列表数据失败:', error);this.loadmoreStatus = "error";this.loading = false; // 隐藏骨架屏this.loadmoreStatus = "error";}}}},

5.提升用户体验

1)骨架屏​:

数据加载前显示骨架屏,骨架屏与真实布局高度一致,避免空白页带来的视觉焦虑。

代码实现:

<!-- 骨架屏结构与真实场馆列表保持DOM结构一致 -->
<u-skeleton avatarSize="88"  // 匹配场馆封面图尺寸rows="2"         // 模拟描述文字行数rowsWidth="90%"  // 模拟文字长度:animate="true"  // 呼吸动画减少等待焦虑
/>
2)回到顶部​:

滚动时显示 u-back-top 按钮,优化长列表浏览。

template:

<!-- 回到上方按钮 --><u-back-top :scroll-top="scrollTop"></u-back-top>

script

// 用来控制滚动到最上方,在data(){}中设置scrollTop: 0,
// 在滑动过程实时获取现在的滚动条位置,并保存当前的滚动条位置onPageScroll(e) {this.scrollTop = e.scrollTop;},
3)错误提示​:

通过 u-toast 显示操作反馈(如生成数据成功提示)。

6.样式与交互设计

  • 响应式布局​:通过 Flex 布局适配不同屏幕尺寸。

  • 动效反馈​:骨架屏动画、按钮点击态(:active 样式)提升操作感。

六、思路和建议

在首页的搭建过程中可以采用从上到下的搭建方式,从顶部位置信息栏开始,到搜索框,再到场馆分类,附近场馆列表。

思路上要注意,由于附近场馆列表的信息中有相关位置信息,所以这里的逻辑是需要定位完成才可以显示,所以要先进行定位,然后才能通过定位信息进一步展示出附近场馆信息,这里附近的范围为10km。也就是说,如果你附近10km没有场馆,附近场馆列表就没有数据显示,这进一步说明了先进行定位是必要的。

建议:定位功能可以使用浏览器自带的,也可以使用腾讯地图,谷歌地图等的api,当然在使用前,你需要看一下这个定位功能是否在你想展示的平台都兼容,比如说你想要做一个网页的平台,你选择的定位功能必须要在浏览器上兼容;如果你想做小程序,你就必须选择能和你的小程序(如微信小程序、支付宝小程序、抖音小程序等)能够兼容的定位功能。

项目说明和其他介绍:

SaaS场快订平台项目说明【持续更新】-CSDN博客

具体代码可以查看相关开源仓库,项目介绍视频可见:

场快订高并发场馆预订平台开源啦,我的第一个开源项目欢迎大家多多支持!_哔哩哔哩_bilibili

完整的开源说明请见:

场快订场馆预定平台开源说明-CSDN博客

感谢你的支持,希望我的文章对你有所帮助!

http://www.xdnf.cn/news/384643.html

相关文章:

  • MacOS Python3安装
  • Vue Router
  • 【Linux系统】第四节—详解yum+vim
  • Java原生结合MQTTX---完成心跳对话(附带源码)
  • 同一个虚拟环境中conda和pip安装的文件存储位置解析
  • ALLinSSL:一站式SSL证书管理解决方案
  • ubuntu使用Postfix外部SMTP代理发送邮件
  • spring中的@Value注解详解
  • MCP Streamable HTTP 传输层的深度解析及实战分析
  • 前端代理问题
  • Ingrees 控制器与 Ingress 资源的区别
  • 容器技术 20 年:颠覆、重构与重塑软件世界的力量
  • A1062 PAT甲级JAVA题解 Talent and Virtue
  • 《Hadoop 权威指南》笔记
  • CDGP主观题题库与范例解答
  • 2021-11-16 C++歌手去掉2最高2最低均分
  • 438. 找到字符串中所有字母异位词(滑动窗口)
  • 判断点是否在立方体内
  • 计算机网络笔记(二十)——4.2网际协议IP
  • 滑动窗口,438找出字符串中所有字母的异位词
  • cpu缓存一致性
  • C语言模糊不清的知识
  • BC12-字符金字塔
  • 【C++贪心 位运算】B3930 烹饪问题|普及
  • RESTful API 与传统 API 设计:深度对比与完整实践指南RESTful 与 传统 API 的核心区别
  • 基于STM32的LCD信号波形和FFT频谱显示
  • Pandas 内存不足 或 UDF 执行慢
  • Python面向对象编程:初识类与对象
  • c++学习之路(3)
  • sched_fair 调度:负载权重、虚拟运行时间与最小虚拟时间