基于uniapp的开源盲盒系统:前端H5、小程序和APP与后端PHP TP6框架的完美结合

基于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();

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

济南壹软网络科技有限公司

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值