仿乐优商城后台管理-前端vue+后端thinkphp5.1+数据库mysql项目开发----前端第二天

仿乐优电商前端后台管理开发第二天


目录


内容

一、功能实现

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-stepper-items:
          • v-stepper-content
            • v-card
              • v-layout:布局
                • v-flex
                  • span :标题
                • v-flex
                  • v-upload: 自定义文件上传组件
            • v-row:行布局
              • v-btn:按钮
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、品牌新增组件使用详解
  1. 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 是否显示对话框
  1. 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,起到容器作用
  1. v-cascader:自定义级联选择框

详细参考博文“vuetify 学习第二天之v-combobox-自定义级联组件v-cascader封装

  1. 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
  1. 其他组件使用前面已经介绍过或者使用相对简单,不在详述

3.1.4、品牌修改

3.1.4.1、与品牌新增差异

    品牌修改基本上和品牌新增相同,组件相同,不同之处在于,品牌修改表单数据需要初始化。相同部分参考上面,不在赘述。

3.1.4.2、初始化
  1. 基本表单输入框与v-upload初始化,直接赋初值即可
  2. 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。

  1. 源代码[email protected]
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
  1. 图示
  • 图示@3.1.3.5-1:在这里插入图片描述

4、后记

    到此品牌页面全部完成。

后记
    本项目为参考某马视频thinkphp5.1-乐优商城前后端项目开发,相关视频及配套资料可自行度娘或者联系本人。上面为自己编写的开发文档,持续更新。欢迎交流,本人QQ:806797785

    前端项目源代码地址:https://gitee.com/gaogzhen/vue-leyou
    后端thinkphp源代码地址:https://gitee.com/gaogzhen/leyou-backend-thinkphp

发布了17 篇原创文章 · 获赞 3 · 访问量 2682

猜你喜欢

转载自blog.csdn.net/gaogzhen/article/details/103905658