从零开始,搭建一个简单的购物平台(十九)前端商城部分

本文涉及的产品
文本翻译,文本翻译 100万字符
图片翻译,图片翻译 100张
语种识别,语种识别 100万字符
简介: 从零开始,搭建一个简单的购物平台(十九)前端商城部分

从零开始,搭建一个简单的购物平台(十八)前端商城部分:

https://blog.csdn.net/time_____/article/details/108918489

项目源码(持续更新):https://gitee.com/DieHunter/myCode/tree/master/shopping


上篇文章后,前端商城部分基本功能已实现,包括商品列表,商品分类,首页商品展示,商品详情,购物车,用户登录注册,剩余内容:用户信息修改,提交订单,订单展示等,这篇文章将对剩余功能部分完结。


用户信息修改的后端接口已经在管理平台实现,这里直接进行验证调用即可


之前的修改用户信息功能在测试中体现出来了一个bug,因为生成Token的字段是用户名,当修改用户信息时,如果修改了用户名,就会导致token验证失败,于是我们需要修复token生成方式,将之前的用户名生成改成_id生成,新版代码已经提交至码云


修复后效果:


image.png


下面介绍一下实现流程 ,这里我们把info用户信息界面和登录界面放在单页面中,通过v-if条件渲染,条件是checkToken是否通过


bussiness.js,验证token是否有效


import Vue from "vue";
import config from "../../config/config";
const { ServerApi, StorageName } = config;
export default class UserInfoBussiness extends Vue {
  constructor(_vueComponent) {
    super();
    this.vueComponent = _vueComponent;
  }
  checkToken() {//验证Token函数,若token正确,则直接登录成功,若未成功,则切换至登录界面
    let token = this.$storage.getStorage(StorageName.Token);
    if (!token || !token.length) return;
    this.$axios
      .get(ServerApi.token, {
        params: {
          token
        }
      })
      .then(res => {
        switch (res.result) {
          case -999://token请求抛发错误,token过期或错误
            this.vueComponent.isLogin = false;//显示登录页面
            this.$storage.clearStorage(StorageName.Token);//清除之前的token
            break;
          case 1://验证token成功
            this.vueComponent.userInfo = res.data;
            this.vueComponent.isLogin = true;//显示个人信息页面
            break;
          default:
            this.vueComponent.isLogin = false;
            this.$storage.clearStorage(StorageName.Token);
            break;
        }
      })
      .catch(err => {});
  }
}

info.vue组件


<template>
  <div>
    <Top :title="isLogin?'我的':'登录'"></Top>
    <div class="content">
      <UserInfo v-if="isLogin" :userInfo="userInfo"></UserInfo>
      <Login v-else></Login>
    </div>
    <TabBar></TabBar>
  </div>
</template>
<script>
import UserInfoBussiness from "./bussiness";
import TabBar from "../../components/tabBar/tabBar";
import UserInfo from "../../components/userInfo/userInfo";
import Login from "../../components/login/login";
import Top from "../../components/top/top";
import config from "../../config/config";
const { EventName } = config;
export default {
  components: {
    Top,
    UserInfo,
    Login,
    TabBar
  },
  data() {
    return {
      isLogin: false,
      userInfoBussiness: null,
      userInfo: null
    };
  },
  created() {
    this.userInfoBussiness = new UserInfoBussiness(this);
    this.$events.onEvent(EventName.IsLogin, () => {
      this.userInfoBussiness.checkToken();//退出登录响应事件,重重页面
    });
    this.userInfoBussiness.checkToken();//初始化先验证token
  },
  destroyed() {
    this.$events.offEvent(EventName.IsLogin);
  }
};
</script>
<style lang="less" scoped>
@import "../../style/init.less";
</style>

在用户登录成功后,我们需要一个组件显示用户信息,这个没有任何逻辑,纯渲染,所以暂不做介绍


<template>
  <ul class="userInfo">
    <router-link to="/UpdateInfo">
      <li>
        <img :src="imgPath+userInfo.headPic" alt />
        <span>{{userInfo.username}}</span>
        <div class="iconfont icon-fanhui"></div>
      </li>
    </router-link>
    <li>
      <mt-cell :title="userInfo.phoneNum"></mt-cell>
      <mt-cell :title="userInfo.mailaddress+userInfo.mailurl"></mt-cell>
      <mt-cell :title="userInfo.alladdress.join('-')+'-'+userInfo.address"></mt-cell>
      <mt-cell :title="userInfo.descript"></mt-cell>
    </li>
  </ul>
