引言

在当今数字化时代,在线工具网站为用户提供了便捷的服务和功能,本文分享了我使用UniApp和Django开发的一款多平台在线工具网站。通过这个项目,我探索了跨平台开发与强大的后端框架结合的优势,实现了用户友好的界面和稳健的功能。

技术选型

我选择UniApp作为前端开发工具,主要因为它能够高效地实现同时支持多个平台(Web、小程序、App)的开发需求。而Django作为后端框架,则因其快速开发能力、自带后台管理系统、优秀的安全性和灵活的数据管理而备受推崇。

项目环境

前端框架:uniapp
前端编辑器:HbuilderX 版本号4.24
编辑器官网:HBuilderX-高效极客技巧
后端解释器版本:python 3.10.5
后端框架:django==3.0
后端编辑器:vscode 版本1.92.0

项目创建

创建前端uniapp项目创建可参考
 uniapp创建项目
后端django项目创建可参考:
 django创建项目

项目概述

我的在线工具网站旨在为用户提供各种实用工具和服务,包括但不限于实用工具、查询工具、休闲娱乐工具、在线转换、在线游戏、在线加密等功能。用户可以通过Web浏览器、微信小程序或安卓访问和使用这些工具。前端开发(UniApp)页面设计与布局在UniApp中,我采用了简洁直观的页面设计,保证了用户操作的流畅性和友好性。每个工具都有清晰的功能入口,以便用户快速上手。
使用uniapp+Django开发的在线工具网站_django

功能实现

项目采用屏幕自适应,当屏幕宽度小于600px的时候,则一行三个工具,如果屏幕尺寸够大,则一行四个工具,可以完美在移动端适配
使用uniapp+Django开发的在线工具网站_移动端_02
主界面代码:

<template>
	<view class="loading" v-if="!dataLoaded">
		<view class="item item-1"></view>
		<view class="item item-2"></view>
		<view class="item item-3"></view>
		<view class="item item-4"></view>
	</view>
	<!-- 主要容器 -->
	<view class="container" v-if="dataLoaded">
		<view class="search-box">
			<image class="search-icon" src="/static/search.png">
			</image>
			<input v-model="keyword" @input="handleSearch" placeholder="请输入搜索关键词" class="search-input" />
		</view>
		<!-- 循环渲染多个盒子 -->
		<view v-for="(category, index) in filteredToolCategories" :key="index" class="category-box">
			<view class="category-name">{{ category.title }}</view>
			<!-- 使用循环渲染每个工具 -->
			<view class="tool-list">
				<view v-for="(tool, idx) in category.tools" :key="idx" class="tool-item"
					@click="handleToolClick(tool.link)">
					<image :src="tool.icon" class="tool-icon"></image>
					<text class="tool-name">{{ tool.name }}</text>
				</view>
			</view>
		</view>
		<!-- 底部米白色色块 -->
		<view class="footer-block">
			<!-- 底部文字信息 -->
			<view class="footer-text">
				本站工具严禁用于非法用途,本站不承担任何因使用工具造成的损失及责任。
			</view>

			<!-- 底部导航链接 -->
			<view class="footer-links">
				<navigator url="/pages/about/about" open-type="navigate">关于我们</navigator>
				<navigator url="/pages/serviceAgreement/serviceAgreement" open-type="navigate">服务条款</navigator>
				<navigator url="/pages/PrivacyPolicy/PrivacyPolicy" open-type="navigate">隐私政策</navigator>
				<a rel="nofollow" href="https://support.qq.com/products/660601" target="_blank">反馈建议</a>

			</view>
			<!-- 版权信息 -->
			<view class="footer-text">Copyright © 2024 TT在线工具 All Rights Reserved 滇ICP备2024034889号-1 </view>
		</view>
	</view>
</template>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.

script:

