文章目錄
1.品牌的新增
昨天我們完成了品牌的查詢,接下來就是新增功能。點擊新增品牌按鈕
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
來安裝。不過我們在項目中已經集成了,大家無需安裝:
這個工具的名字:QS,即Query String,請求參數字符串。
什麼是請求參數字符串?例如: name=jack&age=21
QS工具可以便捷的實現 JS的Object與QueryString的轉換。
在我們的項目中,將QS注入到了Vue的原型對象中,我們可以通過this.$qs
來獲取這個工具:
我們將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);
}
}
結構:
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
在上傳文件過程中,我們需要對上傳的內容進行校驗:
- 校驗文件大小
- 校驗文件的媒體類型
- 校驗文件的內容
文件大小在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即可:
@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.上傳和下載流程
上傳
- Client通過Tracker server查找可用的Storage server。
- Tracker server向Client返回一臺可用的Storage server的IP地址和端口號。
- Client直接通過Tracker server返回的IP地址和端口與其中一臺Storage server建立連接並進行文件上傳。
- 上傳完成,Storage server返回Client一個文件ID,文件上傳結束。
下載
- Client通過Tracker server查找要下載文件所在的的Storage server。
- Tracker server向Client返回包含指定文件的某個Storage server的IP地址和端口號。
- Client直接通過Tracker server返回的IP地址和端口與其中一臺Storage server建立連接並指定要下載文件。
- 下載文件成功。
3.4.安裝和使用
3.5.java客戶端
餘慶先生提供了一個Java客戶端,但是作爲一個C程序員,寫的java代碼可想而知。而且已經很久不維護了。
這裏推薦一個開源的FastDFS客戶端,支持最新的SpringBoot2.0。
配置使用極爲簡單,支持連接池,支持自動生成縮略圖,狂拽酷炫吊炸天啊,有木有。
接下來,我們就用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.引入配置類
純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("保存失敗!");
});