自学阉割版奶茶点单uniapp小程序

涉及技术

vue3+sass+typescript+pinia+uniapp+微信小程序基础

编写工具:Vscode、微信小程序开发

项目介绍

这是一个基于瑞幸咖啡点单小程序创建的一个仅用于自己学习uniapp技术的项目。此项目不涉及服务器、网络等知识,数据内容以及数据类型均由自己编写。

此项目共分为三大块(即三个tabbar页面):index页面、order页面、my页面。

项目制作

项目创建与配置基础

项目创建

创配置Vscode

  1. 安装uni-app插件:uni-create-view、uni-helper、uniapp小程序拓展
  2. 安装类型声明文件
  3. manifest.json配置weixin-appid
  4. 运行npm run dev:mp-weixin打包,生成dist文件夹dist/dev/mp-weixin
  5. 打开微信开发小程序,引入dist/dev/mp-weiixn,vscode更改自动更新微信小程序

项目基础配置

  • 安装类型声明文件
    pnpm i -D @types/wechat-miniprogram @uni-helper/uni-app-types
  • 配置tsconfig.json
    {
      "extends": "@vue/tsconfig/tsconfig.json",
      "compilerOptions": {
        "sourceMap": true,
        "ignoreDeprecations": "5.0",
        "baseUrl": ".",
        "paths": {
          "@/*": [
            "./src/*"
          ]
        },
        "lib": [
          "esnext",
          "dom"
        ],
        "types": [
          "@dcloudio/types", // uni-app API 类型
          "miniprogram-api-typings", // 原生微信小程序类型
          "@uni-helper/uni-app-types" // uni-app 组件类型
        ]
      },
      // vue 编译器类型,校验标签类型
      "vueCompilerOptions": {
        "nativeTags": [
          "block",
          "component",
          "template",
          "slot"
        ],
      },
      "include": [
        "src/**/*.ts",
        "src/**/*.d.ts",
        "src/**/*.tsx",
        "src/**/*.vue"
      ]
    }
  • 安装sass和sass-loader
  • 安装uni-ui和配置easycom,在项目中使用uni前缀的标签时,会自动导入uni-ui中对应的标签。 
    //安装uni-ui
    cnpm i @dcloudio/uni-ui
    
    //pages.json
    {
    "easycom": {
    		"autoscan": true,
    		"custom": {
    			// uni-ui 规则如下配置
    			"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
    		}
    	},
    }

项目基本框架

tabbar页面配置

在pages.json中配置所有页面

  • pages中,配置所有页面包括tabbar页面和非tabbar页面,当前需要创建三个tabbar页面(index、order、my)
  • 在src/pages/下创建页面文件夹,在此文件夹下创建对应页面vue文件(src/pages/index/index.vue)(src/pages/orders/orders.vue)...
  • 创建好三个页面并在pages中配置好后,在tabBar中配置所有tabbar页面,tabbar页面数量区间【2-5】。只有配置了tabbar后中才能显示底部导航栏
{
	"pages": [ //pages数组中第一项表示应用启动页
		{
			"path": "pages/index/index",//相对src下页面vue文件位置
			"style": {
				"navigationBarTitleText": "uni-app", //此页面顶部栏标题
				"navigationStyle": "custom" //自定义此页面导航栏(顶部栏样式)
			}
		},
		{
			"path": "pages/my/my",
			"style": {
				"navigationBarTitleText": "my",
				"navigationStyle": "custom"
			}
		},
		{
			"path": "pages/order/order",
			"style": {
				"navigationBarTitleText": "order",
				"navigationStyle": "custom"
			}
		}
	],
	// 设置 TabBar
	"tabBar": {
		"color": "#333",
		"selectedColor": "#27ba9b",
		"backgroundColor": "#fff",
		"borderStyle": "white",
		"list": [
			{
				"text": "首页",
				"pagePath": "pages/index/index",
				"iconPath": "static/tabbarICON/home_default.png",
				"selectedIconPath": "static/tabbarICON/home_selected.png"
			},
			{
				"text": "点单",
				"pagePath": "pages/order/order",
                    //默认状态下此导航栏的图标
				"iconPath": "static/tabbarICON/category_default.png",
                    //点击后的图标
				"selectedIconPath": "static/tabbarICON/category_selected.png"
			},
			{
				"text": "我的",
				"pagePath": "pages/my/my",
				"iconPath": "static/tabbarICON/user_default.png",
				"selectedIconPath": "static/tabbarICON/user_selected.png"
			}
		]
	},
	"globalStyle": { //全局样式
		"navigationBarTextStyle": "black",
		"navigationBarTitleText": "uni-app",
		"navigationBarBackgroundColor": "#F8F8F8",
		"backgroundColor": "#F8F8F8",
		"enablePullDownRefresh": true
	},
	"subPackages": [ //分包配置
		。。。
	],
	"preloadRule": {
		// 配置哪个分页预加载哪些子包
		。。。
	}
}