<script>
	export default {
		data() {
			return {
				toolCategories: [],
				keyword: '', // 查找工具
				dataLoaded: false, // 标记数据是否加载完成

				// 弹出提示
				modalConfig: {
					showModal: 'false', //弹出组件是否显示
					title: '提示', // 弹窗标题
					content: '提示内容', // 弹窗内容
				}
			};
		},
		computed: {
			filteredToolCategories() {
				if (!this.keyword) {
					return this.toolCategories;
				}
				return this.toolCategories.map(category => {
					return {
						...category,
						tools: category.tools.filter(tool => tool.name.includes(this.keyword))
					};
				}).filter(category => category.tools.length > 0);
			}

		},
		created() {
			this.fetchToolData();
		},
		// 加载完成后调用方法
		mounted() {
			// 判断是否为ture 是的话调用函数 否则不调用
			if (this.modalConfig.showModal === 'true') {
				this.showModalDialog();
			} else {
				console.log('加载动画函数未被调用');
			}

		},
		methods: {
			async fetchToolData() {
				try {
					const res = await uni.request({
						url: 'https://ttgj.top/tools/',
						method: 'GET',
					});
					const data = res.data;
					// 对分类根据返回的sort字段进行排序
					data.sort((a, b) => a.category.sort - b.category.sort);
					data.forEach(item => {
						const title = item.category.title;
						const name = item.name;
						const link = item.link;
						const icon = item.icon;

						let existingCategory = this.toolCategories.find(category => category.title === title);
						if (!existingCategory) {
							existingCategory = {
								title: title,
								tools: []
							};
							this.toolCategories.push(existingCategory);
						}
						existingCategory.tools.push({
							name: name,
							link: link,
							icon: icon
						});
					});
					this.dataLoaded = true; // 数据加载完成后显示页面内容
				} catch (error) {
					console.error('获取工具列表错误:', error);
				}
			},

			handleSearch() {
				// 由于已在 computed 中实现过滤,此处无需额外代码
			},
			handleToolClick(link) {
				console.log('跳转值:', link);

				//如果小于768则认为移动端
				const isMobile = window.innerWidth < 768;
				//判断isMobile是否为true 是的话执行这个条件
				if (isMobile) {
					if (link.startsWith('http')) {
						console.log('http链接', link);
						window.open(link, '_blank'); // 新标签页打开外部链接
					} else {
						// 移动端,使用uni.navigateTo方法
						uni.navigateTo({
							url: "/pages/" + link,
							animationType: 'push',
						});
					}
					//不为true执行这个条件
				} else {
					// 非移动端(假设是PC端),使用window.open方法
					if (link.startsWith('http')) {
						console.log('http链接', link);
						window.open(link, '_blank'); // 新标签页打开外部链接
					} else {
						console.log('path路径');
						const currentOrigin = window.location.origin; // 获取当前页面的 origin,例如:http://localhost:5173
						window.open(currentOrigin + '/pages/' + link, '_blank'); // 新标签页打开内部链接
					}
				}
			},
			showModalDialog() {
				uni.showModal({
					title: this.modalConfig.title,
					content: this.modalConfig.content,
					showCancel: true,
					cancelText: '取消',
					confirmText: '确认',
					success: (res) => {
						if (res.confirm) {
							console.log('用户点击了确认按钮');
							// 可以在这里添加确认按钮的逻辑处理
						} else if (res.cancel) {
							console.log('用户点击了取消按钮');
							// 可以在这里添加取消按钮的逻辑处理
						}
						// 关闭模态弹窗后,将 isModalVisible 设置为 false
						this.isModalVisible = false;
					}
				});
			}

		}
	};
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.

style:

