ゼロから、シンプルなショッピングプラットフォーム(18)のフロントエンドモールパーツを構築し
ます:https ://blog.csdn.net/time_____/article/details/108918489
プロジェクトのソースコード(継続的な更新):https://gitee.com/ DieHunter / myCode / tree / master / Shopping
前回の記事の後、フロントエンドモールのいくつかの基本機能が実装されました。これには、製品リスト、製品分類、ホームページの製品表示、製品の詳細、ショッピングカート、ユーザーのログインと登録、残りのコンテンツ:ユーザー情報の変更、注文の送信、注文の表示などが含まれます。記事は残りの機能で終わります。
ユーザー情報を変更するためのバックエンドインターフェイスが管理プラットフォームに実装されており、ここで直接確認呼び出しを行うことができます
トークンを生成するフィールドがユーザー名であるため、ユーザー情報を変更する以前の機能でテストのバグが明らかになりました。ユーザー情報を変更するときに、ユーザー名を変更するとトークンの検証が失敗するため、トークンの生成方法を修正する必要があります。 、以前のユーザー名の世代を_idの世代に変更すると、新しいバージョンのコードがCodeCloudに送信されます
修理後の効果:
以下に実装プロセスを説明します。ここでは、情報ユーザー情報インターフェイスとログインインターフェイスを1つのページに配置し、v-if条件を介してレンダリングします。条件は、checkTokenが合格するかどうかです。
bussiness.js、トークンが有効であることを確認します
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にジャンプすると、アバターが別のコンポーネントとしてアップロードされます。
ネイティブjsアップロードファイルの落とし穴は次のとおりです
。Axiosは投稿ファイルヘッダーファイルをアップロードして「multipart / form-data」リクエストをシミュレートします。このリクエスト形式はapplication / x-www-form-urlencodedとは異なり、セパレータを宣言する必要があります。シンボル「境界」。
headers: {
"Content-Type": "multipart/form-data;boundary=ABC"//ABC内容自行填写
},
現時点では、落とし穴があります。ABCなどの単純なセパレータコンテンツを使用してファイルを直接アップロードすると、サーバーがファイルを認識せず、ファイルの開始位置を見つけられない可能性があるため、次のような複雑な文字が必要になります。 new Date()。getTime()は、変更後、次の構成でランダムな文字を生成します
headers: {
"Content-Type": "multipart/form-data;boundary=" + new Date().getTime()
},
アバターコンポーネントをアップロードする際には、公式の入力要素を置き換えるコントロールを作成する必要があります。つまり、画像をクリックし、JSを使用して入力ファイルのアップロードイベントを実行し、サーバーに送信します。サーバーがキャッシュを保存すると、画像ファイルのアドレスがフロントエンドに送信されます。ファイルを読んで表示します。以下は
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 runbuildを実行してwebpackをパックします
https環境を構成する必要がある場合は、この記事を参照してください。
この記事では、フォルダの命名規則とモジュールコンポーネントの割り当てについて説明します。
このシリーズの記事がお役に立てば幸いです。シリーズ全体または特定の記事をお読みになりましたら、よろしくお願いいたします。
概要:このブログの時点で、「ゼロからシンプルなショッピングプラットフォームを構築する」シリーズの記事はすべて終わりました。以下は、プロジェクト全体の簡単な概要といくつかの注意点です。
- ビルド環境と構成ファイル:独自のテクノロジースタックと利点を深く理解し、自分または製品のニーズに最も適したテクノロジーを選択し、プロジェクトカタログの構築を完了する必要があります。たとえば、フロントエンドはモジュール式およびコンポーネント化された開発を開発するのに最適です。習慣として、フォルダとファイルを各基本コンポーネントに細分化してみてください。
- コンポーネントとフレームワークの公式ドキュメントをコアとして、オンラインで問題を見つけ、自分で問題を解決することを学ぶことが非常に必要です。
- 車輪の作り方を学ぶインターネット上には他の人が書いたフレームワーク、コンポーネント、jsライブラリがたくさんありますが、関数、カプセル化関数、コンポーネントを自分で書く必要があります。時間の節約などの理由ではありません。自分で書くと改善できます。独自のプログラミングのアイデアと実用的なアプリケーション能力、そして比較的成功したクラスやコンポーネント、さらにはメソッドを書くとき、あなたは大きな達成感を得るでしょう
- オブジェクト指向のプログラミング言語は、コードの結合を減らし、結束を改善し、コードをより堅牢にします。私はこれを改善するために一生懸命取り組んでいます。この方法でコードを書くことは、多くのメソッドを取り除き、再利用性を改善し、コードの量を減らすのに役立ちます。率直に言って、誰かがプロジェクトに必要なコードは3000行だけかもしれませんが、私は5000行必要かもしれません
- このプロジェクトは、フロントエンドとバックエンドの分離を使用してフルスタックで完了しましたが、実際の開発では、フロントエンドとバックエンドが2人以上で開発される可能性があります。現時点では、セルフテストのインターフェイスと機能が必要です。フロントエンドはmock.jsをビルドするか、easymockを使用します。シミュレーションリクエストの場合、バックエンドはpostman、SoapUI、およびその他のツールを使用してインターフェイスにアクセスできます
- フロントエンドとバックエンドは、複数の繰り返し要求を防ぐ必要があります。フロントエンドは、スロットルを使用して、バックエンドへの繰り返し要求を防ぐだけでなく、データベースへの悪意のある攻撃を防ぎます(このプロジェクトでは実装されていません)。タイムスタンプ付きのパラメーターを渡して、IPを作成します。または、ユーザーは指定された回数だけ短時間でリクエストできます
- フロントエンドとバックエンドのキャッシュを巧みに使用し、フロントエンドはCookieとローカルストレージを使用し、バックエンドは一時キャッシュファイルを生成します
- フロントエンドとバックエンドの暗号化処理、トークン、暗号化パラメータ、Bcrypt暗号化パスワード