lrf 1 rok pred
rodič
commit
35f836d6b2

+ 1 - 1
src/layout/parts/Header.vue

@@ -29,7 +29,7 @@
 import { siteInfo } from '@/layout/site'
 import { UserStore } from '@/store/user'
 const userStore = UserStore()
-const user = computed(() => userStore.user)
+const user = userStore.user
 let collapse = ref(false)
 const router = useRouter()
 // 退出登录

+ 3 - 57
src/layout/parts/Sidebar.vue

@@ -13,41 +13,7 @@
             :text-color="styleInfo.textColor"
             :active-text-color="styleInfo.activeColor"
           >
-            <template v-for="item in items">
-              <template v-if="item.type === '0'">
-                <el-sub-menu :index="item._id" :key="item._id">
-                  <template #title>
-                    <i :class="['iconfont', item.icon]"></i>
-                    <span>{{ item.name }}</span>
-                  </template>
-                  <template v-for="subItem in item.children">
-                    <!-- TODO:这里有问题需要改成自引用输出方式.实现无线嵌套.目前只是最多三级 -->
-                    <template v-if="subItem.type === '0'">
-                      <el-sub-menu v-if="subItem.children && subItem.children.length > 0" :index="subItem._id" :key="subItem._id">
-                        <template #title>
-                          <i :class="['iconfont', subItem.icon]"></i>
-                          <span>{{ subItem.name }}</span>
-                        </template>
-                        <el-menu-item v-for="(threeItem, i) in subItem.children" :key="i" :index="threeItem.path">
-                          <i :class="['iconfont', threeItem.icon]"></i>
-                          <span>{{ threeItem.name }}</span>
-                        </el-menu-item>
-                      </el-sub-menu>
-                    </template>
-                    <el-menu-item v-else-if="subItem.type === '1'" :index="subItem.path" :key="subItem.path">
-                      <i :class="['iconfont', subItem.icon]"></i>
-                      <span>{{ subItem.name }}</span>
-                    </el-menu-item>
-                  </template>
-                </el-sub-menu>
-              </template>
-              <template v-else>
-                <el-menu-item :index="item.path" :key="item.path">
-                  <i :class="['iconfont', item.icon]"></i>
-                  <span>{{ item.name }}</span>
-                </el-menu-item>
-              </template>
-            </template>
+            <menu-item :items="items"></menu-item>
           </el-menu>
         </el-col>
       </el-col>
@@ -58,32 +24,12 @@
 <script setup>
 import { menuInfo } from '@/layout/site'
 import { UserStore } from '@/store/user'
-
+import menuItem from './sidebar/items.vue'
 const route = useRoute()
 const userStore = UserStore()
-const user = computed(() => userStore.user)
 let onRoutes = route.path
 const styleInfo = ref(menuInfo.info)
-let items = ref(menuInfo.menuList)
-
-const getMenu = async () => {
-  const menus = user.value.menus
-  const newMenus = [...menus]
-  items.value = newMenus
-}
-
-watch(
-  user,
-  (newVal) => {
-    if (newVal && newVal._id) {
-      if (newVal && newVal._id) getMenu()
-      else ElMessage({ message: `暂无用户信息,无法获取菜单信息`, type: 'error' })
-    }
-  },
-  {
-    deep: true
-  }
-)
+let items = ref(userStore.menus)
 </script>
 <style scoped lang="scss">
 .sidebar::-webkit-scrollbar {

+ 30 - 0
src/layout/parts/sidebar/items.vue

@@ -0,0 +1,30 @@
+<template>
+  <template v-for="item in items" :key="item._id">
+    <template v-if="item.type === '0'">
+      <el-sub-menu :index="item._id" :key="item._id">
+        <template #title>
+          <i :class="['iconfont', item.icon]"></i>
+          <span>{{ item.name }}</span>
+        </template>
+        <menu-item :items="item.children"></menu-item>
+      </el-sub-menu>
+    </template>
+    <template v-else-if="item.type === '1'">
+      <el-menu-item :index="item.path" :key="item.path">
+        <i :class="['iconfont', item.icon]"></i>
+        <span>{{ item.name }}</span>
+      </el-menu-item>
+    </template>
+  </template>
+</template>
+
+<script>
+export default {
+  name: 'menuItem',
+  props: {
+    items: { type: Array, default: () => [] }
+  }
+}
+</script>
+
+<style scoped></style>

+ 2 - 0
src/main.js

@@ -1,6 +1,7 @@
 import { createApp } from 'vue'
 import { setupStore } from '@/store'
 import i18n from '@/lang/index'
+import { InitCheckResult } from './utils/checkResult'
 
 import App from './App.vue'
 import router from './router'
@@ -15,3 +16,4 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
   app.component(key, component)
 }
 app.use(router).use(i18n).mount('#app')
