後臺管理系統之品牌管理

1.品牌的新增

昨天我們完成了品牌的查詢,接下來就是新增功能。點擊新增品牌按鈕

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JRKHqvpF-1586523104232)(assets/1545222288968.png)]

1.1.頁面實現

1.2.後臺實現新增

1.2.1.controller

還是一樣,先分析四個內容:

  • 請求方式:POST
  • 請求路徑:/brand
  • 請求參數:brand對象,外加商品分類的id數組cids
  • 返回值:無,只需要響應狀態碼

代碼:

    /**
     * 新增品牌
     * @param brand
     * @param cids
     */
    @PostMapping
    public ResponseEntity<Void> saveBrand(Brand brand, @RequestParam("cids") List<Long> cids){
        this.brandService.saveBrand(brand, cids);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

1.2.2.Service

這裏要注意,我們不僅要新增品牌,還要維護品牌和商品分類的中間表。

    /**
     * 新增品牌
     *
     * @param brand
     * @param cids
     */
    @Transactional
    public void saveBrand(Brand brand, List<Long> cids) {

        // 先新增brand
        this.brandMapper.insertSelective(brand);

        // 在新增中間表
        cids.forEach(cid -> {
            this.brandMapper.insertCategoryAndBrand(cid, brand.getId());
        });
    }

這裏調用了brandMapper中的一個自定義方法,來實現中間表的數據新增

1.2.3.Mapper

通用Mapper只能處理單表,也就是Brand的數據,因此我們手動編寫一個方法及sql,實現中間表的新增:

public interface BrandMapper extends Mapper<Brand> {

    /**
     * 新增商品分類和品牌中間表數據
     * @param cid 商品分類id
     * @param bid 品牌id
     * @return     ${修改字符時會引起sql注入}
     */
    @Insert("INSERT INTO tb_category_brand(category_id, brand_id) VALUES (#{cid},#{bid})")
    int insertBrandAndCategory(@Param("cid") Long cid, @Param("bid") Long bid);
}

1.2.4.測試

在這裏插入圖片描述
400:請求參數不合法

1.3.解決400

1.3.1.原因分析

我們填寫表單並提交,發現報錯了。查看控制檯的請求詳情:

發現請求的數據格式是JSON格式。

原因分析:

axios處理請求體的原則會根據請求數據的格式來定:

  • 如果請求體是對象:會轉爲json發送

  • 如果請求體是String:會作爲普通表單請求發送,但需要我們自己保證String的格式是鍵值對。

    如:name=jack&age=12

1.3.2.QS工具

QS是一個第三方庫,我們可以用npm install qs --save來安裝。不過我們在項目中已經集成了,大家無需安裝:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lExk8lyX-1586523104239)(assets/1530696509189.png)]

這個工具的名字:QS,即Query String,請求參數字符串。

什麼是請求參數字符串?例如: name=jack&age=21

QS工具可以便捷的實現 JS的Object與QueryString的轉換。

在我們的項目中,將QS注入到了Vue的原型對象中,我們可以通過this.$qs來獲取這個工具:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UzwhoXAa-1586523104239)(assets/1539821449329.png)]

我們將this.$qs對象打印到控制檯:

created(){
    console.log(this.$qs);
}

發現其中有3個方法:

這裏我們要使用的方法是stringify,它可以把Object轉爲QueryString。

測試一下,使用瀏覽器工具,把qs對象保存爲一個臨時變量temp1,然後調用stringify方法:

成功將person對象變成了 name=zhangsan&age=30的字符串了

1.3.3.解決問題

修改頁面,對參數處理後發送:

在這裏插入圖片描述

1.4.新增完成後關閉窗口

我們發現有一個問題:新增不管成功還是失敗,窗口都一致在這裏,不會關閉。

這樣很不友好,我們希望如果新增失敗,窗口保持;但是新增成功,窗口關閉纔對。

因此,我們需要在新增的ajax請求完成以後,關閉窗口

