跳至主要內容

曲师商城

sixkey大约 19 分钟项目文档项目

曲师商城

1、项目需求分析

用户需求分析

  • 注册和登录功能:
    • 用户可以通过填写注册表单完成账号注册。
    • 用户可以使用注册的账号和密码进行登录。
    • 提供忘记密码功能,用户可以通过邮箱或手机验证码重置密码。
  • 商品浏览和搜索功能:
    • 用户可以浏览商品列表,查看商品的基本信息、价格、图片等。
    • 用户可以通过关键词搜索商品,系统返回相关商品的结果。
  • 购物车管理功能:
    • 用户可以将感兴趣的商品添加到购物车。
    • 用户可以修改购物车中商品的数量。
    • 用户可以删除购物车中的商品。
    • 用户可以清空购物车。
  • 下单和支付功能:
    • 用户可以选择购物车中的商品完成下单。
    • 用户需要填写配送地址和选择支付方式。
    • 系统生成订单并显示订单详情。
    • 用户进行支付操作,可以选择在线支付或货到付款。
  • 查看订单状态功能:
    • 用户可以查看订单的物流信息和状态,了解订单的进展情况。
    • 用户可以查看订单的详细信息,如商品清单、收货地址等。
  • 取消订单功能:
    • 用户可以在一定条件下取消未发货的订单,如退货期未过等。
  • 评价和回复功能:
    • 用户可以对购买的商品进行评价,并选择评分和文字评论。
    • 商家可以回复用户的评价。
  • 优惠券和积分功能:
    • 用户可以领取优惠券和积分,用于抵扣购物金额或兑换商品。
  • 促销活动功能:
    • 商家可以发布促销活动,如限时折扣、满减等。
    • 用户可以参与促销活动,享受相应的优惠。
  • 客服支持功能:
    • 提供在线客服功能,解答用户的问题和处理售后问题。
  • 搜索和推荐功能:
    • 用户可以通过关键词搜索商品,系统根据用户的浏览和购买记录推荐相关商品。
  • 移动端适配功能:
    • 商城需要适配不同的移动设备,提供良好的移动端用户体验。

商家入驻代办

商家需求分析

  • 商户入驻功能:
    • 商家可以申请入驻商城,并在平台上销售自己的商品。
    • 商家需要提供相关的营业执照、品牌授权等信息进行认证。
  • 商品管理功能:
    • 商家可以发布、修改、删除自己的商品。
    • 商家可以设置商品的基本信息、价格、库存等。
    • 商家可以管理商品分类和品牌信息。
  • 订单管理功能:
    • 商家可以查看自己店铺的订单列表,包括待处理订单和已完成订单。
    • 商家可以接单、发货、退款等操作。
  • 物流管理功能:
    • 商家可以更新自己店铺的物流信息,如发货状态、快递单号等。

2、技术选型

针对曲师商城项目,考虑以下技术选型:

  1. 前端开发框架:Vue主流的前端框架。
  2. 后端开发语言:Java常用的后端开发语言。
  3. 数据库:MySQL常见的关系型型数据库。
  4. 服务器端框架:Spring Boot、SpringCloud常用的后端框架,简化开发流程和提高开发效率。
  5. 缓存技术:Redis常用的缓存技术,用于提高系统性能和减轻数据库压力。
  6. 消息队列:RabbitMQ常用的消息队列技术,用于处理异步任务和解耦系统模块之间的通信。
  7. 搜索引擎:Elasticsearch用于全文搜索和数据分析的搜索引擎技术。
  8. 版本控制工具:Git作为代码版本控制工具,用于团管理代码的版本。
  9. 容器化技术:Docker用于将应用打包成容器,实现快速部署和可移植性。
  10. 云服务:根据实际需求选择合适的云服务提供商(如AWS、阿里云、腾讯云等),用于部署和托管商城系统。

3、项目设计

用户模块

系统架构设计:
  • 前端采用React框架,使用Redux进行状态管理。
  • 后端采用Node.js和Express框架,使用MongoDB作为数据库。
  • 使用Nginx作为反向代理服务器,负载均衡和静态资源处理。
  • 使用Redis作为缓存技术,提高系统性能。
  • 使用RabbitMQ作为消息队列,处理异步任务和解耦系统模块之间的通信。