</template>
<script>
import Config from "../../config/config";
const { RequestPath, StorageName } = Config;
import { Cell } from "mint-ui";
export default {
  name: "userinfotop",
  props: ["userInfo"],//父组件传递用户信息至当前组件,并渲染
  data() {
    return {
      imgPath: RequestPath
    };
  },
  created() {
    this.$storage.saveStorage(StorageName.UserInfo, this.userInfo);
  }
};
</script>
<style lang="less" scoped>
@import "../../style/init.less";
.userInfo {
  li:nth-child(1) {
    .h(230);
    width: 100%;
    .mcolor();
    .l_h(230);
    margin-top: -1px;
    color: #fff;
    > img,
    > span {
      display: inline-block;
      vertical-align: middle;
      margin-left: unit(40 / @pxtorem, rem);
    }
    > img {
      .w(145);
      .h(145);
      border-radius: 100%;
    }
    > span {
      .f_s(40);
    }
    > div {
      height: 100%;
      float: right;
      padding-left: unit(40 / @pxtorem, rem);
      transform: rotateY(180deg);
    }
  }
}
</style>

通过点击头像框路由跳转至UpdateInfo,用户信息修改页,我们将头像上传单独写成组件

image.png



这里有一个原生js上传文件的坑:

axios上传post文件头文件需模拟 "multipart/form-data"请求,而这种请求格式与application/x-www-form-urlencoded有所不同,需要声明一个分隔符‘boundary’。


headers: {
          "Content-Type": "multipart/form-data;boundary=ABC"//ABC内容自行填写
  },

那么这时,坑人的地方来了,直接以ABC这种简单的分隔符内容上传文件可能会导致服务端对文件不识别,无法找到文件起始位置,所以我们需要一个复杂的字符,比如使用new Date().getTime()生成随机字符,修改后就有以下配置


headers: {
          "Content-Type": "multipart/form-data;boundary=" + new Date().getTime()
        },

上传头像组件中,我们要自己写一个控件替代官方的input元素,也就是点击图片使用JS执行input文件上传事件,并提交到服务端,服务端存好缓存后将图片文件地址发送到前端,前端读取文件并展示,以下是头像上传的所有过程

uploadPic.vue


<template>
  <div class="uploadPic">
    <img :src="picPath" @click="clickHandler" alt />
    <input class="picFile" id="picFile" type="file" @change="uploadPic" accept="image/*" />
  </div>
</template>
<script>
import Config from "../../config/config";
import UploadBussiness from "./bussiness";
const { StorageName, RequestPath, UploadKey } = Config;
export default {
  name: "uploadPic",
  props: ["picFile"],
  data() {
    return {
      imgPath: RequestPath,
      picPath: ""
    };
  },
  created() {
    this.picPath = this.imgPath + this.picFile;
    this._uploadBussiness = new UploadBussiness(this);
  },
  methods: {
    clickHandler() {//点击头像模拟至点击文件上传input-file标签
      document.querySelector("#picFile").click();
    },
    uploadPic(e) {
      let _picFile = new FormData();//新建FormData文件
      _picFile.append("token", this.$storage.getStorage(StorageName.Token));//将token添加至文件属性中
      _picFile.append(UploadKey.headKey, e.target.files[0]);//文件校验字段
      this._uploadBussiness.uploadPic(_picFile);//上传文件
    }
  }
};
</script>
<style lang="less" scoped>
@import "../../style/init.less";
.uploadPic {
  img {
    width: 100%;
    height: 100%;
  }
  .picFile {
    display: none;
  }
}
</style>
bussiness.js
import Vue from 'vue'
import config from "../../config/config"
import {
  Toast
} from "mint-ui";
const {
  UploadName,
  EventName,
  UploadKey
} = config
export default class UploadBussiness extends Vue {
  constructor(_vueComponent) {
    super()
    this.vueComponent = _vueComponent
  }
  uploadPic(data) {
    this.$axios
      .post(UploadName.headPic, data, {
        headers: {
          "Content-Type": "multipart/form-data;boundary=" + new Date().getTime()//axios上传post文件头文件需模拟 "multipart/form-data"请求,而这种请求格式与application/x-www-form-urlencoded有所不同,需要声明一个分隔符‘boundary’。
        },
      }).then(res => {
        Toast(res.msg);
        switch (res.result) {
          case 1://上传成功后显示图片
            let fileRead = new FileReader();//新建文件读取实例
            fileRead.readAsDataURL(data.get(UploadKey.headKey));//readAsDataURL读取本地图片信息
            fileRead.onload = () => {
              this.vueComponent.picPath = fileRead.result
            }
            this.$events.emitEvent(EventName.UploadPic, res.headPath)
            break;
          default:
            break;
        }
      })
  }
}