项目页面制作

index页面

样式 
  • 首先将index页面分为三大块:顶部栏轮播图,中部主要内容区,和中部各个卡片内容区。
  • 拆分组件,减轻单个页面代码量。在index文件夹中创建components文件夹,存放有关index页面的所有组件,(LkSwiper:顶部广告轮播、OperateCard:操作栏、AdSwiper:广告栏、WelfareCard:福利中心、SaleRankCard:好喝榜),其中AdSwiper卡片可共用于其它页面,AdSwiper组件放在src下的components文件夹中,作为全局组件(src/components/AdSwiper.vue)。
  • 中部主要内容区宽度占比为96%,并垂直居中。
逻辑 
  • index作为进入小程序后的最先显示的页面,在生命周期onReady中获取到所有数据后,将所有数据传分配给所有子组件,子组件接收数据后并渲染。而不是每个子组件都请求一次数据。父组件在子组件标签中绑定数据属性,并传递数据,子组件使用defineProps接收数据。
    父组件
    <AdSwiper :list="bannerList" />
    其它父组件
    <AdSwiper :list="otherBannerList" />
    
    子组件
    defineProps<{
    list:BannerItem[] //BannerItem为bannerList中成员的数据类型
    }>()
  • 对于全局组件,如AdSwiper组件,所接收的数据来自不同的父组件,在子组件标签中添绑定的属性需一致,如上list属性,属性值如bannerList则可以是不同的父组件数据。
  • 所有可点击区域(如图标、文字链接)都可用navigator标签包裹,直接将对应跳转页面路径写入navigator标签url属性中。
    <navigator url="/pages/my/my">....</navigator>
    跳转到my页面
  •  到店取/幸运送功能,点击到店取或幸运送,跳转到pages/order/order页面,传入自取或外送信息标识参数,order页面接收标识,显示自取或外送。

order页面

 

基本样式
  • 首先分为三大块:顶部栏、中部内容栏、底部点单详细栏,前两大块分别对应两个组件OroderTopBanner、OrderTabCard。
    • order设置了自定义顶部栏样式,"navigationStyle": "custom",需要设置一个安全距离,避免顶部栏内容渗入到手机顶部任务栏中。
      <view class="viewport"
          :style="{ paddingTop: safeAreaInsets?.top + 'px' }" > .... </view>
      
      //获取安全距离,作为order页面padding-top
      const { safeAreaInsets } = uni.getSystemInfoSync();
    • 自提和外送选择器,没有在uni-ui找到合适的模板,自己写
  • <template>
    ...
    <view class="pick-method" @tap="switchMethod">
            <view :class="isTakeOut ? '' : 'active'">自提</view>
            <view :class="isTakeOut ? 'active' : ''">外送</view>
    </view>
    ...
    </template>
    
    <script setup lang="ts">
    import { ref } from "vue";
    
    //通过isTakeOut决定是否为该view添加acitve类名,拥有active类名背景样式为蓝色
    let isTakeOut = ref(false); //true为自提,false为外送
    const switchMethod = () => {
    //点击pick-method切换
      isTakeOut.value = !isTakeOut.value;
    };
    const changeTakeout = (param?: string) => {
    //根据index页面跳转过来传递的参数标识,切换isTakeOut
      console.log(param);
      if (param === "0") {
        isTakeOut.value = false;
      } else if (param === "1") {
        isTakeOut.value = true;
      }
    };
    //将方法暴露出去,供此子组件的父组件order使用
    defineExpose({
      changeTakeout,
    });
    </script>
    
    <style lang="scss">
    .pick-method {
          width: 170rpx;
          height: 50rpx;
          display: flex;
          background-color: rgba(161, 156, 156, 0.507);
          border-radius: 13px;
          align-items: center;
          margin-right: 40rpx;
          view {
            width: 50%;
            height: 50rpx;
            line-height: 50rpx;
            border-radius: 16px;
            text-align: center;
          }
          .active {
            background-color: blue;
            color: white;
            border-radius: 13px;
          }
        }
    </style>
