基于Uniapp+SpringBoot的社区化旅游地图APP
目录
1.目的意义
随着中国旅游业的迅猛发展,游客对详尽旅游信息,便捷导航服务的需求日益增长。然而,现有旅游信息系统存在信息过载、个性化不足、操作繁琐和互动性差等问题,虽有基于社区的旅游地图系统在部分地区应用,但缺乏统一标准与深入研究。
从技术层面看,在地图选点功能中,传统地图跨平台兼容性不足,在个性化线路设计方面,现有研究多缺乏社区化且难以广泛推广。旅游攻略领域,UGC 内容虽已成为重要信息来源,但存在商业化与社区调性平衡、交易转化率低等问题。总体而言,现有旅游攻略 APP 虽初步实现 “地图 + 社区 + 攻略” 功能,但仍需通过技术整合与用户激励打破数据孤岛,提升整体效能 。
本产品聚焦社区化旅游地图系统设计,旨在整合地图标记、路线分享、文章发布等功能,强化社区互动,满足用户个性化旅游规划需求,提升旅游服务质量。与传统旅游攻略 APP 相比,该系统具备五大特色:支持地图选点与点位标记,解决信息过载;提供行进路线自定义绘制,增强个性化服务;实现旅游路线分享,丰富内容生态;开放文章发布功能,减少数据冗余;构建社区地图促进用户互动。跨越平台使用:Uniapp支持一套代码同时发布Android、微信小程序等多端平台。
2.总体设计
核心功能上:
(1)地图按分类选点:结合GPS定位技术,通过用户选择的地址与分类信息,批量在地图上展示地点列表,支持批量导入。
(2)路线、地图的社区化:用户可将自己的路线地图分享至社区,社区展示其他用户发布的地图路线。
(3)社区文章:用户可发布文章,以图片+富文本记录的形式,展现多方面资料。
(4)文章关联地点与地图:绑定地图+文章的形式为用户提供多元、详尽的资料信息,避免了多个APP来回切换的冗余操作。
(5)点位标记:结合腾讯地图API实现用户进行个性化点位标记,记录经纬度和个性化信息,如添加备注、评分等。所有标记信息将实时同步至后端。
(6)附近热点功能:结合定位技术将用户所在位置的筛选出周边地点,支持拖动展示更多地点,点击展示地址关联文章。
技术上:
一、框架和语言:
(1)Uniapp:基于Vue2的框架,用于前端开发,同时兼容微信小程序 API,通过 Vue.js 实现界面交互。
(2)Java:后端开发的主要语言,用于构建后端业务逻辑和处理数据库操作。
(3)SpringBoot:基于Spring的开发框架,用于后端开发,简化Spring应用开发,提供快速、高效、灵活的开发体验。
(4)MySQL:作为持久化存储,通过 Spring Data JPA 或 MyBatis Plus 进行数据操作
二、组件和API
(1)ThorUI组件:基于Vue2的UI组件库,提供丰富的UI组件,如按钮、列表、表单等,用于快速构建美观、一致的用户界面。
(2)腾讯地图API:它集成了腾讯地图服务,实现地图展示、定位、路线规划等功能,为用户提供准确的地图信息和便捷的导航服务。
(3)七牛云存储API:实现图片文件的云端存储。
(4)MyBatis Plus:基于MyBatis的增强型工具,提供高效的CRUD操作、分页、逻辑删除等功能,简化数据库开发。
(5)uni-map:基于uni-app的地图组件,支持定位、标记、路径绘制等功能,适用于展示位置、路线规划等场景
3.功能演示-移动端







改进方向:
(1)完善标签信息管理,加入标签系统后,系统可以更有针对性地加入更多的实用、娱乐性内容,比如针对探寻历史文化的旅行攻略、川菜美食推荐等,优化用户体验,满足用户多样化的需求。
(2)加强数据分析与应用:通过对用户行为数据的分析和挖掘,结合算法了解用户的喜好和行为模式,从而为他们提供更加个性化和精准的服务和建议。
(3)进一步加强社区化,建立主题群组:通过针对某一具体地点、项目创立群组,群组内共享地图、地点信息,具备聊天功能,提高用户参与感,加强互动体验。
(4) UI、配色等不合理不统一。
4.部分源码、数据库设计