说完了上传头像组件后,来实现一下修改用户信息,之前上传的头像地址会通过组件传参传递到父组件中,伴随着其他信息一起提交到服务端,服务端将收到的头像缓存地址解析成文件并保存,修改用户信息组件中可以复用一个省市县选择器组件,即之前在商品详情中使用的商品数量选择,其他的表单元素都是基本的文本类型


updataForm.vue
<template>
  <div class="update">
    <!-- <img :src="imgPath+userInfo.headPic" alt /> -->
    <UploadPic class="uploadPic" :picFile="userInfo.headPic"></UploadPic>
    <mt-field
      placeholder="请输入用户名"
      :state="userInfo.username.length?'success':'error'"
      v-model="userInfo.username"
    ></mt-field>
    <mt-field
      placeholder="请输入手机号"
      :state="userInfo.phoneNum.length?'success':'error'"
      v-model="userInfo.phoneNum"
      type="number"
    ></mt-field>
    <mt-radio v-model="userInfo.sex" :options="sexOption"></mt-radio>
    <mt-button class="btn" @click="selectAddress">{{userInfo.alladdress.join('-')}}</mt-button>
    <mt-field
      placeholder="请输入详细地址"
      :state="userInfo.address.length?'success':'error'"
      v-model="userInfo.address"
    ></mt-field>
    <mt-field
      placeholder="请输入个性签名"
      :state="userInfo.descript.length?'success':'error'"
      v-model="userInfo.descript"
    ></mt-field>
    <mt-button class="submit" type="primary" @click="submit">修改信息</mt-button>
    <div class="shopPicker">
      <mt-popup v-model="popupVisible" position="bottom">
        <mt-picker
          :slots="myAddressSlots"
          value-key="name"
          :visibleItemCount="7"
          @change="changeAddress"
        ></mt-picker>
      </mt-popup>
    </div>
  </div>
</template>
<script>
import UpdateBussiness from "./bussiness";
import Config from "../../config/config";
import { Field, Button, Picker, Popup, Radio } from "mint-ui";
import address from "../../config/city";
import UploadPic from "../uploadPic/uploadPic";
const { StorageName, RequestPath, EventName } = Config;
export default {
  name: "updateForm",
  data() {
    return {
      imgPath: RequestPath,
      updateBussiness: null,
      popupVisible: false,//控制picker显示
      selectArea: null,
      sexOption: [//性别配置
        {
          label: "男",
          value: "man"
        },
        {
          label: "女",
          value: "woman"
        }
      ],
      myAddressSlots: [//省市县联动配置
        {
          flex: 1,
          defaultIndex: 0,
          values: [],
          className: "slot1",
          textAlign: "center"
        },
        {
          divider: true,
          content: "-",
          className: "slot2"
        },
        {
          flex: 1,
          values: [],
          className: "slot3",
          textAlign: "center"
        },
        {
          divider: true,
          content: "-",
          className: "slot4"
        },
        {
          flex: 1,
          values: [],
          className: "slot5",
          textAlign: "center"
        }
      ],
      userInfo: this.$storage.getStorage(StorageName.UserInfo)//获取缓存的用户信息,用于显示默认项
    };
  },
  components: {
    UploadPic
  },
  created() {
    this.$events.onEvent(EventName.UploadPic, headPic => {//上传头像后将新地址保存至当前组件
      this.userInfo.headPic = headPic;
    });
    this.updateBussiness = new UpdateBussiness(this);
  },
  destroyed() {
    this.$events.offEvent(EventName.UploadPic);
  },
  methods: {
    selectAddress() {//显示picker
      this.myAddressSlots[0].values = address;
      this.popupVisible = true;
    },
    changeAddress(picker, values) {//三级联动
      if (values[0]) {
        this.userInfo.alladdress = [values[0].name];
        picker.setSlotValues(1, values[0].children);
        if (values[1]) {
          this.userInfo.alladdress.push(values[1].name);
          picker.setSlotValues(2, values[1].children);
          if (values[2]) {
            this.userInfo.alladdress.push(values[2].name);
          }
        }
      }
    },
    submit() {
      this.updateBussiness.submitData();//提交信息
    }
  }
};
</script>
<style lang="less" scoped>
@import "../../style/init.less";
.update {
  .uploadPic {
    overflow: hidden;
    .w(200);
    .h(200);
    .mg(unit(30 / @pxtorem, rem) auto);
    border-radius: 100%;
  }
  .btn {
    width: 100%;
    .h(100);
    background: #fff;
  }
  .submit {
    margin-top: unit(30 / @pxtorem, rem);
    width: 100%;
    // z-index: 100;
  }
}
</style>
bussiness.js
import Vue from 'vue'
import config from "../../config/config"
import {
  Toast
} from "mint-ui";
const {
  ServerApi,
  StorageName,
  EventName
} = config
export default class UpdateBussiness extends Vue {
  constructor(_vueComponent) {
    super()
    this.vueComponent = _vueComponent
  }
  submitData() {
    for (const key in this.vueComponent.userInfo) {//表单非空判断
      let value = this.vueComponent.userInfo[key]
      if (!value.length && value != true && value != 0 && typeof value == 'string') {
        Toast('请填写完整的信息');
        return
      }
    }
    this.$axios
      .post(ServerApi.user.updateUser, {
        crypto: this.$crypto.setCrypto({
          token: this.$storage.getStorage(StorageName.Token),
          ...this.vueComponent.userInfo
        })
      }).then(res => {
        switch (res.result) {
          case 1:
            Toast(res.msg);
            history.go(-1)
            break;
          default:
            break;
        }
      })
  }
}


