前提:启动renren-fast前后端

路由设置

这一节演示了如何通过renren-fast-vue新建组件和导航目录菜单

1. 商品系统新增侧边导航目录

image.png

2. 商品系统新增分类维护菜单

image.png
之后,在分类维护中维护商品的三级分类。

3. 前端脚手架路由规范

当点击侧边栏目录新增的分类维护时,会跳转到 /product-category,对应新增菜单设置的路由 product/category.
image.png
页面:对应前端项目 src->views->modules->product->category.vue 页面组件

API网关配置和跨域配置

这一节会解决一个前端向多个后端服务请求的问题(API网关),此外还有网关服务与前端前后分离的跨域问题。

1. 前端工程配置API网关作为唯一接口

打开 static->config->index.js 配置统一请求地址

  1. // api网关作为接口请求地址,由网关分发到不同服务
  2. window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

2. 将renren-fast接入网关服务配置

这里是因为配置第一步的网关地址后,renren-fast-vue 本身要请求到 renren-fast 的请求也会转到网关,所以这里要配置网关转发到renren-fast的接口。

将renren-fast注册为nacos服务

  1. 引入 common依赖
  2. 配置nacos地址,

    1. spring:
    2. application:
    3. name: renren-fast
    4. cloud:
    5. nacos:
    6. discovery:
    7. server-addr: 192.168.163.131:8848
  3. 主启动类增加开启注解。

    1. @EnableDiscoveryClient
    2. @SpringBootApplication
    3. public class RenrenApplication {
    4. public static void main(String[] args) {
    5. SpringApplication.run(RenrenApplication.class, args);
    6. }
    7. }

    网关增加路由断言转发不同服务

    mall-gateway application.yaml
    前端 /api/** 请求会被转发到renren-fast服务 /renren-fast/**
    前端 /api/product/** 请求会被转发到mall-product服务 /product/**

    1. spring:
    2. cloud:
    3. gateway:
    4. routes:
    5. - id: product_route
    6. uri: lb://mall-product
    7. predicates:
    8. - Path=/api/product/**
    9. filters:
    10. - RewritePath=/api/(?<segment>.*),/$\{segment}
    11. - id: admin_route
    12. uri: lb://renren-fast
    13. predicates:
    14. - Path=/api/**
    15. filters:
    16. - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

    3. 网关服务配置跨域

    1. @Configuration
    2. public class MallCorsConfiguration {
    3. @Bean // 添加过滤器
    4. public CorsWebFilter corsWebFilter(){
    5. // 基于url跨域,选择reactive包下的
    6. UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
    7. // 跨域配置信息
    8. CorsConfiguration corsConfiguration = new CorsConfiguration();
    9. // 允许跨域的头
    10. corsConfiguration.addAllowedHeader("*");
    11. // 允许跨域的请求方式
    12. corsConfiguration.addAllowedMethod("*");
    13. // 允许跨域的请求来源
    14. corsConfiguration.addAllowedOrigin("*");
    15. // 是否允许携带cookie跨域
    16. corsConfiguration.setAllowCredentials(true);
    17. // 任意url都要进行跨域配置
    18. source.registerCorsConfiguration("/**",corsConfiguration);
    19. return new CorsWebFilter(source);
    20. }
    21. }

    如果springboot版本高于2.4的话 corsConfiguration.addAllowedOrigin(““);要替换成corsConfiguration.addAllowedOriginPattern(““);

renren-fast跨域配置删除

删除 src/main/java/io/renren/config/CorsConfig.java 中的配置内容
这里会导致请求头添加重复,导致跨域失败

Access to XMLHttpRequest at ‘http://localhost:88/api/sys/login‘ from origin ‘http://localhost:8001‘ has been blocked by CORS policy: The ‘Access-Control-Allow-Origin’ header contains multiple values ‘http://localhost:8001, http://localhost:8001‘, but only one is allowed.

前端页面

  1. <template>
  2. <div>
  3. <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>
  4. <el-button v-if="draggable" @click="batchSave">批量保存</el-button>
  5. <el-button type="danger" @click="batchDelete">批量删除</el-button>
  6. <el-tree
  7. :data="menus"
  8. :props="defaultProps"
  9. :expand-on-click-node="false"
  10. show-checkbox
  11. node-key="catId"
  12. :default-expanded-keys="expandedKey"
  13. :draggable="draggable"
  14. :allow-drop="allowDrop"
  15. @node-drop="handleDrop"
  16. ref="menuTree"
  17. >
  18. <span class="custom-tree-node" slot-scope="{ node, data }">
  19. <span>{{ node.label }}</span>
  20. <span>
  21. <el-button
  22. v-if="node.level <=2"
  23. type="text"
  24. size="mini"
  25. @click="() => append(data)"
  26. >Append</el-button>
  27. <el-button type="text" size="mini" @click="edit(data)">edit</el-button>
  28. <el-button
  29. v-if="node.childNodes.length==0"
  30. type="text"
  31. size="mini"
  32. @click="() => remove(node, data)"
  33. >Delete</el-button>
  34. </span>
  35. </span>
  36. </el-tree>
  37. <el-dialog
  38. :title="title"
  39. :visible.sync="dialogVisible"
  40. width="30%"
  41. :close-on-click-modal="false"
  42. >
  43. <el-form :model="category">
  44. <el-form-item label="分类名称">
  45. <el-input v-model="category.name" autocomplete="off"></el-input>
  46. </el-form-item>
  47. <el-form-item label="图标">
  48. <el-input v-model="category.icon" autocomplete="off"></el-input>
  49. </el-form-item>
  50. <el-form-item label="计量单位">
  51. <el-input v-model="category.productUnit" autocomplete="off"></el-input>
  52. </el-form-item>
  53. </el-form>
  54. <span slot="footer" class="dialog-footer">
  55. <el-button @click="dialogVisible = false">取 消</el-button>
  56. <el-button type="primary" @click="submitData">确 定</el-button>
  57. </span>
  58. </el-dialog>
  59. </div>
  60. </template>
  61. <script>
  62. //这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
  63. //例如:import 《组件名称》 from '《组件路径》';
  64. export default {
  65. //import引入的组件需要注入到对象中才能使用
  66. components: {},
  67. props: {},
  68. data() {
  69. return {
  70. pCid: [],
  71. draggable: false,
  72. updateNodes: [],
  73. maxLevel: 0,
  74. title: "",
  75. dialogType: "", //edit,add
  76. category: {
  77. name: "",
  78. parentCid: 0,
  79. catLevel: 0,
  80. showStatus: 1,
  81. sort: 0,
  82. productUnit: "",
  83. icon: "",
  84. catId: null
  85. },
  86. dialogVisible: false,
  87. menus: [],
  88. expandedKey: [],
  89. defaultProps: {
  90. children: "children",
  91. label: "name"
  92. }
  93. };
  94. },
  95. //计算属性 类似于data概念
  96. computed: {},
  97. //监控data中的数据变化
  98. watch: {},
  99. //方法集合
  100. methods: {
  101. getMenus() {
  102. this.$http({
  103. url: this.$http.adornUrl("/product/category/list/tree"),
  104. method: "get"
  105. }).then(({data}) => {
  106. console.log("成功获取到菜单数据...", data.data);
  107. this.menus = data.data;
  108. });
  109. },
  110. batchDelete() {
  111. let catIds = [];
  112. let checkedNodes = this.$refs.menuTree.getCheckedNodes();
  113. console.log("被选中的元素", checkedNodes);
  114. for (let i = 0; i < checkedNodes.length; i++) {
  115. catIds.push(checkedNodes[i].catId);
  116. }
  117. this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {
  118. confirmButtonText: "确定",
  119. cancelButtonText: "取消",
  120. type: "warning"
  121. })
  122. .then(() => {
  123. this.$http({
  124. url: this.$http.adornUrl("/product/category/delete"),
  125. method: "post",
  126. data: this.$http.adornData(catIds, false)
  127. }).then(({data}) => {
  128. this.$message({
  129. message: "菜单批量删除成功",
  130. type: "success"
  131. });
  132. this.getMenus();
  133. });
  134. })
  135. .catch(() => {
  136. });
  137. },
  138. batchSave() {
  139. this.$http({
  140. url: this.$http.adornUrl("/product/category/update/sort"),
  141. method: "post",
  142. data: this.$http.adornData(this.updateNodes, false)
  143. }).then(({data}) => {
  144. this.$message({
  145. message: "菜单顺序等修改成功",
  146. type: "success"
  147. });
  148. //刷新出新的菜单
  149. this.getMenus();
  150. //设置需要默认展开的菜单
  151. this.expandedKey = this.pCid;
  152. this.updateNodes = [];
  153. this.maxLevel = 0;
  154. // this.pCid = 0;
  155. });
  156. },
  157. handleDrop(draggingNode, dropNode, dropType, ev) {
  158. console.log("handleDrop: ", draggingNode, dropNode, dropType);
  159. //1、当前节点最新的父节点id
  160. let pCid = 0;
  161. let siblings = null;
  162. if (dropType == "before" || dropType == "after") {
  163. pCid =
  164. dropNode.parent.data.catId == undefined
  165. ? 0
  166. : dropNode.parent.data.catId;
  167. siblings = dropNode.parent.childNodes;
  168. } else {
  169. pCid = dropNode.data.catId;
  170. siblings = dropNode.childNodes;
  171. }
  172. this.pCid.push(pCid);
  173. //2、当前拖拽节点的最新顺序,
  174. for (let i = 0; i < siblings.length; i++) {
  175. if (siblings[i].data.catId == draggingNode.data.catId) {
  176. //如果遍历的是当前正在拖拽的节点
  177. let catLevel = draggingNode.level;
  178. if (siblings[i].level != draggingNode.level) {
  179. //当前节点的层级发生变化
  180. catLevel = siblings[i].level;
  181. //修改他子节点的层级
  182. this.updateChildNodeLevel(siblings[i]);
  183. }
  184. this.updateNodes.push({
  185. catId: siblings[i].data.catId,
  186. sort: i,
  187. parentCid: pCid,
  188. catLevel: catLevel
  189. });
  190. } else {
  191. this.updateNodes.push({catId: siblings[i].data.catId, sort: i});
  192. }
  193. }
  194. //3、当前拖拽节点的最新层级
  195. console.log("updateNodes", this.updateNodes);
  196. },
  197. updateChildNodeLevel(node) {
  198. if (node.childNodes.length > 0) {
  199. for (let i = 0; i < node.childNodes.length; i++) {
  200. var cNode = node.childNodes[i].data;
  201. this.updateNodes.push({
  202. catId: cNode.catId,
  203. catLevel: node.childNodes[i].level
  204. });
  205. this.updateChildNodeLevel(node.childNodes[i]);
  206. }
  207. }
  208. },
  209. allowDrop(draggingNode, dropNode, type) {
  210. //1、被拖动的当前节点以及所在的父节点总层数不能大于3
  211. //1)、被拖动的当前节点总层数
  212. console.log("allowDrop:", draggingNode, dropNode, type);
  213. //
  214. this.countNodeLevel(draggingNode);
  215. //当前正在拖动的节点+父节点所在的深度不大于3即可
  216. let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
  217. console.log("深度:", deep);
  218. // this.maxLevel
  219. if (type == "inner") {
  220. // console.log(
  221. // `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
  222. // );
  223. return deep + dropNode.level <= 3;
  224. } else {
  225. return deep + dropNode.parent.level <= 3;
  226. }
  227. },
  228. countNodeLevel(node) {
  229. //找到所有子节点,求出最大深度
  230. if (node.childNodes != null && node.childNodes.length > 0) {
  231. for (let i = 0; i < node.childNodes.length; i++) {
  232. if (node.childNodes[i].level > this.maxLevel) {
  233. this.maxLevel = node.childNodes[i].level;
  234. }
  235. this.countNodeLevel(node.childNodes[i]);
  236. }
  237. }
  238. },
  239. edit(data) {
  240. console.log("要修改的数据", data);
  241. this.dialogType = "edit";
  242. this.title = "修改分类";
  243. this.dialogVisible = true;
  244. //发送请求获取当前节点最新的数据
  245. this.$http({
  246. url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
  247. method: "get"
  248. }).then(({data}) => {
  249. //请求成功
  250. console.log("要回显的数据", data);
  251. this.category.name = data.data.name;
  252. this.category.catId = data.data.catId;
  253. this.category.icon = data.data.icon;
  254. this.category.productUnit = data.data.productUnit;
  255. this.category.parentCid = data.data.parentCid;
  256. this.category.catLevel = data.data.catLevel;
  257. this.category.sort = data.data.sort;
  258. this.category.showStatus = data.data.showStatus;
  259. /**
  260. * parentCid: 0,
  261. catLevel: 0,
  262. showStatus: 1,
  263. sort: 0,
  264. */
  265. });
  266. },
  267. append(data) {
  268. console.log("append", data);
  269. this.dialogType = "add";
  270. this.title = "添加分类";
  271. this.dialogVisible = true;
  272. this.category.parentCid = data.catId;
  273. this.category.catLevel = data.catLevel * 1 + 1;
  274. this.category.catId = null;
  275. this.category.name = "";
  276. this.category.icon = "";
  277. this.category.productUnit = "";
  278. this.category.sort = 0;
  279. this.category.showStatus = 1;
  280. },
  281. submitData() {
  282. if (this.dialogType == "add") {
  283. this.addCategory();
  284. }
  285. if (this.dialogType == "edit") {
  286. this.editCategory();
  287. }
  288. },
  289. //修改三级分类数据
  290. editCategory() {
  291. var {catId, name, icon, productUnit} = this.category;
  292. this.$http({
  293. url: this.$http.adornUrl("/product/category/update"),
  294. method: "post",
  295. data: this.$http.adornData({catId, name, icon, productUnit}, false)
  296. }).then(({data}) => {
  297. this.$message({
  298. message: "菜单修改成功",
  299. type: "success"
  300. });
  301. //关闭对话框
  302. this.dialogVisible = false;
  303. //刷新出新的菜单
  304. this.getMenus();
  305. //设置需要默认展开的菜单
  306. this.expandedKey = [this.category.parentCid];
  307. });
  308. },
  309. //添加三级分类
  310. addCategory() {
  311. console.log("提交的三级分类数据", this.category);
  312. this.$http({
  313. url: this.$http.adornUrl("/product/category/save"),
  314. method: "post",
  315. data: this.$http.adornData(this.category, false)
  316. }).then(({data}) => {
  317. this.$message({
  318. message: "菜单保存成功",
  319. type: "success"
  320. });
  321. //关闭对话框
  322. this.dialogVisible = false;
  323. //刷新出新的菜单
  324. this.getMenus();
  325. //设置需要默认展开的菜单
  326. this.expandedKey = [this.category.parentCid];
  327. });
  328. },
  329. remove(node, data) {
  330. var ids = [data.catId];
  331. this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
  332. confirmButtonText: "确定",
  333. cancelButtonText: "取消",
  334. type: "warning"
  335. })
  336. .then(() => {
  337. this.$http({
  338. url: this.$http.adornUrl("/product/category/delete"),
  339. method: "post",
  340. data: this.$http.adornData(ids, false)
  341. }).then(({data}) => {
  342. this.$message({
  343. message: "菜单删除成功",
  344. type: "success"
  345. });
  346. //刷新出新的菜单
  347. this.getMenus();
  348. //设置需要默认展开的菜单
  349. this.expandedKey = [node.parent.data.catId];
  350. });
  351. })
  352. .catch(() => {
  353. });
  354. console.log("remove", node, data);
  355. }
  356. },
  357. //生命周期 - 创建完成(可以访问当前this实例)
  358. created() {
  359. this.getMenus();
  360. },
  361. //生命周期 - 挂载完成(可以访问DOM元素)
  362. mounted() {
  363. },
  364. beforeCreate() {
  365. }, //生命周期 - 创建之前
  366. beforeMount() {
  367. }, //生命周期 - 挂载之前
  368. beforeUpdate() {
  369. }, //生命周期 - 更新之前
  370. updated() {
  371. }, //生命周期 - 更新之后
  372. beforeDestroy() {
  373. }, //生命周期 - 销毁之前
  374. destroyed() {
  375. }, //生命周期 - 销毁完成
  376. activated() {
  377. } //如果页面有keep-alive缓存功能,这个函数会触发
  378. };
  379. </script>
  380. <style scoped>
  381. </style>