(1)功能模块设计:
  • 用户模块:包括用户注册、登录、个人信息管理等功能。
  • 商品模块:包括商品的添加、修改、删除、查询等功能。
  • 订单模块:包括下单、支付、查看订单状态等功能。
  • 物流模块:用于商家更新订单的物流信息。
  • 评价模块:用户可以对购买的商品进行评价,商家可以回复评价。
  • 优惠券和积分模块:用户可以领取优惠券和积分,用于抵扣购物金额或兑换商品。
  • 促销活动模块:商家可以发布促销活动,如限时折扣、满减等。
  • 客服支持模块:提供在线客服功能,解答用户的问题和处理售后问题。
  • 搜索和推荐模块:用户可以通过关键词搜索商品,系统会根据用户的浏览和购买记录推荐相关商品。
  • 数据统计和分析模块:统计商城的访问量、销售额等数据,为商家提供数据分析和决策支持。
(2)数据库设计:
  • 用户表:存储用户的基本信息,如用户名、密码、邮箱等。
  • 商品表:存储商品的基本信息,如商品名称、价格、库存等。
  • 分类表:存储商品的分类信息,如服装、电子产品等。
  • 品牌表:存储商品的品牌信息,如Nike、Apple等。
  • 订单表:存储订单的基本信息,如订单号、下单时间、支付状态等。
  • 物流表:存储订单的物流信息,如物流公司、运单号等。
  • 评价表:存储用户对商品的评价信息,如评分、评论内容等。
  • 优惠券表:存储优惠券的基本信息,如优惠码、面额等。
  • 积分表:存储用户的积分信息,如积分余额、获取途径等。
(3)接口设计:
  • 用户接口:包括注册接口、登录接口、个人信息接口等。
  • 商品接口:包括添加商品接口、修改商品接口、删除商品接口、查询商品接口等。
  • 订单接口:包括下单接口、支付接口、查看订单接口等。
  • 物流接口:用于商家更新订单的物流信息接口。
  • 评价接口:包括添加评价接口、回复评价接口等。
  • 优惠券和积分接口:包括领取优惠券接口、使用优惠券接口、积分获取接口等。
  • 促销活动接口:包括发布促销活动接口、参与促销活动接口等。
  • 客服支持接口:提供在线客服功能的相关接口。
  • 搜索和推荐接口:包括商品搜索接口、推荐商品接口等。

商家模块

  1. 商家注册与认证流程:明确商家注册的步骤和要求,如填写公司信息、上传证件等。同时,设计一个认证流程,确保商家的身份和资质真实有效。
  2. 商品管理功能:细化商品管理的界面和操作流程,包括添加商品、编辑商品信息、删除商品等功能。可以考虑提供批量操作和导入导出功能,方便商家进行大量商品的管理。
  3. 订单管理功能:细化订单管理的界面和操作流程,包括查看订单详情、接单、发货、退款等功能。可以考虑提供订单筛选和排序功能,方便商家快速找到需要处理的订单。
  4. 物流管理功能:细化物流管理的界面和操作流程,包括查询物流状态、更新物流信息等功能。可以考虑提供物流公司选择和运单号生成功能,方便商家与物流公司进行交互。
  5. 评价管理功能:细化评价管理的界面和操作流程,包括查看买家的评价、回复评价等功能。可以考虑提供评价分类和搜索功能,方便商家查找和管理评价信息。
  6. 数据统计与分析功能:细化数据统计与分析的界面和报表展示,包括销售额统计、订单量统计、用户评价统计等功能。可以考虑提供图表展示和数据导出功能,方便商家进行数据分析和决策。
  7. 营销活动支持功能:细化营销活动的设置和管理流程,包括优惠券、满减活动等。可以考虑提供活动规则设置、活动发布和推广等功能,方便商家进行营销活动的管理和执行。
  8. 客服支持功能:细化客服支持的界面和沟通方式,包括在线聊天工具、电话支持等。可以考虑提供智能客服机器人和问题解答库,提高客服效率和质量。

技术积累

后端

一、gateway路由路径匹配