用户信息修改就介绍到这里,下一步将对项目的最后一步订单的前端部分进行分享


订单的后端逻辑与接口在管理系统中已经介绍完毕,前端部分就是很简单的数据渲染和状态修改



首先,订单是基于用户和商品绑定的,所以,我们在购物车中实现新增订单功能,添加成功后跳转至订单查询界面,除此之外,在用户信息界面,添加用户的所有订单列表可以查看和付款(由于只是一个项目案例,所以这里没有实现支付功能)


orderList.vue组件,几乎都是页面渲染,没有什么逻辑功能,就不做说明


<template>
  <div class="content">
    <div class="orderTop">
      <div>
        <div>
          <p class="fontcl">
            下单时间:
            <span>{{new Date(orderList.orderTime).toLocaleString()}}</span>
          </p>
          <p class="fontcl">
            订单编号:
            <span>{{orderList.orderId}}</span>
          </p>
        </div>
        <div
          :class="orderList.orderState==0?'noPay':orderList.orderState==4?'isFinish':'isPay'"
        >{{orderState[orderList.orderState||0].name}}</div>
      </div>
      <div>
        <div>
          <span class="icon-yonghuming iconfont">{{orderList.username}}</span>
          <span class="icon-shoujihao iconfont">{{orderList.phoneNum}}</span>
        </div>
        <div class="fontcl">{{orderList.address}}</div>
      </div>
    </div>
    <ul class="orderList">
      <li v-for="(item,index) in orderList.shopList" :key="index">
        <img :src="imgPath+item.shopPic" alt />
        <div>
          {{item.shopName+item.shopScale}}
          <br />
          ¥{{item.shopPrice}}
        </div>
        <span>×{{item.shopCount}}</span>
      </li>
    </ul>
    <div class="submitOrder">
      <span>付款合计:¥{{orderList.orderPrice}}</span>
      <span @click="submitOrder" v-show="orderList.orderState==0">去付款</span>
    </div>
  </div>