// 绘制路线
async drawRoute() {
// 检查是否有足够的地点
if (this.addressList.length < 2) {
uni.showToast({
title: '至少需要两个地点',
icon: 'none'
});
return;
}
// 更新地图标记点
this.covers = this.addressList.map((item, index) => ({
id: index,
latitude: item.latitude,
longitude: item.longitude,
iconPath: '../../static/map/mark.png',
width: 35,
height: 35
}));
try {
// 准备途经点数据
const points = this.addressList.map(item => ({
lat: item.latitude,
lng: item.longitude
}));
// 调用腾讯地图API获取路线
const res = await multiPoint(points);
if (res.status === 0 && res.result?.routes?.[0]) {
// 清空之前的路线点
this.polyline = [{
points: [],
color: "#31c27c",
width: 10,
arrowLine: true,
borderWidth: 2
}];
// 获取路线坐标点
const coors = res.result.routes[0].polyline;
const pl = [];
// 坐标解压(腾讯地图路线坐标特殊处理)
const kr = 1000000;
let lat = 0, lng = 0;
for (let i = 0; i < coors.length; i++) {
if (i === 0) {
lat = coors[i] / kr;
lng = coors[i + 1] / kr;
i++;
} else {
lat += coors[i] / kr;
lng += coors[i + 1] / kr;
i++;
}
pl.push({ latitude: lat, longitude: lng });
}
// 更新路线点
this.polyline[0].points = pl;
// 更新地图视野范围
this.includePoints = [...this.covers.map(marker => ({
latitude: marker.latitude,
longitude: marker.longitude
}))];
uni.showToast({
title: '路线绘制成功',
icon: 'success'
});
} else {
throw new Error('路线规划失败');
}
} catch (err) {
console.error('路线规划失败:', err);
uni.showToast({
title: '路线规划失败',
icon: 'error'
});
}
},
// 滚动到底部
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query.select('.address-scroll').boundingClientRect(data => {
if (data) {
const scrollView = uni.createSelectorQuery().in(this).select(
'.address-scroll');
scrollView.scrollOffset({
complete: (res) => {
scrollView.scrollTo({
top: data.height,
duration: 300
});
}
});
}
}).exec();
});
},
// 删除地址
deleteAddress(index) {
// 如果只剩一个地址,不允许删除
if (this.addressList.length <= 1) {
uni.showToast({
title: '至少保留一个地址',
icon: 'none'
});
return;
}
// 显示确认弹窗
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
if (res.confirm) {
this.addressList.splice(index, 1);
// 更新地图标记
// this.drawRoute();
}
}
});
}
uploadImage: function(index, url, serverUrl) {
let _this = this;
return new Promise((resolve, reject) => {
uni.uploadFile({
url: this.serverUrl || serverUrl,
name: this.fileKeyName,
header: this.header,
formData: this.formData,
filePath: url,
success: function(res) {
if (res.statusCode == 200) {
// //返回结果 此处需要按接口实际返回进行修改
// let d = JSON.parse(res.data.replace(/\ufeff/g, "") || "{}")
// //判断code,以实际接口规范判断
// if (d.code % 100 === 0) {
// // 上传成功 d.url 为上传后图片地址,以实际接口返回为准
// d.url && (_this.imageList[index] = d.url)
// _this.$set(_this.statusArr, index, d.url ? "1" : "3")
// } else {
// // 上传失败
// _this.$set(_this.statusArr, index, "3")
// }
//返回结果 此处需要按接口实际返回进行修改
let d = JSON.parse(res.data.replace(/\ufeff/g, "") || "{}")
//判断code,以实际接口规范判断
if (d.return_code == 10000) {
// 上传成功 d.url 为上传后图片地址,以实际接口返回为准
d.data.url && (_this.imageList[index] = d.data.url)
_this.$set(_this.statusArr, index, d.data.url ? "1" : "3")
} else {
// 上传失败
_this.$set(_this.statusArr, index, "3")
}
resolve(index)
} else {
_this.$set(_this.statusArr, index, "3")
reject(index)
}
},
fail: function(res) {
_this.$set(_this.statusArr, index, "3")
reject(index)
}
})
})
},
delImage: function(index) {
this.imageList.splice(index, 1)
this.statusArr.splice(index, 1)
this.$emit("remove", {
index: index,
params: this.params
})
this.change()
},
previewImage: function(index) {
if (!this.imageList.length) return;
uni.previewImage({
current: this.imageList[index],
loop: true,
urls: this.imageList
})
},
/**
* 当属性serverUrl传空时,父级调用该方法一次性上传所有图片
* @param serverUrl 服务器接口地址
**/
uploadAllImage(serverUrl) {
if (!serverUrl) {
this.toast('服务器接口地址不能为空!');
return;
}
let imageArr = [...this.imageList]
const len = imageArr.length
for (let i = 0; i < len; i++) {
//如果是服务器地址图片则无需再次上传
if (imageArr[i].startsWith('https')) {
continue;
} else {
this.$set(this.statusArr, i, "2")
this.uploadImage(i, imageArr[i], serverUrl).then(() => {
if (i === len - 1) {
this.change(true)
}
}).catch(() => {
if (i === len - 1) {
this.change(true)
}
})
}
}
}
更多推荐
所有评论(0)