<style scoped lang="scss">
	.container {
		display: flex;
		flex-wrap: wrap;
		padding: 50rpx;
	}

	.search-box {
		margin-bottom: 20px;
		width: 100%;
		display: flex;
		align-items: center;
		background-color: #F9F7F7;
		border-radius: 15rpx;
		padding: 10rpx 20rpx;
	}

	.search-icon {
		width: 35rpx;
		height: 35rpx;
		margin-right: 10rpx;
		/* 设置搜索图标与输入框之间的间距 */
	}

	.search-input {
		flex: 1;
		font-size: 35rpx;
		color: #333333;
		border: none;
		background: none;
		outline: none;
	}

	.category-box {
		width: 100%;
		margin-bottom: 20px;
		padding: 10px;
		border: 1px solid #ccc;
		box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
		border-radius: 10px;
	}

	// 分类名字
	.category-name {
		font-size: 16px;
		color: #F9F7F7;
		margin-bottom: 10px;
		background-color: #71C9CE;
		padding: 5px 10px;
		display: inline-block;
		border-radius: 5px;
	}

	// 工具列表父元素
	.tool-list {
		display: flex;
		flex-wrap: wrap;
	}

	// 工具名字和图标
	.tool-item {
		width: calc(25% - 10px);
		/*表示将父容器的宽度分成四等份(每份占25%),然后再从每份中减去10像素的间距*/
		/* 一行四个工具,间隔为10px */
		margin-right: 10px;
		margin-bottom: 10px;
		margin-top: 20px;
		/* 添加工具和标题之间 */
		display: flex;
		flex-direction: column;
		align-items: center;

	}

	// 当页面宽度小于500px 则执行这个代码
	@media (max-width: 500px) {
		.tool-item {
			width: calc(33% - 10px);
			/*表示将父容器的宽度分成四等份(每份占33%),然后再从每份中减去10像素的间距*/
			/* 一行三个工具,间隔为10px */
			margin-right: 10px;
			margin-bottom: 10px;
			margin-top: 20px;
			/* 添加工具和标题之间 */
			display: flex;
			flex-direction: column;
			align-items: center;

		}
	}

	// 工具图标
	.tool-icon {
		width: 40px;
		height: 40px;
		margin-bottom: 5px;
	}

	// 工具名字
	.tool-name {
		font-size: 14px;
		text-align: center;
		white-space: nowrap;
		/* 防止换行 */
		overflow: hidden;
		/* 隐藏超出部分 */
		text-overflow: ellipsis;
		/* 如果文本超出部分被隐藏,则显示省略号 */
		display: inline-block;
	}

	// 当页面宽度小于500px 则执行这个代码
	@media (max-width: 500px) {
		.tool-name {
			font-size: 14px;
			text-align: center;
			width: 99%;
			/* 宽度超过99%则显示省略号 */
			white-space: nowrap;
			/* 防止换行 */
			overflow: hidden;
			/* 隐藏超出部分 */
			text-overflow: ellipsis;
			/* 如果文本超出部分被隐藏,则显示省略号 */
			display: inline-block;
		}
	}

	.footer-block {
		bottom: 0;
		/* 固定在底部 */
		box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
		/* 添加轻微阴影效果 */
		padding: 20px;
		text-align: center;
		margin: 0 auto;
		/* 居中 */
		width: 100%;
		/* 控制宽度,左右展开 */
	}

	.footer-text {
		font-size: 14px;
		color: #666;
		margin-bottom: 10px;
	}

	.footer-links {
		display: flex;
		/* 使用 Flexbox 布局 */
		justify-content: center;
		/* 居中对齐 */
		gap: 20px;
		/* 设置子元素之间的间距 */
		margin-bottom: 10px;
	}

	a {
		text-decoration: none;
		font-size: 14px;
		color: #007bff;
	}

	a:hover {
		text-decoration: underline;
		/* 悬停时下划线效果 */
	}

	.footer-links navigator {
		font-size: 14px;
		color: #007bff;
		/* 链接颜色 */
	}

	.footer-links navigator:hover {
		text-decoration: underline;
		/* 悬停时下划线效果 */
	}



	.loading {
		position: absolute;
		width: 200px;
		height: 200px;
		top: 0;
		bottom: 0;
		left: 0;
		right: 0;
		margin: auto;
	}

	.item {
		width: 100px;
		height: 100px;
		position: absolute;
	}

	.item-1 {
		background-color: #FA5667;
		top: 0;
		left: 0;
		z-index: 1;
		animation: item-1_move 1.8s cubic-bezier(.6, .01, .4, 1) infinite;
	}

	.item-2 {
		background-color: #7A45E5;
		top: 0;
		right: 0;
		animation: item-2_move 1.8s cubic-bezier(.6, .01, .4, 1) infinite;
	}

	.item-3 {
		background-color: #1B91F7;
		bottom: 0;
		right: 0;
		z-index: 1;
		animation: item-3_move 1.8s cubic-bezier(.6, .01, .4, 1) infinite;
	}

	.item-4 {
		background-color: #FAC24C;
		bottom: 0;
		left: 0;
		animation: item-4_move 1.8s cubic-bezier(.6, .01, .4, 1) infinite;
	}

	@keyframes item-1_move {

		0%,
		100% {
			transform: translate(0, 0)
		}

		25% {
			transform: translate(0, 100px)
		}

		50% {
			transform: translate(100px, 100px)
		}

		75% {
			transform: translate(100px, 0)
		}
	}

	@keyframes item-2_move {

		0%,
		100% {
			transform: translate(0, 0)
		}

		25% {
			transform: translate(-100px, 0)
		}

		50% {
			transform: translate(-100px, 100px)
		}

		75% {
			transform: translate(0, 100px)
		}
	}

	@keyframes item-3_move {

		0%,
		100% {
			transform: translate(0, 0)
		}

		25% {
			transform: translate(0, -100px)
		}

		50% {
			transform: translate(-100px, -100px)
		}

		75% {
			transform: translate(-100px, 0)
		}
	}

	@keyframes item-4_move {

		0%,
		100% {
			transform: translate(0, 0)
		}

		25% {
			transform: translate(100px, 0)
		}

		50% {
			transform: translate(100px, -100px)
		}

		75% {
			transform: translate(0, -100px)
		}
	}
