总览项目

版本:3.4.0
技术栈:vue3,vueX,vite,yarn

该项目勇于尝试新技术,3.5.0的某个版本将状态管理工具由vuex替换为了Pinia
Vue官方也推荐使用Pinia(甚至官网都把VueX的链接删了hhh)
但状态管理工具上手起来很方便,且本质都是使用LocalStorge等本地存储

编写风格:vue3语法糖
<script setup>里编写代码和组件内容,而不是在export default中编写组件。
这种写法可在vue的官方文档-API-单文件组件中看到,是一种语法糖。
也可以在script标签上声明使用ts。

<script setup lang='ts' name= '组件名'>
interface Tree {
  id: number
  label: string
  children?: Tree[]
}
</script>

声明const类型的引用(ref),来代替曾经在data()中声明变量的方法:

const deptOptions = ref(undefined);
const title = ref("");
const open= ref(false);

const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    userName: undefined,
    phonenumber: undefined,
    status: undefined,
    deptId: undefined
  },
  rules: {
    userName: [{ required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: "用户名称长度必须介于 2 和 20 之间", trigger: "blur" }],
    nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
    password: [{ required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }],
    email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
    phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
  }

});

const { queryParams, form, rules } = toRefs(data);

    /** 新增按钮操作 */
    function handleAdd(row) {
      if (row != null && row.label) {
        form.value.label = row.label;
      } else {
        form.value.label = '顶级分类';
      }
      open.value = true;
      title.value = "添加商品管理";
    }

ES6:const指向对象时类似指针,指向的内存不变,但内部的数据可以变。然后在js中通过变量名.value.属性名的形式改变值,在template中通过变量名.属性名获取值。

  • 类似单例模式?

获得的其实是真正对象的代理(Proxy对象)
let只有局部的作用域,所以声明全局变量时不用

vue教程-createAPP

使用$refs等功能的时候,如果不在组件内,会报错:未定义。要先获得当前实例的代理,然后使用proxy.$refs

const { proxy } = getCurrentInstance();
console.log(form)
ObjectRefImpl {_object: Proxy, _key: 'form', _defaultValue: undefined, __v_isRef: true}
console.log(form.value)
Proxy {searchValue: null, createBy: null, createTime: '2022-04-27 02:37:03', updateBy: null, updateTime: null,}

vite

ruoyi-cloud-vue3使用了vite、yarn
vite热部署?

  • 哪些变化可以触发热部署?比如改了label,就刷新才行

vite

vite.config.js

import { defineConfig, loadEnv } from 'vite'
import path from 'path'
import createVitePlugins from './vite/plugins'

// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd())
  const { VITE_APP_ENV } = env
  return {
    // 部署生产环境和开发环境下的URL。
    // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
    // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
    base: VITE_APP_ENV === 'production' ? '/ruoyi/' : '/',
    plugins: createVitePlugins(env, command === 'build'),
    resolve: {
      // https://cn.vitejs.dev/config/#resolve-alias
      alias: {
        // 设置路径
        '~': path.resolve(__dirname, './'),
        // 设置别名
        '@': path.resolve(__dirname, './src')
      },
      // https://cn.vitejs.dev/config/#resolve-extensions
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
    },
    // vite 相关配置
    server: {
      port: 80,
      host: true,
      open: true,
      proxy: {
        // https://cn.vitejs.dev/config/#server-proxy
        '/dev-api': {
          target: 'http://localhost:8080',
          changeOrigin: true,
          rewrite: (p) => p.replace(/^\/dev-api/, '')
        }
      }
    },
    //fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file
    css: {
      postcss: {
        plugins: [
          {
            postcssPlugin: 'internal:charset-removal',
            AtRule: {
              charset: (atRule) => {
                if (atRule.name === 'charset') {
                  atRule.remove();
                }
              }
            }
          }
        ]
      }
    }
  }
})

环境变量:

生产环境:.env.prop
开发环境:.env.development
环境:
然后通过import 导入(貌似vite.config.js直接用env)

网络请求:封装axios

request.js:axios工厂模式创造实例、设置拦截器等。

// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: import.meta.env.VITE_APP_BASE_API,
  // 超时
  timeout: 10000
})
。。。
export default service

ES6:export default 默认暴漏的对象。和单纯的export相比,当其他文件用import导入的名字不用和export导出时的名字一一对应,也可以自己起别名,如:

import request from '@/utils/request'

导航栏是前端的,但左侧菜单栏是从后端拿到的。拿到后还要根据自己权限来看是否要渲染

  • 权限保存到前端哪儿了?

路由:

util/ruoyi.js

/**
 * 构造树型结构数据
 * @param {*} data 数据源
 * @param {*} id id字段 默认 'id'
 * @param {*} parentId 父节点字段 默认 'parentId'
 * @param {*} children 孩子节点字段 默认 'children'
 */
export function handleTree(data, id, parentId, children) {
  let config = {
    id: id || 'id',
    parentId: parentId || 'parentId',
    childrenList: children || 'children'
  };

  var childrenListMap = {};
  var nodeIds = {};
  var tree = [];

  for (let d of data) {
    let parentId = d[config.parentId];
    if (childrenListMap[parentId] == null) {
      childrenListMap[parentId] = [];
    }
    nodeIds[d[config.id]] = d;
    childrenListMap[parentId].push(d);
  }

  for (let d of data) {
    let parentId = d[config.parentId];
    if (nodeIds[parentId] == null) {
      tree.push(d);
    }
  }

  for (let t of tree) {
    adaptToChildrenList(t);
  }

  function adaptToChildrenList(o) {
    if (childrenListMap[o[config.id]] !== null) {
      o[config.childrenList] = childrenListMap[o[config.id]];
    }
    if (o[config.childrenList]) {
      for (let c of o[config.childrenList]) {
        adaptToChildrenList(c);
      }
    }
  }
  return tree;
}
  • //用了||的写法来设定默认值,巧妙。看来默认是左边?
  • nodeIds默认是对象也可以数组,直接用[ ] ?
  • [ ]