中部内容区
  • 中部内容分为两大区域:经典菜单和会员卡,通过左右滑动中部或直接点击tab栏可切换经典菜单和会员卡 。siwper具有两个swiper-item项,分别为经典菜单、会员卡。scroll-view和swiper的宽度限制在与屏幕宽度同宽,并且两个swiper-item项宽度继承swiper同宽。
  • swiper添加@change事件(current 改变时会触发 change 事件,event.detail = {current: current, source: source}),并绑定一个current属性,为swiper子项目siwper-item设置唯一标识item-goodsId,当前显示的swiper-item与swiper-item中的item-goodsId值和当前swiper属性current的值是否一致有关
  • .tab-swiper {
        width: 100%;
        height: calc(100vh - 245rpx);
    
        .menu-card {
          background-color: rgba(198, 189, 189, 0.42);
        }
        .vip-card {
          width: 100%;
          background-color: rgb(43, 133, 79);
        }
      }

  • tab-bar中放置两个导航,为两个导航添加点击事件,并传入对应TABID,在点击经典菜单时调用switchTab方法并传入参数0,将参数0赋值给targetIndex。swiper中current属性获取最新的targetIndex为0,则显示item-goodsId为0的swiper-item项。自此,通过点击导航栏项切换swiper-item项 完成。
    <view class="tab-bar">
          <view
            class="tab-item"
            v-for="item in tabs"
            :key="item.TABID"
            :class="item.TABID === targetIndex ? ' active' : ''"
            @tap="switchTab(item.TABID)"
          >
            {{ item.title }}
          </view>
    </view>
    </view>
    <swiper class="tab-swiper" @change="onChange" :current="targetIndex">
        <swiper-item class="menu-card" item-goodsId="0">...</swiper-item>
        <swiper-item class="vip-card" item-goodsId="1">...</swiper-item>
    </scroll-view>
    //
    const tabs = [
      { TABID: 0, title: "经典菜单" },
      { TABID: 1, title: "会员卡" },
    ];
    let targetIndex = ref<number>(0); //tab切换目标组件
    //tab点击事件
    const switchTab = (goodsId: number) => {
      targetIndex.value = goodsId;
      console.log(targetIndex.value);
    };
    //swiper的@change事件,current的值发生改变后,
    const onChange = (e: any) => {
      targetIndex.value = e.detail.current;
      console.log(targetIndex.value);
      console.log(e.detail.current);
    };