</style>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.
  • 274.
  • 275.
  • 276.
  • 277.
  • 278.
  • 279.
  • 280.
  • 281.
  • 282.
  • 283.
  • 284.
  • 285.
  • 286.
  • 287.
  • 288.
  • 289.
  • 290.
  • 291.
  • 292.
  • 293.
  • 294.
  • 295.
  • 296.
  • 297.
  • 298.
  • 299.
  • 300.
  • 301.
  • 302.
  • 303.
  • 304.
  • 305.
  • 306.
  • 307.
  • 308.

运行效果:
使用uniapp+Django开发的在线工具网站_移动端_03

后端开发(Django)

数据库设计

我使用Django自带的ORM设计了数据库,包括工具分类名、工具名字及图片,确保了数据的安全和可靠性。以下是一个简化的模型示例:

from django.db import models  
  
class ToolCategory(models.Model):  
    title = models.CharField(max_length=100,verbose_name='分类名')
    sort = models.IntegerField(default=1, verbose_name='排序')


    class Meta:
        # admin后台显示的子标题
        verbose_name='分类'   #增加按钮显示增加用户以及标题
        verbose_name_plural='工具分类列表'

    def __str__(self):  
        return self.title
  
class Tool(models.Model):  
    name = models.CharField(max_length=100,verbose_name='工具名')
    icon = models.ImageField(upload_to='tool_ico',verbose_name='图标')  #从本地上传
    link = models.CharField(max_length=100,default='pathname/pathname',verbose_name='工具路径-链接')
    category = models.ForeignKey(ToolCategory, on_delete=models.CASCADE,verbose_name='工具分类')
    # 其他字段,如描述、使用方法等  

    class Meta:
        verbose_name='工具'   #增加按钮显示增加用户以及标题
        verbose_name_plural='工具列表'

    def __str__(self):  
        return self.name
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

API开发:

通过Django的视图函数和路由设置,我设计了一组RESTful API来处理前端请求,并与数据库进行交互。以下是一个简化的视图函数示例:

from django.shortcuts import render

# Create your views here.

from rest_framework import viewsets, mixins
from rest_framework.response import Response
from .models import Tool, ToolCategory
from .serializers import ToolSerializer, ToolCategorySerializer  # 假设你创建了这些序列化器


class ToolCategoryViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = ToolCategory.objects.all()
    serializer_class = ToolCategorySerializer


class ToolViewSet(viewsets.ModelViewSet):
    queryset = Tool.objects.all().select_related('category')  # 预加载关联的ToolCategory对象
    serializer_class = ToolSerializer

    def get_queryset(self):
        # 你可以在这里添加额外的查询逻辑,比如过滤等
        return super().get_queryset()#在后端数据库添加数据后返回工具名字、工具图片、工具分类等信息:
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

使用uniapp+Django开发的在线工具网站_ico_04

成果展示

与用户体验经过不懈努力,我的在线工具网站已正式上线并稳定运行。该网站支持多平台访问,无论是PC端还是移动端,用户都能轻松使用。网站的用户体验得到了广大用户的积极反馈,特别是其界面设计的友好性和功能的完备性,更是获得了用户的高度认可。

总结与展望

通过此次项目,我不仅在技术上取得了显著的进步,还深刻体会到了UniApp和Django这两个框架在实际项目中的强大应用价值。它们为我提供了灵活的开发方式和高效的开发效率,使得我能够更专注于实现项目的核心功能。展望未来,我计划继续对在线工具网站进行优化和扩展,引入更多实用的功能和服务,以进一步提升用户体验,满足用户多样化的需求。

结语

如果你也对使用UniApp和Django开发项目充满兴趣,或者想要了解更多关于我的项目细节和技术实现,我非常欢迎你的联系。让我们携手共进,共同探索更多前沿技术和创新应用!

项目访问方式

为了让你更便捷地体验我的在线工具网站,以下是访问方式:
项目网站地址: TT工具
微信小程序访问二维码:
使用uniapp+Django开发的在线工具网站_ico_05