+InitCheckResult(app)

+ 100 - 16
src/router/guard.js

@@ -1,25 +1,109 @@
 import { AxiosWrapper } from '@/utils/axios-wrapper'
 import { checkResult } from '@/utils/checkResult'
 import { UserStore } from '@/store/user'
-
-export const registerBeforeRouter = (router) => {
+import { cloneDeep, omit } from 'lodash-es'
+// 检查路由是否存在
+const hasNecessaryRoute = (to, router) => {
+  // 将默认注册的路由平铺成一维数组
+  const routesOneDimensional = toOneDimensional(router.getRoutes())
+  return routesOneDimensional.find((f) => f.path === to.path)
+}
+// 获取用户信息,返回菜单
+const getUserMeta = async (token) => {
+  const userStore = UserStore()
+  const axios = new AxiosWrapper()
+  const result = await axios.$get(`/token/tokenView`, null, {
+    headers: {
+      token: token
+    }
+  })
+  if (checkResult(result)) {
+    userStore.setUser(result.data)
+    const resetMenusResult = resetMenus(result.data.menus)
+    const storeMenus = toRaw(userStore.menus)
+    if (storeMenus.length <= 0) {
+      userStore.setMenus(resetMenusResult)
+    }
+    return result.data.menus
+  }
+}
+// 注册前置守卫
+export const registerBeforeRouter = async (router) => {
   router.beforeEach(async (to, from, next) => {
-    const userStore = UserStore()
-    const axios = new AxiosWrapper()
     document.title = `${to.meta.title} `
     const token = localStorage.getItem('token')
-    if (to.name != 'login') {
-      if (token) {
-        const result = await axios.get(`${import.meta.env.VITE_REQUEST_BASE}/token/tokenView`, null, {
-          headers: {
-            token: token
-          }
-        })
-        if (checkResult(result)) {
-          userStore.setUser(result.data)
-        }
+    if (token) {
+      if (to.path === '/login') next()
+      const menus = await getUserMeta(token)
+      // 检查目的地路由是否注册
+      const hasRoute = hasNecessaryRoute(to, router)
+      if (hasRoute) {
+        // 注册了直接进入
         next()
-      } else next('/login')
-    } else next()
+      } else {
+        // 没注册就先注册再重定向进入直到进入为止
+        await addUserRoutes(menus, router)
+        next({ ...to, replace: true })
+      }
+    } else {
+      next('/login')
+    }
   })
 }
+/**
+ * 将路由数组一维化
+ * @param {Array} routes 路由数组
+ * @returns 一维路由数组
+ */
+const toOneDimensional = (routes) => {
+  const result = []
+  for (const r of routes) {
+    const { children = [], ...others } = r
+    result.push(others)
+    if (children.length > 0) result.push(...toOneDimensional(children))
+  }
+  return result
+}
+// 添加路由
+export const addUserRoutes = async (menus, router) => {
+  return new Promise((resolve, reject) => {
+    // 将用户菜单转换成普通对象
+    const menuArr = toRaw(menus)
+    // 将用户菜单平铺成一维数组,并将目录过滤出去.目录不需要注册,不是组件
+    const menuOneDimensional = toOneDimensional(menuArr).filter((f) => f.type !== '0')
+    // 将默认注册的路由平铺成一维数组
+    const routesOneDimensional = toOneDimensional(router.getRoutes())
+    // 过滤出需要注册的路由数据
+    const needRegistRoute = menuOneDimensional.filter((f) => !routesOneDimensional.find((od) => od.path === f.path))
+    const loadComponent = import.meta.glob('../views/**/*.vue')
+    // 注册路由
+    for (const r of needRegistRoute) {
+      let { path, component, route_name: name, name: title, in_admin_frame } = r
+      // 判断有没有route_name,没有就用component地址最后两个拼一起
+      if (!name) {
+        const list = component.split('/')
+        name = list.shift().join('')
+      }
+      const c = {
+        path,
+        name,
+        meta: { title },
+        component: loadComponent[`../views${component}.vue`]
+      }
+      if (in_admin_frame === '0') router.addRoute('AdminFrame', c)
+      else router.addRoute(c)
+    }
+    resolve()
+  })
+}
+const resetMenus = (menus) => {
+  const cMenus = cloneDeep(menus)
+  const result = []
+  for (const m of cMenus) {
+    const mid = omit(m, ['is_use', 'order_num', 'in_admin_frame', 'route_name'])
+    const { children } = mid
+    if (children) mid.children = resetMenus(children)
+    result.push(mid)
+  }
+  return result
+}

+ 6 - 4
src/router/index.js

@@ -7,17 +7,19 @@ const router = createRouter({
   routes: [
     {
       path: '/login',
-      name: 'login',
-      meta: { title: '账号登录' },
+      name: 'Login',
+      meta: { title: '系统登录' },
       component: () => import('@/views/login/index.vue')
     },
     {
       path: '/',
-      meta: { title: '系统首页' },
+      name: 'AdminFrame',
+      redirect: '/home',
       component: () => import('@/layout/index.vue'),
       children: [
         {
-          path: '/',
+          path: '/home',
+          name: 'home',
           meta: { title: '系统首页' },
           component: () => import('@/views/home/index.vue')
         }

+ 40 - 0
src/store/api/system/menus.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/menus'
+const axios = new AxiosWrapper()
+
+export const MenusStore = defineStore('menus', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, '_id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 5 - 1
src/store/user.js

@@ -1,6 +1,7 @@
 import { defineStore } from 'pinia'
 export const UserStore = defineStore('user', () => {
   const user = ref({})
+  const menus = ref([])
   /**
    * 将用户信息存起来;用户信息是在路由变更时,路由前置守卫去服务进行兑换而来
    * @param {String} payload token串
@@ -15,5 +16,8 @@ export const UserStore = defineStore('user', () => {
     user.value = {}
     localStorage.removeItem('token')
   }
-  return { user, setUser, logOut }
+  const setMenus = (payload) => {
+    menus.value = payload
+  }
+  return { user, setUser, logOut, menus, setMenus }
 })

+ 4 - 0
src/utils/checkResult.js

@@ -17,3 +17,7 @@ export const checkResult = (res, okText, errText) => {
   // Message({ message: _errText || errmsg, duration: 60000 });
   return false
 }
+
+export const InitCheckResult = (app) => {
+  app.provide('$checkRes', checkResult)
+}

+ 1 - 1
src/views/home/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div id="index" style="">
-    <p>index</p>
+    <p><router-link to="/system/menus">menus</router-link></p>
   </div>
 </template>
 

+ 100 - 0
src/views/system/menus/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" style="text-align: right; padding: 10px">
+        <el-button type="primary" size="small" @click="toAdd()">添加</el-button>
+      </el-col>
+      <el-col :span="24">
+        <menu-table></menu-table>
+      </el-col>
+    </el-row>
+    <el-dialog v-model="dialog" title="菜单信息" :destroy-on-close="false" @close="toClose">
+      <menu-info></menu-info>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import menuTable from './parts/menu-table.vue'
+import menuInfo from './parts/menu-info.vue'
+import { cloneDeep, get, omit } from 'lodash-es'
+import { MenusStore } from '@/store/api/system/menus'
+const store = MenusStore()
+const $checkRes = inject('$checkRes')
+const dialog = ref(false)
+const list = ref([])
+const form = ref({})
+const typeList = [
+  { label: '目录', value: '0' },
+  { label: '菜单', value: '1' },
+  { label: '子页面', value: '2' }
+]
+
+onMounted(() => {
+  search()
+})
+
+// #region 接口函数
+const search = async () => {
+  const res = await store.query()
+  if ($checkRes(res)) {
+    list.value = res.data
+  }
+}
+const toSave = async () => {
+  const data = cloneDeep(omit(form.value, ['children', 'parent_name']))
+  let res
+  if (get(data, '_id')) {
+    res = await store.update(data)
+  } else res = await store.create(data)
+  if ($checkRes(res, true)) {
+    search()
+    toClose()
+  }
+}
+const toDelete = async (row) => {
+  ElMessageBox.confirm('您确定删除该数据?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    const res = await store.del(row._id)
+    if ($checkRes(res, true)) {
+      search()
+    }
+  })
+}
+// #endregion
+
+// #region 工具函数
+const toAddNext = (row) => {
+  const obj = { parent_id: row._id, is_use: '0' }
+  form.value = obj
+  dialog.value = true
+}
+const toUpdate = (row) => {
+  form.value = cloneDeep(row)
+  dialog.value = true
+}
+
+const toAdd = () => {
+  dialog.value = true
+  form.value = { is_use: '0' }
+}
+const toClose = () => {
+  form.value = {}
+  dialog.value = false
+}
+// #endregion
+
+// provide
+provide('menuTree', list)
+provide('form', form)
+provide('typeList', typeList)
+provide('toUpdate', toUpdate)
+provide('toAddNext', toAddNext)
+provide('toDelete', toDelete)
+provide('toSave', toSave)
+</script>
+
+<style scoped></style>

+ 50 - 0
src/views/system/menus/parts/menu-info.vue

@@ -0,0 +1,50 @@
+<template>
+  <el-tabs v-model="tab" type="card">
+    <el-tab-pane label="基本信息" name="basic">
+      <info v-bind="$attrs"></info>
+    </el-tab-pane>
+
+    <!-- <el-tab-pane label="设置" name="config" v-if="form.type && form.type !== '0'">
+          <el-row>
+            <el-col :span="24" style="text-align: right; margin: 10px 0">
+              <el-button size="mini" type="primary" @click="toAddConfig()">添加功能</el-button>
+            </el-col>
+            <el-col :span="24">
+              <el-table :data="form.config">
+                <el-table-column align="center" label="中文">
+                  <template #default="{ row }">
+                    <el-input v-model="row.zh"></el-input>
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="code">
+                  <template #default="{ row }">
+                    <el-input v-model="row.code"></el-input>
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="操作">
+                  <template #default="{ $index }">
+                    <el-button size="mini" type="danger" @click="deleteConfig($index)">删除</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-col>
+          </el-row>
+        </el-tab-pane> -->
+  </el-tabs>
+  <el-row type="flex" justify="space-around" style="margin-top: 10px">
+    <el-col :span="6">
+      <el-button @click="toSave" size="small" type="primary">保存</el-button>
+    </el-col>
+  </el-row>
+</template>
+
+<script setup>
+import info from './parts/info.vue'
+const tab = ref('basic')
+const emits = defineEmits(['toSave'])
+const toSave = () => {
+  emits('toSave')
+}
+</script>
+
+<style scoped></style>

+ 58 - 0
src/views/system/menus/parts/menu-table.vue

@@ -0,0 +1,58 @@
+<template>
+  <div id="menu-table">
+    <el-table :data="data" row-key="_id" border>
+      <el-table-column align="center" label="菜单名称" prop="name"></el-table-column>
+      <el-table-column align="center" label="父级菜单" prop="parent_name"></el-table-column>
+      <el-table-column align="center" label="图标" width="80">
+        <template #default="{ row }"><span :class="['iconfont', row.icon]"></span></template>
+      </el-table-column>
+      <el-table-column align="center" label="顺序" sortable prop="order_num" width="80"></el-table-column>
+      <el-table-column align="center" label="路由地址" prop="path"></el-table-column>
+      <el-table-column align="center" label="组件地址" prop="component"></el-table-column>
+      <el-table-column align="center" label="菜单类型" prop="type">
+        <template #default="{ row }">{{ getType(row) }} </template>
+      </el-table-column>
+      <el-table-column align="center" label="状态" prop="is_use" width="80">
+        <template #default="{ row }">{{ getStatus(row) }} </template>
+      </el-table-column>
+      <el-table-column align="center" label="备注" prop="remark"> </el-table-column>
+      <el-table-column align="center" label="操作">
+        <template #default="{ row }">
+          <el-link :underline="false" type="primary" size="mini" @click="toUpdate(row)" style="margin-right: 10px">修改</el-link>
+          <el-link :underline="false" type="primary" size="mini" @click="toAddNext(row)" style="margin-right: 10px">添加下一级</el-link>
+          <el-link :underline="false" type="danger" size="mini" @click="toDelete(row)">删除</el-link>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup>
+const data = inject('menuTree', [])
+const typeList = inject('typeList', [])
+const toUpdate = inject('toUpdate')
+const toAddNext = inject('toAddNext')
+const toDelete = inject('toDelete')
+const getType = (row) => {
+  let word = ''
+  const r = typeList.find((f) => f.value === row.type)
+  if (r) word = r.label
+  return word
+}
+const getStatus = (row) => {
+  let word = ''
+  switch (row.is_use) {
+    case '0':
+      word = '启用'
+      break
+    case '1':
+      word = '禁用'
+      break
+
+    default:
+      break
+  }
+  return word
+}
+</script>
+<style scoped></style>

+ 8 - 0
src/views/system/menus/parts/parts/func.vue

@@ -0,0 +1,8 @@
+<template>
+  <div id="func">
+    <p>func</p>
+  </div>
+</template>
+
+<script setup></script>
+<style scoped></style>

+ 89 - 0
src/views/system/menus/parts/parts/info.vue

@@ -0,0 +1,89 @@
+<template>
+  <el-form label-position="left" label-width="120px">
+    <el-form-item label="菜单名称">
+      <el-input v-model="form.name" placeholder="请填写菜单名称"></el-input>
+    </el-form-item>
+    <el-form-item label="菜单类型">
+      <el-select v-model="form.type" placeholder="请选择菜单类型">
+        <el-option v-for="(i, index) in typeList" :key="`t${index}`" :label="i.label" :value="i.value"></el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="父级菜单">
+      <el-select v-model="form.parent_id" placeholder="" :disabled="true">
+        <el-option v-for="(i, index) in getOneDimensionList()" :key="`m${index}`" :label="i.name" :value="i._id"></el-option>
+      </el-select>
+    </el-form-item>
+    <template v-if="form.type === '1' || form.type === '2'">
+      <el-form-item label="路由地址">
+        <el-input v-model="form.path" placeholder="请填写路由地址"></el-input>
+      </el-form-item>
+      <el-form-item label="组件地址">
+        <el-input v-model="form.component" placeholder="请填写组件地址"></el-input>
+      </el-form-item>
+    </template>
+    <el-form-item label="顺序">
+      <el-input-number v-model="form.order_num"></el-input-number>
+    </el-form-item>
+    <el-form-item label="图标">
+      <el-select v-model="form.icon" clearable filterable placeholder="请选择图标">
+        <el-option v-for="item in iconList" :key="item.dict_label" :label="item.dict_label" :value="item.dict_label">
+          <span style="float: left" :class="['iconfont', item.dict_label]"></span>
+          <span style="float: right; color: #8492a6; font-size: 13px">{{ item.dict_label }}</span>
+        </el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="状态">
+      <el-radio-group v-model="form.is_use">
+        <el-radio label="0">使用</el-radio>
+        <el-radio label="1">禁用</el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="备注">
+      <el-input v-model="form.remark" placeholder="请输入备注" type="textarea" :autosize="{ minRows: 5, maxRows: 5 }"></el-input>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { cloneDeep } from 'lodash-es'
+const menuTree = inject('menuTree')
+const typeList = inject('typeList')
+const form = inject('form')
+
+// const props = defineProps({
+//   menuTree: { type: Array, default: () => [] },
+//   data: { type: Object, default: () => {} }
+// })
+// const emits = defineEmits(['update:form'])
+// // v-model
+// const form = computed({
+//   get() {
+//     return props.data
+//   },
+//   set(value) {
+//     emits('update:form', value)
+//   }
+// })
+
+// #region 整理数组
+const getOneDimensionList = () => {
+  let dup = cloneDeep(menuTree.value)
+  let arr = getAllChild(dup)
+  return arr
+}
+
+const getAllChild = (children) => {
+  let arr = []
+  for (const i of children) {
+    const { children, ...others } = i
+    arr.push(others)
+    if (children) {
+      const marr = getAllChild(children)
+      arr.push(...marr)
+    }
+  }
+  return arr
+}
+// #endregion
+</script>
+<style scoped></style>

+ 9 - 0
src/views/system/role/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script setup></script>
+
+<style scoped></style>

+ 9 - 0
src/views/test/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div id="index">
+    <p><router-link to="/">返回</router-link></p>
+  </div>
+</template>
+
+<script setup></script>
+
+<style scoped></style>