但問題在於,控制窗口是否顯示的標記在父組件:MyBrand.vue中。子組件如何才能操作父組件的屬性?或者告訴父組件該關閉窗口了?

之前我們講過一個父子組件的通信,有印象嗎?

  • 第一步:在父組件中定義一個函數,用來關閉窗口,不過之前已經定義過了。父組件在使用子組件時,綁定事件,關聯到這個函數:Brand.vue
<!--對話框的內容,表單-->
<v-card-text class="px-5" style="height:400px">
    <brand-form @close="closeWindow" :oldBrand="oldBrand" :isEdit="isEdit"/>
</v-card-text>
  • 第二步,子組件通過this.$emit調用父組件的函數:BrandForm.vue

在這裏插入圖片描述

測試一下,保存成功:

我們優化一下,關閉的同時重新加載數據:

closeWindow(){
    // 關閉窗口
    this.show = false;
    // 重新加載數據
    this.getDataFromServer();
}

2.實現圖片上傳

剛纔的新增實現中,我們並沒有上傳圖片,接下來我們一起完成圖片上傳邏輯。

文件的上傳並不只是在品牌管理中有需求,以後的其它服務也可能需要,因此我們創建一個獨立的微服務,專門處理各種上傳。

2.1.搭建項目

2.1.1.創建module

2.1.2.依賴

我們需要EurekaClient和web依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.upload</groupId>
    <artifactId>leyou-upload</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

2.1.3.編寫配置

server:
  port: 8082
spring:
  application:
    name: upload-service
  servlet:
    multipart:
      max-file-size: 5MB # 限制文件上傳的大小
# Eureka
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒發送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不發送就過期

需要注意的是,我們應該添加了限制文件大小的配置

2.1.4.引導類

@SpringBootApplication
@EnableDiscoveryClient
public class LeyouUploadApplication {

    public static void main(String[] args) {
        SpringApplication.run(LeyouUploadApplication.class, args);
    }
}

結構:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-i7XMPg99-1586523104243)(assets/1530717442747.png)]

2.2.編寫上傳功能

文件上傳功能,也是自定義組件完成的,參照自定義組件用法指南

2.2.1.controller

編寫controller需要知道4個內容:結合用法指南

  • 請求方式:上傳肯定是POST
  • 請求路徑:/upload/image
  • 請求參數:文件,參數名是file,SpringMVC會封裝爲一個接口:MultipartFile
  • 返回結果:上傳成功後得到的文件的url路徑,也就是返回String

代碼如下:

@Controller
@RequestMapping("upload")
public class UploadController {

    @Autowired
    private UploadService uploadService;

    /**
     * 圖片上傳
     * @param file
     * @return
     */
    @PostMapping("image")
    public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile file){
        String url = this.uploadService.upload(file);
        if (StringUtils.isBlank(url)) {
            return ResponseEntity.badRequest().build();
        }
        return ResponseEntity.status(HttpStatus.CREATED).body(url);
    }
}

2.2.2.service

在上傳文件過程中,我們需要對上傳的內容進行校驗:

  1. 校驗文件大小
  2. 校驗文件的媒體類型
  3. 校驗文件的內容

文件大小在Spring的配置文件中設置,因此已經會被校驗,我們不用管。

具體代碼:

@Service
public class UploadService {

    private static final List<String> CONTENT_TYPES = Arrays.asList("image/jpeg", "image/gif");

    private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class);

    public String upload(MultipartFile file) {

        String originalFilename = file.getOriginalFilename();
        // 校驗文件的類型
        String contentType = file.getContentType();
        if (!CONTENT_TYPES.contains(contentType)){
            // 文件類型不合法,直接返回null
            LOGGER.info("文件類型不合法:{}", originalFilename);
            return null;
        }

        try {
            // 校驗文件的內容
            BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
            if (bufferedImage == null){
                LOGGER.info("文件內容不合法:{}", originalFilename);
                return null;
            }

            // 保存到服務器
            file.transferTo(new File("C:\\leyou\\images\\" + originalFilename));

            // 生成url地址,返回
            return "http://image.leyou.com/" + originalFilename;
        } catch (IOException e) {
            LOGGER.info("服務器內部錯誤:{}", originalFilename);
            e.printStackTrace();
        }
        return null;
    }
}