经典菜单点单 
  • 在经典菜单中,分为左右两个不一致的可滑动区域。左侧内容为各种不同款的商品归类(导航),每个导航包含多中款的商品,在点击不同导航时,右侧滑动到对应商品种类区域。在滑动右侧商品时,某个种类商品区域到达swiper顶部边界,左侧跟随切换到对应导航(高亮)。
  • 左侧每个导航都有对应的tabId(要求tabId不为纯数字,而应是字符串如‘tab1’),通过tabId判断当前导航是否active高亮显示,右侧scroll-view中,绑定scroll-into-view属性,当scroll-into-view所绑定的值与scroll-view中的某个子项view的id值一致时,则自动滑动到该子项。即点击左侧某个导航时,触发switchMenuTab方法并传入当前导航的tabId,在switchMenuTab方法中,将参数值赋值给targetMenuIndex,从而改变右侧scroll-view中scroll-into-view的值,继而直接自动滑动到右侧scroll-view所有子项view中id与scroll-into-view的值一致的子项。点击左侧导航栏显示右侧对应区域就完成了。
  • <scroll-view scroll-y class="left-tab">
      <view
          class="tab-item-title"
          v-for="item in allMenu"
          :key="item.tabid"
          @tap="switchMenuTab(item.tabid)"
          :class="item.tabid == targetMenuIndex ? 'active' : ''"
          >
         {{ item.categoryList[0] }} //导航名称
      </view>
    </scroll-view>
    <scroll-view
                scroll-y
                class="right-tab"
                :scroll-into-view="targetMenuIndex"
                @scroll="rightScroll"
              >
                <view
                  class="tab-container"
                  v-for="item in allMenu"
                  :key="item.tabid"
                  :id="item.tabid"
                >
             ...
                </view>
    </scroll-view>
    //
    let targetMenuIndex = ref("tab1"); //默认显示右侧第一个子项
    const switchMenuTab = (goodsId: any) => {
      targetMenuIndex.value = goodsId;
      console.log(targetMenuIndex.value, goodsId);
    };
  •  滑动右侧,左侧对应导航也同步高亮,计算右侧每个子项目view盒子顶部距离父盒子scroll-view,将所有处理后的距离存储到toTopDistance数组中。同时右侧scroll-view中添加@scroll事件,当用户滚动右侧scroll-view时,则触发rightScroll方法。
  • let toTopDistance: any = [];
    const getRightTabInfor = () => {
      const instance = getCurrentInstance();
      const query = uni.createSelectorQuery().in(instance);
      // 获取右侧所有格子的信息
      query
        .selectAll(".tab-container")
        .boundingClientRect((data) => {
          const formatData = JSON.parse(JSON.stringify(data));
          for (let i = 0; i < formatData.length; i++) {
            console.log(formatData[i].top ) //获取的是每个子项顶部距离屏幕顶部的距离(px)
            // 获取所有子盒子距离父盒子顶部距离时
            let temp = (formatData[i].top - 260).toFixed(0); //需要减去大概整个顶部栏高度
            toTopDistance.push(temp); //将所有子项目的距离存储到toTopDistance数组中
          }
        })
        .exec();
    };
    const rightScroll = (e: any) => {
    //scroll事件可以获取当前一些信息
      let SCROLL_TOP = e.detail.scrollTop; //当前滚动的距离(从scroll-view框架顶部到内容超出框架顶部的距离)
    //当前超出框架顶部的距离大于等于0并且小于第二个盒子顶部距离框架顶部的距离时,左侧导航显示的是tab1导航
      if (0 <= SCROLL_TOP && SCROLL_TOP < toTopDistance[1]) {
        targetMenuIndex.value = `tab1`;
      } else if (toTopDistance[1] <= SCROLL_TOP && SCROLL_TOP < toTopDistance[2]) {
        targetMenuIndex.value = `tab2`;
      } else if (toTopDistance[2] <= SCROLL_TOP && SCROLL_TOP < toTopDistance[3]) {
        targetMenuIndex.value = `tab3`;
      } else if (toTopDistance[3] <= SCROLL_TOP && SCROLL_TOP < toTopDistance[4]) {
        targetMenuIndex.value = `tab4`;
      }
    };
    onMounted(() => {
      getRightTabInfor();
    });

自提外送选择器逻辑
  • 在OrderTopBanner子组件中的自提外送选择器,通过isTaKeOut的value布尔值来决定谁高亮蓝色背景。点击选择框调用switchMehtod方法,改变isTakeOut值来切换选项。
  • 同时定义changeTakeOut方法,到店取跳转到order页面传入参数为1,幸运送传入参数为2,此处并不是真正意义上跳转传参,而是在点击到店取/幸运送时,执行跳转并将参数保存到storage中,跳转至order页面后,在onShow中获取当前storage中的参数值,并立即清理该storage。
  • 获取到参数后,将参数传给子组件OrderTopBanner暴露的changeTakeOut方法,此方法获取参数判断是自取还是外送,从而改变isTakeOut的value值,继而改变选项框的高亮背景选项。
  • 为幸运送和到店取添加tap事件
    uni.setStorageSync("order-key", '1'); //保存不同的参数到storage中
    uni.setStorageSync("order-key", '0'); 
    uni.switchTab({ //跳转到order页面
        url: /pages/order/order,
      });
    
    
    //OrderTopBanner
    ...
    const changeTakeout = (param?: string) => {
      console.log(param);
    //参数为0,说明是从幸运送跳转来的
      if (param === "0") {
        isTakeOut.value = false;
      } else if (param === "1") {
        isTakeOut.value = true;
      }
    };
    defineExpose({ //将changeTakeout方法暴露给父组件,因为参数只能从父组件获取
      changeTakeout,
    });
    ...
    
    // order/order
    获取子组件
    <OrderTopBanner ref="RefChild" />
    
    const RefChild = ref(); //获取子组件实例对象
    const callChildFn = () => { //创建callChildFn方法调用子组件方法
      let orderKey = uni.getStorageSync("order-key"); //从storage中获取参数
      uni.removeStorageSync("order-key"); //获取后就移除该参数
      RefChild.value.changeTakeout(orderKey); //调用子组件的chagneTakeout方法,并传入参数
    };
    
    onShow(()=>{
        callChildFn() //在跳转到order页面后立即执行此方法
    })

