1.递归查询树形结构
@Overridepublic List<CategoryEntity> listWithTree() {//1.查出所有分类List<CategoryEntity> all = this.list();//2.组装成父子的属性结构List<CategoryEntity> level1Menus = all.stream().filter(c -> c.getParentCid().equals(0L)).map(categoryEntity -> categoryEntity.setChildren(getChildrenCategory(all,categoryEntity.getCatId())))//大于放后面 升序.sorted(Comparator.comparing(CategoryEntity::getSort)).collect(Collectors.toList());return level1Menus;}private List<CategoryEntity> getChildrenCategory(List<CategoryEntity> allList, long pCatId) {List<CategoryEntity> collect = allList.stream().filter(a -> a.getParentCid() == pCatId).sorted(Comparator.comparing(CategoryEntity::getSort)).collect(Collectors.toList());if (collect.isEmpty()) {return new ArrayList<>();} else {for (CategoryEntity categoryEntity : collect) {Long catId = categoryEntity.getCatId();List<CategoryEntity> childrenCategory = getChildrenCategory(allList, catId);categoryEntity.setChildren(childrenCategory);}return collect;}}
2.网关统一配置跨域问题
2.1添加 网关过滤器
package com.jmj.gulimall.gateway.config;import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.reactive.CorsWebFilter;@Configuration
public class GulimallCorsConfiguration {//网关gateway是使用webflex 进行编程的 响应式 所以用的都是reactive 包下@Beanpublic CorsWebFilter corsWebFilter() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();CorsConfiguration corsConfiguration = new CorsConfiguration();//1.配置跨域corsConfiguration.addAllowedHeader("*");//允许哪些头跨域corsConfiguration.addAllowedMethod("*");//允许哪些请求跨域corsConfiguration.addAllowedOrigin("*");//允许哪些请求来源 跨域corsConfiguration.setAllowCredentials(true);//是否允许携带cookie进行跨域 允许 否则跨域请求将会丢失一些相关cookie信息source.registerCorsConfiguration("/**", corsConfiguration);return new CorsWebFilter(source);}}
但是我们发现还是出现了问题
这是因为网关配置了跨域,而网关转发的微服务也配置了跨域,所以返回了两个响应头被允许,
但是浏览器只希望有一个,所以报错,这时,我们只需要去把微服务的跨域注释掉就好了。
这是人人fast 服务的跨域配置,也是Spring MVC的 配置
/*** Copyright (c) 2016-2019 人人开源 All rights reserved.* <p>* https://www.renren.io* <p>* 版权所有,侵权必究!*/package io.renren.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowCredentials(true).allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").maxAge(3600);}
}
这是网关的路由,是有优先级的,从上优先级最高,如果匹配不上就会依次遍历。
如果不调换优先级,路径会被网关转发到 renrenfast 服务当中,导致没有token 返回invalid token
spring:cloud:gateway:routes:- id: test_routeuri: http://www.baidu.compredicates:#访问的路径就是 如果是/hello?url=baidu 就转发到 https://www.baidu.com/hello?url=baidu- Query=url,baidu- id: test1_routeuri: http://www.hao123.compredicates:- Query=url,hao123- id: product_routuri: lb://gulimall-productpredicates:- Path=/api/product/**filters:- RewritePath=/api/(?<segment>.*),/$\{segment}- id: admin_routeuri: lb://renren-fastpredicates:- Path=/api/**filters:- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}##前端项目,/api
3.拖拽修改前端代码
<!-- -->
<template><div><el-switch v-model="isDraggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch><el-tree:data="menus":props="defaultProps"show-checkboxnode-key="catId":expand-on-click-node="false":default-expanded-keys="expandedkey"@node-expand="expend"@node-collapse="nodeClose":draggable="isDraggable":allow-drop="allowDrop"@node-drop="handleDrop"><span class="custom-tree-node" slot-scope="{ node, data }"><!-- 插槽,代替了原来的 label 里所显示的内容 将插槽内容显示在原来的每个结点上面 --><span>{{ node.label }}</span><span><el-buttonv-if="node.level <= 2"type="text"size="mini"@click="() => append(data)">Append</el-button><el-button type="text" size="mini" @click="edit(data)">Edit</el-button><el-buttonv-if="node.childNodes.length == 0"type="text"size="mini"@click="() => remove(node, data)">Delete</el-button></span></span></el-tree><el-dialog:title="submitTitle":visible.sync="dialogVisible":close-on-click-modal="false"width="30%"><el-form :model="category"><el-form-item label="分类名称"><el-input v-model="category.name" autocomplete="off"></el-input></el-form-item><el-form-item label="图标"><el-input v-model="category.icon" autocomplete="off"></el-input></el-form-item><el-form-item label="计量单位"><el-inputv-model="category.productUnit"autocomplete="off"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="cancelForm">取 消</el-button><el-button type="primary" @click="submitForm">确 定</el-button></div></el-dialog></div>
</template><script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
class UpdateCategoryData {constructor(catId, parentCid, sort, catLevel) {this.catId = catId;this.parentCid = parentCid;this.sort = sort;this.catLevel = catLevel;}
}
export default {//import引入的组件需要注入到对象中才能使用components: {},props: {},data() {return {isDraggable: false,submitType: "",submitTitle: "",dialogVisible: false,menus: [],defaultProps: {children: "children",label: "name",},expandedkey: [],copyCategory: {},category: {catId: null,name: "",parentCid: 0,catLevel: 0,showStatus: 1,productUnit: "",icon: "",sort: 0,},};},//监听属性 类似于data概念computed: {},//监控data中的数据变化watch: {},//方法集合methods: {/**** @param {*} draggingNode 拖拽的节点* @param {*} dropNode 拖拽到的哪个节点* @param {*} dropType 拖拽类型, 前后或者 内部* @param {*} event 事件对象*/handleDrop(draggingNode, dropNode, dropType, event) {console.log(draggingNode, dropNode, dropType);//1.当前节点最新的父节点Id,let updateArray = new Array();let draggingNodeLevel = draggingNode.level;if (dropType == "inner") {let dropNodeId = dropNode.data.catId;let dropNodeLevel = dropNode.level;let childrenNew = dropNode.data.children;for (const index in childrenNew) {let { catId } = childrenNew[index];let updateCategoryData = new UpdateCategoryData(catId,dropNodeId,index,dropNodeLevel + 1);updateArray.push(updateCategoryData);}let div = dropNodeLevel + 1 - draggingNodeLevel;//递归把子节点都遍历完if (div != 0) {this.recursivelyTraverseChildNodes(draggingNode.data.children,div,updateArray);}} else {//往前插入节点或者后插入节点let parentLevel = dropNode.parent.level;console.log(parentLevel);let parentChildrenArr = {};if (parentLevel == 0) {parentChildrenArr = dropNode.parent.data;} else {parentChildrenArr = dropNode.parent.data.children;}let parentCid = dropNode.data.parentCid;for (const index in parentChildrenArr) {let { catId } = parentChildrenArr[index];let updateCategoryData = new UpdateCategoryData(catId,parentCid,index,parentLevel + 1);updateArray.push(updateCategoryData);}let div = parentLevel + 1 - draggingNodeLevel;console.log("parentLevel", parentLevel);console.log("draggingNodeLevel", draggingNodeLevel);//递归把子节点都遍历完if (div != 0) {this.recursivelyTraverseChildNodes(draggingNode.data.children,div,updateArray);}}console.log(updateArray);//发送http请求修改this.$http({url: this.$http.adornUrl("/product/category/updateList"),method: "post",data: this.$http.adornData(updateArray, false),}).then(({ data }) => {this.success("修改位置与排序成功");this.getMenus();});},recursivelyTraverseChildNodes(children, div, arr) {if (children == null || children.length == 0) {//没有子节点了return;} else {for (const child of children) {let updateCategoryData = new UpdateCategoryData(child.catId,child.parentCid,child.sort,child.catLevel + div);arr.push(updateCategoryData);this.recursivelyTraverseChildNodes(child.children, div, arr);}}},allowDrop(draggingNode, dropNode, type) {//1、被拖动的当前节点以及所在的父节点总层数不能大于3console.log("allowDrop", draggingNode, dropNode, type);let level = this.countNodeLevel(draggingNode.data,draggingNode.data.catLevel);if (type == "inner") {if (dropNode.level + level <= 3) {return true;} else {return false;}}if (type == "next") {if (dropNode.parent.level + level <= 3) {return true;} else {return false;}}if (type == "prev") {if (dropNode.parent.level + level <= 3) {return true;} else {return false;}}//1)被拖动的当前节点总层数return false;},//递归查找该节点加上子类最深层级一共有几层;包含自己算countNodeLevel(node, l) {let children = node.children;let levelMax = 0;if (children.length == 0) {return node.catLevel - l + 1;} else {for (let child of children) {let level = this.countNodeLevel(child, l);if (level > levelMax) {levelMax = level;}}}return levelMax;},resetCategory() {Object.assign(this.category, this.copyCategory);//如果你希望在 console.log 输出的时候看到对象的当前状态,//你可以在赋值操作之前进行 console.log,或者使用对象解构等方法创建一个新的对象进行输出,以确保输出的是当前状态的副本而不是对象的引用。let categoryre = {};Object.assign(categoryre, this.category);console.log("执行了重置", categoryre);},submitForm() {if (this.submitType == "add") {this.addCategory();}if (this.submitType == "edit") {this.updateCategory();}},updateCategory() {this.$http({url: this.$http.adornUrl("/product/category/update"),method: "post",data: this.$http.adornData(this.category, false),}).then(({ data }) => {this.success("修改成功");this.getMenus();this.resetCategory();this.dialogVisible = false;});},edit(data) {this.submitType = "edit";this.submitTitle = "修改分类菜单";console.log("正在修改数据", data);this.dialogVisible = true;//发送http请求获取回显数据this.$http({url: this.$http.adornUrl(`/product/category/info/${data.catId}`),method: "get",}).then(({ data }) => {let categoryEdit = data.data;console.log("回显数据", categoryEdit);this.category.catId = categoryEdit.catId;this.category.name = categoryEdit.name;this.category.parentCid = categoryEdit.parentCid;this.category.catLevel = categoryEdit.catLevel;this.category.showStatus = categoryEdit.showStatus;this.category.productUnit = categoryEdit.productUnit;this.category.icon = categoryEdit.icon;this.category.sort = categoryEdit.sort;console.log("category被回显数据", this.category);});},cancelForm() {this.resetCategory();this.dialogVisible = false;},append(data) {this.resetCategory();console.log("append", this.category);this.category.parentCid = data.catId;this.category.catLevel = data.catLevel + 1;this.category.showStatus = 1;this.category.sort = 0;this.dialogVisible = true;this.submitType = "add";this.submitTitle = "添加分类菜单";},addCategory() {console.log("提交三级分类的数据", this.category);this.$http({url: this.$http.adornUrl("/product/category/save"),method: "post",data: this.$http.adornData(this.category, false),}).then(({ data }) => {this.success("添加分类成功");this.getMenus();});this.resetCategory();this.dialogVisible = false;},remove(node, data) {console.log("remove", node, data);let ids = [data.catId];// console.log(this); //vuethis.$confirm(`是否删除【${data.name}】菜单`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {// console.log(this); //vuethis.$http({url: this.$http.adornUrl("/product/category/delete"),method: "post",data: this.$http.adornData(ids, false),}).then(({ data }) => {// console.log(this); //vuethis.$message({message: "菜单删除成功",type: "success",});// node.visible = false;this.getMenus();//设置需要默认展开的菜单});}).catch(() => {this.$message({message: "取消了删除",type: "warning",});});},expend(data, node, _) {console.log("展开了", node.data.catId);this.expandedkey.push(node.data.catId);},nodeClose(data, node, _) {let id = node.data.catId;console.log("收起了", id);let index = this.expandedkey.indexOf(id);this.expandedkey.splice(index, 1);},getMenus() {this.$http({url: this.$http.adornUrl("/product/category/list/tree"),method: "get",}).then(({ data }) => {console.log("成功获取到菜单数据:", data.data);this.menus = data.data;});},success(msg) {this.$message({message: msg,type: "success",});},error(msg) {this.$message({message: msg,type: "warning",});},},//生命周期 - 创建完成(可以访问当前this实例)created() {this.getMenus();Object.assign(this.copyCategory, this.category);},//生命周期 - 挂载完成(可以访问DOM元素)mounted() {},beforeCreate() {}, //生命周期 - 创建之前beforeMount() {}, //生命周期 - 挂载之前beforeUpdate() {}, //生命周期 - 更新之前updated() {}, //生命周期 - 更新之后beforeDestroy() {}, //生命周期 - 销毁之前destroyed() {}, //生命周期 - 销毁完成activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
4.批量删除
封装复用方法 闭包
<!-- -->
<template><div><el-switchv-model="isDraggable"active-text="开启拖拽"inactive-text="关闭拖拽"></el-switch><el-button type="danger" round @click="batchDelete">批量删除</el-button><el-tree:data="menus":props="defaultProps"show-checkboxnode-key="catId":expand-on-click-node="false":default-expanded-keys="expandedkey"@node-expand="expend"@node-collapse="nodeClose":draggable="isDraggable":allow-drop="allowDrop"@node-drop="handleDrop"ref="menuTree"><span class="custom-tree-node" slot-scope="{ node, data }"><!-- 插槽,代替了原来的 label 里所显示的内容 将插槽内容显示在原来的每个结点上面 --><span>{{ node.label }}</span><span><el-buttonv-if="node.level <= 2"type="text"size="mini"@click="() => append(data)">Append</el-button><el-button type="text" size="mini" @click="edit(data)">Edit</el-button><el-buttonv-if="node.childNodes.length == 0"type="text"size="mini"@click="() => remove(node, data)">Delete</el-button></span></span></el-tree><el-dialog:title="submitTitle":visible.sync="dialogVisible":close-on-click-modal="false"width="30%"><el-form :model="category"><el-form-item label="分类名称"><el-input v-model="category.name" autocomplete="off"></el-input></el-form-item><el-form-item label="图标"><el-input v-model="category.icon" autocomplete="off"></el-input></el-form-item><el-form-item label="计量单位"><el-inputv-model="category.productUnit"autocomplete="off"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="cancelForm">取 消</el-button><el-button type="primary" @click="submitForm">确 定</el-button></div></el-dialog></div>
</template><script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
class UpdateCategoryData {constructor(catId, parentCid, sort, catLevel) {this.catId = catId;this.parentCid = parentCid;this.sort = sort;this.catLevel = catLevel;}
}
export default {//import引入的组件需要注入到对象中才能使用components: {},props: {},data() {return {isDraggable: false,submitType: "",submitTitle: "",dialogVisible: false,menus: [],defaultProps: {children: "children",label: "name",},expandedkey: [],copyCategory: {},category: {catId: null,name: "",parentCid: 0,catLevel: 0,showStatus: 1,productUnit: "",icon: "",sort: 0,},};},//监听属性 类似于data概念computed: {},//监控data中的数据变化watch: {},//方法集合methods: {confirm(msg,success,error){this.$confirm(msg, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {success();}).catch(()=>{error();});},batchDelete() {let deleteArray = this.$refs.menuTree.getCheckedNodes();console.log("被选中的元素", deleteArray);let ids = deleteArray.map((c) => {return c.catId;});console.log("被选中的Id", ids);// this.$confirm(`是否批量删除`, "提示", {// confirmButtonText: "确定",// cancelButtonText: "取消",// type: "warning",// }).then(() => {// this.$http({// url: this.$http.adornUrl("/product/category/delete"),// method: "post",// data: this.$http.adornData(ids, false),// }).then(({ data }) => {// this.success("批量删除成功");// this.getMenus();// });// }).catch((error)=>{// this.error("取消批量删除");// });this.confirm("是否批量删除",()=>{this.$http({url: this.$http.adornUrl("/product/category/delete"),method: "post",data: this.$http.adornData(ids, false),}).then(({ data }) => {this.success("批量删除成功");this.getMenus();});},()=>{this.error("取消批量删除");})},/**** @param {*} draggingNode 拖拽的节点* @param {*} dropNode 拖拽到的哪个节点* @param {*} dropType 拖拽类型, 前后或者 内部* @param {*} event 事件对象*/handleDrop(draggingNode, dropNode, dropType, event) {console.log(draggingNode, dropNode, dropType);//1.当前节点最新的父节点Id,let updateArray = new Array();let draggingNodeLevel = draggingNode.level;if (dropType == "inner") {let dropNodeId = dropNode.data.catId;let dropNodeLevel = dropNode.level;let childrenNew = dropNode.data.children;for (const index in childrenNew) {let { catId } = childrenNew[index];let updateCategoryData = new UpdateCategoryData(catId,dropNodeId,index,dropNodeLevel + 1);updateArray.push(updateCategoryData);}let div = dropNodeLevel + 1 - draggingNodeLevel;//递归把子节点都遍历完if (div != 0) {this.recursivelyTraverseChildNodes(draggingNode.data.children,div,updateArray);}} else {//往前插入节点或者后插入节点let parentLevel = dropNode.parent.level;console.log(parentLevel);let parentChildrenArr = {};if (parentLevel == 0) {parentChildrenArr = dropNode.parent.data;} else {parentChildrenArr = dropNode.parent.data.children;}let parentCid = dropNode.data.parentCid;for (const index in parentChildrenArr) {let { catId } = parentChildrenArr[index];let updateCategoryData = new UpdateCategoryData(catId,parentCid,index,parentLevel + 1);updateArray.push(updateCategoryData);}let div = parentLevel + 1 - draggingNodeLevel;console.log("parentLevel", parentLevel);console.log("draggingNodeLevel", draggingNodeLevel);//递归把子节点都遍历完if (div != 0) {this.recursivelyTraverseChildNodes(draggingNode.data.children,div,updateArray);}}console.log(updateArray);//发送http请求修改this.$http({url: this.$http.adornUrl("/product/category/updateList"),method: "post",data: this.$http.adornData(updateArray, false),}).then(({ data }) => {this.success("修改位置与排序成功");this.getMenus();});},recursivelyTraverseChildNodes(children, div, arr) {if (children == null || children.length == 0) {//没有子节点了return;} else {for (const child of children) {let updateCategoryData = new UpdateCategoryData(child.catId,child.parentCid,child.sort,child.catLevel + div);arr.push(updateCategoryData);this.recursivelyTraverseChildNodes(child.children, div, arr);}}},allowDrop(draggingNode, dropNode, type) {//1、被拖动的当前节点以及所在的父节点总层数不能大于3console.log("allowDrop", draggingNode, dropNode, type);let level = this.countNodeLevel(draggingNode.data,draggingNode.data.catLevel);if (type == "inner") {if (dropNode.level + level <= 3) {return true;} else {return false;}}if (type == "next") {if (dropNode.parent.level + level <= 3) {return true;} else {return false;}}if (type == "prev") {if (dropNode.parent.level + level <= 3) {return true;} else {return false;}}//1)被拖动的当前节点总层数return false;},//递归查找该节点加上子类最深层级一共有几层;包含自己算countNodeLevel(node, l) {let children = node.children;let levelMax = 0;if (children.length == 0) {return node.catLevel - l + 1;} else {for (let child of children) {let level = this.countNodeLevel(child, l);if (level > levelMax) {levelMax = level;}}}return levelMax;},resetCategory() {Object.assign(this.category, this.copyCategory);//如果你希望在 console.log 输出的时候看到对象的当前状态,//你可以在赋值操作之前进行 console.log,或者使用对象解构等方法创建一个新的对象进行输出,以确保输出的是当前状态的副本而不是对象的引用。let categoryre = {};Object.assign(categoryre, this.category);console.log("执行了重置", categoryre);},submitForm() {if (this.submitType == "add") {this.addCategory();}if (this.submitType == "edit") {this.updateCategory();}},updateCategory() {this.$http({url: this.$http.adornUrl("/product/category/update"),method: "post",data: this.$http.adornData(this.category, false),}).then(({ data }) => {this.success("修改成功");this.getMenus();this.resetCategory();this.dialogVisible = false;});},edit(data) {this.submitType = "edit";this.submitTitle = "修改分类菜单";console.log("正在修改数据", data);this.dialogVisible = true;//发送http请求获取回显数据this.$http({url: this.$http.adornUrl(`/product/category/info/${data.catId}`),method: "get",}).then(({ data }) => {let categoryEdit = data.data;console.log("回显数据", categoryEdit);this.category.catId = categoryEdit.catId;this.category.name = categoryEdit.name;this.category.parentCid = categoryEdit.parentCid;this.category.catLevel = categoryEdit.catLevel;this.category.showStatus = categoryEdit.showStatus;this.category.productUnit = categoryEdit.productUnit;this.category.icon = categoryEdit.icon;this.category.sort = categoryEdit.sort;console.log("category被回显数据", this.category);});},cancelForm() {this.resetCategory();this.dialogVisible = false;},append(data) {this.resetCategory();console.log("append", this.category);this.category.parentCid = data.catId;this.category.catLevel = data.catLevel + 1;this.category.showStatus = 1;this.category.sort = 0;this.dialogVisible = true;this.submitType = "add";this.submitTitle = "添加分类菜单";},addCategory() {console.log("提交三级分类的数据", this.category);this.$http({url: this.$http.adornUrl("/product/category/save"),method: "post",data: this.$http.adornData(this.category, false),}).then(({ data }) => {this.success("添加分类成功");this.getMenus();});this.resetCategory();this.dialogVisible = false;},remove(node, data) {console.log("remove", node, data);let ids = [data.catId];// console.log(this); //vuethis.$confirm(`是否删除【${data.name}】菜单`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {// console.log(this); //vuethis.$http({url: this.$http.adornUrl("/product/category/delete"),method: "post",data: this.$http.adornData(ids, false),}).then(({ data }) => {// console.log(this); //vuethis.$message({message: "菜单删除成功",type: "success",});// node.visible = false;this.getMenus();//设置需要默认展开的菜单});}).catch(() => {this.$message({message: "取消了删除",type: "warning",});});},expend(data, node, _) {console.log("展开了", node.data.catId);this.expandedkey.push(node.data.catId);},nodeClose(data, node, _) {let id = node.data.catId;console.log("收起了", id);let index = this.expandedkey.indexOf(id);this.expandedkey.splice(index, 1);},getMenus() {this.$http({url: this.$http.adornUrl("/product/category/list/tree"),method: "get",}).then(({ data }) => {console.log("成功获取到菜单数据:", data.data);this.menus = data.data;});},success(msg) {this.$message({message: msg,type: "success",});},error(msg) {this.$message({message: msg,type: "warning",});},},//生命周期 - 创建完成(可以访问当前this实例)created() {this.getMenus();Object.assign(this.copyCategory, this.category);},//生命周期 - 挂载完成(可以访问DOM元素)mounted() {},beforeCreate() {}, //生命周期 - 创建之前beforeMount() {}, //生命周期 - 挂载之前beforeUpdate() {}, //生命周期 - 更新之前updated() {}, //生命周期 - 更新之后beforeDestroy() {}, //生命周期 - 销毁之前destroyed() {}, //生命周期 - 销毁完成activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>