這裏有一個問題:爲什麼圖片地址需要使用另外的url?

  • 圖片不能保存在服務器內部,這樣會對服務器產生額外的加載負擔
  • 一般靜態資源都應該使用獨立域名,這樣訪問靜態資源時不會攜帶一些不必要的cookie,減小請求的數據量

2.2.3.測試上傳

我們通過RestClient工具來測試:

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
上傳成功!

2.3.繞過網關

圖片上傳是文件的傳輸,如果也經過Zuul網關的代理,文件就會經過多次網路傳輸,造成不必要的網絡負擔。在高併發時,可能導致網絡阻塞,Zuul網關不可用。這樣我們的整個系統就癱瘓了。

所以,我們上傳文件的請求就不經過網關來處理了。

x 2.3.1.Zuul的路由過濾

Zuul中提供了一個ignored-patterns屬性,用來忽略不希望路由的URL路徑,示例:

zuul.ignored-patterns: /upload/**

路徑過濾會對一切微服務進行判定。

Zuul還提供了ignored-services屬性,進行服務過濾:

zuul.ignored-services: upload-servie

我們這裏採用忽略服務:

zuul:
  ignored-services:
    - upload-service # 忽略upload-service服務

上面的配置採用了集合語法,代表可以配置多個。

2.3.2.Nginx的rewrite指令

現在,我們修改頁面的訪問路徑:

<v-upload
      v-model="brand.image" 
      url="/upload/image" 
      :multiple="false" 
      :pic-width="250" :pic-height="90"
      />

查看頁面的請求路徑:

可以看到這個地址不對,依然是去找Zuul網關,因爲我們的系統全局配置了URL地址。怎麼辦?

有同學會想:修改頁面請求地址不就好了。

注意:原則上,我們是不能把除了網關以外的服務對外暴露的,不安全。

既然不能修改頁面請求,那麼就只能在Nginx反向代理上做文章了。

我們修改nginx配置,將以/api/upload開頭的請求攔截下來,轉交到真實的服務地址:

location /api/upload {
    proxy_pass http://127.0.0.1:8082;
    proxy_connect_timeout 600;
    proxy_read_timeout 600;
}

這樣寫大家覺得對不對呢?

顯然是不對的,因爲ip和端口雖然對了,但是路徑沒變,依然是:http://127.0.0.1:8002/api/upload/image

前面多了一個/api

Nginx提供了rewrite指令,用於對地址進行重寫,語法規則:

rewrite "用來匹配路徑的正則" 重寫後的路徑 [指令];

我們的案例:

	server {
        listen       80;
        server_name  api.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    	# 上傳路徑的映射
		location /api/upload {	
			proxy_pass http://127.0.0.1:8082;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
			
			rewrite "^/api/(.*)$" /$1 break; 
        }
		
        location / {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
        }
    }
  • 首先,我們映射路徑是/api/upload,而下面一個映射路徑是 / ,根據最長路徑匹配原則,/api/upload優先級更高。也就是說,凡是以/api/upload開頭的路徑,都會被第一個配置處理

  • proxy_pass:反向代理,這次我們代理到8082端口,也就是upload-service服務

  • rewrite "^/api/(.*)$" /$1 break,路徑重寫:

    • "^/api/(.*)$":匹配路徑的正則表達式,用了分組語法,把/api/以後的所有部分當做1組

    • /$1:重寫的目標路徑,這裏用$1引用前面正則表達式匹配到的分組(組編號從1開始),即/api/後面的所有。這樣新的路徑就是除去/api/以外的所有,就達到了去除/api前綴的目的

    • break:指令,常用的有2個,分別是:last、break

      • last:重寫路徑結束後,將得到的路徑重新進行一次路徑匹配
      • break:重寫路徑結束後,不再重新匹配路徑。

      我們這裏不能選擇last,否則以新的路徑/upload/image來匹配,就不會被正確的匹配到8082端口了

修改完成,輸入nginx -s reload命令重新加載配置。然後再次上傳試試。

2.4.跨域問題

重啓nginx,再次上傳,發現跟上次的狀態碼已經不一樣了,但是依然報錯:

不過慶幸的是,這個錯誤已經不是第一次見了,跨域問題。

我們在upload-service中添加一個CorsFilter即可:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WhVIHKxQ-1586523104245)(assets/1530722617437.png)]

@Configuration
public class LeyouCorsConfiguration {

    @Bean
    public CorsFilter corsFilter() {
        //1.添加CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
        //1) 允許的域,不要寫*,否則cookie就無法使用了
        config.addAllowedOrigin("http://manage.leyou.com");
        //3) 允許的請求方式
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("POST");
        // 4)允許的頭信息
        config.addAllowedHeader("*");

        //2.添加映射路徑,我們攔截一切請求
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);

        //3.返回新的CorsFilter.
        return new CorsFilter(configSource);
    }
}

再次測試:

不過,非常遺憾的是,訪問圖片地址,卻沒有響應。

這是因爲我們並沒有任何服務器對應image.leyou.com這個域名。。

這個問題,我們暫時放下,回頭再來解決。

2.5.文件上傳的缺陷

先思考一下,現在上傳的功能,有沒有什麼問題?

上傳本身沒有任何問題,問題出在保存文件的方式,我們是保存在服務器機器,就會有下面的問題:

  • 單機器存儲,存儲能力有限
  • 無法進行水平擴展,因爲多臺機器的文件無法共享,會出現訪問不到的情況
  • 數據沒有備份,有單點故障風險
  • 併發能力差

這個時候,最好使用分佈式文件存儲來代替本地文件存儲。

3.FastDFS

3.1.什麼是分佈式文件系統

分佈式文件系統(Distributed File System)是指文件系統管理的物理存儲資源不一定直接連接在本地節點上,而是通過計算機網絡與節點相連。

通俗來講:

  • 傳統文件系統管理的文件就存儲在本機。
  • 分佈式文件系統管理的文件存儲在很多機器,這些機器通過網絡連接,要被統一管理。無論是上傳或者訪問文件,都需要通過管理中心來訪問

3.2.什麼是FastDFS

FastDFS是由淘寶的餘慶先生所開發的一個輕量級、高性能的開源分佈式文件系統。用純C語言開發,功能豐富:

  • 文件存儲
  • 文件同步
  • 文件訪問(上傳、下載)
  • 存取負載均衡
  • 在線擴容

適合有大容量存儲需求的應用或系統。同類的分佈式文件系統有谷歌的GFS、HDFS(Hadoop)、TFS(淘寶)等。

3.3.FastDFS的架構

3.3.1.架構圖

先上圖:在這裏插入圖片描述

FastDFS兩個主要的角色:Tracker Server 和 Storage Server 。

  • Tracker Server:跟蹤服務器,主要負責調度storage節點與client通信,在訪問上起負載均衡的作用,和記錄storage節點的運行狀態,是連接client和storage節點的樞紐。
  • Storage Server:存儲服務器,保存文件和文件的meta data(元數據),每個storage server會啓動一個單獨的線程主動向Tracker cluster中每個tracker server報告其狀態信息,包括磁盤使用情況,文件同步情況及文件上傳下載次數統計等信息
  • Group:文件組,多臺Storage Server的集羣。上傳一個文件到同組內的一臺機器上後,FastDFS會將該文件即時同步到同組內的其它所有機器上,起到備份的作用。不同組的服務器,保存的數據不同,而且相互獨立,不進行通信。
  • Tracker Cluster:跟蹤服務器的集羣,有一組Tracker Server(跟蹤服務器)組成。
  • Storage Cluster :存儲集羣,有多個Group組成。

3.3.2.上傳和下載流程

上傳

  1. Client通過Tracker server查找可用的Storage server。
  2. Tracker server向Client返回一臺可用的Storage server的IP地址和端口號。
  3. Client直接通過Tracker server返回的IP地址和端口與其中一臺Storage server建立連接並進行文件上傳。
  4. 上傳完成,Storage server返回Client一個文件ID,文件上傳結束。

下載

  1. Client通過Tracker server查找要下載文件所在的的Storage server。
  2. Tracker server向Client返回包含指定文件的某個Storage server的IP地址和端口號。
  3. Client直接通過Tracker server返回的IP地址和端口與其中一臺Storage server建立連接並指定要下載文件。
  4. 下載文件成功。

3.4.安裝和使用

Linux下安裝使用FastDFS

3.5.java客戶端

餘慶先生提供了一個Java客戶端,但是作爲一個C程序員,寫的java代碼可想而知。而且已經很久不維護了。

這裏推薦一個開源的FastDFS客戶端,支持最新的SpringBoot2.0。

配置使用極爲簡單,支持連接池,支持自動生成縮略圖,狂拽酷炫吊炸天啊,有木有。

地址:tobato/FastDFS_client

接下來,我們就用FastDFS改造leyou-upload工程。

3.5.1.引入依賴

在父工程中,我們已經管理了依賴,版本爲:

<fastDFS.client.version>1.26.2</fastDFS.client.version>

因此,這裏我們直接在taotao-upload工程的pom.xml中引入座標即可:

<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
</dependency>

3.5.2.引入配置類

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oznfmH5o-1586523104247)(assets/1528206263148.png)]

純java配置:

@Configuration
@Import(FdfsClientConfig.class)
// 解決jmx重複註冊bean的問題
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
    
}

3.5.3.編寫FastDFS屬性

在application.yml配置文件中追加如下內容:

fdfs:
  so-timeout: 1501 # 超時時間
  connect-timeout: 601 # 連接超時時間
  thumb-image: # 縮略圖
    width: 60
    height: 60
  tracker-list: # tracker地址:你的虛擬機服務器地址+端口(默認是22122)
    - 192.168.56.101:22122

3.5.4.配置hosts

將來通過域名:image.leyou.com這個域名訪問fastDFS服務器上的圖片資源。所以,需要代理到虛擬機地址:

配置hosts文件,使image.leyou.com可以訪問fastDFS服務器
192.168.56.101

3.5.5.測試

創建測試類:

把以下內容copy進去:

@SpringBootTest
@RunWith(SpringRunner.class)
public class FastDFSTest {

    @Autowired
    private FastFileStorageClient storageClient;

    @Autowired
    private ThumbImageConfig thumbImageConfig;

    @Test
    public void testUpload() throws FileNotFoundException {
        // 要上傳的文件
        File file = new File("C:\\Users\\joedy\\Pictures\\xbx1.jpg");
        // 上傳並保存圖片,參數:1-上傳的文件流 2-文件的大小 3-文件的後綴 4-可以不管他
        StorePath storePath = this.storageClient.uploadFile(
                new FileInputStream(file), file.length(), "jpg", null);
        // 帶分組的路徑
        System.out.println(storePath.getFullPath());
        // 不帶分組的路徑
        System.out.println(storePath.getPath());
    }

    @Test
    public void testUploadAndCreateThumb() throws FileNotFoundException {
        File file = new File("C:\\Users\\joedy\\Pictures\\xbx1.jpg");
        // 上傳並且生成縮略圖
        StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
                new FileInputStream(file), file.length(), "png", null);
        // 帶分組的路徑
        System.out.println(storePath.getFullPath());
        // 不帶分組的路徑
        System.out.println(storePath.getPath());
        // 獲取縮略圖路徑
        String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
        System.out.println(path);
    }
}

結果:

group1/M00/00/00/wKg4ZVsWl5eAdLNZAABAhya2V0c424.jpg
M00/00/00/wKg4ZVsWl5eAdLNZAABAhya2V0c424.jpg
group1/M00/00/00/wKg4ZVsWmD-ARnWiAABAhya2V0c772.png
M00/00/00/wKg4ZVsWmD-ARnWiAABAhya2V0c772.png
M00/00/00/wKg4ZVsWmD-ARnWiAABAhya2V0c772_60x60.png

3.5.6.改造上傳邏輯

@Service
public class UploadService {

    @Autowired
    private FastFileStorageClient storageClient;

    private static final List<String> CONTENT_TYPES = Arrays.asList("image/jpeg", "image/gif");

    private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class);

    public String upload(MultipartFile file) {

        String originalFilename = file.getOriginalFilename();
        // 校驗文件的類型
        String contentType = file.getContentType();
        if (!CONTENT_TYPES.contains(contentType)){
            // 文件類型不合法,直接返回null
            LOGGER.info("文件類型不合法:{}", originalFilename);
            return null;
        }

        try {
            // 校驗文件的內容
            BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
            if (bufferedImage == null){
                LOGGER.info("文件內容不合法:{}", originalFilename);
                return null;
            }

            // 保存到服務器
            // file.transferTo(new File("C:\\leyou\\images\\" + originalFilename));
            String ext = StringUtils.substringAfterLast(originalFilename, ".");
            StorePath storePath = this.storageClient.uploadFile(file.getInputStream(), file.getSize(), ext, null);

            // 生成url地址,返回
            return "http://image.leyou.com/" + storePath.getFullPath();
        } catch (IOException e) {
            LOGGER.info("服務器內部錯誤:{}", originalFilename);
            e.printStackTrace();
        }
        return null;
    }
}

只需要把原來保存文件的邏輯去掉,然後上傳到FastDFS即可。

3.5.7.測試

通過RestClient測試:

3.6.頁面測試上傳

4.修改品牌(作業)

修改的難點在於回顯。

當我們點擊編輯按鈕,希望彈出窗口的同時,看到原來的數據:

4.1.點擊編輯出現彈窗

這個比較簡單,修改show屬性爲true即可實現,我們綁定一個點擊事件:

<v-icon small class="mr-2" @click="editItem(props.item)">
    edit
</v-icon>

然後編寫事件,改變show 的狀態:

如果僅僅是這樣,編輯按鈕與新增按鈕將沒有任何區別,關鍵在於,如何回顯呢?

4.2.回顯數據

回顯數據,就是把當前點擊的品牌數據傳遞到子組件(MyBrandForm)。而父組件給子組件傳遞數據,通過props屬性。

  • 第一步:在編輯時獲取當前選中的品牌信息,並且記錄到data中

    先在data中定義屬性,用來接收用來編輯的brand數據:

    我們在頁面觸發編輯事件時,把當前的brand傳遞給editBrand方法:

    <v-btn color="info" @click="editBrand(props.item)">編輯</v-btn>
    

    然後在editBrand中接收數據,賦值給oldBrand:

    editItem(oldBrand){
        // 使編輯窗口可見
        this.dialog = true;
        // 初始化編輯的數據
        this.oldBrand = oldBrand;
    }
    
  • 第二步:把獲取的brand數據 傳遞給子組件

    <!--對話框內容-->
    <v-card-text class="px-5">
        <!--這是一個表單-->
        <my-brand-form @close="close" :oldBrand="oldBrand"></my-brand-form>
    </v-card-text>
    
  • 第三步:在子組件(MyBrandForm.vue)中通過props接收要編輯的brand數據,Vue會自動完成回顯

    接收數據:

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-diduTVtH-1586523104251)(assets/1528211066645.png)]

    通過watch函數監控oldBrand的變化,把值copy到本地的brand:

    watch: {
        oldBrand: {// 監控oldBrand的變化
            handler(val) {
                if(val){
                    // 注意不要直接賦值,否則這邊的修改會影響到父組件的數據,copy屬性即可
                    this.brand =  Object.deepCopy(val)
                }else{
                    // 爲空,初始化brand
                    this.brand = {
                        name: '',
                        letter: '',
                        image: '',
                        categories: []
                    }
                }
            },
                deep: true
        }
    }
    
    • Object.deepCopy 自定義的對象進行深度複製的方法。
    • 需要判斷監聽到的是否爲空,如果爲空,應該進行初始化

測試:發現數據回顯了,除了商品分類以外:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vhFF24TJ-1586523104251)(assets/1528211235872.png)]

4.3.商品分類回顯

爲什麼商品分類沒有回顯?

因爲品牌中並沒有商品分類數據。我們需要在進入編輯頁面之前,查詢商品分類信息:

4.3.1.後臺提供接口

controller

/**
     * 通過品牌id查詢商品分類
     * @param bid
     * @return
     */
