仿樂優商城後臺管理-前端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.messagemessage。

  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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章