基于uniapp的开源盲盒系统:前端H5、小程序和APP与后端PHP TP6框架的完美结合
随着互联网技术的飞速发展,多平台应用开发已经成为企业和个人开发者追求的目标。UniApp作为一种使用Vue.js开发跨平台应用的前端框架,因其一次编写、多端运行的特性而受到广泛欢迎。今天,我们将介绍一个利用UniApp开发的开源盲盒系统,它支持H5页面、小程序以及APP,并且后端采用了PHP的ThinkPHP6 (TP6)框架。
项目概述源码及演示: ceshi.66demo.cn
这个开源盲盒系统是一个全功能的在线抽盲盒平台,用户可以通过Web页面、微信小程序或手机APP进行访问和购买。系统的前端部分基于UniApp框架,使得开发者能够以一套代码同时部署到多个平台,大大提高了开发效率。后端则采用流行的PHP TP6框架,保证了数据处理的高效性和稳定性。
技术栈特点
UniApp
跨平台: UniApp可以编译到iOS、Android、H5、小程序等多个平台,极大地减少了开发与维护成本。
高效开发: 基于Vue.js的开发模式,让开发者可以利用其丰富的生态系统和组件库快速实现功能。
易于上手: 对于熟悉Vue的开发者来说,UniApp的学习曲线平缓,可以快速掌握。
ThinkPHP6 (TP6)
快速开发: TP6框架遵循约定优于配置的原则,简化了开发流程,让开发者更多关注业务逻辑。
功能强大: 拥有ORM、中间件、模型事件等高级功能,满足复杂的业务需求。
社区活跃: 拥有庞大且活跃的社区,遇到问题时可以容易找到解决方案和资源。
系统功能
用户认证: 提供用户注册、登录、找回密码等功能,并保障用户信息安全。
盲盒购买: 用户可以浏览不同系列的盲盒产品,进行选购并在线支付。
订单管理: 用户可以查看订单状态,跟踪物流信息,并进行售后服务。
互动分享: 支持用户评价盲盒内容,分享到社交平台增加用户粘性。
后台管理: 管理员可以管理商品、订单、用户和数据统计分析等。
项目搭建
环境准备: 安装Node.js、HBuilderX(UniApp官方IDE)、PHP环境及Composer。
前端开发: 使用HBuilderX创建UniApp项目,根据需要选择H5、小程序或APP模块进行开发。
后端配置: 通过Composer安装TP6框架,配置数据库和路由,开始后端逻辑编码。
接口对接: 设计RESTful API供前端调用,实现前后端数据交互。
系统测试: 在不同的平台和设备上进行测试,确保兼容性和用户体验。
部署上线: 将后端代码部署到服务器,前端分别打包上传到对应的平台。
uniapp部分代码参照
<!--
* @Date: 2022-11-21 11:38:47
* @LastEditTime: 2023-02-09 09:52:48
* @Description: 首页
-->
<template>
<view
class="page-wrap common_bg"
:style="{
backgroundImage: `url(${imgBaseUrl}${'/static/img/index_top_bg.png'})`
}"
>
<!-- 状态栏 -->
<view
class="status-bar common_bg"
:style="{
height: `${sysConfig.statusBarHeight}px`,
width: '100%',
backgroundImage: `url(${imgBaseUrl}${'/static/img/index_top_bg.png'})`
}"
></view>
<view
:style="{
height: `${sysConfig.statusBarHeight}px`,
width: '100%'
}"
></view>
<u-gap height="88"></u-gap>
<view
class="page-wrap-header common_bg"
:style="{
top: `${sysConfig.statusBarHeight}px`,
backgroundImage: `url(${imgBaseUrl}${'/static/img/index_top_bg.png'})`
}"
>
<view
@click="$common.to({ url: '/package/index/search' })"
class="input-box"
>
<view>请输入商品关键词</view>
<view class="icon">
<cimage src="/static/icon/search.png" mode="scaleToFill" />
</view>
</view>
</view>
<mescroll-body
ref="mescrollRef"
@init="mescrollInit"
@down="downCallback"
@up="getList"
:down="downOption"
:up="upOption"
>
<swiper
v-if="swiperList.length > 0"
class="banner"
autoplay
:circular="true"
>
<swiper-item v-for="(item, i) of swiperList" :key="i">
<view @click="$common.bannerTo(item)" class="swiper-pic">
<cimage :src="item.thumb" mode="scaleToFill" />
</view>
</swiper-item>
</swiper>
<!-- 商品分类 -->
<template v-if="classifyList.length > 0">
<swiper @change="classChange" class="classify" :circular="false">
<swiper-item v-for="(item, i) of classifyList" :key="i">
<view
@click="toClassify(a)"
v-for="(a, b) in item"
:key="b"
class="item"
>
<view class="item-pic">
<cimage :src="a.thumb" mode="scaleToFill" />
</view>
<view class="title">{{ a.title }}</view>
</view>
</swiper-item>
</swiper>
<view class="dot-list">
<view
v-for="(item, i) in classifyList"
:key="i"
class="dot-list-item"
:class="{
act: i == classCur
}"
></view>
</view>
</template>
<view v-if="cardList.length > 0" class="card-list">
<view
v-for="(item, i) in cardList"
:key="i"
@click="$common.bannerTo(item)"
class="card-list-item common_bg"
:style="{
backgroundImage: `url(${item.thumb})`
}"
>
<!-- <view class="title">{{ item.title }}</view>
<view class="desc hang1"></view> -->
</view>
</view>
<view class="list-title">
<view class="list-title-l">
<view class="icon">
<cimage src="/static/icon/bao_zhang.png" mode="scaleToFill" />
</view>
购物保障
</view>
<view
class="list-title-r common_bg"
:style="{
backgroundImage: `url(${imgBaseUrl}${'/static/img/index_title_bg.png'})`
}"
>
<text>全新正品</text>
<text>精致好物</text>
<text>极速发货</text>
</view>
</view>
<view class="goods-list">
<!-- 左列 -->
<view class="goods-list-col">
<template v-for="(item, i) in listData">
<view
@click="toMallDetail(item)"
v-if="i % 2 == 0"
:key="i"
class="goods-list-col-item"
>
<view class="pic">
<cimage :src="item.thumb" mode="scaleToFill" />
</view>
<view class="tag-list">
<view class="tag-list-item zheng">正品保障</view>
<view class="tag-list-item">{{ item.cat_desc }}</view>
</view>
<view class="name hang1">
{{ item.title }}
</view>
<view class="price-num">
<view class="price">
¥
<text>
{{ item.price }}
<text>¥{{ item.old_price }}</text>
</text>
</view>
</view>
</view>
</template>
</view>
<!-- 右列 -->
<view class="goods-list-col">
<template v-for="(item, i) in listData">
<view
@click="toMallDetail(item)"
v-if="i % 2 != 0"
:key="i"
class="goods-list-col-item"
>
<view class="pic">
<cimage :src="item.thumb" mode="scaleToFill" />
</view>
<view class="tag-list">
<view class="tag-list-item zheng">正品保障</view>
<view class="tag-list-item">{{ item.cat_desc }}</view>
</view>
<view class="name hang1">
{{ item.title }}
</view>
<view class="price-num">
<view class="price">
¥
<text>
{{ item.price }}
<text>¥{{ item.old_price }}</text>
</text>
</view>
</view>
</view>
</template>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
return {
// 下拉刷新的配置(可选, 绝大部分情况无需配置)
downOption: {
auto: false
},
// 上拉加载的配置(可选, 绝大部分情况无需配置)
upOption: {
auto: false,
page: {
size: 20 // 每页数据的数量,默认10
}
},
listData: [],
classCur: 0,
swiperList: [],
cardList: [],
classifyList: []
}
},
computed: {
...mapGetters(['sysConfig'])
},
onShareAppMessage() {
return {
title: `开盲盒 赢好礼!`,
// imageUrl: this.pageData.box.thumb,
path: `/pages/index/index`
}
},
onLoad(options) {},
onReady() {
this.downCallback()
},
methods: {
/**
* @description: 商城详情
* @param {*} item
* @return {*}
*/
toMallDetail(item) {
this.$common.to({
url: '/package/mall/mall-detail',
query: {
id: item.id
}
})
},
/**
* @description: 获取分类列表
* @return {*}
*/
getClassify() {
this.req({
url: '/v1/shop/cat',
data: {},
Loading: true,
success: res => {
if (res.code == 200) {
if (res.data.length > 0) {
let arr = []
res.data.map((item, i) => {
/* 计算要放入数据的数组下标 */
let idx = Math.floor(i / 5)
console.log(idx)
/* 如果不存在该数组,创建空数组 */
if (!arr[idx]) {
arr[idx] = []
}
/* 向数组中放入数据 */
arr[idx].push(item)
})
this.classifyList = arr
console.log(arr)
}
}
}
})
},
/**
* @description: 下拉刷新
* @return {*}
*/
async downCallback() {
/* 获取轮播图 */
this.$common.getBanner(1).then(res => {
this.swiperList = res
})
/* 获取分类 */
this.getClassify()
/* 获取分类下方卡片列表 */
this.$common.getBanner(2).then(res => {
this.cardList = res
})
this.mescroll.resetUpScroll()
this.mescroll.scrollTo(0, 0)
},
/**
* @description: 获取商品列表数据
* @param {*}
* @return {*}
*/
getList({ num, size }) {
this.req({
url: '/v1/shop/list',
data: {
page: num,
per_page: size
},
Loading: true,
success: res => {
if (res.code == 200) {
if (num == 1) {
this.listData = []
}
this.listData = [...this.listData, ...res.data.data]
this.mescroll.endBySize(res.data.data.length, res.data.total)
}
}
})
},
/**
* @description: 前往分类详情页面
* @return {*}
*/
toClassify(item) {
console.log(item)
this.$common.to({
type: 1,
url: '/package/index/classify-detail',
query: {
catId: item.id
}
})
},
/**
* @description: 商品分类页面切换
* @param {*} e
* @return {*}
*/
classChange(e) {
console.log(e)
this.classCur = e.detail.current
}
}
}
</script>
<style lang="scss">
.page-wrap {
min-height: 100vh;
background-size: 100% auto;
background-color: #f4f7fe;
.status-bar {
// background: #fff;
background-size: 100% auto;
position: fixed;
left: 0;
width: 100%;
box-sizing: border-box;
z-index: 10;
}
&-header {
height: 88rpx;
display: flex;
align-items: center;
padding-left: 30rpx;
// background: #fff;
background-size: 100% auto;
position: fixed;
left: 0;
width: 100%;
box-sizing: border-box;
z-index: 10;
.input-box {
border-radius: 999rpx;
background: #f6f5f5;
width: 400rpx;
height: 56rpx;
padding: 0 30rpx;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24rpx;
font-family: PingFang SC;
font-weight: 500;
color: #b2b2b2;
.icon {
width: 50rpx;
height: 50rpx;
}
}
}
.banner {
width: 100%;
height: 400rpx;
margin: 30rpx auto 0;
swiper-item {
display: flex;
justify-content: center;
.swiper-pic {
width: 690rpx;
height: 100%;
border-radius: 10rpx;
overflow: hidden;
}
}
}
.classify {
width: 100%;
height: 150rpx;
margin-top: 30rpx;
swiper-item {
box-sizing: border-box;
padding: 0 30rpx;
display: flex;
.item {
width: 106rpx;
&-pic {
width: 100%;
height: 106rpx;
}
.title {
font-size: 24rpx;
font-family: PingFang SC;
font-weight: 500;
color: #051a2b;
text-align: center;
margin-top: 10rpx;
}
}
.item + .item {
margin-left: 40rpx;
}
}
}
.dot-list {
display: flex;
justify-content: center;
align-items: center;
margin-top: 30rpx;
&-item {
width: 10rpx;
height: 10rpx;
background: #e1e1e1;
border-radius: 100rpx;
margin: 0 4rpx;
transition: all 0.3s;
&.act {
width: 20rpx;
background: #000000;
}
}
}
.card-list {
width: 690rpx;
border-radius: 10rpx;
overflow: hidden;
background: #fff;
margin: 20rpx auto 0;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
padding: 1rpx 30rpx 30rpx;
&-item {
margin-top: 30rpx;
width: 300rpx;
height: 122rpx;
display: flex;
flex-flow: column nowrap;
justify-content: center;
box-sizing: border-box;
padding-left: 15rpx;
padding-right: 140rpx;
.title {
font-size: 26rpx;
font-family: Source Han Sans CN;
font-weight: 800;
color: #051a2b;
}
.desc {
font-size: 22rpx;
font-family: PingFang SC;
font-weight: 500;
color: #051a2b;
}
}
}
.list-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14rpx;
padding: 0 30rpx;
&-l {
display: flex;
align-items: center;
.icon {
width: 50rpx;
height: 50rpx;
}
font-size: 22rpx;
font-family: PingFang SC;
font-weight: bold;
color: #333333;
}
&-r {
width: 382rpx;
height: 40rpx;
display: flex;
justify-content: center;
align-items: center;
text {
font-size: 20rpx;
font-family: PingFang SC;
font-weight: bold;
color: #2a4e6a;
}
text + text {
margin-left: 20rpx;
}
}
}
.goods-list {
display: flex;
justify-content: space-between;
padding: 14rpx 30rpx 10rpx;
&-col {
&-item {
width: 340rpx;
border-radius: 10rpx;
overflow: hidden;
background: #ffffff;
margin-bottom: 30rpx;
.pic {
height: 340rpx;
}
.tag-list {
padding: 1rpx 15rpx 0;
display: flex;
flex-flow: row wrap;
&-item {
border-radius: 5rpx;
overflow: hidden;
margin-right: 10rpx;
margin-top: 10rpx;
height: 40rpx;
box-sizing: border-box;
padding: 0 10rpx;
display: flex;
align-items: center;
font-size: 20rpx;
font-family: PingFang SC;
font-weight: 500;
color: #eb989c;
border: 2rpx solid #eb989c;
&.zheng {
color: #fff;
background: #333333;
border-color: #333333;
}
}
}
.name {
padding: 10rpx 15rpx;
font-size: 26rpx;
font-family: PingFang SC;
font-weight: 500;
color: #333333;
}
.price-num {
padding: 0 15rpx 10rpx;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: center;
.price {
font-size: 24rpx;
font-family: PingFang SC;
font-weight: bold;
color: #333333;
text {
font-size: 32rpx;
text {
margin-left: 10rpx;
font-size: 20rpx;
font-family: PingFang SC;
font-weight: 500;
text-decoration: line-through;
color: #999999;
}
}
}
.num {
font-size: 20rpx;
font-family: PingFang SC;
font-weight: 500;
color: #999999;
}
}
}
}
}
}
</style>
后端php案例
<?php
// 引入ThinkPHP核心文件
require __DIR__ . '/vendor/autoload.php';
use think\facade\Route;
use think\Request;
// 定义路由规则
Route::get('index', 'IndexController@index'); // 首页
Route::get('products', 'ProductController@list'); // 商品列表
Route::get('product/detail', 'ProductController@detail'); // 商品详情
Route::post('order/create', 'OrderController@create'); // 创建订单
Route::get('order/list', 'OrderController@list'); // 订单列表
Route::get('user/login', 'UserController@login'); // 用户登录
Route::post('user/register', 'UserController@register'); // 用户注册
// 处理请求
$request = Request::instance();
$response = $request->run();
$response->send();