目录

1.目的意义

2.总体设计

3.功能演示-移动端

4.部分源码、数据库设计

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)
							}
						})
					}
				}
			}

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