匹配多个路径

      routes:
        - id: service-user
          uri: lb://service-user
          predicates:
            - Path= /common/**,/admin/**  #匹配多个路径

二、redis缓存

1、缓存更新策略

内存淘汰超时剔除主动更新
说明不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。编写业务逻辑,在修改数据库的同时,更新缓存。
一致性一般
维护成本

业务场景

  • 低一致性需求:使用内存淘汰机制。
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。

三个问题需要考虑:

(1)删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(建议)

(2)如何保证缓存与数据库的操作的同时成功或失败?

  • 单体系统:将缓存与数据库操作放在一个事务
  • 分布式系统:利用TCC等分布式事务方案

(3)先操作缓存还是先操作数据库?

  • 先删除缓存,再操作数据库
  • 先操作数据库,再删除缓存(建议)

本项目使用主动更新 + 删除缓存 + 先操作数据库,再删除缓存

2、缓存三大问题解决方案

(1)、缓存穿透

(2)、缓存击穿

(3)、缓存雪崩

缓存穿透

简单理解:缓存穿透是指客户端请求的数据在缓存中和数据库都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

8f30241f4a924a734157ba22a51bc72f.png

解决方案

(1)缓存空对象

  • 优点:实现简单,维护方便
  • 缺点:
    • 额外的内存消耗
    • 可能造成短期的数据不一致
2ed2ad626e4cf23bb5de7a205d98b513.png
2ed2ad626e4cf23bb5de7a205d98b513.png

(2)布隆过滤器

  • 优点:内存占用较少,没有多余key
  • 缺点:
    • 实现复杂
    • 存在误判可能
3765c356f0617e06ec5e94b65e44dca0.png
3765c356f0617e06ec5e94b65e44dca0.png

本项目采用缓存空对象解决

代码片段

//如果数据中不存在则缓存空对象返回,两分钟后过期
                redisTemplate.opsForValue().set(GoodsConstant.GOODS + userId,"",RedisConstant.CACHE_NULL_EXPIRE, TimeUnit.MINUTES);

其他解决方案:

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流
缓存雪崩

**简单理解:缓存雪崩**是指同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

840e066d60bc1a94ddadea5a3cc1a817.png

解决方案

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

本项目使用给不同的key的TTL添加随机值

缓存击穿

简单理解:缓存击穿问题也叫热点key问题,即使也给被高并发访问并且缓存重建页较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

31e844250335c6063d24e88449660556.png

解决方案

  • 互斥锁
  • 逻辑过期
解决方案优点缺点
互斥锁(1)没有额外的内存消耗。(2)保证一致性。(3)实现简单(1)线程需要等待,性能受影响。(2)可能有死锁风险
逻辑过期线程不需要等待,性能较好。(1)有额外的内存消耗。(2)不保证一致性。(3)实现复杂

互斥锁

61b82942c04d801283e35bb2dee7a490.png

一下是一个简单的使用互斥锁解决的流程

397072438d11e859412b812ed4856a3a.png

获取互斥锁方法

/**
     * 获取锁方法
     * @param key
     */
    private boolean tryLock(String key,Integer userId){
        //获取锁,要对锁设置一个过期时间,防止某些异常导致死锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, userId, RedisConstant.CACHE_LOCK_EXPIRE, TimeUnit.SECONDS);
        //如果为true则说明获取到了锁,若返回false则说明前面已经有线程获取到了锁
        return BooleanUtil.isTrue(flag);
    }

释放互斥锁方法

/**
     * 释放锁
     * @param key
     */
    private void unLock(String key){
        //直接删除锁的键即可
        redisTemplate.delete(key);
    }

核心代码