前端流程梳理

项目结构:

  • src
    • api 为每个功能模块分别封装网络接口
    • components 复用的组件
    • layout 整体的页面框架,如菜单栏、tabbar、主体部分
    • views 主题部份内的页面
      • error
      • system等各个模块
    • utils
    • store
    • router
  • App.vue
  • permission.js
  • main.js
  • setting.js

页面显示流程

App.vue,内有router-view,在里面渲染router.js中配置的第一个路由,即

path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]

layout是前端整体框架,包含导航栏、菜单栏、appmain

  • 这里userouter等都是灰色的,是不需要了嘛?封装到main.js了吗?

因为菜单栏从后端拿(根据用户动态显示),路由后面只配置了导航栏的

@/views/redirect/index.vue,好疑惑的写法

<template>
  <div></div>
</template>

<script setup>
import { useRoute, useRouter } from 'vue-router'

const route = useRoute();
const router = useRouter();
const { params, query } = route
const { path } = params

router.replace({ path: '/' + path, query })
</script>

似乎是自动跳转并且带参数
默认跳转到ocalhost/login?redirect=/index
index在后面有配置,也是Layout组件。应该是想跳转到index,没登陆被拦截到了login(看参数redirect=/index猜的)

Q:useRoute干啥的,他咋知道参数是redirect=/index?
A:vue自带的拿参数的。 params, query是想用两个名字而已

index的路由

    path: '',
    component: Layout,
    redirect: '/index',
    children: [
      {
        path: '/index',
        component: () => import('@/views/index'),
        name: 'Index',
        meta: { title: '首页', icon: 'dashboard', affix: true }
      }
    ]

Layout的appmain组件放主体内容。
AppMain.vue组件中用

 <component :is="Component" :key="route.path"/>

通过子路由加载@/views/index
应该是同通过改变route.path改变主题部分的内容。一开始传入的是index。这里也就是一开始放着若依一大堆简介的地方
以后路由跳转时,route.path也会动态改变,达到改变appmian中component标签的作用

登录态判断

在根目录permisson.js中(好多primission.js),定义router.beforeEach方法,在跳转前判断是否有token。要跳转的路径在router.beforeEach方法的参数next方法里跳转
如果本地没有token,就看是否白名单,白名单内放行不然跳转到登录

如果有token,
设置title

to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
  • 如果去login,登录了还去干啥,next给他改成/
  • 如果不去login
     if (store.getters.roles.length === 0) {
          isRelogin.show = true
          // 判断当前用户是否已拉取完user_info信息
          store.dispatch('GetInfo').then(() => {
            isRelogin.show = false
            store.dispatch('GenerateRoutes').then(accessRoutes => {
              // 根据roles权限生成可访问的路由表
              accessRoutes.forEach(route => {
                if (!isHttp(route.path)) {
                  router.addRoute(route) // 动态添加可访问路由表
                }
              })
              next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
            })
          }).catch(err => {
            store.dispatch('LogOut').then(() => {
              ElMessage.error(err)
              next({ path: '/' })
            })
          })
    
    api里的login、getInfo等方法只负责发请求,用store调用,然后store把数据存在本地。
    用store获取用户信息(角色等)、生成路由,都只有每次路由跳转时做

auth.js只负责GetToken之类的

动态渲染侧边栏菜单

根据用户权限、角色动态渲染,发生在router.beforeEach方法中,拿到用户信息后

// 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue')

modules是一个数组。然后export了loadView方法,根据传入的view返回.vue文件

大致思路:

修改store中的sidebarRoutes数组
然后layou页面的sideBar组件会根据sidebarRoutes数组动态渲染标签
根据sidebarRoutes数组动态渲染多个siderbarItem
通过siderbarItem的item属性将当前sidebarRoutes的值传入
siderbarItem如果要渲染子菜单,就在自己里面渲染siderbarItem

根目录下setting.js和layout界面框架

其实是对应的

 /**
   * 是否显示顶部导航
   */
  topNav: true,
  • src
    • layout
    • appmain 除去导航栏和侧边导航栏的主体部分,views内容在这里
    • navbar 默认固定上放的导航栏
    • tagsview appmain上方的小标签,类似浏览器新页面的标签的那个
    • sidebar 左侧边栏
      • sidebarItem
      • logo
    • index.vue 包含didebar、navbar、appmain

navbar

在这里插入图片描述从左到右建议结构依次是

<div navbar的div> 
<hamburger id="hamburger-container" :is-active="getters.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!$store.state.settings.topNav" />
    <top-nav id="topmenu-container" class="topmenu-container" v-if="$store.state.settings.topNav" />

    <div class="right-menu">
			...
			个人中心等
		</div>
</div>

添加模块

bug集锦

表单输入不进内容

默认生成代码的表单的ref都是form,和保存表单内容的form变量重名。
要修改ref和下面的proxy.resetForm(“form”);

Edge有些按钮不显示

      v-hasPermi="['forum:item:edit']",默认的权限不对 
Logo

快速构建 Web 应用程序

更多推荐