</template>
<script>
import OrderBussiness from "./bussiness";
import Config from "../../config/config";
import ShopType from "../../config/shopType";
export default {
  name: "orderList",
  data() {
    return {
      orderState: ShopType.orderState,
      imgPath: Config.RequestPath,
      orderList: [],//订单详情
      orderBussiness: null,
    };
  },
  created() {
    this.orderBussiness = new OrderBussiness(this);
    this.orderBussiness.getOrderList();
  },
  methods: {
    submitOrder() {
      this.orderBussiness.sendOrderPay(this.orderList);//支付
    },
  },
};
</script>
<style lang="less" scoped>
@import "../../style/init.less";
.content {
  font-size: unit(32 / @pxtorem, rem);
  .fontcl {
    .cl(#979797);
  }
  .orderTop {
    > div {
      padding-left: unit(35 / @pxtorem, rem);
      padding-right: unit(35 / @pxtorem, rem);
    }
    > div:nth-child(1) {
      .h(160);
      border-bottom: unit(3 / @pxtorem, rem) solid #e8e8e8;
      > div:nth-child(1) {
        float: left;
        p {
          .l_h(80);
          span {
            .cl(#000);
          }
        }
      }
      > div:nth-child(2) {
        float: right;
        .h(160);
        .l_h(160);
      }
      .isFinish {
        .cl(@mainColor);
      }
      .isPay {
        .cl(#000);
      }
      .noPay {
        .cl(#A71A2D);
      }
    }
    > div:nth-child(2) {
      .h(180);
      border-bottom: unit(30 / @pxtorem, rem) solid #f3f3f3;
      > div:nth-child(1) {
        overflow: hidden;
        .l_h(100);
        span:nth-child(1) {
          float: left;
        }
        span:nth-child(2) {
          float: right;
        }
      }
      > div:nth-child(2) {
        width: 100%;
      }
    }
  }
  .orderList {
    li {
      .h(250);
      padding-left: unit(20 / @pxtorem, rem);
      padding-right: unit(35 / @pxtorem, rem);
      > div,
      > span,
      img {
        display: inline-block;
        vertical-align: middle;
      }
      img {
        .w(220);
        .h(220);
        margin-right: unit(30 / @pxtorem, rem);
      }
      > div {
        .l_h(60);
      }
      > span {
        vertical-align: top;
        margin-top: unit(50 / @pxtorem, rem);
        float: right;
      }
    }
  }
  .submitOrder {
    .h(130);
    width: 100%;
    position: fixed;
    bottom: 0;
    background: #fff;
    border-top: unit(3 / @pxtorem, rem) solid #cdcdcd;
    span:nth-child(1) {
      float: left;
      .pd(unit(40 / @pxtorem, rem));
      .cl(#852332);
    }
    span:nth-child(2) {
      .mcolor();
      .pd(unit(45 / @pxtorem, rem) unit(110 / @pxtorem, rem));
      float: right;
      .cl(#fff);
    }
  }
}
</style>


获取订单列表和提交订单支付状态的bussiness.js


import Vue from "vue";
import { MessageBox } from "mint-ui";
import config from "../../config/config";
import Clone from "../../utils/clone";
const { ServerApi, StorageName, EventName, DefaultPageConfig } = config;
export default class OrderBussiness extends Vue {
  constructor(_vueComponent) {
    super();
    this.vueComponent = _vueComponent;
    this._defaultPageConfig = Clone.shallowClone(DefaultPageConfig);
  }
  getOrderList() {//获取个人订单信息列表
    this._defaultPageConfig.token = this.$storage.getStorage(StorageName.Token);
    this._defaultPageConfig.orderId = this.vueComponent.$route.query.orderId;
    this.$axios
      .get(ServerApi.order.orderList, {
        params: {
          crypto: this.$crypto.setCrypto(this._defaultPageConfig)
        }
      })
      .then(res => {
        switch (res.result) {
          case 1:
            this.vueComponent.orderList = res.data.list[0];
            break;
          default:
            break;
        }
      });
  }
  sendOrderPay(data) {
    MessageBox("提示", "本案例仅为参考,未开通支付功能");
    data.orderState = 1;//修改订单状态为已支付
    data.token = this.$storage.getStorage(StorageName.Token);
    this.$axios
      .post(ServerApi.order.updateOrder, {
        crypto: this.$crypto.setCrypto(data)
      })
      .then(res => {
        switch (res.result) {
          case 1:
            break;
          default:
            break;
        }
      });
  }
}

订单功能完成


项目整体打包


通过运行   npm run build   进行webpack打包


生产环境部署可以参照我之前的一篇文章


如果需要配置https环境可以参照这篇文章


文件夹的命名规则以及模块组件的分配在这篇文章有说到


希望这个系列的文章对你有帮助,如果你阅读完了整个系列或者某篇文章,非常感谢你的支持

总结:到这篇博客为止,《从零开始,搭建一个简单的购物平台》系列的文章全部完结,以下是本人完成整个项目的一个小总结以及一些注意点:


搭建环境及配置文件:对自己的技术栈以及优势需要深入了解,并且选择最适合自己或者是产品需求所需要的技术,完成项目目录的搭建,比如前端最好养成模块化,组件化开发的习惯,尽量将文件夹以及文件细分到每个基本组件。

以组件和框架的官方文档为核心,学会自己上网查找问题,自己动手解决问题非常有必要。

学会造轮子,虽然网上有大量的框架,组件,别人写好的js库,但是自己动手写函数,封装功能以及组件是非常有必要的,并不是节省时间或者其他方面的原因,自己写能提升自己编程思路和实际应用能力,而且当自己写出了一个比较成功的类或者组件,甚至方法时,会有很大的成就感

面向对象编程语言,减少代码耦合度,提高内聚性,使代码健壮性更加强大,这点我自己正在努力改善,这样写代码有利于把很多方法剥离,可以提升复用性,减少代码量,说白了,一个项目别人可能只需要3000行代码,而我可能需要5000行

这个项目我是全栈完成的,采用的是前后端分离,但是实际开发中,前后端可能是两个或者多个人开发,这时需要自测接口及功能,前端搭建mock.js或使用easymock来进行模拟请求,后端可以使用postman,SoapUI等工具进行接口访问

前端和后端需要防止多次重复请求,前端通过节流的方式,防止对后端重复请求,但是也要防止数据库的恶意攻击(这个项目中没有实现),通过参数附带时间戳,使一个ip或者一个用户只能在短时间内请求规定次数

巧用前后端缓存,前端使用cookie和localstorage,后端生成temp缓存文件

前后端加密处理,token,Crypto加密参数,Bcrypt加密密码

相关文章
|
2月前
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
|
3月前
|
前端开发 JavaScript 程序员
成功解决:尚硅谷中的谷粒商城前端项目运行依赖问题。【详细图解+问题说明+解决思路】
这篇文章介绍了如何解决尚硅谷谷粒商城前端项目中遇到的依赖问题,通过修改`package.json`和`package-lock.json`中的`node-sass`和`sass-loader`版本,成功解决了node版本与这些依赖的兼容性问题。
成功解决:尚硅谷中的谷粒商城前端项目运行依赖问题。【详细图解+问题说明+解决思路】
|
6月前
|
前端开发 数据可视化 JavaScript
探索前端可视化开发:低代码平台原理与实践
【4月更文挑战第7天】本文探讨了低代码平台在前端开发中的应用,介绍了其模型驱动、组件化和自动化部署的原理,强调了提升效率、降低技术门槛、灵活适应变更和保证一致性等优势。建议开发者明确适用场景,选择合适平台,并培养团队低代码技能,同时规划与现有技术栈的融合,实施持续优化治理。低代码平台正改变开发格局,为业务创新和数字化转型提供新途径。
299 0
|
6月前
|
前端开发 数据可视化
探索前端开发中的新趋势:低代码平台的应用与挑战
【2月更文挑战第8天】随着前端开发领域的不断发展,低代码平台作为一种新兴的开发方式正逐渐受到关注。本文将探讨低代码平台在前端开发中的应用现状、优势以及挑战,带领读者深入了解这一新趋势。
|
4月前
|
前端开发 开发者
领域驱动使用问题之在图研发平台中,领域驱动设计是如何帮助解决前端开发面临的问题的
领域驱动使用问题之在图研发平台中,领域驱动设计是如何帮助解决前端开发面临的问题的
|
4月前
|
移动开发 前端开发 应用服务中间件
挂售转卖竞拍商城系统源码/竞拍系统/转拍闪拍系统/后端PHP+前端UNiapp源码
挂售转卖竞拍商城系统源码/竞拍系统/转拍闪拍系统/后端PHP+前端UNiapp源码 亲测可用
126 1
|
5月前
|
前端开发 小程序
新版校园跑腿外卖独立版+APP+小程序前端外卖配送平台源码
同城校园跑腿外卖配送平台源码,支持自定义diy 你可以设计你的页面,设计你自己的风格,支持多校园,独立版本,多商户,有用户端,骑手端,商家端,强大的功能
189 3
|
6月前
|
前端开发 JavaScript Java
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
|
6月前
|
SQL 前端开发 JavaScript
前端vite+vue3结合后端node+koa——实现代码模板展示平台(支持模糊搜索+分页查询)
前端vite+vue3结合后端node+koa——实现代码模板展示平台(支持模糊搜索+分页查询)
152 4
|
6月前
|
Dart 前端开发 Android开发
【Flutter前端技术开发专栏】Flutter中的平台特定代码实现
【4月更文挑战第30天】Flutter旨在实现跨平台移动应用开发,但有时需针对iOS或Android编写特定代码。平台通道是关键机制,允许Dart代码与原生代码交互。通过`MethodChannel`等实现跨平台通信,然后在iOS和Android上响应调用。条件编译则在编译时决定特定平台代码。本文展示了如何在Flutter中处理平台特定功能,包括示例代码和总结。
150 0
【Flutter前端技术开发专栏】Flutter中的平台特定代码实现