@Override
    public List<GoodsDto> getList(Integer userId) {
        //先去redis中查询是否有缓存的数据
        List<GoodsDto> GoodsDtoList = (List<GoodsDto>) redisTemplate.opsForValue().get(GoodsConstant.GOODS + userId);
        if(!CollectionUtils.isEmpty(GoodsDtoList)){
            log.info("命中了redis中的商品缓存数据,成功进入缓存---------------");
            return GoodsDtoList;
        }else{
            //未命中redis中的缓存数据
            String lockKey = RedisConstant.GOODS_LOCK + userId;
            //尝试获取锁
            try {
                boolean isLock = this.tryLock(lockKey, userId);
                if(!isLock){
                    //未获取到锁,休眠一段时间等待前一个线程重建缓存
                    Thread.sleep(RedisConstant.THREAD_SLEEP);
                    //重新获取,递归
                    return getList(userId);
                }
                //获取到了锁
                //再去redis中查询是否有缓存的数据
                List<GoodsDto> GoodsList = (List<GoodsDto>) redisTemplate.opsForValue().get(GoodsConstant.GOODS + userId);
                if(!CollectionUtils.isEmpty(GoodsList)){
                    log.info("命中了双重检测redis中的商品缓存数据,成功进入缓存---------------");
                    return GoodsList;
                }
                //根据商户id获取商户所属的店铺
                QueryWrapper<Store> wrapper = new QueryWrapper<>();
                wrapper.eq("user_id",userId);
                List<Store> stores = storeMapper.selectList(wrapper);
                if(CollectionUtils.isEmpty(stores)){
                    //如果数据中不存在则缓存空对象返回来解决redis缓存穿透,两分钟后过期
                    List<UserDto> dtoList = new ArrayList<>();
                    dtoList.add(new UserDto());
                    redisTemplate.opsForValue().set(GoodsConstant.GOODS + userId,dtoList,RedisConstant.CACHE_NULL_EXPIRE, TimeUnit.MINUTES);
                }else{
                    //进行缓存重建
                    List<GoodsDto> goodsDtoList = this.getGoodsDtoList(stores,userId);
                    return goodsDtoList;
                }
            }catch (Exception e){
                throw new ServiceException(ResponseEnum.ERROR);
            }finally {
                //释放锁
                this.unLock(lockKey);
            }
        }
        return null;
    }

逻辑过期

4a8016d4ea953c618d68b72cb2a3fa99.png

三、防止重复提交

使用redis + lua脚本保证原子性操作

lua脚本

if(redis.call('get', KEYS[1]) == ARGV[1]) then
return redis.call('del', KEYS[1])
else return 0 end

核心代码

private static final DefaultRedisScript<Boolean> REDIS_SCRIPT;

    /**
     * 静态代码块中初始化lua脚本
     */
    static {
        REDIS_SCRIPT = new DefaultRedisScript<Boolean>();
        //读取项目中的lua文件
        REDIS_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        REDIS_SCRIPT.setResultType(Boolean.class);
    }

    @Override
    public void saveOrderInfo(OrderDto orderDto,String orderId) {
        //TODO: 需要做防止重复提交和锁定库存操作
        if(StringUtils.isEmpty(orderId)){
            throw new ServiceException(ResponseEnum.ORDER_ID_EXCEPTION);
        }
        //lua脚本保证原子性操作
        //拿着orderId到redis中查询
        //如果redis中有相同orderId,则说明是正常提交订单,把redis中的orderId删除,
        // 这个过程要保证原子性操作,由lua脚本保证。
        Boolean flag = (Boolean) redisTemplate.execute(REDIS_SCRIPT, Arrays.asList(RedisConstant.ORDER_ID + orderId), orderId);
        //如果redis中没有相同orderId,则说明是重复提交订单,不再往下进行
        if(!flag){
            //返回false则说明是重复提交
            throw new ServiceException(ResponseEnum.DO_NOT_REPLACE_SUBMIT);
        } 
    }
}

(四)、分布式锁实现库存锁定,解决商品超卖问题

使用redisson实现分布式锁解决

maven依赖

<dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.11.2</version>
    </dependency>

redisson配置类

@Data
@Configuration
@ConfigurationProperties("spring.redis")
public class RedissonConfig {

    private String host;

    private String addresses;

    private String password;

    private String port;

    private int timeout = 3000;
    private int connectionPoolSize = 64;
    private int connectionMinimumIdleSize=10;
    private int pingConnectionInterval = 60000;
    private static String ADDRESS_PREFIX = "redis://";