订单栏
  • 订单栏分为有订单情况和没订单情况(isCollapsed为true),另一部分是弹出框(不使用自带弹出框)部分和弹出框遮罩层部分,当存在订单时,点击order-card会弹出弹出框,显示所有加入购物车的订单。
  • 在有订单情况下点击触发onTapUncollpasedBanner,弹出弹出框,显示遮罩层。通过动态style控制display的值。遮罩层就是低于弹出层层级一级的半透明灰黑背景,点击背景后关闭遮罩层和弹出层。
  • const popup = ref();
    const popupOverlay = ref();
    let isPopupOverlay: boolean = false;
    let isShowPopup: boolean = false;
    let customPopupStyle = ref({
      display: "none",
      transform: "translateY(400rpx)",
    });
    let customPopupOverlayStyle = ref({
      display: "none",
    });
    const onTapUncollpasedBanner = () => {
      // 有订单,默认展开显示 订单,点击打开popup展示所有订单
      isShowPopup = !isShowPopup;
      isPopupOverlay = !isPopupOverlay;
      if (isShowPopup === false) {
        customPopupStyle.value.display = "none";
        customPopupOverlayStyle.value.display = "none";
      } else if (isShowPopup === true) {
        customPopupStyle.value.display = "flex";
        customPopupOverlayStyle.value.display = "block";
      }
    };