@GetMapping("bid/{bid}")
public ResponseEntity<List<Category>> queryByBrandId(@PathVariable("bid") Long bid) {
    List<Category> list = this.categoryService.queryByBrandId(bid);
    if (list == null || list.size() < 1) {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(list);
}

Service

public List<Category> queryByBrandId(Long bid) {
    return this.categoryMapper.queryByBrandId(bid);
}

mapper

因爲需要通過中間表進行子查詢,所以這裏要手寫Sql:

/**
     * 根據品牌id查詢商品分類
     * @param bid
     * @return
     */
@Select("SELECT * FROM tb_category WHERE id IN (SELECT category_id FROM tb_category_brand WHERE brand_id = #{bid})")
List<Category> queryByBrandId(Long bid);

4.3.2.前臺查詢分類並渲染

我們在編輯頁面打開之前,先把數據查詢完畢:

editBrand(oldBrand){
    // 根據品牌信息查詢商品分類
    this.$http.get("/item/category/bid/" + oldBrand.id)
        .then(({data}) => {
        // 控制彈窗可見:
        this.dialog = true;
        // 獲取要編輯的brand
        this.oldBrand = oldBrand
        // 回顯商品分類
        this.oldBrand.categories = data;
    })
}

再次測試:數據成功回顯了

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rTUIjo7d-1586523104252)(assets/1526222999115.png)]