    /**
     * 自动装配
     *
     */
    @Bean
    RedissonClient redissonSingle() {
        Config config = new Config();
        //  判断redis 的host是否为空
        if(StringUtils.isEmpty(host)){
            throw new RuntimeException("host is  empty");
        }
        //  配置host,port等参数
        SingleServerConfig serverConfig = config.useSingleServer()
                //redis://127.0.0.1:7181
                .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
                .setTimeout(this.timeout)
                .setPingConnectionInterval(pingConnectionInterval)
                .setConnectionPoolSize(this.connectionPoolSize)
                .setConnectionMinimumIdleSize(this.connectionMinimumIdleSize);
        //  判断进入redis 是否密码
        if(!StringUtils.isEmpty(this.password)) {
            serverConfig.setPassword(this.password);
        }
        // RedissonClient redisson = Redisson.create(config);
        return Redisson.create(config);
    }
}

核心代码

前端

一、后台:由ant design vue开发

(一)、页面过渡效果

在路由文件index.js中引入具体的动画效果,推荐Animate.css | A cross-browser library of CSS animations.open in new window Animate.css动画

安装Animate.css

npm install animate.css --save

引入到路由的index.js文件中

const router = createRouter({
  history,
  routes: [
    {
      //登录页面
      path: "/login",
      name: "Login",
      component: () => import("@/views/login/Login.vue"),
      meta: {
        title: "登录页",
          //这里的这个动画直接去Animate.css中复制即可
        transition: "animate__bounceInRight",
      },
    },
    {
      //后台
      path: "/",
      name: "Layout",
      component: () => import("@/components/layout/Home.vue"),
      redirect: "/index",
      children: [
        {
          //首页
          path: "/index",
          name: "Index",
          component: () => import("@/views/index/Index.vue"),
          meta: {
            title: "首页",
              //这里的这个动画直接去Animate.css中复制即可
            transition: "animate__fadeInLeftBig",
          },
        },
        {
          //首页
          path: "/user",
          name: "User",
          component: () => import("@/views/user/User.vue"),
          meta: {
            title: "用户",
              //这里的这个动画直接去Animate.css中复制即可
            transition: "animate__backInDown",
          },
        },
      ],
    },
    {
      path: "/:pathMatch(.*)*",
      name: "NotFound",
      component: () => import("@/views/404/NotFound.vue"),
    },
  ],
});

在App.vue中修改

<template>
  <router-view #default="{route,Component}">
      <---->一定要加这个前缀animate__animated</---->
    <transition name="fade" :enter-active-class="`animate__animated ${route.meta.transition}`">
      <component :is="Component" />
    </transition>
  </router-view>
</template>

<script setup>
//要引入Animate.css的样式
import 'animate.css'
</script>

(二)、ant desgin vue表格渲染时报错

data 判断为空,且 horizonScroll 判断为 true 时,ResizeObserver 包裹下的 childrenwidth 可能会发生变化,这时候就有可能会触发 ResizeObserver loop completed with undelivered notifications 报错。

所以,如果我们传入非空的值的话,是不是就消除这个报错了呢?我们来实践open in new window 一下。

antd 版本大于等于 4.0 的时候

  1. 当初始化 tabledataSource 赋值空数组时:
react
复制代码<Table columns={[]} dataSource={[]} scroll={{ x: 100 }} />

控制台就会报错:ResizeObserver loop limit exceeded

  1. 当初始化 tabledataSource 赋值 [{}] 时:
react
复制代码<Table columns={[]} dataSource={[{}]} scroll={{ x: 100 }} />

控制台的报错就没了(如果想要更清楚地观察结果,可以在右侧的界面上刷新一下)。

那当 antd 版本小于 4.0 的时候,dataSource 的不同赋值会不会触发这个报错呢?

答案是:不会。

因为 antd 3.x 或以下的版本,没用上 ResizeObserver 这个功能。

二、前台:有Ant design vue开发

(一)、关于如何展示金额小数点问题解决

1、写一个函数,然后使用插值语法调用函数

//展示价格的小数方法
function towNumber(val) {
  return val.toFixed(2);
}

2、比如这个位置展示金额

<span style="font-size: 18px; color: red; font-weight: bold">{{towNumber(item.price)}}</span>

(二)、关于vue3.0 引入 ant-design-vue 报Uncaught TypeError: Cannot read properties of undefined (reading 'value') 的解决办法

其实很简单 就是因为版本的问题

根目录执行下面这个命令就行了

cnpm i --save ant-design-vue@next -S

cnpm 和 npm 都行

(三)、验证手机号正则表达式

//数据校验
var reg_tel =
  /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