添加订单(加入购物车)
 
  • 新建商品详情分包src/pagesOrder,分包下创建两个页面GoodsDetail页面和Purchase页面,在pages.json中配置分包,在进入order页面时加载分包pagesOrder。
  • "subPackages": [
    {
    			// 进入order页后预加载一下分包
    			"root": "pagesOrder",
    			"pages": [
    				{
    					"path": "Purchase/Purchase",
    					"style": {
    						"navigationBarTitleText": "Purchase"
    					}
    				},
    				{
    					"path": "GoodsDetail/GoodsDetail",
    					"style": {
    						"navigationBarTitleText": "GoodsDetail",
    						"navigationStyle": "custom"
    					}
    				}
    			]
    		},
    {
    ....其它分包
    }
    ],
    "preloadRule": {
    		// 配置哪个页面预加载哪些分包
    		"pages/order/order": {
    			"network": "all",
    			"packages": [
    				"pagesOrder"
    			]
    		},
    {
    ...
    }
    	}
  • 点击order页右侧商品,跳转到商品详情页面GoodsDetail并传入此商品编号,GoodsDetail页面接收商品编号,获取此商品数据。选择商品类型和数量,将此商品所有类型及数量的商品存放在currGoodsOrder数组中。
  • 点击+图标,新增一个此商品到currGoodsOrder中(currGoodsOrder.push()),点击➖图标减去最新添加的商品(currGoodsOrder.pop())。
  • 点击加入购物车,将所有商品currGoodsOrder存储到本地存储中,order获取后清除。
  • //GoodsDetail页面
    const onAddShoppingCar = () => {
    ...
      // 添加入购物车
      // 跳转到order界面,并将当前订单商品传递给order
      uni.setStorageSync(
        "banner-orders",
        JSON.parse(JSON.stringify(currGoodsOrder))
      );
      uni.switchTab({
        url: "/pages/order/order",
        success: (success) => {
          uni.showToast({
            title: "添加成功",
            position: "center",
          });
        },
      });
    ...
    };
    //order页面接收加入购物车的商品数据
    // pushBannerOrders:获取新增购物车订单
    const pushBannerOrders = () => {
      // 从参数中获取新的order,并新增到bannerOrders中
      let order: OrderItem = Array.from(uni.getStorageSync("banner-orders"));
      removeDuplicateOrder(order);
      // 获取后清除这个order
      uni.removeStorageSync("banner-orders");
    };
    
    onShow(()=>{
        ...
        pushBannerOrdes();
        ...
    })

  • order页获取加入购物车数据后进行处理,(一次加入购物车的currGoodsOrder数据都是同一个商品,只是区分商品不同类型),对新数据进行去重等处理,调用removeDuplicateOrder方法。ordre页面有一个自己的bannerOrders数组,存放所有订单。第一次加入一种商品时,bannerOders为空,按currGoodsOrder商品类型划分,同一种类型的商品只显示一次,不同类型和不同商品都独占一行。重复的商品并且重复的类型都只显示一次。
  • const removeDuplicateOrder = (ORDER: OrderItem[]) => {
      // 1.处理ORDER
      // 创建新数组接收去重后的ORDER
      let newOrders: OrderItem[] = [];
      // 在ORDER中,所有订单出了goodsCustom和goodsCustomFormat存在不一致外,无法通过深度判断goodsCustom(对象)去重,所以使用goodsCustomFormat是否存在相同的
      // 使用map存储判断后的数据
      let map = new Map();
      for (let item of ORDER) {
        if (!map.has(item.goodsCustomFormat)) {
          map.set(item.goodsCustomFormat, item);
        }
      }
      newOrders = [...map.values()];
      // 判断newORders中每个成员在ORDER中的数量并创建newOrdersNum变量接收
      let newOrdersNum: number[] = [];
      newOrders.forEach((item) => {
        let i = 0;
        ORDER.forEach((ITEM) => {
          if (ITEM.goodsCustomFormat === item.goodsCustomFormat) {
            i++;
          }
        });
        newOrdersNum.push(i);
      });
      // 将newOrdersNum数组中的数据按顺序赋值给每个newOrders中的成员的goodsNum属性
      for (let i in newOrders) {
        newOrders[i].goodsNum = newOrdersNum[i];
      }
    
      // newOrders中一定是同goodsId的商品,区分在同goodId是否有不同goodsCustomFormat,而newOrders是已经去重的数组,每个成员都是同goodsId不同goodsCustomFormat。
      // 在为bannerOrders注入数据时,判断如下:
      // 1.判断当前bannerOrders中是否存在当前需要注入的数据newOrders的商品种类(judge,判断bannerOrders中是否存在与newOrders中的goodsId一致的商品)
      // 1.1 newOrders中的商品在bannerOrders中存在,判断bannerOrders中该数据的goodsCustomFormat是否与当前newOrders中的某个成员的goodsCustomFormat一致
      // 1.1.1 item.goodsCustomFormat == ITEM.goodsCustomFormat即存在与newOrders中某个成员同商品同商品类型的数据,则增加bannerOrders中的goodsNum
      // 1.1.2 在bannerOrders中找不到同类型goodsCustomFormat的商品,则将newOrders中的该数据push到bannerOrders中
      // 1.2 newOrders中的商品在bannerOrders中不存在,即newOrders中的goodsId在bannerOrders中找不到,则直接将整个newOrders拼接到bannerOrders中(完成第一次添
      // 加购物车后,第二次不同的商品添加入购物车,直接将第二次的所有商品拼接到当前的bannerOrders中)
      let judge = bannerOrders.value.findIndex(
        (item: OrderItem) => item.goodsId === newOrders[0].goodsId
      );
      if (judge !== -1) {
        //1.1
        bannerOrders.value.forEach((item) => {
          newOrders.forEach((ITEM) => {
            // 1.1
            if (item.goodsCustomFormat == ITEM.goodsCustomFormat) {
              // 1.1.1
              item.goodsNum += ITEM.goodsNum;
            } else if (
              bannerOrders.value.findIndex(
                (item3) => item3.goodsCustomFormat === ITEM.goodsCustomFormat
              ) === -1
            ) {
              // 1.1.2
              bannerOrders.value.push(ITEM);
            }
          });
        });
      } else {
        // 1.2
        bannerOrders.value = bannerOrders.value.concat(newOrders);
      }
    };

my页面

  • my页面与index页面大致相同
个人信息修改页 

  • 个人信息页为属于my页面的分包下的一个页面myInfo。
  • 在myInfo页面中展示所有用户信息,并且可修改其中信息,修改后发送请求以更新数据。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值