mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-09-18 03:39:58 +08:00
feat: 对接后端登录和菜单
This commit is contained in:
parent
f2e82e725f
commit
d54810ab02
@ -7,6 +7,7 @@ import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { ProNaiveUIResolver } from 'pro-naive-ui-resolver'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
@ -65,6 +66,7 @@ export function createVitePlugins(env: ImportMetaEnv) {
|
||||
],
|
||||
}),
|
||||
NaiveUiResolver(),
|
||||
ProNaiveUIResolver(),
|
||||
],
|
||||
}),
|
||||
|
||||
|
@ -70,7 +70,6 @@
|
||||
"signInTitle": "Login",
|
||||
"accountRuleTip": "Please enter account",
|
||||
"passwordRuleTip": "Please enter password",
|
||||
"or": "Or",
|
||||
"rememberMe": "Remember me",
|
||||
"forgotPassword": "Forget the password?",
|
||||
"signIn": "Sign in",
|
||||
|
@ -101,7 +101,6 @@
|
||||
"passwordPlaceholder": "输入密码",
|
||||
"accountRuleTip": "请输入账户",
|
||||
"passwordRuleTip": "请输入密码",
|
||||
"or": "其他",
|
||||
"signIn": "登录",
|
||||
"rememberMe": "记住我",
|
||||
"forgotPassword": "忘记密码?",
|
||||
|
17
package.json
17
package.json
@ -37,28 +37,28 @@
|
||||
"UnoCSS"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite --mode dev --port 9980",
|
||||
"dev": "vite --mode dev",
|
||||
"dev:prod": "vite --mode production",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"preview": "vite preview --port 9981",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . && vue-tsc --noEmit",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:check": "npx @eslint/config-inspector",
|
||||
"sizecheck": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"@vueuse/core": "^13.7.0",
|
||||
"alova": "^3.3.4",
|
||||
"array-to-tree": "^3.3.2",
|
||||
"colord": "^2.9.3",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts": "^6.0.0",
|
||||
"md-editor-v3": "^5.6.1",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"pro-naive-ui": "^2.4.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"pro-naive-ui": "^3.0.3",
|
||||
"quill": "^2.0.3",
|
||||
"radash": "^12.1.1",
|
||||
"vue": "^3.5.18",
|
||||
"vue": "^3.5.20",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1"
|
||||
@ -73,6 +73,7 @@
|
||||
"eslint": "^9.29.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"naive-ui": "^2.42.0",
|
||||
"pro-naive-ui-resolver": "^1.0.2",
|
||||
"sass": "^1.89.2",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"typescript": "^5.8.3",
|
||||
|
@ -38,11 +38,20 @@ const appStore = useAppStore()
|
||||
const naiveLocale = computed(() => {
|
||||
return naiveI18nOptions[appStore.lang] ? naiveI18nOptions[appStore.lang] : naiveI18nOptions.enUS
|
||||
})
|
||||
|
||||
const propOverrides = {
|
||||
ProModalForm: {
|
||||
labelWidth: 120,
|
||||
labelPlacement: 'left',
|
||||
preset: 'card',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider
|
||||
class="wh-full"
|
||||
<pro-config-provider
|
||||
:prop-overrides="propOverrides"
|
||||
abstract
|
||||
inline-theme-disabled
|
||||
:theme="appStore.colorMode === 'dark' ? darkTheme : null"
|
||||
:locale="naiveLocale.locale"
|
||||
@ -53,5 +62,5 @@ const naiveLocale = computed(() => {
|
||||
<router-view />
|
||||
<Watermark :show-watermark="appStore.showWatermark" />
|
||||
</naive-provider>
|
||||
</n-config-provider>
|
||||
</pro-config-provider>
|
||||
</template>
|
||||
|
@ -20,9 +20,96 @@ const value = defineModel('value', { type: String })
|
||||
// 包含的图标库系列名,更多:https://icon-sets.iconify.design/
|
||||
const nameList = ['icon-park-outline', 'carbon', 'ant-design']
|
||||
|
||||
interface CacheItem {
|
||||
data: IconList
|
||||
}
|
||||
|
||||
// 获取单个图标库数据
|
||||
async function fetchIconList(name: string): Promise<IconList> {
|
||||
return await fetch(`https://api.iconify.design/collection?prefix=${name}`).then(res => res.json())
|
||||
const cacheKey = `icon_list_${name}`
|
||||
|
||||
try {
|
||||
// 先从sessionStorage读取缓存
|
||||
const cachedData = sessionStorage.getItem(cacheKey)
|
||||
if (cachedData) {
|
||||
const cache: CacheItem = JSON.parse(cachedData)
|
||||
|
||||
// 如果缓存数据存在,直接返回
|
||||
if (cache.data) {
|
||||
return cache.data
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`读取图标库缓存失败: ${name}`, error)
|
||||
}
|
||||
|
||||
try {
|
||||
// 缓存不存在,从API获取数据
|
||||
const data = await fetch(`https://api.iconify.design/collection?prefix=${name}`)
|
||||
.then(res => res.json())
|
||||
|
||||
// 将数据缓存到sessionStorage
|
||||
try {
|
||||
const cacheItem: CacheItem = {
|
||||
data,
|
||||
}
|
||||
sessionStorage.setItem(cacheKey, JSON.stringify(cacheItem))
|
||||
}
|
||||
catch (cacheError) {
|
||||
console.warn(`缓存图标库数据失败: ${name}`, cacheError)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`获取图标库数据失败: ${name}`, error)
|
||||
|
||||
// 如果API请求失败,尝试使用缓存数据作为降级方案
|
||||
try {
|
||||
const cachedData = sessionStorage.getItem(cacheKey)
|
||||
if (cachedData) {
|
||||
const cache: CacheItem = JSON.parse(cachedData)
|
||||
if (cache.data) {
|
||||
return cache.data
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (fallbackError) {
|
||||
console.warn(`读取降级缓存失败: ${name}`, fallbackError)
|
||||
}
|
||||
|
||||
// 如果所有方法都失败,返回空的图标库数据
|
||||
return {
|
||||
prefix: name,
|
||||
icons: [],
|
||||
title: name,
|
||||
total: 0,
|
||||
categories: {},
|
||||
uncategorized: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除指定图标库的缓存
|
||||
function clearIconCache(name?: string) {
|
||||
try {
|
||||
if (name) {
|
||||
// 清除指定图标库的缓存
|
||||
const cacheKey = `icon_list_${name}`
|
||||
sessionStorage.removeItem(cacheKey)
|
||||
}
|
||||
else {
|
||||
// 清除所有图标库缓存
|
||||
nameList.forEach((iconName) => {
|
||||
const cacheKey = `icon_list_${iconName}`
|
||||
sessionStorage.removeItem(cacheKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('清除图标库缓存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有图标库数据
|
||||
@ -62,6 +149,12 @@ onMounted(async () => {
|
||||
iconList.value = await fetchIconAllList(nameList)
|
||||
})
|
||||
|
||||
// 在开发环境下暴露清除缓存函数到全局,方便调试
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-expect-error 开发环境下扩展window对象
|
||||
window.clearIconCache = clearIconCache
|
||||
}
|
||||
|
||||
// 当前tab
|
||||
const currentTab = shallowRef(0)
|
||||
// 当前tag
|
||||
|
@ -13,18 +13,18 @@ export function usePermission() {
|
||||
|
||||
if (!authStore.userInfo)
|
||||
return false
|
||||
const { role } = authStore.userInfo
|
||||
const { roles } = authStore.userInfo
|
||||
|
||||
// 角色为super可直接通过
|
||||
let has = role.includes('super')
|
||||
let has = roles.includes('super')
|
||||
if (!has) {
|
||||
if (isArray(permission))
|
||||
// 角色为数组, 判断是否有交集
|
||||
has = permission.some(i => role.includes(i))
|
||||
has = permission.some(i => roles.includes(i))
|
||||
|
||||
if (isString(permission))
|
||||
// 角色为字符串, 判断是否包含
|
||||
has = role.includes(permission)
|
||||
has = roles.includes(permission)
|
||||
}
|
||||
return has
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ function handleSelect(key: string | number) {
|
||||
<n-avatar
|
||||
round
|
||||
class="cursor-pointer"
|
||||
:src="userInfo?.avatar"
|
||||
:src="userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${userInfo!.username}`"
|
||||
>
|
||||
<template #fallback>
|
||||
<div class="wh-full flex-center">
|
||||
|
@ -8,7 +8,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 1,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'workbench',
|
||||
@ -20,7 +20,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'page',
|
||||
componentPath: '/dashboard/workbench/index.vue',
|
||||
id: 101,
|
||||
pid: 1,
|
||||
parentId: 1,
|
||||
},
|
||||
{
|
||||
name: 'monitor',
|
||||
@ -31,7 +31,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'page',
|
||||
componentPath: '/dashboard/monitor/index.vue',
|
||||
id: 102,
|
||||
pid: 1,
|
||||
parentId: 1,
|
||||
},
|
||||
{
|
||||
name: 'multi',
|
||||
@ -42,7 +42,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 2,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'multi2',
|
||||
@ -53,7 +53,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'page',
|
||||
componentPath: '/demo/multi/multi-2/index.vue',
|
||||
id: 201,
|
||||
pid: 2,
|
||||
parentId: 2,
|
||||
},
|
||||
{
|
||||
name: 'multi2-detail',
|
||||
@ -66,7 +66,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'page',
|
||||
componentPath: '/demo/multi/multi-2/detail/index.vue',
|
||||
id: 20101,
|
||||
pid: 2,
|
||||
parentId: 2,
|
||||
},
|
||||
{
|
||||
name: 'multi3',
|
||||
@ -77,7 +77,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 202,
|
||||
pid: 2,
|
||||
parentId: 2,
|
||||
},
|
||||
{
|
||||
name: 'multi4',
|
||||
@ -87,7 +87,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:list',
|
||||
componentPath: '/demo/multi/multi-3/multi-4/index.vue',
|
||||
id: 20201,
|
||||
pid: 202,
|
||||
parentId: 202,
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
@ -98,7 +98,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 3,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'commonList',
|
||||
@ -108,7 +108,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:list-view',
|
||||
componentPath: '/demo/list/common-list/index.vue',
|
||||
id: 301,
|
||||
pid: 3,
|
||||
parentId: 3,
|
||||
},
|
||||
{
|
||||
name: 'cardList',
|
||||
@ -118,7 +118,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:view-grid-list',
|
||||
componentPath: '/demo/list/card-list/index.vue',
|
||||
id: 302,
|
||||
pid: 3,
|
||||
parentId: 3,
|
||||
},
|
||||
{
|
||||
name: 'draggableList',
|
||||
@ -128,7 +128,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:menu-fold',
|
||||
componentPath: '/demo/list/draggable-list/index.vue',
|
||||
id: 303,
|
||||
pid: 3,
|
||||
parentId: 3,
|
||||
},
|
||||
{
|
||||
name: 'demo',
|
||||
@ -139,7 +139,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 4,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'fetch',
|
||||
@ -149,7 +149,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:international',
|
||||
componentPath: '/demo/fetch/index.vue',
|
||||
id: 401,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'echarts',
|
||||
@ -159,7 +159,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:chart-proportion',
|
||||
componentPath: '/demo/echarts/index.vue',
|
||||
id: 402,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'map',
|
||||
@ -170,7 +170,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
keepAlive: true,
|
||||
componentPath: '/demo/map/index.vue',
|
||||
id: 403,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'editor',
|
||||
@ -181,7 +181,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 404,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'editorMd',
|
||||
@ -191,7 +191,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'ri:markdown-line',
|
||||
componentPath: '/demo/editor/md/index.vue',
|
||||
id: 40401,
|
||||
pid: 404,
|
||||
parentId: 404,
|
||||
},
|
||||
{
|
||||
name: 'editorRich',
|
||||
@ -201,7 +201,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:edit-one',
|
||||
componentPath: '/demo/editor/rich/index.vue',
|
||||
id: 40402,
|
||||
pid: 404,
|
||||
parentId: 404,
|
||||
},
|
||||
{
|
||||
name: 'clipboard',
|
||||
@ -211,7 +211,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:clipboard',
|
||||
componentPath: '/demo/clipboard/index.vue',
|
||||
id: 405,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'icons',
|
||||
@ -221,7 +221,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'local:cool',
|
||||
componentPath: '/demo/icons/index.vue',
|
||||
id: 406,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'QRCode',
|
||||
@ -231,7 +231,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:two-dimensional-code',
|
||||
componentPath: '/demo/qr-code/index.vue',
|
||||
id: 407,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'cascader',
|
||||
@ -241,7 +241,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:add-subset',
|
||||
componentPath: '/demo/cascader/index.vue',
|
||||
id: 408,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'dict',
|
||||
@ -251,7 +251,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/demo/dict/index.vue',
|
||||
id: 409,
|
||||
pid: 4,
|
||||
parentId: 4,
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
@ -262,7 +262,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 5,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'documentsVue',
|
||||
@ -272,7 +272,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'logos:vue',
|
||||
componentPath: '/demo/documents/vue/index.vue',
|
||||
id: 501,
|
||||
pid: 5,
|
||||
parentId: 5,
|
||||
},
|
||||
{
|
||||
name: 'documentsVite',
|
||||
@ -282,7 +282,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'logos:vitejs',
|
||||
componentPath: '/demo/documents/vite/index.vue',
|
||||
id: 502,
|
||||
pid: 5,
|
||||
parentId: 5,
|
||||
},
|
||||
{
|
||||
name: 'documentsVueuse',
|
||||
@ -293,7 +293,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
href: 'https://vueuse.org/guide/',
|
||||
componentPath: 'null',
|
||||
id: 503,
|
||||
pid: 5,
|
||||
parentId: 5,
|
||||
},
|
||||
|
||||
{
|
||||
@ -305,7 +305,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
href: 'https://nova-admin-docs.netlify.app/',
|
||||
componentPath: '2333333',
|
||||
id: 504,
|
||||
pid: 5,
|
||||
parentId: 5,
|
||||
},
|
||||
{
|
||||
name: 'documentsPublic',
|
||||
@ -316,7 +316,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
href: '/public',
|
||||
componentPath: 'null',
|
||||
id: 505,
|
||||
pid: 5,
|
||||
parentId: 5,
|
||||
},
|
||||
{
|
||||
name: 'permission',
|
||||
@ -327,7 +327,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 6,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'permissionDemo',
|
||||
@ -337,7 +337,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:right-user',
|
||||
componentPath: '/demo/permission/permission/index.vue',
|
||||
id: 601,
|
||||
pid: 6,
|
||||
parentId: 6,
|
||||
},
|
||||
{
|
||||
name: 'justSuper',
|
||||
@ -350,7 +350,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:wrong-user',
|
||||
componentPath: '/demo/permission/just-super/index.vue',
|
||||
id: 602,
|
||||
pid: 6,
|
||||
parentId: 6,
|
||||
},
|
||||
{
|
||||
name: 'setting',
|
||||
@ -361,7 +361,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 7,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'accountSetting',
|
||||
@ -371,7 +371,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:every-user',
|
||||
componentPath: '/setting/account/index.vue',
|
||||
id: 701,
|
||||
pid: 7,
|
||||
parentId: 7,
|
||||
},
|
||||
{
|
||||
name: 'dictionarySetting',
|
||||
@ -381,7 +381,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/setting/dictionary/index.vue',
|
||||
id: 702,
|
||||
pid: 7,
|
||||
parentId: 7,
|
||||
},
|
||||
{
|
||||
name: 'menuSetting',
|
||||
@ -391,7 +391,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:application-menu',
|
||||
componentPath: '/setting/menu/index.vue',
|
||||
id: 703,
|
||||
pid: 7,
|
||||
parentId: 7,
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
@ -401,7 +401,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'icon-park-outline:info',
|
||||
componentPath: '/demo/about/index.vue',
|
||||
id: 8,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
{
|
||||
name: 'userCenter',
|
||||
@ -412,7 +412,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
icon: 'carbon:user-avatar-filled-alt',
|
||||
componentPath: '/build-in/user-center/index.vue',
|
||||
id: 999,
|
||||
pid: null,
|
||||
parentId: null,
|
||||
},
|
||||
|
||||
]
|
||||
|
@ -1,17 +1,22 @@
|
||||
import { request } from '../http'
|
||||
|
||||
/* 示例列表接口 */
|
||||
export function fetchUserList() {
|
||||
return request.Get('/userList')
|
||||
}
|
||||
|
||||
// 获取所有路由信息
|
||||
export function fetchAllRoutes() {
|
||||
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]>>('/getUserRoutes')
|
||||
return request.Get<Api.Response<AppRoute.RowRoute[]>>('/getUserRoutes')
|
||||
}
|
||||
|
||||
// 获取所有用户信息
|
||||
export function fetchUserPage() {
|
||||
return request.Get<Service.ResponseResult<Entity.User[]>>('/userPage')
|
||||
return request.Get<Api.Response<Entity.User[]>>('/userPage')
|
||||
}
|
||||
// 获取所有角色列表
|
||||
export function fetchRoleList() {
|
||||
return request.Get<Service.ResponseResult<Entity.Role[]>>('/role/list')
|
||||
return request.Get<Api.Response<Entity.Role[]>>('/role/list')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,5 +27,5 @@ export function fetchRoleList() {
|
||||
*/
|
||||
export function fetchDictList(code?: string) {
|
||||
const params = { code }
|
||||
return request.Get<Service.ResponseResult<Entity.Dict[]>>('/dict/list', { params })
|
||||
return request.Get<Api.Response<Entity.Dict[]>>('/dict/list', { params })
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { request } from '../http'
|
||||
|
||||
export function fetchUserList() {
|
||||
return request.Get('/userList')
|
||||
}
|
@ -1,25 +1,50 @@
|
||||
import { request } from '../http'
|
||||
|
||||
interface Ilogin {
|
||||
interface LoginParams {
|
||||
userName: string
|
||||
password: string
|
||||
captchaId?: string
|
||||
captcha?: string
|
||||
}
|
||||
|
||||
export function fetchLogin(data: Ilogin) {
|
||||
const methodInstance = request.Post<Service.ResponseResult<Api.Login.Info>>('/login', data)
|
||||
methodInstance.meta = {
|
||||
authRole: null,
|
||||
interface ResponseLogin {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
}
|
||||
return methodInstance
|
||||
|
||||
interface ResponseRefreshToken {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
export function fetchUpdateToken(data: any) {
|
||||
const method = request.Post<Service.ResponseResult<Api.Login.Info>>('/updateToken', data)
|
||||
method.meta = {
|
||||
authRole: 'refreshToken',
|
||||
|
||||
interface ResponseCaptchaImage {
|
||||
captchaId: string
|
||||
captchaImage: string
|
||||
enabled: boolean
|
||||
}
|
||||
return method
|
||||
|
||||
export function fetchLogin(data: LoginParams) {
|
||||
return request.Post<Api.Response<ResponseLogin>>('/login', data, {
|
||||
meta: { authRole: null },
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCaptchaImage() {
|
||||
return request.Get<Api.Response<ResponseCaptchaImage>>('/captcha', {
|
||||
meta: { authRole: null },
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUserInfo() {
|
||||
return request.Get<Api.Response<Entity.User>>('/userInfo')
|
||||
}
|
||||
|
||||
export function fetchRefreshToken(data: any) {
|
||||
return request.Post<Api.Response<ResponseRefreshToken>>('/refreshToken', data, {
|
||||
meta: { authRole: 'refreshToken' },
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUserRoutes(params: { id: number }) {
|
||||
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]>>('/getUserRoutes', { params })
|
||||
return request.Get<Api.Response<AppRoute.RowRoute[]>>('/getUserRoutes', { params })
|
||||
}
|
||||
|
119
src/service/api/system/dict.ts
Normal file
119
src/service/api/system/dict.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { request } from '../../http'
|
||||
|
||||
// 字典类型查询参数接口
|
||||
interface DictTypeQueryParams {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
dictName?: string
|
||||
dictType?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
// 字典数据查询参数接口
|
||||
interface DictDataQueryParams {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
dictType?: string
|
||||
dictLabel?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* ==================== 字典类型管理 ====================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典类型-创建
|
||||
* POST /dict/types
|
||||
*/
|
||||
export function createDictType(data: Partial<Entity.DictType>) {
|
||||
return request.Post<Api.Response<Entity.DictType>>('/dict/types', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典类型-列表
|
||||
* GET /dict/types
|
||||
*/
|
||||
export function getDictTypeList(params?: DictTypeQueryParams) {
|
||||
return request.Get<Api.ListResponse<Entity.DictType>>('/dict/types', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典类型-详情
|
||||
* GET /dict/types/{id}
|
||||
*/
|
||||
export function getDictTypeById(id: number) {
|
||||
return request.Get<Api.Response<Entity.DictType>>(`/dict/types/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典类型-更新
|
||||
* PUT /dict/types/{id}
|
||||
*/
|
||||
export function updateDictType(id: number, data: Partial<Entity.DictType>) {
|
||||
return request.Put<Api.Response<Entity.DictType>>(`/dict/types/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典类型-删除
|
||||
* DELETE /dict/types/{id}
|
||||
*/
|
||||
export function deleteDictType(id: number) {
|
||||
return request.Delete<Api.Response<boolean>>(`/dict/types/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* ==================== 字典数据管理 ====================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典数据-创建
|
||||
* POST /dict/data
|
||||
*/
|
||||
export function createDictData(data: Partial<Entity.DictData>) {
|
||||
return request.Post<Api.Response<Entity.DictData>>('/dict/data', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据-列表
|
||||
* GET /dict/data
|
||||
*/
|
||||
export function getDictDataList(params?: DictDataQueryParams) {
|
||||
return request.Get<Api.ListResponse<Entity.DictData>>('/dict/data', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据-详情
|
||||
* GET /dict/data/{id}
|
||||
*/
|
||||
export function getDictDataById(id: number) {
|
||||
return request.Get<Api.Response<Entity.DictData>>(`/dict/data/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据-更新
|
||||
* PUT /dict/data/{id}
|
||||
*/
|
||||
export function updateDictData(id: number, data: Partial<Entity.DictData>) {
|
||||
return request.Put<Api.Response<Entity.DictData>>(`/dict/data/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据-删除
|
||||
* DELETE /dict/data/{id}
|
||||
*/
|
||||
export function deleteDictData(id: number) {
|
||||
return request.Delete<Api.Response<boolean>>(`/dict/data/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* ==================== 字典数据查询 ====================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典数据-按类型查询
|
||||
* GET /dict/data/type/{dictType}
|
||||
*/
|
||||
export function getDictDataByType(dictType: string) {
|
||||
return request.Get<Api.Response<Entity.DictData[]>>(`/dict/data/type/${dictType}`)
|
||||
}
|
49
src/service/api/system/menu.ts
Normal file
49
src/service/api/system/menu.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { request } from '../../http'
|
||||
|
||||
/**
|
||||
* 创建菜单
|
||||
* POST /menu
|
||||
*/
|
||||
export function createMenu(data: Partial<Entity.Menu>) {
|
||||
return request.Post<Api.Response<Entity.Menu>>('/menu', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询菜单
|
||||
* GET /menu
|
||||
*/
|
||||
export function getMenuList() {
|
||||
return request.Get<Api.Response<Entity.Menu[]>>('/menu')
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询菜单详情
|
||||
* GET /menu/{id}
|
||||
*/
|
||||
export function getMenuById(id: number) {
|
||||
return request.Get<Api.Response<Entity.Menu>>(`/menu/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新菜单信息
|
||||
* PUT /menu/{id}
|
||||
*/
|
||||
export function updateMenu(id: number, data: Partial<Entity.Menu>) {
|
||||
return request.Put<Api.Response<Entity.Menu>>(`/menu/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
* DELETE /menu/{id}
|
||||
*/
|
||||
export function deleteMenu(id: number) {
|
||||
return request.Delete<Api.Response<any>>(`/menu/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询菜单树
|
||||
* GET /menu/selectTree
|
||||
*/
|
||||
export function selectMenuTree() {
|
||||
return request.Get<Api.Response<Entity.TreeNode[]>>('/menu/selectTree')
|
||||
}
|
50
src/service/api/system/role.ts
Normal file
50
src/service/api/system/role.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { request } from '../../http'
|
||||
|
||||
// 角色查询参数接口
|
||||
interface RoleQueryParams {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
roleName?: string
|
||||
roleKey?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
* POST /role
|
||||
*/
|
||||
export function createRole(data: Partial<Entity.Role>) {
|
||||
return request.Post<Api.Response<Entity.Role>>('/role', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询角色
|
||||
* GET /role
|
||||
*/
|
||||
export function getRoleList(params?: RoleQueryParams) {
|
||||
return request.Get<Api.ListResponse<Entity.Role>>('/role', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色详情
|
||||
* GET /role/{id}
|
||||
*/
|
||||
export function getRoleById(id: number) {
|
||||
return request.Get<Api.Response<Entity.Role>>(`/role/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色信息
|
||||
* PUT /role/{id}
|
||||
*/
|
||||
export function updateRole(id: number, data: Partial<Entity.Role>) {
|
||||
return request.Put<Api.Response<Entity.Role>>(`/role/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* DELETE /role/{id}
|
||||
*/
|
||||
export function deleteRole(id: number) {
|
||||
return request.Delete<Api.Response<boolean>>(`/role/${id}`)
|
||||
}
|
51
src/service/api/system/user.ts
Normal file
51
src/service/api/system/user.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { request } from '../../http'
|
||||
|
||||
// 用户查询参数接口
|
||||
interface UserQueryParams {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
username?: string
|
||||
gender?: 'male' | 'female' | 'unknown'
|
||||
userStatus?: number
|
||||
deptId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
* POST /user
|
||||
*/
|
||||
export function createUser(data: Partial<Entity.User>) {
|
||||
return request.Post<Api.Response<Entity.User>>('/user', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表(分页)
|
||||
* GET /user
|
||||
*/
|
||||
export function getUserList(params?: UserQueryParams) {
|
||||
return request.Get<Api.ListResponse<Entity.User>>('/user', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取单个用户信息
|
||||
* GET /user/{id}
|
||||
*/
|
||||
export function getUserById(id: number) {
|
||||
return request.Get<Api.Response<Entity.User>>(`/user/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* PUT /user/{id}
|
||||
*/
|
||||
export function updateUser(id: number, data: Partial<Entity.User>) {
|
||||
return request.Put<Api.Response<Entity.User>>(`/user/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* DELETE /user/{id}
|
||||
*/
|
||||
export function deleteUser(id: number) {
|
||||
return request.Delete<Api.Response<boolean>>(`/user/${id}`)
|
||||
}
|
@ -7,14 +7,15 @@ import type { VueHookType } from 'alova/vue'
|
||||
import {
|
||||
DEFAULT_ALOVA_OPTIONS,
|
||||
DEFAULT_BACKEND_OPTIONS,
|
||||
ERROR_STATUS,
|
||||
|
||||
} from './config'
|
||||
import {
|
||||
handleBusinessError,
|
||||
handleRefreshToken,
|
||||
handleResponseError,
|
||||
handleServiceResult,
|
||||
} from './handle'
|
||||
|
||||
import type { AlovaConfig, BackendConfig, ErrorStatus } from './type'
|
||||
|
||||
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<VueHookType>({
|
||||
// 服务端判定token过期
|
||||
refreshTokenOnSuccess: {
|
||||
@ -45,8 +46,8 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
|
||||
|
||||
// docs path of alova.js https://alova.js.org/
|
||||
export function createAlovaInstance(
|
||||
alovaConfig: Service.AlovaConfig,
|
||||
backendConfig?: Service.BackendConfig,
|
||||
alovaConfig: AlovaConfig,
|
||||
backendConfig?: BackendConfig,
|
||||
) {
|
||||
const _backendConfig = { ...DEFAULT_BACKEND_OPTIONS, ...backendConfig }
|
||||
const _alovaConfig = { ...DEFAULT_ALOVA_OPTIONS, ...alovaConfig }
|
||||
@ -63,12 +64,13 @@ export function createAlovaInstance(
|
||||
method.config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
method.data = new URLSearchParams(method.data as URLSearchParams).toString()
|
||||
}
|
||||
alovaConfig.beforeRequest?.(method)
|
||||
alovaConfig.beforeRequest?.(method as any)
|
||||
}),
|
||||
responded: onResponseRefreshToken({
|
||||
// 请求成功的拦截器
|
||||
onSuccess: async (response, method) => {
|
||||
const { status } = response
|
||||
let errorMessage = ''
|
||||
|
||||
if (status === 200) {
|
||||
// 返回blob数据
|
||||
@ -79,19 +81,23 @@ export function createAlovaInstance(
|
||||
const apiData = await response.json()
|
||||
// 请求成功
|
||||
if (apiData[_backendConfig.codeKey] === _backendConfig.successCode)
|
||||
return handleServiceResult(apiData)
|
||||
return apiData
|
||||
|
||||
// 业务请求失败
|
||||
const errorResult = handleBusinessError(apiData, _backendConfig)
|
||||
return handleServiceResult(errorResult, false)
|
||||
errorMessage = apiData[_backendConfig.msgKey]
|
||||
}
|
||||
else {
|
||||
// 接口请求失败
|
||||
const errorResult = handleResponseError(response)
|
||||
return handleServiceResult(errorResult, false)
|
||||
const errorCode = response.status as ErrorStatus
|
||||
errorMessage = ERROR_STATUS[errorCode] || ERROR_STATUS.default
|
||||
}
|
||||
window.$message?.error(errorMessage)
|
||||
throw new Error(errorMessage)
|
||||
},
|
||||
onError: (error, method) => {
|
||||
const tip = `[${method.type}] - [${method.url}] - ${error.message}`
|
||||
window.$message?.warning(tip)
|
||||
throw new Error(tip)
|
||||
},
|
||||
|
||||
onComplete: async (_method) => {
|
||||
|
@ -30,6 +30,3 @@ export const ERROR_STATUS = {
|
||||
504: $t('http.504'),
|
||||
505: $t('http.505'),
|
||||
}
|
||||
|
||||
/** 没有错误提示的code */
|
||||
export const ERROR_NO_TIP_STATUS = [10000]
|
||||
|
@ -1,68 +1,6 @@
|
||||
import { fetchUpdateToken } from '@/service'
|
||||
import { fetchRefreshToken } from '@/service'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { local } from '@/utils'
|
||||
import {
|
||||
ERROR_NO_TIP_STATUS,
|
||||
ERROR_STATUS,
|
||||
} from './config'
|
||||
|
||||
type ErrorStatus = keyof typeof ERROR_STATUS
|
||||
|
||||
/**
|
||||
* @description: 处理请求成功,但返回后端服务器报错
|
||||
* @param {Response} response
|
||||
* @return {*}
|
||||
*/
|
||||
export function handleResponseError(response: Response) {
|
||||
const error: Service.RequestError = {
|
||||
errorType: 'Response Error',
|
||||
code: 0,
|
||||
message: ERROR_STATUS.default,
|
||||
data: null,
|
||||
}
|
||||
const errorCode: ErrorStatus = response.status as ErrorStatus
|
||||
const message = ERROR_STATUS[errorCode] || ERROR_STATUS.default
|
||||
Object.assign(error, { code: errorCode, message })
|
||||
|
||||
showError(error)
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @param {Record} data 接口返回的后台数据
|
||||
* @param {Service} config 后台字段配置
|
||||
* @return {*}
|
||||
*/
|
||||
export function handleBusinessError(data: Record<string, any>, config: Required<Service.BackendConfig>) {
|
||||
const { codeKey, msgKey } = config
|
||||
const error: Service.RequestError = {
|
||||
errorType: 'Business Error',
|
||||
code: data[codeKey],
|
||||
message: data[msgKey],
|
||||
data: data.data,
|
||||
}
|
||||
|
||||
showError(error)
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 统一成功和失败返回类型
|
||||
* @param {any} data
|
||||
* @param {boolean} isSuccess
|
||||
* @return {*} result
|
||||
*/
|
||||
export function handleServiceResult(data: any, isSuccess: boolean = true) {
|
||||
const result = {
|
||||
isSuccess,
|
||||
errorType: null,
|
||||
...data,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 处理接口token刷新
|
||||
@ -77,7 +15,7 @@ export async function handleRefreshToken() {
|
||||
}
|
||||
|
||||
// 刷新token
|
||||
const { data } = await fetchUpdateToken({ refreshToken: local.get('refreshToken') })
|
||||
const { data } = await fetchRefreshToken({ refreshToken: local.get('refreshToken') })
|
||||
if (data) {
|
||||
local.set('accessToken', data.accessToken)
|
||||
local.set('refreshToken', data.refreshToken)
|
||||
@ -87,12 +25,3 @@ export async function handleRefreshToken() {
|
||||
await authStore.logout()
|
||||
}
|
||||
}
|
||||
|
||||
export function showError(error: Service.RequestError) {
|
||||
// 如果error不需要提示,则跳过
|
||||
const code = Number(error.code)
|
||||
if (ERROR_NO_TIP_STATUS.includes(code))
|
||||
return
|
||||
|
||||
window.$message.error(error.message)
|
||||
}
|
||||
|
24
src/service/http/type.d.ts
vendored
Normal file
24
src/service/http/type.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
import type { Method } from 'alova'
|
||||
import type {
|
||||
ERROR_STATUS,
|
||||
} from './config'
|
||||
|
||||
export type ErrorStatus = keyof typeof ERROR_STATUS
|
||||
|
||||
export interface AlovaConfig {
|
||||
baseURL: string
|
||||
timeout?: number
|
||||
beforeRequest?: (method: Method<globalThis.Ref<unknown>>) => void
|
||||
}
|
||||
|
||||
/** 后端接口返回的数据结构配置 */
|
||||
export interface BackendConfig {
|
||||
/** 表示后端请求状态码的属性字段 */
|
||||
codeKey?: string
|
||||
/** 表示后端请求数据的属性字段 */
|
||||
dataKey?: string
|
||||
/** 表示后端消息的属性字段 */
|
||||
msgKey?: string
|
||||
/** 后端业务上定义的成功请求的状态 */
|
||||
successCode?: number | string
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
export * from './api/list'
|
||||
export * from './api/login'
|
||||
export * from './api/system'
|
||||
export * from './api/demo'
|
||||
export * from './api/test'
|
||||
export * from './api/system/user'
|
||||
export * from './api/system/menu'
|
||||
export * from './api/system/role'
|
||||
export * from './api/system/dict'
|
||||
|
@ -1,24 +1,22 @@
|
||||
import { router } from '@/router'
|
||||
import { fetchLogin } from '@/service'
|
||||
import { fetchLogin, fetchUserInfo } from '@/service'
|
||||
import { local } from '@/utils'
|
||||
import { useRouteStore } from './router'
|
||||
import { useTabStore } from './tab'
|
||||
|
||||
interface AuthStatus {
|
||||
userInfo: Api.Login.Info | null
|
||||
token: string
|
||||
userInfo: Entity.User | null
|
||||
}
|
||||
export const useAuthStore = defineStore('auth-store', {
|
||||
state: (): AuthStatus => {
|
||||
return {
|
||||
userInfo: local.get('userInfo'),
|
||||
token: local.get('accessToken') || '',
|
||||
userInfo: null,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
/** 是否登录 */
|
||||
isLogin(state) {
|
||||
return Boolean(state.token)
|
||||
return Boolean(state.userInfo)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -52,28 +50,32 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
},
|
||||
|
||||
/* 用户登录 */
|
||||
async login(userName: string, password: string) {
|
||||
try {
|
||||
const { isSuccess, data } = await fetchLogin({ userName, password })
|
||||
if (!isSuccess)
|
||||
return
|
||||
async login(userName: string, password: string, captchaId?: string, captcha?: string) {
|
||||
const loginData: any = { userName, password }
|
||||
|
||||
// 如果提供了验证码相关参数,则添加到登录数据中
|
||||
if (captchaId && captcha) {
|
||||
loginData.captchaId = captchaId
|
||||
loginData.captcha = captcha
|
||||
}
|
||||
|
||||
const { data } = await fetchLogin(loginData)
|
||||
|
||||
// 处理登录信息
|
||||
|
||||
await this.handleLoginInfo(data)
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('[Login Error]:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/* 处理登录返回的数据 */
|
||||
async handleLoginInfo(data: Api.Login.Info) {
|
||||
// 将token和userInfo保存下来
|
||||
local.set('userInfo', data)
|
||||
async handleLoginInfo(data: any) {
|
||||
// 将token保存下来
|
||||
local.set('accessToken', data.accessToken)
|
||||
if (data.refreshToken) {
|
||||
local.set('refreshToken', data.refreshToken)
|
||||
this.token = data.accessToken
|
||||
this.userInfo = data
|
||||
}
|
||||
|
||||
const res = await fetchUserInfo()
|
||||
this.userInfo = res.data
|
||||
|
||||
// 添加路由和菜单
|
||||
const routeStore = useRouteStore()
|
||||
@ -87,4 +89,7 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
})
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
storage: localStorage,
|
||||
},
|
||||
})
|
||||
|
@ -36,16 +36,11 @@ export const useDictStore = defineStore('dict-store', {
|
||||
},
|
||||
|
||||
async getDictByNet(code: string) {
|
||||
const { data, isSuccess } = await fetchDictList(code)
|
||||
if (isSuccess) {
|
||||
const { data } = await fetchDictList(code)
|
||||
Reflect.set(this.dictMap, code, data)
|
||||
// 同步至session
|
||||
session.set('dict', this.dictMap)
|
||||
return data
|
||||
}
|
||||
else {
|
||||
throw new Error(`Failed to get ${code} dictionary from network, check ${code} field or network`)
|
||||
}
|
||||
},
|
||||
initDict() {
|
||||
const dict = session.get('dict')
|
||||
@ -55,4 +50,7 @@ export const useDictStore = defineStore('dict-store', {
|
||||
this.isInitDict = true
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
storage: sessionStorage,
|
||||
},
|
||||
})
|
||||
|
@ -2,9 +2,10 @@ import type { MenuOption } from 'naive-ui'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { usePermission } from '@/hooks'
|
||||
import Layout from '@/layouts/index.vue'
|
||||
import { $t, arrayToTree, renderIcon } from '@/utils'
|
||||
import { $t, renderIcon } from '@/utils'
|
||||
import { clone, min, omit, pick } from 'radash'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import arrayToTree from 'array-to-tree'
|
||||
|
||||
const metaFields: AppRoute.MetaKeys[]
|
||||
= ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType']
|
||||
@ -36,7 +37,10 @@ export function createRoutes(routes: AppRoute.RowRoute[]) {
|
||||
})
|
||||
|
||||
// Generate route tree
|
||||
resultRouter = arrayToTree(resultRouter) as AppRoute.Route[]
|
||||
resultRouter = arrayToTree(resultRouter, {
|
||||
parentProperty: 'parentId',
|
||||
customID: 'id',
|
||||
}) as AppRoute.Route[]
|
||||
|
||||
const appRootRoute: RouteRecordRaw = {
|
||||
path: '/appRoot',
|
||||
@ -98,7 +102,10 @@ export function createMenus(userRoutes: AppRoute.RowRoute[]) {
|
||||
const visibleMenus = resultMenus.filter(route => !route.meta.hide)
|
||||
|
||||
// generate side menu
|
||||
return arrayToTree(transformAuthRoutesToMenus(visibleMenus))
|
||||
return arrayToTree(transformAuthRoutesToMenus(visibleMenus), {
|
||||
parentProperty: 'parentId',
|
||||
customID: 'id',
|
||||
})
|
||||
}
|
||||
|
||||
// render the returned routing table as a sidebar
|
||||
@ -121,7 +128,7 @@ function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
|
||||
.map((item) => {
|
||||
const target: MenuOption = {
|
||||
id: item.id,
|
||||
pid: item.pid,
|
||||
parentId: item.parentId,
|
||||
label:
|
||||
(!item.meta.menuType || item.meta.menuType === 'page')
|
||||
? () =>
|
||||
|
@ -2,8 +2,7 @@ import type { MenuOption } from 'naive-ui'
|
||||
import { router } from '@/router'
|
||||
import { staticRoutes } from '@/router/routes.static'
|
||||
import { fetchUserRoutes } from '@/service'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { $t, local } from '@/utils'
|
||||
import { $t } from '@/utils'
|
||||
import { createMenus, createRoutes, generateCacheRoutes } from './helper'
|
||||
|
||||
interface RoutesStatus {
|
||||
@ -41,15 +40,14 @@ export const useRouteStore = defineStore('route-store', {
|
||||
if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') {
|
||||
try {
|
||||
// Get user's route
|
||||
const result = await fetchUserRoutes({
|
||||
const { data } = await fetchUserRoutes({
|
||||
id: 1,
|
||||
})
|
||||
|
||||
if (!result.isSuccess || !result.data) {
|
||||
if (!data) {
|
||||
throw new Error('Failed to fetch user routes')
|
||||
}
|
||||
|
||||
return result.data
|
||||
return data
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize route info:', error)
|
||||
|
17
src/typings/api/login.d.ts
vendored
17
src/typings/api/login.d.ts
vendored
@ -1,17 +0,0 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
namespace Api {
|
||||
namespace Login {
|
||||
/* 登录返回的用户字段, 该数据是根据用户表扩展而来, 部分字段可能需要覆盖,例如id */
|
||||
interface Info extends Entity.User {
|
||||
/** 用户id */
|
||||
id: number
|
||||
/** 用户角色类型 */
|
||||
role: Entity.RoleType[]
|
||||
/** 访问token */
|
||||
accessToken: string
|
||||
/** 访问token */
|
||||
refreshToken: string
|
||||
}
|
||||
}
|
||||
}
|
63
src/typings/entities/dict.d.ts
vendored
63
src/typings/entities/dict.d.ts
vendored
@ -1,13 +1,62 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/* 字典数据库表字段 */
|
||||
/** 数据库表字段 */
|
||||
namespace Entity {
|
||||
interface DictType {
|
||||
/**
|
||||
* 字典名称
|
||||
*/
|
||||
dictName: string
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
dictType: string
|
||||
/**
|
||||
* 备注信息
|
||||
*/
|
||||
remark?: string
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
status?: number
|
||||
}
|
||||
|
||||
interface Dict {
|
||||
id?: number
|
||||
isRoot?: 0 | 1
|
||||
code: string
|
||||
label: string
|
||||
value?: number
|
||||
interface DictData {
|
||||
/**
|
||||
* 样式属性
|
||||
*/
|
||||
cssClass?: string
|
||||
/**
|
||||
* 字典标签
|
||||
*/
|
||||
dictLabel: string
|
||||
/**
|
||||
* 字典排序
|
||||
*/
|
||||
dictSort?: number
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
dictType: string
|
||||
/**
|
||||
* 字典键值
|
||||
*/
|
||||
dictValue: string
|
||||
/**
|
||||
* 是否默认
|
||||
*/
|
||||
isDefault?: number
|
||||
/**
|
||||
* 表格回显样式
|
||||
*/
|
||||
listClass?: string
|
||||
/**
|
||||
* 备注信息
|
||||
*/
|
||||
remark?: string
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
status?: number
|
||||
}
|
||||
}
|
||||
|
78
src/typings/entities/menu.d.ts
vendored
Normal file
78
src/typings/entities/menu.d.ts
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/** 数据库表字段 */
|
||||
namespace Entity {
|
||||
type MenuType = 'directory' | 'page' | 'permission'
|
||||
|
||||
interface Menu {
|
||||
menuId: number
|
||||
/**
|
||||
* 组件路径
|
||||
*/
|
||||
component: string
|
||||
/**
|
||||
* 菜单图标
|
||||
*/
|
||||
icon: string
|
||||
/**
|
||||
* 是否缓存
|
||||
*/
|
||||
keepAlive: boolean
|
||||
/**
|
||||
* 是否为外链
|
||||
*/
|
||||
isLink: boolean
|
||||
/**
|
||||
* 菜单名称
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* 国际化标识Key
|
||||
*/
|
||||
i18nKey: string
|
||||
/**
|
||||
* 菜单类型
|
||||
*/
|
||||
menuType: MenuType
|
||||
/**
|
||||
* 父菜单ID
|
||||
*/
|
||||
parentId: number
|
||||
/**
|
||||
* 路由地址
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* 高亮菜单路径
|
||||
*/
|
||||
activePath: string
|
||||
/**
|
||||
* 权限标识
|
||||
*/
|
||||
perms: string
|
||||
/**
|
||||
* 备注信息
|
||||
*/
|
||||
remark: string
|
||||
/**
|
||||
* 显示顺序
|
||||
*/
|
||||
sort: number
|
||||
/**
|
||||
* 菜单状态
|
||||
*/
|
||||
status: number
|
||||
/**
|
||||
* 菜单显示状态
|
||||
*/
|
||||
menuVisible: boolean
|
||||
/**
|
||||
* 标签栏显示状态
|
||||
*/
|
||||
tabVisible: boolean
|
||||
/**
|
||||
* 子菜单
|
||||
*/
|
||||
children?: Menu[]
|
||||
}
|
||||
}
|
2
src/typings/entities/message.d.ts
vendored
2
src/typings/entities/message.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/* 角色数据库表字段 */
|
||||
/* 数据库表字段 */
|
||||
namespace Entity {
|
||||
interface Message {
|
||||
id: number
|
||||
|
33
src/typings/entities/role.d.ts
vendored
33
src/typings/entities/role.d.ts
vendored
@ -1,13 +1,34 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/* 角色数据库表字段 */
|
||||
/* 数据库表字段 */
|
||||
namespace Entity {
|
||||
type RoleType = 'super' | 'admin' | 'user'
|
||||
type RoleType = string
|
||||
|
||||
interface Role {
|
||||
/** 用户id */
|
||||
id?: number
|
||||
/** 用户名 */
|
||||
role?: RoleType
|
||||
/**
|
||||
* 菜单ID数组
|
||||
*/
|
||||
menuIds?: number[]
|
||||
/**
|
||||
* 备注信息
|
||||
*/
|
||||
remark?: string
|
||||
/**
|
||||
* 角色权限字符串
|
||||
*/
|
||||
roleKey: string
|
||||
/**
|
||||
* 角色名称
|
||||
*/
|
||||
roleName: string
|
||||
/**
|
||||
* 角色状态
|
||||
*/
|
||||
roleStatus?: number
|
||||
/**
|
||||
* 显示顺序
|
||||
*/
|
||||
sort: number
|
||||
[property: string]: any
|
||||
}
|
||||
}
|
||||
|
43
src/typings/entities/user.d.ts
vendored
43
src/typings/entities/user.d.ts
vendored
@ -1,28 +1,39 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/** 用户数据库表字段 */
|
||||
/** 数据库表字段 */
|
||||
namespace Entity {
|
||||
interface User {
|
||||
/** 用户id */
|
||||
id?: number
|
||||
userId: number
|
||||
/** 部门id */
|
||||
deptId?: any
|
||||
/** 用户名 */
|
||||
userName?: string
|
||||
/* 用户头像 */
|
||||
avatar?: string
|
||||
/* 用户性别 */
|
||||
gender?: 0 | 1
|
||||
/* 用户邮箱 */
|
||||
username: string
|
||||
/** 密码 */
|
||||
password: string
|
||||
/** 昵称 */
|
||||
nickName?: string
|
||||
/** 邮箱 */
|
||||
email?: string
|
||||
/* 用户昵称 */
|
||||
nickname?: string
|
||||
/* 用户电话 */
|
||||
tel?: string
|
||||
/** 用户角色类型 */
|
||||
role?: Entity.RoleType[]
|
||||
/** 手机号 */
|
||||
phone?: string
|
||||
/** 性别 */
|
||||
gender: 'male' | 'female' | 'unknown'
|
||||
/** 头像 */
|
||||
avatar?: string
|
||||
/** 用户状态 */
|
||||
status?: 0 | 1
|
||||
userStatus: number
|
||||
/** 创建人 */
|
||||
createBy?: string
|
||||
/** 创建时间 */
|
||||
createTime: string
|
||||
/** 更新人 */
|
||||
updateBy?: string
|
||||
/** 更新时间 */
|
||||
updateTime: string
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
/** 用户角色类型 */
|
||||
roles: Entity.Role[]
|
||||
}
|
||||
|
||||
}
|
||||
|
22
src/typings/global.d.ts
vendored
22
src/typings/global.d.ts
vendored
@ -1,10 +1,30 @@
|
||||
/* 存放数据库实体表类型, 具体内容在 ./entities */
|
||||
declare namespace Entity {
|
||||
interface TreeNode {
|
||||
id: number
|
||||
label: string
|
||||
children: TreeNode[]
|
||||
}
|
||||
}
|
||||
|
||||
/* 各类接口返回的数据类型, 具体内容在 ./api */
|
||||
declare namespace Api {
|
||||
interface Response<T> {
|
||||
/** 业务状态码 */
|
||||
code: number
|
||||
/** 业务信息 */
|
||||
message: string
|
||||
/** 业务数据 */
|
||||
data: T
|
||||
}
|
||||
|
||||
interface ListResponse<T> extends Response {
|
||||
/** 业务数据 */
|
||||
data: {
|
||||
list: T[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
@ -35,7 +55,7 @@ declare namespace Storage {
|
||||
|
||||
interface Local {
|
||||
/* 存储用户信息 */
|
||||
userInfo: Api.Login.Info
|
||||
userInfo: Entity.User
|
||||
/* 存储访问token */
|
||||
accessToken: string
|
||||
/* 存储刷新token */
|
||||
|
2
src/typings/route.d.ts
vendored
2
src/typings/route.d.ts
vendored
@ -43,7 +43,7 @@ declare namespace AppRoute {
|
||||
/* 路由id */
|
||||
id: number
|
||||
/* 父级路由id,顶级页面为null */
|
||||
pid: number | null
|
||||
parentId: number | null
|
||||
}
|
||||
|
||||
/** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */
|
||||
|
49
src/typings/service.d.ts
vendored
49
src/typings/service.d.ts
vendored
@ -1,49 +0,0 @@
|
||||
/** 请求的相关类型 */
|
||||
declare namespace Service {
|
||||
import type { Method } from 'alova'
|
||||
|
||||
interface AlovaConfig {
|
||||
baseURL: string
|
||||
timeout?: number
|
||||
beforeRequest?: (method: Method<globalThis.Ref<unknown>>) => void
|
||||
}
|
||||
|
||||
/** 后端接口返回的数据结构配置 */
|
||||
interface BackendConfig {
|
||||
/** 表示后端请求状态码的属性字段 */
|
||||
codeKey?: string
|
||||
/** 表示后端请求数据的属性字段 */
|
||||
dataKey?: string
|
||||
/** 表示后端消息的属性字段 */
|
||||
msgKey?: string
|
||||
/** 后端业务上定义的成功请求的状态 */
|
||||
successCode?: number | string
|
||||
}
|
||||
|
||||
type RequestErrorType = 'Response Error' | 'Business Error' | null
|
||||
type RequestCode = string | number
|
||||
|
||||
interface RequestError {
|
||||
/** 请求服务的错误类型 */
|
||||
errorType: RequestErrorType
|
||||
/** 错误码 */
|
||||
code: RequestCode
|
||||
/** 错误信息 */
|
||||
message: string
|
||||
/** 返回的数据 */
|
||||
data?: any
|
||||
}
|
||||
|
||||
interface ResponseResult<T> extends RequestError {
|
||||
/** 请求服务是否成功 */
|
||||
isSuccess: boolean
|
||||
/** 请求服务的错误类型 */
|
||||
errorType: RequestErrorType
|
||||
/** 错误码 */
|
||||
code: RequestCode
|
||||
/** 错误信息 */
|
||||
message: string
|
||||
/** 返回的数据 */
|
||||
data: T
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 将给定的数组转换为树形结构。
|
||||
* @param arr - 原始数组,其中每个元素包含id和pid属性,pid表示父级id。
|
||||
* @returns 返回转换后的树形结构数组。
|
||||
*/
|
||||
export function arrayToTree(arr: any[]) {
|
||||
// 初始化结果数组
|
||||
const res: any = []
|
||||
// 使用Map存储数组元素,以id为键,元素本身为值
|
||||
const map = new Map()
|
||||
|
||||
// 遍历数组,将每个元素以id为键存储到Map中
|
||||
arr.forEach((item) => {
|
||||
map.set(item.id, item)
|
||||
})
|
||||
|
||||
// 再次遍历数组,根据pid将元素组织成树形结构
|
||||
arr.forEach((item) => {
|
||||
// 获取当前元素的父级元素
|
||||
const parent = item.pid && map.get(item.pid)
|
||||
// 如果有父级元素
|
||||
if (parent) {
|
||||
// 如果父级元素已有子元素,则将当前元素追加到子元素数组中
|
||||
if (parent?.children)
|
||||
parent.children.push(item)
|
||||
// 如果父级元素没有子元素,则创建子元素数组,并将当前元素作为第一个元素
|
||||
else
|
||||
parent.children = [item]
|
||||
}
|
||||
// 如果没有父级元素,则将当前元素直接添加到结果数组中
|
||||
else {
|
||||
res.push(item)
|
||||
}
|
||||
})
|
||||
// 返回组织好的树形结构数组
|
||||
return res
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import type { NDateLocale, NLocale } from 'naive-ui'
|
||||
import { i18n } from '@/modules/i18n'
|
||||
import { dateZhCN, zhCN } from 'naive-ui'
|
||||
import { dateZhCN } from 'naive-ui'
|
||||
import { zhCN } from 'pro-naive-ui'
|
||||
|
||||
export function setLocale(locale: App.lang) {
|
||||
i18n.global.locale.value = locale
|
||||
|
@ -1,5 +1,4 @@
|
||||
export * from './storage'
|
||||
export * from './array'
|
||||
export * from './i18n'
|
||||
export * from './icon'
|
||||
export * from './normalize'
|
||||
|
@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInst } from 'naive-ui'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { fetchCaptchaImage } from '@/service'
|
||||
import { local } from '@/utils'
|
||||
import { useBoolean } from '@/hooks'
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
const authStore = useAuthStore()
|
||||
|
||||
function toOtherForm(type: any) {
|
||||
@ -12,8 +14,16 @@ function toOtherForm(type: any) {
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 验证码相关状态
|
||||
const captchaData = ref({
|
||||
captchaId: '',
|
||||
captchaImage: '',
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
const baseRules = {
|
||||
account: {
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
@ -25,13 +35,45 @@ const rules = computed(() => {
|
||||
message: t('login.passwordRuleTip'),
|
||||
},
|
||||
}
|
||||
|
||||
// 如果启用验证码,添加验证码验证规则
|
||||
if (captchaData.value.enabled) {
|
||||
(baseRules as any).captcha = {
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
message: '请输入验证码',
|
||||
}
|
||||
}
|
||||
|
||||
return baseRules
|
||||
})
|
||||
const formValue = ref({
|
||||
account: 'super',
|
||||
pwd: '123456',
|
||||
account: 'admin',
|
||||
pwd: '12345',
|
||||
captcha: '',
|
||||
})
|
||||
const isRemember = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 获取验证码
|
||||
async function getCaptcha() {
|
||||
try {
|
||||
const { data } = await fetchCaptchaImage()
|
||||
captchaData.value = {
|
||||
captchaId: data.captchaId,
|
||||
captchaImage: data.captchaImage,
|
||||
enabled: data.enabled,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取验证码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新验证码
|
||||
function refreshCaptcha() {
|
||||
formValue.value.captcha = ''
|
||||
getCaptcha()
|
||||
}
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
function handleLogin() {
|
||||
@ -39,19 +81,29 @@ function handleLogin() {
|
||||
if (errors)
|
||||
return
|
||||
|
||||
isLoading.value = true
|
||||
const { account, pwd } = formValue.value
|
||||
startLoading()
|
||||
const { account, pwd, captcha } = formValue.value
|
||||
|
||||
if (isRemember.value)
|
||||
if (isRemember.value) {
|
||||
local.set('loginAccount', { account, pwd })
|
||||
else local.remove('loginAccount')
|
||||
}
|
||||
else {
|
||||
local.remove('loginAccount')
|
||||
}
|
||||
|
||||
await authStore.login(account, pwd)
|
||||
isLoading.value = false
|
||||
// 传递验证码相关参数
|
||||
try {
|
||||
await authStore.login(account, pwd, captchaData.value.captchaId, captcha)
|
||||
}
|
||||
catch {
|
||||
refreshCaptcha()
|
||||
}
|
||||
endLoading()
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
checkUserAccount()
|
||||
getCaptcha()
|
||||
})
|
||||
function checkUserAccount() {
|
||||
const loginAccount = local.get('loginAccount')
|
||||
@ -82,6 +134,25 @@ function checkUserAccount() {
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="captchaData.enabled" path="captcha">
|
||||
<div class="flex w-full gap-2">
|
||||
<n-input
|
||||
v-model:value="formValue.captcha"
|
||||
placeholder="请输入验证码"
|
||||
clearable
|
||||
class="flex-1"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center justify-center w-24 h-10 border border-gray-300 rounded cursor-pointer hover:border-primary-500 transition-colors"
|
||||
@click="refreshCaptcha"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
v-html="captchaData.captchaImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-space vertical :size="20">
|
||||
<div class="flex-y-center justify-between">
|
||||
<n-checkbox v-model:checked="isRemember">
|
||||
@ -91,7 +162,7 @@ function checkUserAccount() {
|
||||
{{ $t('login.forgotPassword') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<n-button block type="primary" size="large" :loading="isLoading" :disabled="isLoading" @click="handleLogin">
|
||||
<n-button block type="primary" size="large" :loading="loading" :disabled="loading" @click="handleLogin">
|
||||
{{ $t('login.signIn') }}
|
||||
</n-button>
|
||||
<n-flex>
|
||||
@ -102,26 +173,6 @@ function checkUserAccount() {
|
||||
</n-flex>
|
||||
</n-space>
|
||||
</n-form>
|
||||
<n-divider>
|
||||
<span op-80>{{ $t('login.or') }}</span>
|
||||
</n-divider>
|
||||
<n-space justify="center">
|
||||
<n-button circle>
|
||||
<template #icon>
|
||||
<n-icon><icon-park-outline-wechat /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button circle>
|
||||
<template #icon>
|
||||
<n-icon><icon-park-outline-tencent-qq /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button circle>
|
||||
<template #icon>
|
||||
<n-icon><icon-park-outline-github-one /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -2,88 +2,88 @@
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const { userInfo } = authStore
|
||||
const formRef = ref()
|
||||
const formValue = ref({
|
||||
user: {
|
||||
name: '',
|
||||
age: '',
|
||||
},
|
||||
phone: '',
|
||||
})
|
||||
const rules = {
|
||||
user: {
|
||||
name: {
|
||||
required: true,
|
||||
message: '请输入姓名',
|
||||
trigger: 'blur',
|
||||
},
|
||||
age: {
|
||||
required: true,
|
||||
message: '请输入年龄',
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
},
|
||||
phone: {
|
||||
required: true,
|
||||
message: '请输入电话号码',
|
||||
trigger: ['input'],
|
||||
},
|
||||
|
||||
// 性别显示映射
|
||||
const genderMap = {
|
||||
male: '男',
|
||||
female: '女',
|
||||
unknown: '未知',
|
||||
}
|
||||
|
||||
function handleValidateClick() {
|
||||
formRef.value?.validate((errors: any) => {
|
||||
if (!errors)
|
||||
window.$message.success('验证通过')
|
||||
else window.$message.error('验证不通过')
|
||||
})
|
||||
// 获取问候语
|
||||
function getGreeting() {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 6)
|
||||
return '夜深了'
|
||||
if (hour < 9)
|
||||
return '早上好'
|
||||
if (hour < 12)
|
||||
return '上午好'
|
||||
if (hour < 14)
|
||||
return '中午好'
|
||||
if (hour < 18)
|
||||
return '下午好'
|
||||
if (hour < 22)
|
||||
return '晚上好'
|
||||
return '夜深了'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-card title="个人信息">
|
||||
<n-space size="large">
|
||||
<n-avatar round :size="128" :src="userInfo?.avatar" />
|
||||
<!-- 基本信息卡片 -->
|
||||
<n-card :title="`${getGreeting()},${userInfo?.nickName || userInfo?.username || '用户'},欢迎使用个人中心`">
|
||||
<n-flex gap="16px" align="center">
|
||||
<n-avatar
|
||||
round
|
||||
:size="128"
|
||||
:src="userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${userInfo!.username}`"
|
||||
class="m-x-6"
|
||||
/>
|
||||
|
||||
<n-descriptions label-placement="left" :column="2" :title="`傍晚好,${userInfo?.nickname},这里是简单的个人中心模板`">
|
||||
<n-descriptions-item label="id">
|
||||
{{ userInfo?.id }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions
|
||||
label-placement="left"
|
||||
class="flex-1"
|
||||
:column="2"
|
||||
>
|
||||
<n-descriptions-item label="用户名">
|
||||
{{ userInfo?.userName }}
|
||||
{{ userInfo?.username || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="真实名称">
|
||||
{{ userInfo?.nickname }}
|
||||
<n-descriptions-item label="昵称">
|
||||
{{ userInfo?.nickName || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="角色">
|
||||
{{ userInfo?.role }}
|
||||
<n-descriptions-item label="性别">
|
||||
<n-tag size="small">
|
||||
{{ genderMap[userInfo?.gender || 'unknown'] }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="邮箱">
|
||||
{{ userInfo?.email || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="手机号">
|
||||
{{ userInfo?.phone || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="部门ID">
|
||||
{{ userInfo?.deptId || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="拥有角色" :span="2">
|
||||
<n-space>
|
||||
<n-tag
|
||||
v-for="(role, index) in userInfo?.roles"
|
||||
:key="index"
|
||||
type="primary"
|
||||
size="small"
|
||||
>
|
||||
{{ role?.roleName || role?.name || `角色${index + 1}` }}
|
||||
</n-tag>
|
||||
<n-tag v-if="!userInfo?.roles || userInfo?.roles.length === 0" type="default" size="small">
|
||||
暂无角色
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<n-card title="信息修改">
|
||||
<n-space justify="center">
|
||||
<n-form ref="formRef" class="w-500px" :label-width="80" :model="formValue" :rules="rules">
|
||||
<n-form-item label="姓名" path="user.name">
|
||||
<n-input v-model:value="formValue.user.name" placeholder="输入姓名" />
|
||||
</n-form-item>
|
||||
<n-form-item label="年龄" path="user.age">
|
||||
<n-input v-model:value="formValue.user.age" placeholder="输入年龄" />
|
||||
</n-form-item>
|
||||
<n-form-item label="电话号码" path="phone">
|
||||
<n-input v-model:value="formValue.phone" placeholder="电话号码" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" attr-type="button" block @click="handleValidateClick">
|
||||
验证
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -10,8 +10,7 @@ const subOptions = ref()
|
||||
const currentDict = ref()
|
||||
|
||||
async function getAlldict() {
|
||||
const { data, isSuccess } = await fetchDictList()
|
||||
if (isSuccess) {
|
||||
const { data } = await fetchDictList()
|
||||
options.value = data.map((item) => {
|
||||
return {
|
||||
label: item.label,
|
||||
@ -19,7 +18,6 @@ async function getAlldict() {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
function changeSelect(v: string) {
|
||||
dict(v).then((data) => {
|
||||
currentDict.value = data
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
fetchUpdateToken,
|
||||
fetchRefreshToken,
|
||||
} from '@/service'
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -8,7 +8,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
async function updataToken() {
|
||||
const res = await fetchUpdateToken({ token: 'test token' })
|
||||
const res = await fetchRefreshToken({ token: 'test token' })
|
||||
emit('update', res)
|
||||
}
|
||||
</script>
|
||||
|
@ -14,7 +14,7 @@ function toggleUserRole(role: Entity.RoleType) {
|
||||
|
||||
<template>
|
||||
<n-card title="权限示例">
|
||||
<n-h1> 当前权限:{{ authStore.userInfo!.role }}</n-h1>
|
||||
<n-h1> 当前权限:{{ authStore.userInfo!.roles.map(r => r.roleName) }}</n-h1>
|
||||
<n-button-group>
|
||||
<n-button v-for="item in roleList" :key="item" type="default" @click="toggleUserRole(item)">
|
||||
{{ item }}
|
||||
|
110
src/views/setting/account/columns.tsx
Normal file
110
src/views/setting/account/columns.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
|
||||
import CopyText from '@/components/custom/CopyText.vue'
|
||||
|
||||
// 用户管理columns配置函数
|
||||
interface UserColumnActions {
|
||||
onEdit: (row: Entity.User) => void
|
||||
onDelete: (id: number) => void
|
||||
onStatusChange: (value: 0 | 1, id: number) => void
|
||||
}
|
||||
|
||||
export function createUserColumns(actions: UserColumnActions): DataTableColumns<Entity.User> {
|
||||
return [
|
||||
{
|
||||
title: 'ID',
|
||||
align: 'center',
|
||||
key: 'userId',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
align: 'center',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
align: 'center',
|
||||
key: 'nickName',
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
align: 'center',
|
||||
key: 'gender',
|
||||
render: (row) => {
|
||||
const genderMap = {
|
||||
male: '男',
|
||||
female: '女',
|
||||
unknown: '未知',
|
||||
}
|
||||
return (
|
||||
<NTag size="small">
|
||||
{genderMap[row.gender || 'unknown']}
|
||||
</NTag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
align: 'center',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
align: 'center',
|
||||
key: 'phone',
|
||||
render: (row) => {
|
||||
return row.phone ? <CopyText value={row.phone} /> : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
key: 'userStatus',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NSwitch
|
||||
value={row.userStatus}
|
||||
checked-value={1}
|
||||
unchecked-value={0}
|
||||
onUpdateValue={(value: 0 | 1) =>
|
||||
actions.onStatusChange(value, row.userId)}
|
||||
>
|
||||
{{ checked: () => '启用', unchecked: () => '禁用' }}
|
||||
</NSwitch>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
align: 'center',
|
||||
key: 'createTime',
|
||||
render: (row) => {
|
||||
return row.createTime ? new Date(row.createTime).toLocaleDateString() : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
key: 'actions',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NSpace justify="center">
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => actions.onEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => actions.onDelete(row.userId)}>
|
||||
{{
|
||||
default: () => '确认删除',
|
||||
trigger: () => <NButton size="small" type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { fetchRoleList } from '@/service'
|
||||
import { createUser, fetchRoleList, updateUser } from '@/service'
|
||||
|
||||
interface Props {
|
||||
modalName?: string
|
||||
@ -13,20 +13,26 @@ const {
|
||||
const emit = defineEmits<{
|
||||
open: []
|
||||
close: []
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { bool: modalVisible, setTrue: showModal, setFalse: hiddenModal } = useBoolean(false)
|
||||
|
||||
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
|
||||
const formDefault: Entity.User = {
|
||||
userName: '',
|
||||
const formDefault = {
|
||||
userId: 0,
|
||||
username: '',
|
||||
nickName: '',
|
||||
email: '',
|
||||
tel: '',
|
||||
role: [],
|
||||
status: 1,
|
||||
phone: '',
|
||||
gender: 'unknown' as 'male' | 'female' | 'unknown',
|
||||
deptId: undefined,
|
||||
userStatus: 1,
|
||||
roles: [] as any[],
|
||||
remark: '',
|
||||
}
|
||||
const formModel = ref<Entity.User>({ ...formDefault })
|
||||
const formModel = ref<Partial<Entity.User>>({ ...formDefault })
|
||||
|
||||
type ModalType = 'add' | 'view' | 'edit'
|
||||
const modalType = shallowRef<ModalType>('add')
|
||||
@ -76,20 +82,45 @@ const formRef = ref()
|
||||
async function submitModal() {
|
||||
const handlers = {
|
||||
async add() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.$message.success('模拟新增成功')
|
||||
resolve(true)
|
||||
}, 2000)
|
||||
try {
|
||||
await createUser({
|
||||
username: formModel.value.username!,
|
||||
password: '123456', // 默认密码
|
||||
nickName: formModel.value.nickName,
|
||||
email: formModel.value.email,
|
||||
phone: formModel.value.phone,
|
||||
gender: formModel.value.gender,
|
||||
deptId: formModel.value.deptId,
|
||||
remark: formModel.value.remark,
|
||||
})
|
||||
window.$message.success('用户创建成功')
|
||||
emit('success')
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
window.$message.error('创建用户失败')
|
||||
return false
|
||||
}
|
||||
},
|
||||
async edit() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.$message.success('模拟编辑成功')
|
||||
resolve(true)
|
||||
}, 2000)
|
||||
try {
|
||||
await updateUser(formModel.value.userId!, {
|
||||
nickName: formModel.value.nickName,
|
||||
email: formModel.value.email,
|
||||
phone: formModel.value.phone,
|
||||
gender: formModel.value.gender,
|
||||
userStatus: formModel.value.userStatus,
|
||||
deptId: formModel.value.deptId,
|
||||
remark: formModel.value.remark,
|
||||
})
|
||||
window.$message.success('用户更新成功')
|
||||
emit('success')
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
window.$message.error('更新用户失败')
|
||||
return false
|
||||
}
|
||||
},
|
||||
async view() {
|
||||
return true
|
||||
@ -97,15 +128,31 @@ async function submitModal() {
|
||||
}
|
||||
await formRef.value?.validate()
|
||||
startLoading()
|
||||
await handlers[modalType.value]() && closeModal()
|
||||
const success = await handlers[modalType.value]()
|
||||
endLoading()
|
||||
if (success) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
userName: {
|
||||
username: {
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
trigger: 'blur',
|
||||
},
|
||||
email: {
|
||||
required: false,
|
||||
pattern: /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/,
|
||||
message: '请输入正确的邮箱格式',
|
||||
trigger: 'blur',
|
||||
},
|
||||
phone: {
|
||||
required: false,
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '请输入正确的手机号格式',
|
||||
trigger: 'blur',
|
||||
},
|
||||
}
|
||||
|
||||
const options = ref()
|
||||
@ -129,41 +176,42 @@ async function getRoleList() {
|
||||
>
|
||||
<n-form ref="formRef" :rules="rules" label-placement="left" :model="formModel" :label-width="100" :disabled="modalType === 'view'">
|
||||
<n-grid :cols="2" :x-gap="18">
|
||||
<n-form-item-grid-item :span="1" label="用户名" path="userName">
|
||||
<n-input v-model:value="formModel.userName" />
|
||||
<n-form-item-grid-item :span="1" label="用户名" path="username">
|
||||
<n-input v-model:value="formModel.username" :disabled="modalType === 'edit'" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="昵称" path="nickName">
|
||||
<n-input v-model:value="formModel.nickName" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="性别" path="gender">
|
||||
<n-radio-group v-model:value="formModel.gender">
|
||||
<n-space>
|
||||
<n-radio :value="1">
|
||||
<n-radio value="male">
|
||||
男
|
||||
</n-radio>
|
||||
<n-radio :value="0">
|
||||
<n-radio value="female">
|
||||
女
|
||||
</n-radio>
|
||||
<n-radio value="unknown">
|
||||
未知
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="部门ID" path="deptId">
|
||||
<n-input-number v-model:value="formModel.deptId" class="w-full" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="邮箱" path="email">
|
||||
<n-input v-model:value="formModel.email" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="联系方式" path="tel">
|
||||
<n-input v-model:value="formModel.tel" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="2" label="角色" path="role">
|
||||
<n-select
|
||||
v-model:value="formModel.role" multiple filterable
|
||||
label-field="role"
|
||||
value-field="id"
|
||||
:options="options"
|
||||
/>
|
||||
<n-form-item-grid-item :span="1" label="手机号" path="phone">
|
||||
<n-input v-model:value="formModel.phone" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="2" label="备注" path="remark">
|
||||
<n-input v-model:value="formModel.remark" type="textarea" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="用户状态" path="status">
|
||||
<n-form-item-grid-item :span="1" label="用户状态" path="userStatus">
|
||||
<n-switch
|
||||
v-model:value="formModel.status"
|
||||
v-model:value="formModel.userStatus"
|
||||
:checked-value="1" :unchecked-value="0"
|
||||
>
|
||||
<template #checked>
|
||||
|
@ -1,10 +1,8 @@
|
||||
<script setup lang="tsx">
|
||||
import type { DataTableColumns, FormInst } from 'naive-ui'
|
||||
import CopyText from '@/components/custom/CopyText.vue'
|
||||
import { Gender } from '@/constants'
|
||||
import type { FormInst } from 'naive-ui'
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { fetchUserPage } from '@/service'
|
||||
import { NButton, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
|
||||
import { deleteUser, getUserList, updateUser } from '@/service'
|
||||
import { createUserColumns } from './columns'
|
||||
import TableModal from './components/TableModal.vue'
|
||||
|
||||
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
@ -21,111 +19,58 @@ function handleResetSearch() {
|
||||
const formRef = ref<FormInst | null>()
|
||||
const modalRef = ref()
|
||||
|
||||
function delteteUser(id: number) {
|
||||
window.$message.success(`删除用户id:${id}`)
|
||||
async function delteteUser(id: number) {
|
||||
try {
|
||||
await deleteUser(id)
|
||||
window.$message.success('用户删除成功')
|
||||
getUserPageList() // 重新加载列表
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<Entity.User> = [
|
||||
{
|
||||
title: '姓名',
|
||||
align: 'center',
|
||||
key: 'userName',
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
align: 'center',
|
||||
key: 'gender',
|
||||
render: (row) => {
|
||||
const tagType = {
|
||||
0: 'primary',
|
||||
1: 'success',
|
||||
} as const
|
||||
if (row.gender) {
|
||||
return (
|
||||
<NTag type={tagType[row.gender]}>
|
||||
{Gender[row.gender]}
|
||||
</NTag>
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
align: 'center',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
align: 'center',
|
||||
key: 'tel',
|
||||
render: (row) => {
|
||||
return (
|
||||
<CopyText value={row.tel} />
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
key: 'status',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NSwitch
|
||||
value={row.status}
|
||||
checked-value={1}
|
||||
unchecked-value={0}
|
||||
onUpdateValue={(value: 0 | 1) =>
|
||||
handleUpdateDisabled(value, row.id!)}
|
||||
>
|
||||
{{ checked: () => '启用', unchecked: () => '禁用' }}
|
||||
</NSwitch>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
key: 'actions',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NSpace justify="center">
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => modalRef.value.openModal('edit', row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => delteteUser(row.id!)}>
|
||||
{{
|
||||
default: () => '确认删除',
|
||||
trigger: () => <NButton size="small" type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
// 用户管理columns配置
|
||||
const columns = createUserColumns({
|
||||
onEdit: row => modalRef.value?.openModal('edit', row),
|
||||
onDelete: delteteUser,
|
||||
onStatusChange: handleUpdateDisabled,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
const listData = ref<Entity.User[]>([])
|
||||
function handleUpdateDisabled(value: 0 | 1, id: number) {
|
||||
const index = listData.value.findIndex(item => item.id === id)
|
||||
if (index > -1)
|
||||
listData.value[index].status = value
|
||||
async function handleUpdateDisabled(value: 0 | 1, id: number) {
|
||||
try {
|
||||
// 使用updateUser接口更新用户状态
|
||||
await updateUser(id, { userStatus: value })
|
||||
const index = listData.value.findIndex(item => item.userId === id)
|
||||
if (index > -1) {
|
||||
listData.value[index].userStatus = value
|
||||
}
|
||||
window.$message.success('用户状态更新成功')
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserList() {
|
||||
async function getUserPageList() {
|
||||
startLoading()
|
||||
await fetchUserPage().then((res: any) => {
|
||||
listData.value = res.data.list
|
||||
count.value = res.data.count
|
||||
endLoading()
|
||||
try {
|
||||
const res = await getUserList({
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
username: model.value.condition_1,
|
||||
gender: model.value.condition_2 as any,
|
||||
})
|
||||
listData.value = res.data.list
|
||||
count.value = res.data.total
|
||||
}
|
||||
finally {
|
||||
endLoading()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
getUserPageList()
|
||||
})
|
||||
|
||||
function changePage(page: number, size: number) {
|
||||
@ -181,7 +126,7 @@ const treeData = ref([
|
||||
<n-input v-model:value="model.condition_2" placeholder="请输入" />
|
||||
</n-form-item>
|
||||
<n-flex class="ml-auto">
|
||||
<NButton type="primary" @click="getUserList">
|
||||
<NButton type="primary" @click="getUserPageList">
|
||||
<template #icon>
|
||||
<icon-park-outline-search />
|
||||
</template>
|
||||
@ -212,7 +157,7 @@ const treeData = ref([
|
||||
<Pagination :count="count" @change="changePage" />
|
||||
</NSpace>
|
||||
|
||||
<TableModal ref="modalRef" modal-name="用户" />
|
||||
<TableModal ref="modalRef" modal-name="用户" @success="getUserPageList" />
|
||||
</n-card>
|
||||
</NSpace>
|
||||
</n-flex>
|
||||
|
144
src/views/setting/dictionary/columns.tsx
Normal file
144
src/views/setting/dictionary/columns.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NFlex, NPopconfirm } from 'naive-ui'
|
||||
import CopyText from '@/components/custom/CopyText.vue'
|
||||
|
||||
// 字典类型columns配置函数
|
||||
interface DictTypeColumnActions {
|
||||
onView: (code: string) => void
|
||||
onEdit: (row: Entity.DictType) => void
|
||||
onDelete: (id: number) => void
|
||||
}
|
||||
|
||||
export function createDictTypeColumns(actions: DictTypeColumnActions): DataTableColumns<Entity.DictType> {
|
||||
return [
|
||||
{
|
||||
title: '字典项',
|
||||
key: 'dictName',
|
||||
},
|
||||
{
|
||||
title: '字典码',
|
||||
key: 'dictType',
|
||||
render: (row) => {
|
||||
return (
|
||||
<CopyText value={row.dictType} />
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return (
|
||||
<span>{row.status === 1 ? '正常' : '停用'}</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NFlex justify="center">
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => actions.onView(row.dictType)}
|
||||
>
|
||||
查看字典
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => actions.onEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => actions.onDelete(row.id!)}>
|
||||
{{
|
||||
default: () => (
|
||||
<span>
|
||||
确认删除字典类型
|
||||
<b>{row.dictName}</b>
|
||||
{' '}
|
||||
?
|
||||
</span>
|
||||
),
|
||||
trigger: () => <NButton size="small" type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 字典数据columns配置函数
|
||||
interface DictDataColumnActions {
|
||||
onEdit: (row: Entity.DictData) => void
|
||||
onDelete: (id: number) => void
|
||||
}
|
||||
|
||||
export function createDictDataColumns(actions: DictDataColumnActions): DataTableColumns<Entity.DictData> {
|
||||
return [
|
||||
{
|
||||
title: '字典名称',
|
||||
key: 'dictLabel',
|
||||
},
|
||||
{
|
||||
title: '字典码',
|
||||
key: 'dictType',
|
||||
},
|
||||
{
|
||||
title: '字典值',
|
||||
key: 'dictValue',
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
key: 'dictSort',
|
||||
align: 'center',
|
||||
width: '80px',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return (
|
||||
<span>{row.status === 1 ? '正常' : '停用'}</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
width: '15em',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NFlex justify="center">
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => actions.onEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => actions.onDelete(row.id!)}>
|
||||
{{
|
||||
default: () => (
|
||||
<span>
|
||||
确认删除字典数据
|
||||
<b>{row.dictLabel}</b>
|
||||
{' '}
|
||||
?
|
||||
</span>
|
||||
),
|
||||
trigger: () => <NButton size="small" type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormRules } from 'naive-ui'
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { createDictData, createDictType, updateDictData, updateDictType } from '@/service'
|
||||
|
||||
interface Props {
|
||||
modalName?: string
|
||||
@ -17,17 +18,18 @@ const {
|
||||
const emit = defineEmits<{
|
||||
open: []
|
||||
close: []
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { bool: modalVisible, setTrue: showModal, setFalse: hiddenModal } = useBoolean(false)
|
||||
|
||||
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
|
||||
const formDefault: Entity.Dict = {
|
||||
const formDefault: any = {
|
||||
label: '',
|
||||
code: '',
|
||||
}
|
||||
const formModel = ref<Entity.Dict>({ ...formDefault })
|
||||
const formModel = ref<any>({ ...formDefault })
|
||||
|
||||
type ModalType = 'add' | 'view' | 'edit'
|
||||
const modalType = shallowRef<ModalType>('add')
|
||||
@ -81,20 +83,54 @@ const formRef = ref()
|
||||
async function submitModal() {
|
||||
const handlers = {
|
||||
async add() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.$message.success('模拟新增成功')
|
||||
resolve(true)
|
||||
}, 2000)
|
||||
try {
|
||||
if (isRoot) {
|
||||
// 创建字典类型
|
||||
await createDictType({
|
||||
dictType: formModel.value.label,
|
||||
dictName: formModel.value.code,
|
||||
})
|
||||
window.$message.success('字典类型创建成功')
|
||||
}
|
||||
else {
|
||||
// 创建字典数据
|
||||
await createDictData({
|
||||
dictType: formModel.value.label,
|
||||
dictValue: formModel.value.value,
|
||||
})
|
||||
window.$message.success('字典数据创建成功')
|
||||
}
|
||||
emit('success')
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
async edit() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.$message.success('模拟编辑成功')
|
||||
resolve(true)
|
||||
}, 2000)
|
||||
try {
|
||||
if (isRoot) {
|
||||
// 更新字典类型
|
||||
await updateDictType(formModel.value.id!, {
|
||||
dictType: formModel.value.label as any,
|
||||
dictName: formModel.value.code,
|
||||
})
|
||||
window.$message.success('字典类型更新成功')
|
||||
}
|
||||
else {
|
||||
// 更新字典数据
|
||||
await updateDictData(formModel.value.id!, {
|
||||
dictType: formModel.value.label,
|
||||
dictValue: formModel.value.value,
|
||||
})
|
||||
window.$message.success('字典数据更新成功')
|
||||
}
|
||||
emit('success')
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
async view() {
|
||||
return true
|
||||
@ -102,7 +138,11 @@ async function submitModal() {
|
||||
}
|
||||
await formRef.value?.validate()
|
||||
startLoading()
|
||||
await handlers[modalType.value]() && closeModal()
|
||||
const success = await handlers[modalType.value]()
|
||||
endLoading()
|
||||
if (success) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
const rules: FormRules = {
|
||||
|
@ -1,17 +1,12 @@
|
||||
<script setup lang="tsx">
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import CopyText from '@/components/custom/CopyText.vue'
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { fetchDictList } from '@/service'
|
||||
import { useDictStore } from '@/store'
|
||||
import { NButton, NFlex, NPopconfirm } from 'naive-ui'
|
||||
import { deleteDictData, deleteDictType, getDictDataByType, getDictTypeList } from '@/service'
|
||||
import { createDictDataColumns, createDictTypeColumns } from './columns'
|
||||
import DictModal from './components/DictModal.vue'
|
||||
|
||||
const { bool: dictLoading, setTrue: startDictLoading, setFalse: endDictLoading } = useBoolean(false)
|
||||
const { bool: contentLoading, setTrue: startContentLoading, setFalse: endContentLoading } = useBoolean(false)
|
||||
|
||||
const { getDictByNet } = useDictStore()
|
||||
|
||||
const dictRef = ref<InstanceType<typeof DictModal>>()
|
||||
const dictContentRef = ref<InstanceType<typeof DictModal>>()
|
||||
|
||||
@ -19,126 +14,70 @@ onMounted(() => {
|
||||
getDictList()
|
||||
})
|
||||
|
||||
const dictData = ref<Entity.Dict[]>([])
|
||||
const dictContentData = ref<Entity.Dict[]>([])
|
||||
const dictData = ref<Entity.DictType[]>([])
|
||||
const dictContentData = ref<Entity.DictData[]>([])
|
||||
|
||||
async function getDictList() {
|
||||
startDictLoading()
|
||||
const { data, isSuccess } = await fetchDictList()
|
||||
if (isSuccess) {
|
||||
dictData.value = data
|
||||
try {
|
||||
const { data } = await getDictTypeList()
|
||||
dictData.value = data.list
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取字典类型列表失败', error)
|
||||
}
|
||||
finally {
|
||||
endDictLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const lastDictCode = ref('')
|
||||
async function getDictContent(code: string) {
|
||||
startContentLoading()
|
||||
dictContentData.value = await getDictByNet(code)
|
||||
try {
|
||||
const { data } = await getDictDataByType(code)
|
||||
dictContentData.value = data
|
||||
lastDictCode.value = code
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取字典数据失败', error)
|
||||
}
|
||||
finally {
|
||||
endContentLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const dictColumns: DataTableColumns<Entity.Dict> = [
|
||||
{
|
||||
title: '字典项',
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
title: '字典码',
|
||||
key: 'code',
|
||||
render: (row) => {
|
||||
return (
|
||||
<CopyText value={row.code} />
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NFlex justify="center">
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => getDictContent(row.code)}
|
||||
>
|
||||
查看字典
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => dictRef.value!.openModal('edit', row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => deleteDict(row.id!)}>
|
||||
{{
|
||||
default: () => (
|
||||
<span>
|
||||
确认删除字典
|
||||
<b>{row.label}</b>
|
||||
{' '}
|
||||
?
|
||||
</span>
|
||||
),
|
||||
trigger: () => <NButton size="small" type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
// 字典类型columns配置
|
||||
const dictColumns = createDictTypeColumns({
|
||||
onView: getDictContent,
|
||||
onEdit: row => dictRef.value!.openModal('edit', row),
|
||||
onDelete: id => deleteDict(id, true),
|
||||
})
|
||||
|
||||
const contentColumns: DataTableColumns<Entity.Dict> = [
|
||||
{
|
||||
title: '字典名称',
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
title: '字典码',
|
||||
key: 'code',
|
||||
},
|
||||
{
|
||||
title: '字典值',
|
||||
key: 'value',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
width: '15em',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NFlex justify="center">
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => dictContentRef.value!.openModal('edit', row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => deleteDict(row.id!)}>
|
||||
{{
|
||||
default: () => (
|
||||
<span>
|
||||
确认删除字典值
|
||||
<b>{row.label}</b>
|
||||
{' '}
|
||||
?
|
||||
</span>
|
||||
),
|
||||
trigger: () => <NButton size="small" type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
// 字典数据columns配置
|
||||
const contentColumns = createDictDataColumns({
|
||||
onEdit: row => dictContentRef.value!.openModal('edit', row),
|
||||
onDelete: id => deleteDict(id, false),
|
||||
})
|
||||
|
||||
function deleteDict(id: number) {
|
||||
window.$message.error(`删除字典${id}`)
|
||||
async function deleteDict(id: number, isType: boolean = false) {
|
||||
try {
|
||||
if (isType) {
|
||||
await deleteDictType(id)
|
||||
window.$message.success('删除字典类型成功')
|
||||
getDictList() // 重新加载字典类型列表
|
||||
}
|
||||
else {
|
||||
await deleteDictData(id)
|
||||
window.$message.success('删除字典数据成功')
|
||||
if (lastDictCode.value) {
|
||||
getDictContent(lastDictCode.value) // 重新加载字典数据
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`删除${isType ? '字典类型' : '字典数据'}失败`, error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -197,8 +136,8 @@ function deleteDict(id: number) {
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<DictModal ref="dictRef" modal-name="字典项" is-root />
|
||||
<DictModal ref="dictContentRef" modal-name="字典值" :dict-code="lastDictCode" />
|
||||
<DictModal ref="dictRef" modal-name="字典项" is-root @success="getDictList" />
|
||||
<DictModal ref="dictContentRef" modal-name="字典值" :dict-code="lastDictCode" @success="() => getDictContent(lastDictCode)" />
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
|
109
src/views/setting/menu/columns.tsx
Normal file
109
src/views/setting/menu/columns.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
|
||||
import { createIcon } from '@/utils'
|
||||
import { renderProCopyableText } from 'pro-naive-ui'
|
||||
|
||||
// 菜单管理columns配置函数
|
||||
interface MenuColumnActions {
|
||||
onEdit: (row: Entity.Menu) => void
|
||||
onDelete: (id: number) => void
|
||||
onAdd: (row: Entity.Menu) => void
|
||||
}
|
||||
|
||||
export function createMenuColumns(actions: MenuColumnActions): DataTableColumns<Entity.Menu> {
|
||||
const { onEdit, onDelete, onAdd } = actions
|
||||
|
||||
return [
|
||||
{
|
||||
title: '菜单名称',
|
||||
key: 'title',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
align: 'center',
|
||||
key: 'icon',
|
||||
width: '6em',
|
||||
render: (row) => {
|
||||
return row.icon && createIcon(row.icon, { size: 20 })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '路径',
|
||||
key: 'path',
|
||||
render: row => renderProCopyableText(row.path),
|
||||
},
|
||||
{
|
||||
title: '权限标识',
|
||||
key: 'perms',
|
||||
width: '18em',
|
||||
align: 'center',
|
||||
render: row => renderProCopyableText(row.perms),
|
||||
},
|
||||
{
|
||||
title: '排序值',
|
||||
key: 'sort',
|
||||
align: 'center',
|
||||
width: '6em',
|
||||
},
|
||||
{
|
||||
title: '菜单类型',
|
||||
align: 'center',
|
||||
key: 'menuType',
|
||||
width: '8em',
|
||||
render: (row) => {
|
||||
const menuTypeMap = {
|
||||
directory: { label: '目录', type: 'primary' },
|
||||
page: { label: '菜单', type: 'warning' },
|
||||
permission: { label: '按钮', type: 'success' },
|
||||
} as const
|
||||
const menuInfo = menuTypeMap[row.menuType]
|
||||
return <NTag type={menuInfo.type} bordered={false}>{menuInfo.label}</NTag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
key: 'status',
|
||||
width: '6em',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NTag type={row.status === 0 ? 'success' : 'error'} bordered={false}>
|
||||
{row.status === 0 ? '正常' : '停用'}
|
||||
</NTag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
key: 'actions',
|
||||
width: '16em',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NSpace justify="center">
|
||||
<NButton
|
||||
text
|
||||
type="primary"
|
||||
onClick={() => onAdd(row)}
|
||||
>
|
||||
新建
|
||||
</NButton>
|
||||
<NButton
|
||||
text
|
||||
onClick={() => onEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => onDelete(row.menuId)}>
|
||||
{{
|
||||
default: () => '确认删除',
|
||||
trigger: () => <NButton text type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
43
src/views/setting/menu/components/DirectoryForm.vue
Normal file
43
src/views/setting/menu/components/DirectoryForm.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<pro-input
|
||||
title="国际化标识Key"
|
||||
path="i18nKey"
|
||||
placeholder="Eg: system.user"
|
||||
/>
|
||||
<pro-field
|
||||
title="菜单图标"
|
||||
path="icon"
|
||||
>
|
||||
<template #input="{ inputProps }">
|
||||
<icon-select
|
||||
:value="inputProps.value"
|
||||
@update:value="inputProps.onUpdateValue"
|
||||
/>
|
||||
</template>
|
||||
</pro-field>
|
||||
<pro-input
|
||||
required
|
||||
title="路由路径"
|
||||
tooltip="目录类型的路由路径,如:/system"
|
||||
path="path"
|
||||
class="col-span-2"
|
||||
placeholder="Eg: /system"
|
||||
/>
|
||||
<pro-digit
|
||||
title="排序"
|
||||
tooltip="数字越小,同级中越靠前"
|
||||
path="sort"
|
||||
/>
|
||||
<pro-switch
|
||||
title="启用"
|
||||
path="status"
|
||||
:field-props="{
|
||||
checkedValue: 0,
|
||||
uncheckedValue: 1,
|
||||
}"
|
||||
/>
|
||||
<pro-switch
|
||||
title="菜单可见"
|
||||
path="menuVisible"
|
||||
/>
|
||||
</template>
|
171
src/views/setting/menu/components/MenuModal.vue
Normal file
171
src/views/setting/menu/components/MenuModal.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { createMenu, getMenuById, selectMenuTree, updateMenu } from '@/service'
|
||||
import { createProModalForm } from 'pro-naive-ui'
|
||||
import DirectoryForm from './DirectoryForm.vue'
|
||||
import PageForm from './PageForm.vue'
|
||||
import PermissionForm from './PermissionForm.vue'
|
||||
|
||||
interface Props {
|
||||
modalName?: string
|
||||
}
|
||||
|
||||
const {
|
||||
modalName: _modalName = '',
|
||||
} = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
|
||||
const modalForm = createProModalForm<Partial<Entity.Menu>>({
|
||||
omitEmptyString: false,
|
||||
initialValues: {
|
||||
menuType: 'directory',
|
||||
menuVisible: true,
|
||||
tabVisible: true,
|
||||
status: 0,
|
||||
},
|
||||
onSubmit: submitModal,
|
||||
})
|
||||
|
||||
type ModalType = 'add' | 'edit'
|
||||
const modalType = shallowRef<ModalType>('add')
|
||||
const modalTitle = computed(() => {
|
||||
const titleMap: Record<ModalType, string> = {
|
||||
add: '添加',
|
||||
edit: '编辑',
|
||||
}
|
||||
const menuTypeMap = {
|
||||
directory: '目录',
|
||||
page: '页面',
|
||||
permission: '权限',
|
||||
}
|
||||
const currentMenuType = modalForm.values.value?.menuType || 'directory'
|
||||
return `${titleMap[modalType.value]}${menuTypeMap[currentMenuType]}`
|
||||
})
|
||||
|
||||
const treeData = ref<Entity.TreeNode[]>([])
|
||||
|
||||
// 动态组件映射
|
||||
const formComponents = {
|
||||
directory: DirectoryForm,
|
||||
page: PageForm,
|
||||
permission: PermissionForm,
|
||||
}
|
||||
|
||||
// 当前使用的表单组件
|
||||
const currentFormComponent = computed(() => {
|
||||
const menuType = modalForm.values.value?.menuType || 'directory'
|
||||
return formComponents[menuType as keyof typeof formComponents]
|
||||
})
|
||||
|
||||
async function openModal(type: ModalType = 'add', data?: Partial<Entity.Menu>) {
|
||||
selectMenuTree().then((res) => {
|
||||
treeData.value = res.data
|
||||
})
|
||||
|
||||
modalType.value = type
|
||||
modalForm.open()
|
||||
const handlers = {
|
||||
async add() {
|
||||
// 如果新建传入了menuId,设置为父级菜单
|
||||
if (data?.menuId) {
|
||||
modalForm.values.value.parentId = data.menuId
|
||||
}
|
||||
},
|
||||
async edit() {
|
||||
if (!data?.menuId)
|
||||
return
|
||||
const response = await getMenuById(data.menuId)
|
||||
modalForm.values.value = response.data
|
||||
},
|
||||
}
|
||||
await handlers[type]()
|
||||
}
|
||||
|
||||
async function submitModal(filedValues: Partial<Entity.Menu>) {
|
||||
const handlers = {
|
||||
async add() {
|
||||
try {
|
||||
await createMenu(filedValues)
|
||||
window.$message.success('菜单创建成功')
|
||||
}
|
||||
catch (error) {
|
||||
console.error('创建菜单失败', error)
|
||||
}
|
||||
},
|
||||
async edit() {
|
||||
try {
|
||||
await updateMenu(modalForm.values.value.menuId!, filedValues)
|
||||
window.$message.success('菜单更新成功')
|
||||
}
|
||||
catch (error) {
|
||||
console.error('更新菜单失败', error)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
startLoading()
|
||||
await handlers[modalType.value]()
|
||||
emit('success')
|
||||
modalForm.close()
|
||||
endLoading()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<pro-modal-form
|
||||
:title="modalTitle"
|
||||
:form="modalForm"
|
||||
:loading="submitLoading"
|
||||
width="700px"
|
||||
>
|
||||
<pro-field
|
||||
path="menuType"
|
||||
>
|
||||
<template #input="{ inputProps }">
|
||||
<n-tabs
|
||||
type="segment" animated :value="inputProps.value"
|
||||
@update:value="inputProps.onUpdateValue"
|
||||
>
|
||||
<n-tab name="directory" tab="目录" />
|
||||
<n-tab name="page" tab="页面" />
|
||||
<n-tab name="permission" tab="权限" />
|
||||
</n-tabs>
|
||||
</template>
|
||||
</pro-field>
|
||||
|
||||
<div class="grid grid-cols-2">
|
||||
<pro-tree-select
|
||||
tooltip="不填写则为顶层菜单"
|
||||
title="父级目录"
|
||||
path="parentId"
|
||||
:field-props="{
|
||||
options: treeData,
|
||||
keyField: 'value',
|
||||
}"
|
||||
/>
|
||||
<pro-input
|
||||
required
|
||||
title="标题"
|
||||
path="title"
|
||||
/>
|
||||
<component
|
||||
:is="currentFormComponent"
|
||||
:tree-data="treeData"
|
||||
/>
|
||||
<pro-textarea
|
||||
title="备注"
|
||||
path="remark"
|
||||
class="col-span-2"
|
||||
/>
|
||||
</div>
|
||||
</pro-modal-form>
|
||||
</template>
|
82
src/views/setting/menu/components/PageForm.vue
Normal file
82
src/views/setting/menu/components/PageForm.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<pro-input
|
||||
title="国际化标识Key"
|
||||
path="i18nKey"
|
||||
placeholder="Eg: system.user"
|
||||
/>
|
||||
<pro-field
|
||||
title="菜单图标"
|
||||
path="icon"
|
||||
>
|
||||
<template #input="{ inputProps }">
|
||||
<icon-select
|
||||
:value="inputProps.value"
|
||||
@update:value="inputProps.onUpdateValue"
|
||||
/>
|
||||
</template>
|
||||
</pro-field>
|
||||
<pro-input
|
||||
required
|
||||
title="路由路径"
|
||||
tooltip="页面路由路径,与组件路径对应"
|
||||
path="path"
|
||||
class="col-span-2"
|
||||
placeholder="Eg: /system/user"
|
||||
/>
|
||||
<pro-input
|
||||
title="高亮菜单路径"
|
||||
tooltip="当前路由不在侧边菜单显示,但需要高亮为某个菜单"
|
||||
path="activePath"
|
||||
class="col-span-2"
|
||||
placeholder="Eg: /system/user"
|
||||
/>
|
||||
<pro-input
|
||||
required
|
||||
title="组件路径"
|
||||
tooltip="页面组件的文件路径"
|
||||
path="component"
|
||||
class="col-span-2"
|
||||
placeholder="Eg: /system/user/index.vue"
|
||||
/>
|
||||
<pro-digit
|
||||
title="排序"
|
||||
tooltip="数字越小,同级中越靠前"
|
||||
path="sort"
|
||||
/>
|
||||
<pro-input
|
||||
title="权限标识"
|
||||
tooltip="页面访问权限标识"
|
||||
path="perms"
|
||||
placeholder="Eg: system:user:list"
|
||||
/>
|
||||
<pro-switch
|
||||
title="启用"
|
||||
path="status"
|
||||
:field-props="{
|
||||
checkedValue: 0,
|
||||
uncheckedValue: 1,
|
||||
}"
|
||||
/>
|
||||
<pro-switch
|
||||
title="菜单可见"
|
||||
path="menuVisible"
|
||||
/>
|
||||
<pro-switch
|
||||
title="标签栏可见"
|
||||
path="tabVisible"
|
||||
/>
|
||||
<pro-switch
|
||||
title="页面缓存"
|
||||
tooltip="开启配置后,切换页面数据不会清空"
|
||||
path="keepAlive"
|
||||
/>
|
||||
<pro-switch
|
||||
title="常驻标签栏"
|
||||
path="pinTab"
|
||||
/>
|
||||
<pro-switch
|
||||
title="外链菜单"
|
||||
tooltip="开启配置后,点击菜单会跳转到外链地址"
|
||||
path="isLink"
|
||||
/>
|
||||
</template>
|
22
src/views/setting/menu/components/PermissionForm.vue
Normal file
22
src/views/setting/menu/components/PermissionForm.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<pro-input
|
||||
required
|
||||
title="权限标识"
|
||||
tooltip="按钮权限唯一标识符"
|
||||
path="perms"
|
||||
placeholder="Eg: system:user:add"
|
||||
/>
|
||||
<pro-digit
|
||||
title="排序"
|
||||
tooltip="数字越小,同级中越靠前"
|
||||
path="sort"
|
||||
/>
|
||||
<pro-switch
|
||||
title="启用"
|
||||
path="status"
|
||||
:field-props="{
|
||||
checkedValue: 0,
|
||||
uncheckedValue: 1,
|
||||
}"
|
||||
/>
|
||||
</template>
|
@ -1,296 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
FormItemRule,
|
||||
} from 'naive-ui'
|
||||
import HelpInfo from '@/components/common/HelpInfo.vue'
|
||||
import { Regex } from '@/constants'
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { fetchRoleList } from '@/service'
|
||||
|
||||
interface Props {
|
||||
modalName?: string
|
||||
allRoutes: AppRoute.RowRoute[]
|
||||
}
|
||||
|
||||
const {
|
||||
modalName = '',
|
||||
allRoutes,
|
||||
} = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
open: []
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const { bool: modalVisible, setTrue: showModal, setFalse: hiddenModal } = useBoolean(false)
|
||||
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
|
||||
const formDefault: AppRoute.RowRoute = {
|
||||
name: '',
|
||||
path: '',
|
||||
id: -1,
|
||||
pid: null,
|
||||
title: '',
|
||||
requiresAuth: true,
|
||||
keepAlive: false,
|
||||
hide: false,
|
||||
withoutTab: true,
|
||||
pinTab: false,
|
||||
menuType: 'page',
|
||||
}
|
||||
const formModel = ref<AppRoute.RowRoute>({ ...formDefault })
|
||||
|
||||
type ModalType = 'add' | 'view' | 'edit'
|
||||
const modalType = shallowRef<ModalType>('add')
|
||||
const modalTitle = computed(() => {
|
||||
const titleMap: Record<ModalType, string> = {
|
||||
add: '添加',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
}
|
||||
return `${titleMap[modalType.value]}${modalName}`
|
||||
})
|
||||
|
||||
async function openModal(type: ModalType = 'add', data: AppRoute.RowRoute) {
|
||||
emit('open')
|
||||
modalType.value = type
|
||||
getRoleList()
|
||||
showModal()
|
||||
const handlers = {
|
||||
async add() {
|
||||
formModel.value = { ...formDefault }
|
||||
},
|
||||
async view() {
|
||||
if (!data)
|
||||
return
|
||||
formModel.value = { ...data }
|
||||
},
|
||||
async edit() {
|
||||
if (!data)
|
||||
return
|
||||
formModel.value = { ...data }
|
||||
},
|
||||
}
|
||||
await handlers[type]()
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
hiddenModal()
|
||||
endLoading()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
async function submitModal() {
|
||||
const handlers = {
|
||||
async add() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.$message.success('模拟新增成功')
|
||||
resolve(true)
|
||||
}, 2000)
|
||||
})
|
||||
},
|
||||
async edit() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.$message.success('模拟编辑成功')
|
||||
resolve(true)
|
||||
}, 2000)
|
||||
})
|
||||
},
|
||||
async view() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
await formRef.value?.validate()
|
||||
startLoading()
|
||||
await handlers[modalType.value]() && closeModal()
|
||||
}
|
||||
|
||||
const dirTreeOptions = computed(() => {
|
||||
return filterDirectory(JSON.parse(JSON.stringify(allRoutes)))
|
||||
})
|
||||
|
||||
function filterDirectory(node: any[]) {
|
||||
return node.filter((item) => {
|
||||
if (item.children) {
|
||||
const childDir = filterDirectory(item.children)
|
||||
if (childDir.length > 0)
|
||||
item.children = childDir
|
||||
else
|
||||
Reflect.deleteProperty(item, 'children')
|
||||
}
|
||||
|
||||
return (item.menuType === 'dir')
|
||||
})
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: true,
|
||||
// message: '请输入菜单名称',
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value)
|
||||
return new Error('请输入菜单名称')
|
||||
|
||||
if (!new RegExp(Regex.RouteName).test(value))
|
||||
return new Error('菜单只能包含英文数字_!@#$%^&*~-')
|
||||
|
||||
return true
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
path: {
|
||||
required: true,
|
||||
message: '请输入菜单路径',
|
||||
trigger: 'blur',
|
||||
},
|
||||
componentPath: {
|
||||
required: true,
|
||||
message: '请输入组件路径',
|
||||
trigger: 'blur',
|
||||
},
|
||||
title: {
|
||||
required: true,
|
||||
message: '请输入菜单标题',
|
||||
trigger: 'blur',
|
||||
},
|
||||
}
|
||||
|
||||
const options = ref()
|
||||
async function getRoleList() {
|
||||
const { data } = await fetchRoleList()
|
||||
options.value = data
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="modalVisible" :mask-closable="false" preset="card" :title="modalTitle" class="w-700px"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:rules="rules" label-placement="left" :label-width="100"
|
||||
:model="formModel"
|
||||
:disabled="modalType === 'view'"
|
||||
>
|
||||
<n-grid :cols="2" :x-gap="18">
|
||||
<n-form-item-grid-item :span="2" path="pid">
|
||||
<template #label>
|
||||
父级目录
|
||||
<HelpInfo message="不填写则为顶层菜单" />
|
||||
</template>
|
||||
<n-tree-select
|
||||
v-model:value="formModel.pid" filterable clearable :options="dirTreeOptions" key-field="id"
|
||||
label-field="title" children-field="children" placeholder="请选择父级目录"
|
||||
/>
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="菜单名称" path="name">
|
||||
<n-input v-model:value="formModel.name" placeholder="Eg: system" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="标题" path="title">
|
||||
<n-input v-model:value="formModel.title" placeholder="Eg: My-System" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="2" label="路由路径" path="path">
|
||||
<n-input v-model:value="formModel.path" placeholder="Eg: /system/user" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="菜单类型" path="menuType">
|
||||
<n-radio-group v-model:value="formModel.menuType" name="radiogroup">
|
||||
<n-space>
|
||||
<n-radio value="dir">
|
||||
目录
|
||||
</n-radio>
|
||||
<n-radio value="page">
|
||||
页面
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="图标" path="icon">
|
||||
<icon-select v-model:value="formModel.icon" :disabled="modalType === 'view'" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item v-if="formModel.menuType === 'page'" :span="2" label="组件路径" path="componentPath">
|
||||
<n-input v-model:value="formModel.componentPath" placeholder="Eg: /system/user/index.vue" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" path="order">
|
||||
<template #label>
|
||||
菜单排序
|
||||
<HelpInfo message="数字越小,同级中越靠前" />
|
||||
</template>
|
||||
<n-input-number v-model:value="formModel.order" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item v-if="formModel.menuType === 'page'" :span="1" path="href">
|
||||
<template #label>
|
||||
外链页面
|
||||
<HelpInfo message="填写后,点击菜单将跳转到该地址,组件路径任意填写" />
|
||||
</template>
|
||||
<n-input v-model:value="formModel.href" placeholder="Eg: https://example.com" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="登录访问" path="requiresAuth">
|
||||
<n-switch v-model:value="formModel.requiresAuth" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item
|
||||
v-if="formModel.menuType === 'page'" :span="1" label="页面缓存"
|
||||
path="keepAlive"
|
||||
>
|
||||
<n-switch v-model:value="formModel.keepAlive" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item
|
||||
v-if="formModel.menuType === 'page'" :span="1" label="标签栏可见"
|
||||
path="withoutTab"
|
||||
>
|
||||
<n-switch v-model:value="formModel.withoutTab" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item v-if="formModel.menuType === 'page'" :span="1" label="常驻标签栏" path="pinTab">
|
||||
<n-switch v-model:value="formModel.pinTab" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="1" label="侧边菜单隐藏" path="hide">
|
||||
<n-switch v-model:value="formModel.hide" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item
|
||||
v-if="formModel.menuType === 'page' && formModel.hide" :span="2"
|
||||
path="activeMenu"
|
||||
>
|
||||
<template #label>
|
||||
高亮菜单
|
||||
<HelpInfo message="当前路由不在左侧菜单显示,但需要高亮某个菜单" />
|
||||
</template>
|
||||
<n-input v-model:value="formModel.activeMenu" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="2" path="roles">
|
||||
<template #label>
|
||||
访问角色
|
||||
<HelpInfo message="不填写则表示所有角色都可以访问" />
|
||||
</template>
|
||||
<n-select
|
||||
v-model:value="formModel.roles" multiple filterable
|
||||
label-field="role"
|
||||
value-field="id"
|
||||
:options="options"
|
||||
/>
|
||||
</n-form-item-grid-item>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
<template v-if="modalType !== 'view'" #action>
|
||||
<n-space justify="center">
|
||||
<n-button @click="closeModal">
|
||||
取消
|
||||
</n-button>
|
||||
<n-button type="primary" :loading="submitLoading" @click="submitModal">
|
||||
提交
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,173 +1,102 @@
|
||||
<script setup lang="tsx">
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
<script setup lang="ts">
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { fetchAllRoutes } from '@/service'
|
||||
import { arrayToTree, createIcon } from '@/utils'
|
||||
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
|
||||
import { renderProCopyableText } from 'pro-naive-ui'
|
||||
import TableModal from './components/TableModal.vue'
|
||||
import { deleteMenu, getMenuList } from '@/service'
|
||||
import { createMenuColumns } from './columns'
|
||||
import MenuModal from './components/MenuModal.vue'
|
||||
import arrayToTree from 'array-to-tree'
|
||||
|
||||
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
||||
|
||||
function deleteData(id: number) {
|
||||
window.$message.success(`删除菜单id:${id}`)
|
||||
async function deleteData(id: number) {
|
||||
try {
|
||||
await deleteMenu(id)
|
||||
window.$message.success('删除菜单成功')
|
||||
getAllRoutes() // 重新加载列表
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除菜单失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const tableModalRef = ref()
|
||||
const menuModalRef = ref()
|
||||
|
||||
const columns: DataTableColumns<AppRoute.RowRoute> = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 30,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
align: 'center',
|
||||
key: 'icon',
|
||||
width: '6em',
|
||||
render: (row) => {
|
||||
return row.icon && createIcon(row.icon, { size: 20 })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
align: 'center',
|
||||
key: 'title',
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '路径',
|
||||
key: 'path',
|
||||
render: row => renderProCopyableText(row.path),
|
||||
},
|
||||
{
|
||||
title: '组件路径',
|
||||
key: 'componentPath',
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
render: (row) => {
|
||||
return row.componentPath || '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '排序值',
|
||||
key: 'order',
|
||||
align: 'center',
|
||||
width: '6em',
|
||||
},
|
||||
{
|
||||
title: '菜单类型',
|
||||
align: 'center',
|
||||
key: 'menuType',
|
||||
width: '6em',
|
||||
render: (row) => {
|
||||
const menuType = row.menuType || 'page'
|
||||
const menuTagType: Record<AppRoute.MenuType, NaiveUI.ThemeColor> = {
|
||||
dir: 'primary',
|
||||
page: 'warning',
|
||||
// 菜单管理columns配置
|
||||
const columns = createMenuColumns({
|
||||
onEdit: row => menuModalRef.value.openModal('edit', row),
|
||||
onDelete: deleteData,
|
||||
onAdd: row => menuModalRef.value.openModal('add', row),
|
||||
})
|
||||
|
||||
const tableData = ref<Entity.Menu[]>([])
|
||||
|
||||
// 递归排序菜单树结构
|
||||
function sortMenuTree(menus: Entity.Menu[]): Entity.Menu[] {
|
||||
// 对当前级别的菜单按sort值排序(升序)
|
||||
const sortedMenus = menus.sort((a, b) => {
|
||||
const sortA = a.sort || 0
|
||||
const sortB = b.sort || 0
|
||||
return sortA - sortB
|
||||
})
|
||||
|
||||
// 递归排序子菜单
|
||||
return sortedMenus.map(menu => ({
|
||||
...menu,
|
||||
children: menu.children ? sortMenuTree(menu.children) : undefined,
|
||||
}))
|
||||
}
|
||||
return <NTag type={menuTagType[menuType]}>{menuType}</NTag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
key: 'actions',
|
||||
width: '15em',
|
||||
render: (row) => {
|
||||
return (
|
||||
<NSpace justify="center">
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => tableModalRef.value.openModal('view', row)}
|
||||
>
|
||||
查看
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
onClick={() => tableModalRef.value.openModal('edit', row)}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => deleteData(row.id)}>
|
||||
{{
|
||||
default: () => '确认删除',
|
||||
trigger: () => <NButton size="small" type="error">删除</NButton>,
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const tableData = ref<AppRoute.RowRoute[]>([])
|
||||
async function getAllRoutes() {
|
||||
startLoading()
|
||||
try {
|
||||
const { data } = await getMenuList()
|
||||
const treeData = arrayToTree(data, {
|
||||
parentProperty: 'parentId',
|
||||
customID: 'menuId',
|
||||
})
|
||||
|
||||
// 对树形结构按sort值排序
|
||||
tableData.value = sortMenuTree(treeData)
|
||||
}
|
||||
catch {
|
||||
window.$message.error('获取菜单列表失败')
|
||||
}
|
||||
finally {
|
||||
endLoading()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getAllRoutes()
|
||||
})
|
||||
async function getAllRoutes() {
|
||||
startLoading()
|
||||
const { data } = await fetchAllRoutes()
|
||||
tableData.value = arrayToTree(data)
|
||||
endLoading()
|
||||
}
|
||||
|
||||
const checkedRowKeys = ref<number[]>([])
|
||||
async function handlePositiveClick() {
|
||||
window.$message.success(`批量删除id:${checkedRowKeys.value.join(',')}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<template #header>
|
||||
<NButton type="primary" @click="tableModalRef.openModal('add')">
|
||||
<div>
|
||||
<pro-data-table
|
||||
row-key="menuId"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #title>
|
||||
<n-button type="primary" @click="menuModalRef.openModal('add')">
|
||||
<template #icon>
|
||||
<icon-park-outline-add-one />
|
||||
</template>
|
||||
新建
|
||||
</NButton>
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<template #header-extra>
|
||||
<n-flex>
|
||||
<NButton type="primary" secondary @click="getAllRoutes">
|
||||
<template #toolbar>
|
||||
<n-button type="primary" secondary @click="getAllRoutes">
|
||||
<template #icon>
|
||||
<icon-park-outline-refresh />
|
||||
</template>
|
||||
刷新
|
||||
</NButton>
|
||||
<NPopconfirm
|
||||
@positive-click="handlePositiveClick"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton type="error" secondary>
|
||||
<template #icon>
|
||||
<icon-park-outline-delete-five />
|
||||
</n-button>
|
||||
</template>
|
||||
批量删除
|
||||
</NButton>
|
||||
</template>
|
||||
确认删除所有选中菜单?
|
||||
</NPopconfirm>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-data-table
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
:row-key="(row:AppRoute.RowRoute) => row.id" :columns="columns" :data="tableData"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:scroll-x="1200"
|
||||
/>
|
||||
<TableModal ref="tableModalRef" :all-routes="tableData" modal-name="菜单" />
|
||||
</n-card>
|
||||
</pro-data-table>
|
||||
|
||||
<MenuModal ref="menuModalRef" modal-name="菜单" @success="getAllRoutes" />
|
||||
</div>
|
||||
</template>
|
||||
|
Loading…
x
Reference in New Issue
Block a user