仿乐优电商前端后台管理开发第二天
目录
文章目录
内容
一、功能实现
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. m e s s a g e 。 message。 message。
- 源代码message.js@3.1.5.2-1
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