4.3.3.新增窗口數據干擾

但是,此時卻產生了新問題:新增窗口竟然也有數據?

原因:

如果之前打開過編輯,那麼在父組件中記錄的oldBrand會保留。下次再打開窗口,如果是編輯窗口到沒問題,但是新增的話,就會再次顯示上次打開的品牌信息了。

解決:

新增窗口打開前,把數據置空。
addBrand() {
    // 控制彈窗可見:
    this.dialog = true;
    // 把oldBrand變爲null
    this.oldBrand = null;
}

4.3.4.提交表單時判斷是新增還是修改

新增和修改是同一個頁面,我們該如何判斷?

父組件中點擊按鈕彈出新增或修改的窗口,因此父組件非常清楚接下來是新增還是修改。

因此,最簡單的方案就是,在父組件中定義變量,記錄新增或修改狀態,當彈出頁面時,把這個狀態也傳遞給子組件。

第一步:在父組件中記錄狀態:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ysy8Gru3-1586523104252)(assets/1526224372366.png)]

第二步:在新增和修改前,更改狀態:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mKAff7gW-1586523104252)(assets/1526224447288.png)]

第三步:傳遞給子組件

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-v9Ek5str-1586523104253)(assets/1526224495244.png)]

第四步,子組件接收標記:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Npn9zCVH-1586523104253)(assets/1526224563838.png)]

標題的動態化:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-B0mpvjbr-1586523104253)(assets/1526224628514.png)]

表單提交動態:

axios除了除了get和post外,還有一個通用的請求方式:

// 將數據提交到後臺
// this.$http.post('/item/brand', this.$qs.stringify(params))
this.$http({
    method: this.isEdit ? 'put' : 'post', // 動態判斷是POST還是PUT
    url: '/item/brand',
    data: this.$qs.stringify(this.brand)
}).then(() => {
    // 關閉窗口
    this.$emit("close");
    this.$message.success("保存成功!");
})
    .catch(() => {
    this.$message.error("保存失敗!");
});

5.刪除(作業)

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