function doPhoneLogin() {
  PhoneFormRef.value
    .validate()
    .then(() => {
      //手机号验证
      if (!reg_tel.test(user.value.phone)) {
        message.error("请输入正确的手机号");
      } else {
        message.success("表单验证成功");
        console.log(user.value);
      }
    })
    .catch((error) => {
      console.log("error", error);
    });
}

(四)、验证码倒计时设计

<a-form-item name="code">
                    <a-input-search
                      v-model:value="certification.code"
                      placeholder="请输入验证码"
                      @search="onSearch"
                      style="width: 100%"
                      maxlength="6"
                      minlength="6"
                      size="large"
                      addon-before="验证码"
                    >
                      <template #enterButton>
                        <a-button
                          @click="getCode"
                          style="color: #f56c6c"
                          width="20px"
                          :disabled="disabled"
                        >
                        {{ content }}
                        </a-button>
                      </template>
                    </a-input-search>
                  </a-form-item>
//获取验证码前确认是否是真实手机号
const disabled = ref(false);
const content = ref('获取验证码');
function getCode() {
  Modal.confirm({
    title: "确认此手机号有效?",
    icon: createVNode(ExclamationCircleOutlined),
    content: "确保您所输入的手机号真实有效,否则影响后续操作!",
    okText: "确认",
    cancelText: "取消",
    onOk() {
      var time = 120;
      //禁用获取验证码按钮
      disabled.value = true;
      // 开启定时器
      var timer = setInterval(function () {
        // 判断剩余秒数
        if (time == 0) {
          // 清除定时器和复原按钮
          clearInterval(timer);
          disabled.value = false;
          content.value = "获取验证码";
        } else {
          content.value = `${time}秒后重新获取`;
          time--;
        }
      }, 1000);
    },
    onCancel() {
      message.success("验证码发送失败");
    },
  });
}

(五)、悬浮阴影

<div class="address_style"></div>
.address_style:hover {
  box-shadow: 10px 10px 5px #a0cfff, 10px 10px 5px #fab6b6;
}

.address_style {
  height: 120px;
  background-color: #fff;
  width: 230px;
  opacity: 0.8;
  transition: all 0.5s;
}

(六)、三种路由传参方式

(1)、传字面量

function toDetail(id) {
  router.push({ name: "Detail", query: { id: id } });
}

接收

import { useRoute } from "vue-router";
const route = useRoute();
let id = null;
id = route.query.id;

(2)、传数组

const cartIds = ref([]);
function toSubmitOrder() {
  router.push({ path: "submitorder", query: { id: cartIds.value } });
}

接收

import { useRoute } from "vue-router";
const route = useRoute();
const cartIds = ref([]);
cartIds.value = route.query.id;

(3)、传对象

saveOrder(orderDto.value).then((res) => {
      if (res.code === 200) {
        orderInfoVo.value = res.data;
        message.success("提交订单成功");
        router.push({
          name: "PayFor",
          query: { item: JSON.stringify(orderInfoVo.value) },
        });
      }
    });

接收

import { useRoute } from "vue-router";
const route = useRoute();
const orderInfoVo = JSON.parse(route.query.item);

三、小程序端:由uniapp + uview-plus开发

(一)使用uview-plus模板开发

1、引入uview-plus

在uniapp插件市场搜索uview plus,选择插件后点击右边的“下载插件并导入HBuilderX”

选择创建的项目进行导入即可

2、在项目根目录中的main.js中,引入并使用uview-plus

// main.js
import uviewPlus from '@/uni_modules/uview-plus'
 
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
  const app = createSSRApp(App)
  app.use(uviewPlus)
  return {
    app
  }
}
// #endif

3、在项目根目录的uni.scss中引入uview-plus的全局SCSS主题文件

/* uni.scss */
@import '@/uni_modules/uview-plus/theme.scss';

4、在App.vue的style中引入uview-plus基础样式

<style lang="scss">
    /* 需要给style标签加入lang="scss"属性 */
    @import "@/uni_modules/uview-plus/index.scss";
</style>

5、在使用的页面中使用uview-plus的组件,效果展示

<!-- index.vue -->
<template>
  <view>
    <u-button type="primary" :plain="true" :hairline="true" text="细边"></u-button>
  </view>
</template>

效果展示