仿樂優電商前端後臺管理開發第二天
目錄
文章目錄
內容
一、功能實現
1、主框架分析實現
-
佈局分析
- header : 頭部
- left-navagator: 左側導航菜單
- main: 內容主體
-
適用UI組件:
- header; v-app-bar
- left-navagator: v-navigation-drawer
- main: v-content
- 麪包屑功能標題: v-breadcrumbs
- 具體功能子組件 :
-
實現代碼
<template>
<v-app>
<!--應用程序導航條-->
<v-app-bar>
...
</v-app-bar>
<!--左側菜單導航-->
<v-navigation-drawer>
...
</v-navigation-drawer>
<!--內容主體-展示具體功能-->
<v-content>
<v-breadcrumbs
...
</v-breadcrumbs>
<div>
<!--定義一個路由錨點,Layout的子組件內容將在這裏展示-->
<router-view
</div>
</v-content>
詳細見博文‘vuetify學習第三天之佈局-bars組件’
2、左側菜單
詳細見博文‘vuetify學習第四天-典型導航菜單實現’
3、商品管理
3.1、品牌管理
3.1.1、分析
- 默認:展示品牌列表,圖示@3.3.2.1-1
- 主要功能
- 新增:點擊新增按鈕,彈出新增對話框
- 修改:點擊修改圖標,彈出修改對話框
- 刪除:點擊刪除圖標,彈出刪除確認消息框
- 列表展示
- 搜索:點擊搜索,按首字母索引品牌數據
- 分頁:可以改嗎每頁顯示條目一級翻頁
3.1.2、品牌列表展示
- vuetify 主要實現組件
- v-card: 佈局
- v-data-table: 服務端分頁和排序數據表格
- 具體實現參考博文vuetify 學習第一天之v-data-table_表格組件
3.1.3、品牌新增
3.1.3.1、簡單分析:
- 修改內容
- 基礎:品牌除ID以爲的名稱、首字母
- 複雜
- logo: 文件上傳功能
- 品牌分類:因爲分類分層級,我們用級聯選擇框實現
- 實現:通用實現,對話框,表單提交,根據簡單與複雜性分步驟完成
- 基礎:就是基本的表單輸入框
- 複雜:
- logo:文件上傳組件
- 品牌所屬分類:級聯選擇框組件
3.1.3.2、使用vuetify組件或者自定義組件
- v-dialog:對話框
- v-card:容器
- v-toolbar:標題
- v-stepper:步驟條
- v-stepper-header:步驟條頭部
- v-stepper-step:步驟條頭部顯示數字
- v-stepper-items:步驟條條目
- v-tepper-content 步驟條條目內容
- v-card:容器
- v-form:表單
- v-text-field:輸入框
… - v-cascader:級聯選擇框
- v-text-field:輸入框
- v-form:表單
- v-card:容器
- v-tepper-content 步驟條條目內容
- v-stepper-items:
- v-stepper-content
- v-card
- v-layout:佈局
- v-flex
- span :標題
- v-flex
- v-upload: 自定義文件上傳組件
- v-flex
- v-layout:佈局
- v-row:行佈局
- v-btn:按鈕
…
- v-btn:按鈕
- v-card
- v-stepper-content
- v-stepper-header:步驟條頭部
- v-card:容器
3.1.3.3、效果圖示
- 圖示@3.1.3.3-1:
- 圖示@3.1.3.3-2:
3.1.3.4、源代碼
- 源代碼@3.1.4-1:
<!-- brand component -->
<template>
<div>
<v-card>
<v-card-title>
<v-btn small raised color="primary" @click="showAddedBrandDialog">新增品牌</v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
append-icon="search"
label="Search"
single-line
hide-details
@keyup.enter="searchChanged"
@click:append="searchChanged"
></v-text-field>
</v-card-title>
<v-data-table
:headers="headers"
:items="brandList"
:options.sync="options"
:server-items-length="total"
:loading="loading"
class="elevation-1"
@update:options="optionsChanged"
>
<template v-slot:item.image="{ item }">
<img :src="item.image" width="100" />
</template>
<template v-slot:item.option="{ item }">
<v-icon small class="mr-2" @click="editBrand(item)">edit</v-icon>
<v-icon small @click="deleteBrand(item)">delete</v-icon>
</template>
</v-data-table>
</v-card>
<!-- 添加品牌對話框 -->
<v-dialog v-model="dialog" max-width="500px">
<v-card>
<v-toolbar color="primary" :dark="true">
<v-toolbar-title>{{ dialogTitle }}</v-toolbar-title>
</v-toolbar>
<v-stepper v-model="e1">
<v-stepper-header>
<v-stepper-step :complete="e1 > 1" step="1">基礎信息</v-stepper-step>
<v-divider></v-divider>
<v-stepper-step step="2">品牌LOGO</v-stepper-step>
</v-stepper-header>
<v-stepper-items>
<v-stepper-content step="1">
<v-card class="mb-12" color="grey lighten-5" height="300px">
<v-form ref="addBrandFormRef" v-model="valid">
<v-text-field v-model="brandName" :rules="nameRules" label="品牌名稱" required></v-text-field>
<v-text-field v-model="initial" :rules="initialRules" label="首字母" required></v-text-field>
<v-cascader
v-model="categories"
label="品牌分類"
url="/item/category/list"
multiple
required
/>
</v-form>
</v-card>
<v-btn color="primary" @click="e1 = 2">Continue</v-btn>
<v-spacer></v-spacer>
</v-stepper-content>
<v-stepper-content step="2">
<v-card class="mb-12" color="grey lighten-5" height="300px">
<v-layout column>
<v-flex xs3>
<span style="font-size: 16px; color: #444">品牌LOGO:</span>
</v-flex>
<v-flex>
<v-upload
v-model="image"
url="/upload/image"
:multiple="false"
:pic-width="250"
:pic-height="90"
/>
</v-flex>
</v-layout>
</v-card>
<v-row>
<v-btn color="primary" @click="e1 = 1">Continue</v-btn>
<v-spacer></v-spacer>
<v-btn color="grey lighten-1" @click="closeAddBrandDialog">取消</v-btn>
<v-btn color="grey lighten-2" @click="resetAddBrandForm">重置</v-btn>
<v-btn color="primary" @click="submitAddBrandForm">確認</v-btn>
</v-row>
</v-stepper-content>
</v-stepper-items>
</v-stepper>
</v-card>
</v-dialog>
<!-- 修改品牌對話框 -->
<v-dialog v-model="editedBrandFormDialog" max-width="500px">
<v-card>
<v-toolbar color="primary" :dark="true">
<v-toolbar-title>{{ dialogTitle }}</v-toolbar-title>
</v-toolbar>
<v-stepper v-model="e1">
<v-stepper-header>
<v-stepper-step :complete="e1 > 1" step="1">基礎信息</v-stepper-step>
<v-divider></v-divider>
<v-stepper-step step="2">品牌LOGO</v-stepper-step>
</v-stepper-header>
<v-stepper-items>
<v-stepper-content step="1">
<v-card class="mb-12" color="grey lighten-5" height="300px">
<v-form ref="editedBrandFormRef" v-model="valid">
<v-text-field
v-model="editedBrandForm.name"
:rules="nameRules"
label="品牌名稱"
required
></v-text-field>
<v-text-field
v-model="editedBrandForm.initial"
:rules="initialRules"
label="首字母"
required
></v-text-field>
<v-cascader
v-model="editedCategores"
label="品牌分類"
url="/item/category/list"
multiple
required
/>
</v-form>
</v-card>
<v-btn color="primary" @click="e1 = 2">Continue</v-btn>
<v-spacer></v-spacer>
</v-stepper-content>
<v-stepper-content step="2">
<v-card class="mb-12" color="grey lighten-5" height="300px">
<v-layout column>
<v-flex xs3>
<span style="font-size: 16px; color: #444">品牌LOGO:</span>
</v-flex>
<v-flex>
<v-upload
v-model="editedBrandForm.image"
url="/upload/image"
:multiple="false"
:pic-width="250"
:pic-height="90"
/>
</v-flex>
</v-layout>
</v-card>
<v-row>
<v-btn color="primary" @click="e1 = 1">Continue</v-btn>
<v-spacer></v-spacer>
<v-btn color="grey lighten-1" @click="closeEditBrandDialog">取消</v-btn>
<v-btn color="grey lighten-2" @click="resetEditBrandForm">重置</v-btn>
<v-btn color="primary" @click="submitEditBrandForm">確認</v-btn>
</v-row>
</v-stepper-content>
</v-stepper-items>
</v-stepper>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
data: () => ({
search: "", // 搜索關鍵字
// v-data-table 配置項
options: {
page: 1,
itemsPerPage: 10,
sortBy: ["id"],
sortDesc: [true]
},
total: 0, // 總條目數
pageCount: 1, // 總頁數
brandList: [], // 當前頁品牌數據列表
loading: false, // 表格數據加載條
// 表格頭信息
headers: [
{ text: "ID", value: "id" },
{ text: "名稱", value: "name", sortable: false },
{ text: "Logo", value: "image", sortable: false },
{ text: "首字母", value: "initial" },
{ text: "操作", value: "option", sortable: false }
],
dialog: false, // 對話框顯示與隱藏標誌
editedFlag: false, // 對話框標題添加與修改標誌
e1: 1, // 步驟條
valid: false, // 表單校驗結果標誌
brandName: "", // 品牌名稱
// 品牌名稱校驗規則
nameRules: [
v => !!v || "Name is required",
v => (v && v.length >= 1) || "Name must be greater than 1 characters"
],
initial: "", // 品牌首字母
// 品牌首字母校驗規則
initialRules: [
v => !!v || "Initial is required",
v => /^[A-Z]$/.test(v) || "Initial must be a capital letter"
],
image: "", // LOGO
categories: [], // 級聯分類信息
// 被修改的品牌表單對象
editedBrandForm: {
id: 0,
name: "",
initial: "",
image: ""
},
editedBrandFormDialog: false, // 是否顯示修改品牌表單對話框
editedCategores: [] //修改品牌分類列表
}),
created() {
this.getBrandList();
},
beforeUpdate() {
// console.log(this.editedCategores);
},
computed: {
dialogTitle() {
return this.editedFlag ? "修改品牌" : "添加新品牌";
}
},
methods: {
// 獲取分頁搜索品牌列表
getBrandList() {
this.axios
.get("/item/brand/page", {
params: {
search: this.search,
page: this.options.page,
rows: this.options.itemsPerPage,
sortBy: this.options.sortBy.join(","),
sortDesc: this.options.sortDesc.join(",")
}
})
.then(resp => {
// console.log(resp);
if (resp.status != 200) {
// 報錯提示
}
this.brandList = resp.data.items;
// console.log(this.brandList);
this.total = resp.data.total;
this.options.pageStop = resp.data.totalPage;
});
},
// 編輯品牌
editBrand(item) {
console.log(item.id);
// 顯示修改品牌對話框
this.editedBrandFormDialog = true;
// 1、初始化被修改品牌表單對象
this.editedBrandForm.id = item.id;
this.editedBrandForm.name = item.name;
this.editedBrandForm.initial = item.initial;
this.editedBrandForm.image = item.image;
// console.log(this.editedBrandForm);
// 2、初始化品牌分類
this.getCategoriesByBid(item.id);
// 3、初始化標題
this.editedFlag = true;
},
// 根據品牌ID獲取分類
getCategoriesByBid(bid) {
this.axios.get(`/item/brand/categories/${bid}`).then(resp => {
if (resp.status != 200) {
// 報錯提示
this.$message.error("根據品牌ID查詢分類信息出錯");
}
// 獲取成功
// console.log(resp.data);
// 初始化分類信息
this.editedCategores = resp.data;
});
},
// 關閉修改品牌對話框
closeEditBrandDialog() {
this.$refs.editedBrandFormRef.reset();
this.editedCategores = [];
this.e1 = 1;
this.editedBrandFormDialog = false;
},
// 重置修改品牌表單
resetEditBrandForm() {
this.$refs.editedBrandFormRef.reset();
this.editedCategores = [];
},
// 提交修改品牌表單
submitEditBrandForm() {
// console.log(this.editedBrandForm);
const param = {
brand: this.editedBrandForm,
categories: this.editedCategores.map(o => o.id)
};
console.log(param);
this.axios.put("/item/brand/editBrand", param).then(resp => {
if (resp.status != 200) {
this.$message.error("修改品牌失敗");
}
// console.log(resp.data);
this.getBrandList();
this.closeEditBrandDialog();
});
},
// 刪除指定品牌
deleteBrand(item) {
// console.log(item);
// return
// 刪除確認
this.$message
.confirm("此操作將會永久刪除該商品,確定要刪除嗎")
.then(() => {
// 確認刪除,執行刪除操作
this.axios
.delete('item/brand', {
params: {
bid: item.id,
image: item.image
}
})
.then(resp => {
// console.log(resp)
if (resp.status !== 204) {
return this.$message.error("刪除商品失敗");
}
// 成功刪除,重新獲取數據
this.getBrandList();
});
})
.catch(() => {
// 取消刪除
this.$message.info("已取消刪除");
});
},
searchChanged() {
if (this.search !== "") {
// console.log(this.search);
this.getBrandList();
}
},
// 分組、排序項改變,重新向後端請求數據
optionsChanged() {
// console.log(this.options);
this.getBrandList();
},
// 顯示添加品牌對話框
showAddedBrandDialog() {
// 1、初始化對話框標題
this.editedFlag = false;
// 2、顯示對話框
this.dialog = true;
},
// 重置添加品牌表單
resetAddBrandForm() {
// 情況輸入框內容
this.$refs.addBrandFormRef.reset();
// 手動情況商品分類
this.categories = [];
},
// 關閉添加品牌對話框
closeAddBrandDialog() {
this.e1 = 1;
this.dialog = false;
},
// 提交添加品牌表單
submitAddBrandForm() {
// 校驗
if (!this.$refs.addBrandFormRef.validate()) {
this.$message.eror("填寫內容不符合要求");
}
// 發送添加請求
// console.log(this.categories);
// 1、品牌參數
const param = {
name: this.brandName,
initial: this.initial,
image: this.image
};
// 2、分類ID cids
param.cids = this.categories.map(c => c.id).join(",");
// 3、發送後端
// console.log(param);
this.axios.post("item/brand", param).then(resp => {
if (resp.status != 201) {
this.$message.error("添加品牌失敗");
}
// console.log(resp);
// 添加成功
// 1、清空表單
this.resetAddBrandForm();
// 2、關閉對話框
this.closeAddBrandDialog();
// 3、重新請求品牌列表
this.getBrandList();
});
}
},
components: {}
};
</script>
<style lang='scss' scoped>
</style>
3.1.3.5、品牌新增組件使用詳解
- v-dialog:對話框組件
- 源代碼@3.1.3.5-1:
<v-dialog v-model="dialog" max-width="500px">
...
</v-dialog>
- 常用屬性詳解
名稱 | 類型 | 默認值 | 功能 |
---|---|---|---|
max-width | string/number | none | 最大寬度 |
value | any | undefined | 是否顯示對話框 |
- v-stepper:步驟條
- 本例配置基本結構:
<v-stepper v-model="e1">
<v-stepper-header>
<v-stepper-step :complete="e1 > 1" step="1">基礎信息</v-stepper-step>
<v-divider></v-divider>
<v-stepper-step step="2">品牌LOGO</v-stepper-step>
</v-stepper-header>
<v-stepper-items>
<v-stepper-content step="1">
...
<v-spacer></v-spacer>
</v-stepper-content>
<v-stepper-content step="2">
...
</v-stepper-content>
</v-stepper-items>
</v-stepper>
- v-stepper
- 常用屬性詳解
名稱 | 類型 | 默認值 | 功能 |
---|---|---|---|
value | any | undefined | 默認顯示步驟條目 |
vertical | boolean | false | 是否豎直顯示,默認水平顯示 |
- v-stepper-step
- 常用屬性詳解
名稱 | 類型 | 默認值 | 功能 |
---|---|---|---|
complete | boolean | 完成條件 | |
step | 步驟條目唯一標誌,顯示標題 |
- 其他標籤作用效果同div,起到容器作用
- v-cascader:自定義級聯選擇框
詳細參考博文“vuetify 學習第二天之v-combobox-自定義級聯組件v-cascader封裝”
- v-upload:自定義文件上傳組件
vuetify文件上傳組件比較單調,我們使用element-ui的el-upload 簡單封裝。
- upload.vue源代碼@3.1.3.5-2
<template>
<div>
<el-upload v-if="multiple"
:action="baseUrl + url"
list-type="picture-card"
:on-success="handleSuccess"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
ref="multiUpload"
:file-list="fileList"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-upload ref="singleUpload" v-else
:style="avatarStyle"
class="logo-uploader"
:action="baseUrl + url"
:show-file-list="false"
:on-success="handleSuccess">
<div @mouseover="showBtn=true" @mouseout="showBtn=false">
<i @click.stop="removeSingle" v-show="dialogImageUrl && showBtn" class="el-icon-close remove-btn"></i>
<img v-if="dialogImageUrl" :src="dialogImageUrl" :style="avatarStyle">
<i v-else class="el-icon-plus logo-uploader-icon" :style="avatarStyle"></i>
</div>
</el-upload>
<v-dialog v-model="show" max-width="500">
<img width="500px" :src="dialogImageUrl" alt="">
</v-dialog>
</div>
</template>
<script>
import {Upload} from 'element-ui';
// import config from '../../config/config'
export default {
name: "vUpload",
components: {
elUpload: Upload
},
props: {
url: {
type: String
},
value: {},
multiple: {
type: Boolean,
default: true
},
picWidth: {
type: Number,
default: 150
},
picHeight: {
type: Number,
default: 150
}
},
data() {
return {
showBtn: false,
show: false,
dialogImageUrl: "",
baseUrl: this.$config.api,
avatarStyle: {
width: this.picWidth + 'px',
height: this.picHeight + 'px',
'line-height': this.picHeight + 'px'
},
fileList:[]
}
},
mounted(){
if (!this.value || this.value.length <= 0) {
return;
}
if (this.multiple) {
this.fileList = this.value.map(f => {
return {response: f, url:f}
});
} else {
this.dialogImageUrl = this.value;
}
},
methods: {
handleSuccess(resp, file) {
if (!this.multiple) {
this.dialogImageUrl = file.response;
this.$emit("input", file.response)
} else {
this.fileList.push(file)
this.$emit("input", this.fileList.map(f => f.response))
}
},
handleRemove(file, fileList) {
this.fileList = fileList;
this.$emit("input", fileList.map(f => f.response))
},
handlePictureCardPreview(file) {
this.dialogImageUrl = file.response;
this.show = true;
},
removeSingle() {
this.dialogImageUrl = "";
this.$refs.singleUpload.clearFiles();
}
},
watch: {
value:{
deep:true,
handler(val){
if (this.multiple) {
this.fileList = val.map(f => {
return {response: f,url:f}
});
} else {
this.dialogImageUrl = val;
}
}
}
}
}
</script>
<style scoped>
.logo-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
float: left;
}
.logo-uploader:hover {
border-color: #409EFF;
}
.logo-uploader-icon {
font-size: 28px;
color: #8c939d;
text-align: center;
}
.remove-btn {
position: absolute;
right: 0;
font-size: 16px;
}
.remove-btn:hover {
color: #c22;
}
</style>
- 組件屬性、方法及事件可參考el-upload
- 其他組件使用前面已經介紹過或者使用相對簡單,不在詳述
3.1.4、品牌修改
3.1.4.1、與品牌新增差異
品牌修改基本上和品牌新增相同,組件相同,不同之處在於,品牌修改表單數據需要初始化。相同部分參考上面,不在贅述。
3.1.4.2、初始化
- 基本表單輸入框與v-upload初始化,直接賦初值即可
- v-cascader 初始化
- 根據multiple屬性值,value值類型不同
- true: value類型爲array
- false: value類型爲string
- 如果類型錯誤,則可能出現multiple=false,渲染結果如下圖示@3.1.4.2-1:
,想正確初始化的值初始化不了的錯誤。
3.1.5、品牌刪除
3.1.5.1、結構
刪除的話不需要數據提交或者展示,但是需要給用戶提示,用以最後確認是否要刪除,既使用帶確認取消功能的提示框。
3.1.5.2、確認提示框
vuetify的提示框相對單一,我們使用elment-ui的消息提示框進行簡單封裝,直接掛載到Vue.prototyope.
import {Message, MessageBox} from 'element-ui';
const message = {
info(msg) {
Message({
showClose: true,
message: msg,
type: 'info'
});
},
error(msg) {
Message({
showClose: true,
message: msg,
type: 'error'
});
},
success(msg) {
Message({
showClose: true,
message: msg,
type: 'success'
});
},
warning(msg) {
Message({
showClose: true,
message: msg,
type: 'warning'
});
},
confirm(msg) {
return new Promise((resolve, reject) => {
MessageBox.confirm(msg, '提示', {
confirmButtonText: '確定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
resolve()
})
.catch(() => {
reject()
});
})
},
prompt(msg) {
return new Promise((resolve, reject) => {
MessageBox.prompt(msg, '提示', {
confirmButtonText: '確定',
cancelButtonText: '取消'
}).then(({value}) => {
resolve(value)
}).catch(() => {
reject()
});
})
}
}
export default message;
// 一下爲自定義組件註冊器中實現,前面有詳述
import message from message.js
Vue.prototype.$message = message
- 圖示
- 圖示@3.1.3.5-1:
4、後記
到此品牌頁面全部完成。
後記 :
本項目爲參考某馬視頻thinkphp5.1-樂優商城前後端項目開發,相關視頻及配套資料可自行度娘或者聯繫本人。上面爲自己編寫的開發文檔,持續更新。歡迎交流,本人QQ:806797785
前端項目源代碼地址:https://gitee.com/gaogzhen/vue-leyou
後端thinkphp源代碼地址:https://gitee.com/gaogzhen/leyou-backend-thinkphp