68 changed files with 2540 additions and 33 deletions
@ -0,0 +1,5 @@ |
|||
# 应用标题 |
|||
VITE_APP_TITLE=Vben Admin Naive |
|||
|
|||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 |
|||
VITE_APP_NAMESPACE=vben-web-naive |
|||
@ -0,0 +1,7 @@ |
|||
# public path |
|||
VITE_BASE=/ |
|||
|
|||
# Basic interface address SPA |
|||
VITE_GLOB_API_URL=/api |
|||
|
|||
VITE_VISUALIZER=true |
|||
@ -0,0 +1,16 @@ |
|||
# 端口号 |
|||
VITE_PORT=5566 |
|||
|
|||
VITE_BASE=/ |
|||
|
|||
# 接口地址 |
|||
VITE_GLOB_API_URL=/api |
|||
|
|||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭 |
|||
VITE_NITRO_MOCK=true |
|||
|
|||
# 是否打开 devtools,true 为打开,false 为关闭 |
|||
VITE_DEVTOOLS=false |
|||
|
|||
# 是否注入全局loading |
|||
VITE_INJECT_APP_LOADING=true |
|||
@ -0,0 +1,16 @@ |
|||
VITE_BASE=/ |
|||
|
|||
# 接口地址 |
|||
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api |
|||
|
|||
# 是否开启压缩,可以设置为 none, brotli, gzip |
|||
VITE_COMPRESS=none |
|||
|
|||
# 是否开启 PWA |
|||
VITE_PWA=true |
|||
|
|||
# vue-router 的模式 |
|||
VITE_ROUTER_HISTORY=hash |
|||
|
|||
# 是否注入全局loading |
|||
VITE_INJECT_APP_LOADING=true |
|||
@ -0,0 +1,35 @@ |
|||
<!doctype html> |
|||
<html lang="zh"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> |
|||
<meta name="renderer" content="webkit" /> |
|||
<meta name="description" content="A Modern Back-end Management System" /> |
|||
<meta name="keywords" content="Vben Admin Vue3 Vite" /> |
|||
<meta name="author" content="Vben" /> |
|||
<meta |
|||
name="viewport" |
|||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" |
|||
/> |
|||
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 --> |
|||
<title><%= VITE_APP_TITLE %></title> |
|||
<link rel="icon" href="/favicon.ico" /> |
|||
<script> |
|||
// 生产环境下注入百度统计 |
|||
if (window._VBEN_ADMIN_PRO_APP_CONF_) { |
|||
var _hmt = _hmt || []; |
|||
(function () { |
|||
var hm = document.createElement('script'); |
|||
hm.src = |
|||
'https://hm.baidu.com/hm.js?d20a01273820422b6aa2ee41b6c9414d'; |
|||
var s = document.getElementsByTagName('script')[0]; |
|||
s.parentNode.insertBefore(hm, s); |
|||
})(); |
|||
} |
|||
</script> |
|||
</head> |
|||
<body> |
|||
<div id="app"></div> |
|||
<script type="module" src="/src/main.ts"></script> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,49 @@ |
|||
{ |
|||
"name": "@vben/web-naive", |
|||
"version": "5.0.0", |
|||
"homepage": "https://vben.pro", |
|||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git", |
|||
"directory": "apps/web-naive" |
|||
}, |
|||
"license": "MIT", |
|||
"author": { |
|||
"name": "vben", |
|||
"email": "ann.vben@gmail.com", |
|||
"url": "https://github.com/anncwb" |
|||
}, |
|||
"type": "module", |
|||
"scripts": { |
|||
"build": "pnpm vite build --mode production", |
|||
"build:analyze": "pnpm vite build --mode analyze", |
|||
"dev": "pnpm vite --mode development", |
|||
"preview": "vite preview", |
|||
"typecheck": "vue-tsc --noEmit --skipLibCheck" |
|||
}, |
|||
"imports": { |
|||
"#/*": "./src/*" |
|||
}, |
|||
"dependencies": { |
|||
"@vben/access": "workspace:*", |
|||
"@vben/chart-ui": "workspace:*", |
|||
"@vben/common-ui": "workspace:*", |
|||
"@vben/constants": "workspace:*", |
|||
"@vben/hooks": "workspace:*", |
|||
"@vben/icons": "workspace:*", |
|||
"@vben/layouts": "workspace:*", |
|||
"@vben/locales": "workspace:*", |
|||
"@vben/preferences": "workspace:*", |
|||
"@vben/request": "workspace:*", |
|||
"@vben/stores": "workspace:*", |
|||
"@vben/styles": "workspace:*", |
|||
"@vben/types": "workspace:*", |
|||
"@vben/utils": "workspace:*", |
|||
"@vueuse/core": "^10.11.0", |
|||
"naive-ui": "^2.39.0", |
|||
"pinia": "2.1.7", |
|||
"vue": "^3.4.34", |
|||
"vue-router": "^4.4.0" |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export { default } from '@vben/tailwind-config/postcss'; |
|||
|
After Width: | Height: | Size: 5.3 KiB |
@ -0,0 +1,33 @@ |
|||
import { requestClient } from '#/api/request'; |
|||
|
|||
export namespace AuthApi { |
|||
/** 登录接口参数 */ |
|||
export interface LoginParams { |
|||
password: string; |
|||
username: string; |
|||
} |
|||
|
|||
/** 登录接口返回值 */ |
|||
export interface LoginResult { |
|||
accessToken: string; |
|||
desc: string; |
|||
realName: string; |
|||
refreshToken: string; |
|||
userId: string; |
|||
username: string; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 登录 |
|||
*/ |
|||
export async function login(data: AuthApi.LoginParams) { |
|||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data); |
|||
} |
|||
|
|||
/** |
|||
* 获取用户权限码 |
|||
*/ |
|||
export async function getAccessCodes() { |
|||
return requestClient.get<string[]>('/auth/codes'); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './auth'; |
|||
export * from './menu'; |
|||
export * from './user'; |
|||
@ -0,0 +1,10 @@ |
|||
import type { RouteRecordStringComponent } from '@vben/types'; |
|||
|
|||
import { requestClient } from '#/api/request'; |
|||
|
|||
/** |
|||
* 获取用户所有菜单 |
|||
*/ |
|||
export async function getAllMenus() { |
|||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all'); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
import type { UserInfo } from '@vben/types'; |
|||
|
|||
import { requestClient } from '#/api/request'; |
|||
|
|||
/** |
|||
* 获取用户信息 |
|||
*/ |
|||
export async function getUserInfo() { |
|||
return requestClient.get<UserInfo>('/user/info'); |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './core'; |
|||
@ -0,0 +1,66 @@ |
|||
/** |
|||
* 该文件可自行根据业务逻辑进行调整 |
|||
*/ |
|||
import type { HttpResponse } from '@vben/request'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { RequestClient } from '@vben/request'; |
|||
import { useAccessStore } from '@vben/stores'; |
|||
|
|||
import { message } from '#/naive'; |
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
function createRequestClient(baseURL: string) { |
|||
const client = new RequestClient({ |
|||
baseURL, |
|||
// 为每个请求携带 Authorization
|
|||
makeAuthorization: () => { |
|||
return { |
|||
// 默认
|
|||
key: 'Authorization', |
|||
tokenHandler: () => { |
|||
const accessStore = useAccessStore(); |
|||
return { |
|||
refreshToken: `${accessStore.refreshToken}`, |
|||
token: `${accessStore.accessToken}`, |
|||
}; |
|||
}, |
|||
unAuthorizedHandler: async () => { |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
|
|||
if (preferences.app.loginExpiredMode === 'modal') { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
// 退出登录
|
|||
await authStore.logout(); |
|||
} |
|||
}, |
|||
}; |
|||
}, |
|||
makeErrorMessage: (msg) => message.error(msg), |
|||
|
|||
makeRequestHeaders: () => { |
|||
return { |
|||
// 为每个请求携带 Accept-Language
|
|||
'Accept-Language': preferences.app.locale, |
|||
}; |
|||
}, |
|||
}); |
|||
client.addResponseInterceptor<HttpResponse>((response) => { |
|||
const { data: responseData, status } = response; |
|||
|
|||
const { code, data, message: msg } = responseData; |
|||
if (status >= 200 && status < 400 && code === 0) { |
|||
return data; |
|||
} |
|||
throw new Error(msg); |
|||
}); |
|||
return client; |
|||
} |
|||
|
|||
export const requestClient = createRequestClient(apiURL); |
|||
@ -0,0 +1,52 @@ |
|||
<script lang="ts" setup> |
|||
import { computed } from 'vue'; |
|||
|
|||
import { useNaiveDesignTokens } from '@vben/hooks'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { |
|||
darkTheme, |
|||
dateEnUS, |
|||
dateZhCN, |
|||
enUS, |
|||
GlobalThemeOverrides, |
|||
lightTheme, |
|||
zhCN, |
|||
} from 'naive-ui'; |
|||
|
|||
defineOptions({ name: 'App' }); |
|||
|
|||
const { commonTokens } = useNaiveDesignTokens(); |
|||
|
|||
const tokenLocale = computed(() => |
|||
preferences.app.locale === 'zh-CN' ? zhCN : enUS, |
|||
); |
|||
const tokenDateLocale = computed(() => |
|||
preferences.app.locale === 'zh-CN' ? dateZhCN : dateEnUS, |
|||
); |
|||
const tokenTheme = computed(() => |
|||
preferences.theme.mode === 'dark' ? darkTheme : lightTheme, |
|||
); |
|||
|
|||
const themeOverrides = computed((): GlobalThemeOverrides => { |
|||
return { |
|||
common: commonTokens, |
|||
}; |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<NConfigProvider |
|||
:date-locale="tokenDateLocale" |
|||
:locale="tokenLocale" |
|||
:theme="tokenTheme" |
|||
:theme-overrides="themeOverrides" |
|||
class="h-full" |
|||
> |
|||
<NNotificationProvider> |
|||
<NMessageProvider> |
|||
<RouterView /> |
|||
</NMessageProvider> |
|||
</NNotificationProvider> |
|||
</NConfigProvider> |
|||
</template> |
|||
@ -0,0 +1,35 @@ |
|||
import { createApp } from 'vue'; |
|||
|
|||
import { registerAccessDirective } from '@vben/access'; |
|||
import { initStores } from '@vben/stores'; |
|||
import '@vben/styles'; |
|||
import '@vben/styles/antd'; |
|||
|
|||
import naive from 'naive-ui'; |
|||
|
|||
import { setupI18n } from '#/locales'; |
|||
|
|||
import App from './app.vue'; |
|||
import { router } from './router'; |
|||
|
|||
async function bootstrap(namespace: string) { |
|||
const app = createApp(App); |
|||
|
|||
// 国际化 i18n 配置
|
|||
await setupI18n(app); |
|||
|
|||
// 配置 pinia-tore
|
|||
await initStores(app, { namespace }); |
|||
|
|||
// 安装权限指令
|
|||
registerAccessDirective(app); |
|||
|
|||
// 配置路由及路由守卫
|
|||
app.use(router); |
|||
|
|||
app.use(naive); |
|||
|
|||
app.mount('#app'); |
|||
} |
|||
|
|||
export { bootstrap }; |
|||
@ -0,0 +1,153 @@ |
|||
<script lang="ts" setup> |
|||
import { computed, ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; |
|||
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; |
|||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; |
|||
import { |
|||
BasicLayout, |
|||
LockScreen, |
|||
Notification, |
|||
NotificationItem, |
|||
UserDropdown, |
|||
} from '@vben/layouts'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { |
|||
resetAllStores, |
|||
storeToRefs, |
|||
useAccessStore, |
|||
useUserStore, |
|||
} from '@vben/stores'; |
|||
import { openWindow } from '@vben/utils'; |
|||
|
|||
import { $t } from '#/locales'; |
|||
import { resetRoutes } from '#/router'; |
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
const notifications = ref<NotificationItem[]>([ |
|||
{ |
|||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', |
|||
date: '3小时前', |
|||
isRead: true, |
|||
message: '描述信息描述信息描述信息', |
|||
title: '收到了 14 份新周报', |
|||
}, |
|||
{ |
|||
avatar: 'https://avatar.vercel.sh/1', |
|||
date: '刚刚', |
|||
isRead: false, |
|||
message: '描述信息描述信息描述信息', |
|||
title: '朱偏右 回复了你', |
|||
}, |
|||
{ |
|||
avatar: 'https://avatar.vercel.sh/1', |
|||
date: '2024-01-01', |
|||
isRead: false, |
|||
message: '描述信息描述信息描述信息', |
|||
title: '曲丽丽 评论了你', |
|||
}, |
|||
{ |
|||
avatar: 'https://avatar.vercel.sh/satori', |
|||
date: '1天前', |
|||
isRead: false, |
|||
message: '描述信息描述信息描述信息', |
|||
title: '代办提醒', |
|||
}, |
|||
]); |
|||
|
|||
const userStore = useUserStore(); |
|||
const authStore = useAuthStore(); |
|||
const accessStore = useAccessStore(); |
|||
const showDot = computed(() => |
|||
notifications.value.some((item) => !item.isRead), |
|||
); |
|||
|
|||
const menus = computed(() => [ |
|||
{ |
|||
handler: () => { |
|||
openWindow(VBEN_DOC_URL, { |
|||
target: '_blank', |
|||
}); |
|||
}, |
|||
icon: BookOpenText, |
|||
text: $t('widgets.document'), |
|||
}, |
|||
{ |
|||
handler: () => { |
|||
openWindow(VBEN_GITHUB_URL, { |
|||
target: '_blank', |
|||
}); |
|||
}, |
|||
icon: MdiGithub, |
|||
text: 'GitHub', |
|||
}, |
|||
{ |
|||
handler: () => { |
|||
openWindow(`${VBEN_GITHUB_URL}/issues`, { |
|||
target: '_blank', |
|||
}); |
|||
}, |
|||
icon: CircleHelp, |
|||
text: $t('widgets.qa'), |
|||
}, |
|||
]); |
|||
|
|||
const { loginLoading } = storeToRefs(authStore); |
|||
|
|||
const avatar = computed(() => { |
|||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; |
|||
}); |
|||
|
|||
const router = useRouter(); |
|||
|
|||
async function handleLogout() { |
|||
resetAllStores(); |
|||
resetRoutes(); |
|||
await router.replace(LOGIN_PATH); |
|||
} |
|||
|
|||
function handleNoticeClear() { |
|||
notifications.value = []; |
|||
} |
|||
|
|||
function handleMakeAll() { |
|||
notifications.value.forEach((item) => (item.isRead = true)); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<BasicLayout @clear-preferences-and-logout="handleLogout"> |
|||
<template #user-dropdown> |
|||
<UserDropdown |
|||
:avatar |
|||
:menus |
|||
:text="userStore.userInfo?.realName" |
|||
description="ann.vben@gmail.com" |
|||
tag-text="Pro" |
|||
@logout="handleLogout" |
|||
/> |
|||
</template> |
|||
<template #notification> |
|||
<Notification |
|||
:dot="showDot" |
|||
:notifications="notifications" |
|||
@clear="handleNoticeClear" |
|||
@make-all="handleMakeAll" |
|||
/> |
|||
</template> |
|||
<template #extra> |
|||
<AuthenticationLoginExpiredModal |
|||
v-model:open="accessStore.loginExpired" |
|||
:avatar |
|||
:loading="loginLoading" |
|||
password-placeholder="123456" |
|||
username-placeholder="vben" |
|||
@submit="authStore.authLogin" |
|||
/> |
|||
</template> |
|||
<template #lock-screen> |
|||
<LockScreen :avatar @to-login="handleLogout" /> |
|||
</template> |
|||
</BasicLayout> |
|||
</template> |
|||
@ -0,0 +1,8 @@ |
|||
const BasicLayout = () => import('./basic.vue'); |
|||
|
|||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView); |
|||
|
|||
const AuthPageLayout = () => |
|||
import('@vben/layouts').then((m) => m.AuthPageLayout); |
|||
|
|||
export { AuthPageLayout, BasicLayout, IFrameView }; |
|||
@ -0,0 +1,3 @@ |
|||
# locale |
|||
|
|||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。 |
|||
@ -0,0 +1,31 @@ |
|||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales'; |
|||
|
|||
import type { App } from 'vue'; |
|||
|
|||
import { $t, setupI18n as coreSetup, loadLocalesMap } from '@vben/locales'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
const modules = import.meta.glob('./langs/*.json'); |
|||
|
|||
const localesMap = loadLocalesMap(modules); |
|||
|
|||
/** |
|||
* 加载应用特有的语言包 |
|||
* 这里也可以改造为从服务端获取翻译数据 |
|||
* @param lang |
|||
*/ |
|||
async function loadMessages(lang: SupportedLanguagesType) { |
|||
const appLocaleMessages = await localesMap[lang](); |
|||
return appLocaleMessages.default; |
|||
} |
|||
|
|||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) { |
|||
await coreSetup(app, { |
|||
defaultLocale: preferences.app.locale, |
|||
loadMessages, |
|||
missingWarn: !import.meta.env.PROD, |
|||
...options, |
|||
}); |
|||
} |
|||
|
|||
export { $t, loadMessages, setupI18n }; |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"page": { |
|||
"demos": { |
|||
"title": "Demos", |
|||
"naive": "Naive UI" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"page": { |
|||
"demos": { |
|||
"title": "演示", |
|||
"naive": "Naive UI" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
import { initPreferences } from '@vben/preferences'; |
|||
import { unmountGlobalLoading } from '@vben/utils'; |
|||
|
|||
import { overridesPreferences } from './preferences'; |
|||
|
|||
/** |
|||
* 应用初始化完成之后再进行页面加载渲染 |
|||
*/ |
|||
async function initApplication() { |
|||
// name用于指定项目唯一标识
|
|||
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
|||
const env = import.meta.env.PROD ? 'prod' : 'dev'; |
|||
const appVersion = import.meta.env.VITE_APP_VERSION; |
|||
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`; |
|||
|
|||
// app偏好设置初始化
|
|||
await initPreferences({ |
|||
namespace, |
|||
overrides: overridesPreferences, |
|||
}); |
|||
|
|||
// 启动应用并挂载
|
|||
// vue应用主要逻辑及视图
|
|||
const { bootstrap } = await import('./bootstrap'); |
|||
await bootstrap(namespace); |
|||
|
|||
// 移除并销毁loading
|
|||
unmountGlobalLoading(); |
|||
} |
|||
|
|||
initApplication(); |
|||
@ -0,0 +1,25 @@ |
|||
import { computed } from 'vue'; |
|||
|
|||
import { preferences } from '@vben/preferences'; |
|||
import '@vben/styles'; |
|||
|
|||
import { createDiscreteApi, darkTheme, lightTheme } from 'naive-ui'; |
|||
|
|||
const themeOverridesProviderProps = computed(() => ({ |
|||
themeOverrides: preferences.theme.mode === 'light' ? lightTheme : darkTheme, |
|||
})); |
|||
|
|||
const themeProviderProps = computed(() => ({ |
|||
theme: preferences.theme.mode === 'light' ? lightTheme : darkTheme, |
|||
})); |
|||
|
|||
export const { dialog, loadingBar, message, modal, notification } = |
|||
createDiscreteApi( |
|||
['message', 'dialog', 'notification', 'loadingBar', 'modal'], |
|||
{ |
|||
configProviderProps: themeProviderProps, |
|||
loadingBarProviderProps: themeOverridesProviderProps, |
|||
messageProviderProps: themeOverridesProviderProps, |
|||
notificationProviderProps: themeOverridesProviderProps, |
|||
}, |
|||
); |
|||
@ -0,0 +1,9 @@ |
|||
import { defineOverridesPreferences } from '@vben/preferences'; |
|||
|
|||
/** |
|||
* @description 项目配置文件 |
|||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置 |
|||
*/ |
|||
export const overridesPreferences = defineOverridesPreferences({ |
|||
// overrides
|
|||
}); |
|||
@ -0,0 +1,40 @@ |
|||
import type { |
|||
ComponentRecordType, |
|||
GenerateMenuAndRoutesOptions, |
|||
} from '@vben/types'; |
|||
|
|||
import { generateAccessible } from '@vben/access'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { getAllMenus } from '#/api'; |
|||
import { BasicLayout, IFrameView } from '#/layouts'; |
|||
import { $t } from '#/locales'; |
|||
import { message } from '#/naive'; |
|||
|
|||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); |
|||
|
|||
async function generateAccess(options: GenerateMenuAndRoutesOptions) { |
|||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); |
|||
|
|||
const layoutMap: ComponentRecordType = { |
|||
BasicLayout, |
|||
IFrameView, |
|||
}; |
|||
|
|||
return await generateAccessible(preferences.app.accessMode, { |
|||
...options, |
|||
fetchMenuListAsync: async () => { |
|||
message.loading(`${$t('common.loadingMenu')}...`, { |
|||
duration: 1.5, |
|||
}); |
|||
return await getAllMenus(); |
|||
}, |
|||
// 可以指定没有权限跳转403页面
|
|||
forbiddenComponent, |
|||
// 如果 route.meta.menuVisibleWithForbidden = true
|
|||
layoutMap, |
|||
pageMap, |
|||
}); |
|||
} |
|||
|
|||
export { generateAccess }; |
|||
@ -0,0 +1,132 @@ |
|||
import type { Router } from 'vue-router'; |
|||
|
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useAccessStore, useUserStore } from '@vben/stores'; |
|||
import { startProgress, stopProgress } from '@vben/utils'; |
|||
|
|||
import { useTitle } from '@vueuse/core'; |
|||
|
|||
import { $t } from '#/locales'; |
|||
import { coreRouteNames, dynamicRoutes } from '#/router/routes'; |
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
import { generateAccess } from './access'; |
|||
|
|||
/** |
|||
* 通用守卫配置 |
|||
* @param router |
|||
*/ |
|||
function setupCommonGuard(router: Router) { |
|||
// 记录已经加载的页面
|
|||
const loadedPaths = new Set<string>(); |
|||
|
|||
router.beforeEach(async (to) => { |
|||
to.meta.loaded = loadedPaths.has(to.path); |
|||
|
|||
// 页面加载进度条
|
|||
if (!to.meta.loaded && preferences.transition.progress) { |
|||
startProgress(); |
|||
} |
|||
return true; |
|||
}); |
|||
|
|||
router.afterEach((to) => { |
|||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
|||
|
|||
if (preferences.tabbar.enable) { |
|||
loadedPaths.add(to.path); |
|||
} |
|||
|
|||
// 关闭页面加载进度条
|
|||
if (preferences.transition.progress) { |
|||
stopProgress(); |
|||
} |
|||
|
|||
// 动态修改标题
|
|||
if (preferences.app.dynamicTitle) { |
|||
const { title } = to.meta; |
|||
// useTitle(`${$t(title)} - ${preferences.app.name}`);
|
|||
useTitle(`${$t(title)} - ${preferences.app.name}`); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 权限访问守卫配置 |
|||
* @param router |
|||
*/ |
|||
function setupAccessGuard(router: Router) { |
|||
router.beforeEach(async (to, from) => { |
|||
const accessStore = useAccessStore(); |
|||
const userStore = useUserStore(); |
|||
const authStore = useAuthStore(); |
|||
|
|||
// accessToken 检查
|
|||
if (!accessStore.accessToken) { |
|||
if ( |
|||
// 基本路由,这些路由不需要进入权限拦截
|
|||
coreRouteNames.includes(to.name as string) || |
|||
// 明确声明忽略权限访问权限,则可以访问
|
|||
to.meta.ignoreAccess |
|||
) { |
|||
return true; |
|||
} |
|||
|
|||
// 没有访问权限,跳转登录页面
|
|||
if (to.fullPath !== LOGIN_PATH) { |
|||
return { |
|||
path: LOGIN_PATH, |
|||
// 如不需要,直接删除 query
|
|||
query: { redirect: encodeURIComponent(to.fullPath) }, |
|||
// 携带当前跳转的页面,登录后重新跳转该页面
|
|||
replace: true, |
|||
}; |
|||
} |
|||
return to; |
|||
} |
|||
|
|||
const accessRoutes = accessStore.accessRoutes; |
|||
|
|||
// 是否已经生成过动态路由
|
|||
if (accessRoutes && accessRoutes.length > 0) { |
|||
return true; |
|||
} |
|||
|
|||
// 生成路由表
|
|||
// 当前登录用户拥有的角色标识列表
|
|||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); |
|||
const userRoles = userInfo.roles ?? []; |
|||
|
|||
// 生成菜单和路由
|
|||
const { accessibleMenus, accessibleRoutes } = await generateAccess({ |
|||
roles: userRoles, |
|||
router, |
|||
// 则会在菜单中显示,但是访问会被重定向到403
|
|||
routes: dynamicRoutes, |
|||
}); |
|||
|
|||
// 保存菜单信息和路由信息
|
|||
accessStore.setAccessMenus(accessibleMenus); |
|||
accessStore.setAccessRoutes(accessibleRoutes); |
|||
const redirectPath = (from.query.redirect ?? to.path) as string; |
|||
|
|||
return { |
|||
path: decodeURIComponent(redirectPath), |
|||
replace: true, |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 项目守卫配置 |
|||
* @param router |
|||
*/ |
|||
function createRouterGuard(router: Router) { |
|||
/** 通用 */ |
|||
setupCommonGuard(router); |
|||
/** 权限访问 */ |
|||
setupAccessGuard(router); |
|||
} |
|||
|
|||
export { createRouterGuard }; |
|||
@ -0,0 +1,32 @@ |
|||
import { |
|||
createRouter, |
|||
createWebHashHistory, |
|||
createWebHistory, |
|||
} from 'vue-router'; |
|||
|
|||
import { resetStaticRoutes } from '@vben/utils'; |
|||
|
|||
import { createRouterGuard } from './guard'; |
|||
import { routes } from './routes'; |
|||
|
|||
/** |
|||
* @zh_CN 创建vue-router实例 |
|||
*/ |
|||
const router = createRouter({ |
|||
history: |
|||
import.meta.env.VITE_ROUTER_HISTORY === 'hash' |
|||
? createWebHashHistory(import.meta.env.VITE_BASE) |
|||
: createWebHistory(import.meta.env.VITE_BASE), |
|||
// 应该添加到路由的初始路由列表。
|
|||
routes, |
|||
scrollBehavior: () => ({ left: 0, top: 0 }), |
|||
// 是否应该禁止尾部斜杠。
|
|||
// strict: true,
|
|||
}); |
|||
|
|||
const resetRoutes = () => resetStaticRoutes(router, routes); |
|||
|
|||
// 创建路由守卫
|
|||
createRouterGuard(router); |
|||
|
|||
export { resetRoutes, router }; |
|||
@ -0,0 +1,86 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { DEFAULT_HOME_PATH } from '@vben/constants'; |
|||
|
|||
import { AuthPageLayout } from '#/layouts'; |
|||
import { $t } from '#/locales'; |
|||
import Login from '#/views/_core/authentication/login.vue'; |
|||
|
|||
/** 全局404页面 */ |
|||
const fallbackNotFoundRoute: RouteRecordRaw = { |
|||
component: () => import('#/views/_core/fallback/not-found.vue'), |
|||
meta: { |
|||
hideInBreadcrumb: true, |
|||
hideInMenu: true, |
|||
hideInTab: true, |
|||
title: '404', |
|||
}, |
|||
name: 'FallbackNotFound', |
|||
path: '/:path(.*)*', |
|||
}; |
|||
|
|||
/** 基本路由,这些路由是必须存在的 */ |
|||
const coreRoutes: RouteRecordRaw[] = [ |
|||
{ |
|||
meta: { |
|||
title: 'Root', |
|||
}, |
|||
name: 'Root', |
|||
path: '/', |
|||
redirect: DEFAULT_HOME_PATH, |
|||
}, |
|||
{ |
|||
component: AuthPageLayout, |
|||
meta: { |
|||
title: 'Authentication', |
|||
}, |
|||
name: 'Authentication', |
|||
path: '/auth', |
|||
children: [ |
|||
{ |
|||
name: 'Login', |
|||
path: 'login', |
|||
component: Login, |
|||
meta: { |
|||
title: $t('page.core.login'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'CodeLogin', |
|||
path: 'code-login', |
|||
component: () => import('#/views/_core/authentication/code-login.vue'), |
|||
meta: { |
|||
title: $t('page.core.codeLogin'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'QrCodeLogin', |
|||
path: 'qrcode-login', |
|||
component: () => |
|||
import('#/views/_core/authentication/qrcode-login.vue'), |
|||
meta: { |
|||
title: $t('page.core.qrcodeLogin'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'ForgetPassword', |
|||
path: 'forget-password', |
|||
component: () => |
|||
import('#/views/_core/authentication/forget-password.vue'), |
|||
meta: { |
|||
title: $t('page.core.forgetPassword'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'Register', |
|||
path: 'register', |
|||
component: () => import('#/views/_core/authentication/register.vue'), |
|||
meta: { |
|||
title: $t('page.core.register'), |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export { coreRoutes, fallbackNotFoundRoute }; |
|||
@ -0,0 +1,31 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { mergeRouteModules, traverseTreeValues } from '@vben/utils'; |
|||
|
|||
import { coreRoutes, fallbackNotFoundRoute } from './core'; |
|||
|
|||
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', { |
|||
eager: true, |
|||
}); |
|||
|
|||
// 有需要可以自行打开注释,并创建文件夹
|
|||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
|||
|
|||
/** 动态路由 */ |
|||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); |
|||
|
|||
/** 静态路由列表,访问这些页面可以不需要权限 */ |
|||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
|||
const staticRoutes: RouteRecordRaw[] = []; |
|||
|
|||
/** 路由列表,由基本路由+静态路由组成 */ |
|||
const routes: RouteRecordRaw[] = [ |
|||
...coreRoutes, |
|||
...staticRoutes, |
|||
fallbackNotFoundRoute, |
|||
]; |
|||
|
|||
/** 基本路由列表,这些路由不需要进入权限拦截 */ |
|||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name); |
|||
|
|||
export { coreRouteNames, dynamicRoutes, routes }; |
|||
@ -0,0 +1,39 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { BasicLayout } from '#/layouts'; |
|||
import { $t } from '#/locales'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
component: BasicLayout, |
|||
meta: { |
|||
icon: 'lucide:layout-dashboard', |
|||
order: -1, |
|||
title: $t('page.dashboard.title'), |
|||
}, |
|||
name: 'Dashboard', |
|||
path: '/', |
|||
children: [ |
|||
{ |
|||
name: 'Analytics', |
|||
path: '/analytics', |
|||
component: () => import('#/views/dashboard/analytics/index.vue'), |
|||
meta: { |
|||
affixTab: true, |
|||
icon: 'lucide:area-chart', |
|||
title: $t('page.dashboard.analytics'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'Workspace', |
|||
path: '/workspace', |
|||
component: () => import('#/views/dashboard/workspace/index.vue'), |
|||
meta: { |
|||
title: $t('page.dashboard.workspace'), |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -0,0 +1,32 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { BasicLayout } from '#/layouts'; |
|||
import { $t } from '#/locales'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
component: BasicLayout, |
|||
meta: { |
|||
icon: 'ic:baseline-view-in-ar', |
|||
keepAlive: true, |
|||
order: 1000, |
|||
title: $t('page.demos.title'), |
|||
}, |
|||
name: 'Demos', |
|||
path: '/demos', |
|||
children: [ |
|||
// 权限控制
|
|||
{ |
|||
meta: { |
|||
icon: 'mdi:shield-key-outline', |
|||
title: $t('page.demos.naive'), |
|||
}, |
|||
name: 'NaiveDemos', |
|||
path: '/demos/access', |
|||
component: () => import('#/views/demos/naive/index.vue'), |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -0,0 +1,57 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { VBEN_DOC_URL, VBEN_GITHUB_URL, VBEN_LOGO_URL } from '@vben/constants'; |
|||
|
|||
import { BasicLayout, IFrameView } from '#/layouts'; |
|||
import { $t } from '#/locales'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
component: BasicLayout, |
|||
meta: { |
|||
badgeType: 'dot', |
|||
badgeVariants: 'destructive', |
|||
icon: VBEN_LOGO_URL, |
|||
order: 9999, |
|||
title: $t('page.vben.title'), |
|||
}, |
|||
name: 'VbenProject', |
|||
path: '/vben-admin', |
|||
children: [ |
|||
{ |
|||
name: 'VbenAbout', |
|||
path: '/vben-admin/about', |
|||
component: () => import('#/views/_core/vben/about/index.vue'), |
|||
meta: { |
|||
badgeType: 'dot', |
|||
badgeVariants: 'destructive', |
|||
icon: 'lucide:copyright', |
|||
title: $t('page.vben.about'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'VbenDocument', |
|||
path: '/vben-admin/document', |
|||
component: IFrameView, |
|||
meta: { |
|||
icon: 'lucide:book-open-text', |
|||
iframeSrc: VBEN_DOC_URL, |
|||
keepAlive: true, |
|||
title: $t('page.vben.document'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'VbenGithub', |
|||
path: '/vben-admin/github', |
|||
component: IFrameView, |
|||
meta: { |
|||
icon: 'mdi:github', |
|||
link: VBEN_GITHUB_URL, |
|||
title: 'Github', |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -0,0 +1,111 @@ |
|||
import type { LoginAndRegisterParams } from '@vben/common-ui'; |
|||
import type { UserInfo } from '@vben/types'; |
|||
|
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; |
|||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; |
|||
|
|||
import { defineStore } from 'pinia'; |
|||
|
|||
import { getAccessCodes, getUserInfo, login } from '#/api'; |
|||
import { $t } from '#/locales'; |
|||
import { notification } from '#/naive'; |
|||
|
|||
export const useAuthStore = defineStore('auth', () => { |
|||
const accessStore = useAccessStore(); |
|||
const userStore = useUserStore(); |
|||
const router = useRouter(); |
|||
|
|||
const loginLoading = ref(false); |
|||
|
|||
/** |
|||
* 异步处理登录操作 |
|||
* Asynchronously handle the login process |
|||
* @param params 登录表单数据 |
|||
*/ |
|||
async function authLogin( |
|||
params: LoginAndRegisterParams, |
|||
onSuccess?: () => Promise<void> | void, |
|||
) { |
|||
// 异步处理用户登录操作并获取 accessToken
|
|||
let userInfo: null | UserInfo = null; |
|||
try { |
|||
loginLoading.value = true; |
|||
const { accessToken, refreshToken } = await login(params); |
|||
|
|||
// 如果成功获取到 accessToken
|
|||
if (accessToken) { |
|||
// 将 accessToken 存储到 accessStore 中
|
|||
accessStore.setAccessToken(accessToken); |
|||
accessStore.setRefreshToken(refreshToken); |
|||
|
|||
// 获取用户信息并存储到 accessStore 中
|
|||
const [fetchUserInfoResult, accessCodes] = await Promise.all([ |
|||
fetchUserInfo(), |
|||
getAccessCodes(), |
|||
]); |
|||
|
|||
userInfo = fetchUserInfoResult; |
|||
|
|||
userStore.setUserInfo(userInfo); |
|||
accessStore.setAccessCodes(accessCodes); |
|||
|
|||
if (accessStore.loginExpired) { |
|||
accessStore.setLoginExpired(false); |
|||
} else { |
|||
onSuccess |
|||
? await onSuccess?.() |
|||
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH); |
|||
} |
|||
|
|||
if (userInfo?.realName) { |
|||
notification.success({ |
|||
content: $t('authentication.loginSuccess'), |
|||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, |
|||
duration: 3000, |
|||
}); |
|||
} |
|||
} |
|||
} finally { |
|||
loginLoading.value = false; |
|||
} |
|||
|
|||
return { |
|||
userInfo, |
|||
}; |
|||
} |
|||
|
|||
async function logout() { |
|||
resetAllStores(); |
|||
accessStore.setLoginExpired(false); |
|||
|
|||
// 回登陆页带上当前路由地址
|
|||
await router.replace({ |
|||
path: LOGIN_PATH, |
|||
query: { |
|||
redirect: encodeURIComponent(router.currentRoute.value.fullPath), |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
async function fetchUserInfo() { |
|||
let userInfo: null | UserInfo = null; |
|||
userInfo = await getUserInfo(); |
|||
userStore.setUserInfo(userInfo); |
|||
return userInfo; |
|||
} |
|||
|
|||
function $reset() { |
|||
loginLoading.value = false; |
|||
} |
|||
|
|||
return { |
|||
$reset, |
|||
authLogin, |
|||
fetchUserInfo, |
|||
loginLoading, |
|||
logout, |
|||
}; |
|||
}); |
|||
@ -0,0 +1 @@ |
|||
export * from './auth'; |
|||
@ -0,0 +1,3 @@ |
|||
# \_core |
|||
|
|||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。 |
|||
@ -0,0 +1,30 @@ |
|||
<script lang="ts" setup> |
|||
import type { LoginCodeParams } from '@vben/common-ui'; |
|||
|
|||
import { ref } from 'vue'; |
|||
|
|||
import { AuthenticationCodeLogin } from '@vben/common-ui'; |
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
|
|||
defineOptions({ name: 'CodeLogin' }); |
|||
|
|||
const loading = ref(false); |
|||
|
|||
/** |
|||
* 异步处理登录操作 |
|||
* Asynchronously handle the login process |
|||
* @param values 登录表单数据 |
|||
*/ |
|||
async function handleLogin(values: LoginCodeParams) { |
|||
// eslint-disable-next-line no-console |
|||
console.log(values); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationCodeLogin |
|||
:loading="loading" |
|||
:login-path="LOGIN_PATH" |
|||
@submit="handleLogin" |
|||
/> |
|||
</template> |
|||
@ -0,0 +1,23 @@ |
|||
<script lang="ts" setup> |
|||
import { ref } from 'vue'; |
|||
|
|||
import { AuthenticationForgetPassword } from '@vben/common-ui'; |
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
|
|||
defineOptions({ name: 'ForgetPassword' }); |
|||
|
|||
const loading = ref(false); |
|||
|
|||
function handleSubmit(value: string) { |
|||
// eslint-disable-next-line no-console |
|||
console.log('reset email:', value); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationForgetPassword |
|||
:loading="loading" |
|||
:login-path="LOGIN_PATH" |
|||
@submit="handleSubmit" |
|||
/> |
|||
</template> |
|||
@ -0,0 +1,18 @@ |
|||
<script lang="ts" setup> |
|||
import { AuthenticationLogin } from '@vben/common-ui'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
defineOptions({ name: 'Login' }); |
|||
|
|||
const authStore = useAuthStore(); |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationLogin |
|||
:loading="authStore.loginLoading" |
|||
password-placeholder="123456" |
|||
username-placeholder="vben" |
|||
@submit="authStore.authLogin" |
|||
/> |
|||
</template> |
|||
@ -0,0 +1,10 @@ |
|||
<script lang="ts" setup> |
|||
import { AuthenticationQrCodeLogin } from '@vben/common-ui'; |
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
|
|||
defineOptions({ name: 'QrCodeLogin' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" /> |
|||
</template> |
|||
@ -0,0 +1,25 @@ |
|||
<script lang="ts" setup> |
|||
import type { LoginAndRegisterParams } from '@vben/common-ui'; |
|||
|
|||
import { ref } from 'vue'; |
|||
|
|||
import { AuthenticationRegister } from '@vben/common-ui'; |
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
|
|||
defineOptions({ name: 'Register' }); |
|||
|
|||
const loading = ref(false); |
|||
|
|||
function handleSubmit(value: LoginAndRegisterParams) { |
|||
// eslint-disable-next-line no-console |
|||
console.log('register submit:', value); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationRegister |
|||
:loading="loading" |
|||
:login-path="LOGIN_PATH" |
|||
@submit="handleSubmit" |
|||
/> |
|||
</template> |
|||
@ -0,0 +1,7 @@ |
|||
<script lang="ts" setup> |
|||
import { Fallback } from '@vben/common-ui'; |
|||
</script> |
|||
|
|||
<template> |
|||
<Fallback status="coming-soon" /> |
|||
</template> |
|||
@ -0,0 +1,9 @@ |
|||
<script lang="ts" setup> |
|||
import { Fallback } from '@vben/common-ui'; |
|||
|
|||
defineOptions({ name: 'Fallback403Demo' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<Fallback status="403" /> |
|||
</template> |
|||
@ -0,0 +1,9 @@ |
|||
<script lang="ts" setup> |
|||
import { Fallback } from '@vben/common-ui'; |
|||
|
|||
defineOptions({ name: 'Fallback500Demo' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<Fallback status="500" /> |
|||
</template> |
|||
@ -0,0 +1,9 @@ |
|||
<script lang="ts" setup> |
|||
import { Fallback } from '@vben/common-ui'; |
|||
|
|||
defineOptions({ name: 'Fallback404Demo' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<Fallback status="404" /> |
|||
</template> |
|||
@ -0,0 +1,9 @@ |
|||
<script lang="ts" setup> |
|||
import { Fallback } from '@vben/common-ui'; |
|||
|
|||
defineOptions({ name: 'FallbackOfflineDemo' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<Fallback status="offline" /> |
|||
</template> |
|||
@ -0,0 +1,9 @@ |
|||
<script lang="ts" setup> |
|||
import { About } from '@vben/common-ui'; |
|||
|
|||
defineOptions({ name: 'About' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<About /> |
|||
</template> |
|||
@ -0,0 +1,78 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
grid: { |
|||
bottom: 0, |
|||
containLabel: true, |
|||
left: '1%', |
|||
right: '1%', |
|||
top: '2 %', |
|||
}, |
|||
series: [ |
|||
{ |
|||
areaStyle: {}, |
|||
data: [ |
|||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000, |
|||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222, |
|||
111, |
|||
], |
|||
itemStyle: { |
|||
color: '#5ab1ef', |
|||
}, |
|||
smooth: true, |
|||
type: 'line', |
|||
}, |
|||
{ |
|||
areaStyle: {}, |
|||
data: [ |
|||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000, |
|||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11, |
|||
], |
|||
itemStyle: { |
|||
color: '#019680', |
|||
}, |
|||
smooth: true, |
|||
type: 'line', |
|||
}, |
|||
], |
|||
tooltip: { |
|||
axisPointer: { |
|||
lineStyle: { |
|||
color: '#019680', |
|||
width: 1, |
|||
}, |
|||
}, |
|||
trigger: 'axis', |
|||
}, |
|||
xAxis: { |
|||
axisTick: { |
|||
show: false, |
|||
}, |
|||
boundaryGap: false, |
|||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`), |
|||
type: 'category', |
|||
}, |
|||
yAxis: [ |
|||
{ |
|||
axisTick: { |
|||
show: false, |
|||
}, |
|||
max: 80_000, |
|||
|
|||
type: 'value', |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,80 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
legend: { |
|||
bottom: 0, |
|||
data: ['访问', '趋势'], |
|||
}, |
|||
radar: { |
|||
indicator: [ |
|||
{ |
|||
name: '网页', |
|||
}, |
|||
{ |
|||
name: '移动端', |
|||
}, |
|||
{ |
|||
name: 'Ipad', |
|||
}, |
|||
{ |
|||
name: '客户端', |
|||
}, |
|||
{ |
|||
name: '第三方', |
|||
}, |
|||
{ |
|||
name: '其它', |
|||
}, |
|||
], |
|||
radius: '60%', |
|||
splitNumber: 8, |
|||
}, |
|||
series: [ |
|||
{ |
|||
areaStyle: { |
|||
opacity: 1, |
|||
shadowBlur: 0, |
|||
shadowColor: 'rgba(0,0,0,.2)', |
|||
shadowOffsetX: 0, |
|||
shadowOffsetY: 10, |
|||
}, |
|||
data: [ |
|||
{ |
|||
itemStyle: { |
|||
color: '#b6a2de', |
|||
}, |
|||
name: '访问', |
|||
value: [90, 50, 86, 40, 50, 20], |
|||
}, |
|||
{ |
|||
itemStyle: { |
|||
color: '#5ab1ef', |
|||
}, |
|||
name: '趋势', |
|||
value: [70, 75, 70, 76, 20, 85], |
|||
}, |
|||
], |
|||
itemStyle: { |
|||
// borderColor: '#fff', |
|||
borderRadius: 10, |
|||
borderWidth: 2, |
|||
}, |
|||
symbolSize: 0, |
|||
type: 'radar', |
|||
}, |
|||
], |
|||
tooltip: {}, |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,44 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
series: [ |
|||
{ |
|||
animationDelay() { |
|||
return Math.random() * 400; |
|||
}, |
|||
animationEasing: 'exponentialInOut', |
|||
animationType: 'scale', |
|||
center: ['50%', '50%'], |
|||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'], |
|||
data: [ |
|||
{ name: '外包', value: 500 }, |
|||
{ name: '定制', value: 310 }, |
|||
{ name: '技术支持', value: 274 }, |
|||
{ name: '远程', value: 400 }, |
|||
].sort((a, b) => { |
|||
return a.value - b.value; |
|||
}), |
|||
name: '商业占比', |
|||
radius: '80%', |
|||
roseType: 'radius', |
|||
type: 'pie', |
|||
}, |
|||
], |
|||
|
|||
tooltip: { |
|||
trigger: 'item', |
|||
}, |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,63 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
legend: { |
|||
bottom: '2%', |
|||
left: 'center', |
|||
}, |
|||
series: [ |
|||
{ |
|||
animationDelay() { |
|||
return Math.random() * 100; |
|||
}, |
|||
animationEasing: 'exponentialInOut', |
|||
animationType: 'scale', |
|||
avoidLabelOverlap: false, |
|||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'], |
|||
data: [ |
|||
{ name: '搜索引擎', value: 1048 }, |
|||
{ name: '直接访问', value: 735 }, |
|||
{ name: '邮件营销', value: 580 }, |
|||
{ name: '联盟广告', value: 484 }, |
|||
], |
|||
emphasis: { |
|||
label: { |
|||
fontSize: '12', |
|||
fontWeight: 'bold', |
|||
show: true, |
|||
}, |
|||
}, |
|||
itemStyle: { |
|||
// borderColor: '#fff', |
|||
borderRadius: 10, |
|||
borderWidth: 2, |
|||
}, |
|||
label: { |
|||
position: 'center', |
|||
show: false, |
|||
}, |
|||
labelLine: { |
|||
show: false, |
|||
}, |
|||
name: '访问来源', |
|||
radius: ['40%', '65%'], |
|||
type: 'pie', |
|||
}, |
|||
], |
|||
tooltip: { |
|||
trigger: 'item', |
|||
}, |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,53 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui'; |
|||
|
|||
const chartRef = ref<EchartsUIType>(); |
|||
const { renderEcharts } = useEcharts(chartRef); |
|||
|
|||
onMounted(() => { |
|||
renderEcharts({ |
|||
grid: { |
|||
bottom: 0, |
|||
containLabel: true, |
|||
left: '1%', |
|||
right: '1%', |
|||
top: '2 %', |
|||
}, |
|||
series: [ |
|||
{ |
|||
barMaxWidth: 80, |
|||
// color: '#4f69fd', |
|||
data: [ |
|||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, |
|||
3200, 4800, |
|||
], |
|||
type: 'bar', |
|||
}, |
|||
], |
|||
tooltip: { |
|||
axisPointer: { |
|||
lineStyle: { |
|||
// color: '#4f69fd', |
|||
width: 1, |
|||
}, |
|||
}, |
|||
trigger: 'axis', |
|||
}, |
|||
xAxis: { |
|||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`), |
|||
type: 'category', |
|||
}, |
|||
yAxis: { |
|||
max: 8000, |
|||
splitNumber: 4, |
|||
type: 'value', |
|||
}, |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,90 @@ |
|||
<script lang="ts" setup> |
|||
import type { AnalysisOverviewItem } from '@vben/common-ui'; |
|||
import type { TabOption } from '@vben/types'; |
|||
|
|||
import { |
|||
AnalysisChartCard, |
|||
AnalysisChartsTabs, |
|||
AnalysisOverview, |
|||
} from '@vben/common-ui'; |
|||
import { |
|||
SvgBellIcon, |
|||
SvgCakeIcon, |
|||
SvgCardIcon, |
|||
SvgDownloadIcon, |
|||
} from '@vben/icons'; |
|||
|
|||
import AnalyticsTrends from './analytics-trends.vue'; |
|||
import AnalyticsVisits from './analytics-visits.vue'; |
|||
import AnalyticsVisitsData from './analytics-visits-data.vue'; |
|||
import AnalyticsVisitsSales from './analytics-visits-sales.vue'; |
|||
import AnalyticsVisitsSource from './analytics-visits-source.vue'; |
|||
|
|||
const overviewItems: AnalysisOverviewItem[] = [ |
|||
{ |
|||
icon: SvgCardIcon, |
|||
title: '用户量', |
|||
totalTitle: '总用户量', |
|||
totalValue: 120_000, |
|||
value: 2000, |
|||
}, |
|||
{ |
|||
icon: SvgCakeIcon, |
|||
title: '访问量', |
|||
totalTitle: '总访问量', |
|||
totalValue: 500_000, |
|||
value: 20_000, |
|||
}, |
|||
{ |
|||
icon: SvgDownloadIcon, |
|||
title: '下载量', |
|||
totalTitle: '总下载量', |
|||
totalValue: 120_000, |
|||
value: 8000, |
|||
}, |
|||
{ |
|||
icon: SvgBellIcon, |
|||
title: '使用量', |
|||
totalTitle: '总使用量', |
|||
totalValue: 50_000, |
|||
value: 5000, |
|||
}, |
|||
]; |
|||
|
|||
const chartTabs: TabOption[] = [ |
|||
{ |
|||
label: '流量趋势', |
|||
value: 'trends', |
|||
}, |
|||
{ |
|||
label: '月访问量', |
|||
value: 'visits', |
|||
}, |
|||
]; |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<AnalysisOverview :items="overviewItems" /> |
|||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5"> |
|||
<template #trends> |
|||
<AnalyticsTrends /> |
|||
</template> |
|||
<template #visits> |
|||
<AnalyticsVisits /> |
|||
</template> |
|||
</AnalysisChartsTabs> |
|||
|
|||
<div class="mt-5 w-full md:flex"> |
|||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量"> |
|||
<AnalyticsVisitsData /> |
|||
</AnalysisChartCard> |
|||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源"> |
|||
<AnalyticsVisitsSource /> |
|||
</AnalysisChartCard> |
|||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源"> |
|||
<AnalyticsVisitsSales /> |
|||
</AnalysisChartCard> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,225 @@ |
|||
<script lang="ts" setup> |
|||
import type { |
|||
WorkbenchProjectItem, |
|||
WorkbenchQuickNavItem, |
|||
WorkbenchTodoItem, |
|||
WorkbenchTrendItem, |
|||
} from '@vben/common-ui'; |
|||
|
|||
import { ref } from 'vue'; |
|||
|
|||
import { |
|||
AnalysisChartCard, |
|||
WorkbenchHeader, |
|||
WorkbenchProject, |
|||
WorkbenchQuickNav, |
|||
WorkbenchTodo, |
|||
WorkbenchTrends, |
|||
} from '@vben/common-ui'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
|
|||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue'; |
|||
|
|||
const userStore = useUserStore(); |
|||
|
|||
const projectItems: WorkbenchProjectItem[] = [ |
|||
{ |
|||
color: '', |
|||
content: '不要等待机会,而要创造机会。', |
|||
date: '2021-04-01', |
|||
group: '开源组', |
|||
icon: 'carbon:logo-github', |
|||
title: 'Github', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
content: '现在的你决定将来的你。', |
|||
date: '2021-04-01', |
|||
group: '算法组', |
|||
icon: 'ion:logo-vue', |
|||
title: 'Vue', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
content: '没有什么才能比努力更重要。', |
|||
date: '2021-04-01', |
|||
group: '上班摸鱼', |
|||
icon: 'ion:logo-html5', |
|||
title: 'Html5', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
content: '热情和欲望可以突破一切难关。', |
|||
date: '2021-04-01', |
|||
group: 'UI', |
|||
icon: 'ion:logo-angular', |
|||
title: 'Angular', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
content: '健康的身体是实现目标的基石。', |
|||
date: '2021-04-01', |
|||
group: '技术牛', |
|||
icon: 'bx:bxl-react', |
|||
title: 'React', |
|||
}, |
|||
{ |
|||
color: '#EBD94E', |
|||
content: '路是走出来的,而不是空想出来的。', |
|||
date: '2021-04-01', |
|||
group: '架构组', |
|||
icon: 'ion:logo-javascript', |
|||
title: 'Js', |
|||
}, |
|||
]; |
|||
|
|||
const quickNavItems: WorkbenchQuickNavItem[] = [ |
|||
{ |
|||
color: '#1fdaca', |
|||
icon: 'ion:home-outline', |
|||
title: '首页', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
icon: 'ion:grid-outline', |
|||
title: '仪表盘', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
icon: 'ion:layers-outline', |
|||
title: '组件', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
icon: 'ion:settings-outline', |
|||
title: '系统管理', |
|||
}, |
|||
{ |
|||
color: '#4daf1bc9', |
|||
icon: 'ion:key-outline', |
|||
title: '权限管理', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
icon: 'ion:bar-chart-outline', |
|||
title: '图表', |
|||
}, |
|||
]; |
|||
|
|||
const todoItems = ref<WorkbenchTodoItem[]>([ |
|||
{ |
|||
completed: false, |
|||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '审查前端代码提交', |
|||
}, |
|||
{ |
|||
completed: true, |
|||
content: `检查并优化系统性能,降低CPU使用率。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '系统性能优化', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '安全检查', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '更新项目依赖', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '修复UI显示问题', |
|||
}, |
|||
]); |
|||
const trendItems: WorkbenchTrendItem[] = [ |
|||
{ |
|||
avatar: 'svg:avatar-1', |
|||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`, |
|||
date: '刚刚', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-2', |
|||
content: `关注了 <a>威廉</a> `, |
|||
date: '1个小时前', |
|||
title: '艾文', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-3', |
|||
content: `发布了 <a>个人动态</a> `, |
|||
date: '1天前', |
|||
title: '克里斯', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `发表文章 <a>如何编写一个Vite插件</a> `, |
|||
date: '2天前', |
|||
title: 'Vben', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-1', |
|||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`, |
|||
date: '3天前', |
|||
title: '皮特', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-2', |
|||
content: `关闭了问题 <a>如何运行项目</a> `, |
|||
date: '1周前', |
|||
title: '杰克', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-3', |
|||
content: `发布了 <a>个人动态</a> `, |
|||
date: '1周前', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `推送了代码到 <a>Github</a>`, |
|||
date: '2021-04-01 20:00', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `, |
|||
date: '2021-03-01 20:00', |
|||
title: 'Vben', |
|||
}, |
|||
]; |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<WorkbenchHeader |
|||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar" |
|||
> |
|||
<template #title> |
|||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧! |
|||
</template> |
|||
<template #description> 今日晴,20℃ - 32℃! </template> |
|||
</WorkbenchHeader> |
|||
|
|||
<div class="mt-5 flex flex-col lg:flex-row"> |
|||
<div class="mr-4 w-full lg:w-3/5"> |
|||
<WorkbenchProject :items="projectItems" title="项目" /> |
|||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" /> |
|||
</div> |
|||
<div class="w-full lg:w-2/5"> |
|||
<WorkbenchQuickNav :items="quickNavItems" title="快捷导航" /> |
|||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" /> |
|||
<AnalysisChartCard class="mt-5" title="访问来源"> |
|||
<AnalyticsVisitsSource /> |
|||
</AnalysisChartCard> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,93 @@ |
|||
<script lang="ts" setup> |
|||
import type { NotificationType } from 'naive-ui'; |
|||
|
|||
import { NButton, NCard, NSpace, useMessage, useNotification } from 'naive-ui'; |
|||
|
|||
const notification = useNotification(); |
|||
|
|||
const message = useMessage(); |
|||
function error() { |
|||
message.error('Once upon a time you dressed so fine'); |
|||
} |
|||
|
|||
function warning() { |
|||
message.warning('How many roads must a man walk down'); |
|||
} |
|||
function success() { |
|||
message.success( |
|||
"'Cause you walked hand in hand With another man in my place", |
|||
); |
|||
} |
|||
function loading() { |
|||
message.loading( |
|||
'If I were you, I will realize that I love you more than any other guy', |
|||
); |
|||
} |
|||
|
|||
function notify(type: NotificationType) { |
|||
notification[type]({ |
|||
content: '说点啥呢', |
|||
duration: 2500, |
|||
keepAliveOnHover: true, |
|||
meta: '想不出来', |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<div class="card-box p-5"> |
|||
<h1 class="text-xl font-semibold">naive组件使用演示</h1> |
|||
<div class="text-foreground/80 mt-2">支持多语言,主题功能集成切换等</div> |
|||
</div> |
|||
|
|||
<div class="card-box mt-5 p-5"> |
|||
<div class="mb-3"> |
|||
<span class="text-lg font-semibold">按钮</span> |
|||
</div> |
|||
<div> |
|||
<NSpace> |
|||
<NButton>Default</NButton> |
|||
<NButton type="tertiary"> Tertiary </NButton> |
|||
<NButton type="primary"> Primary </NButton> |
|||
<NButton type="info"> Info </NButton> |
|||
<NButton type="success"> Success </NButton> |
|||
<NButton type="warning"> Warning </NButton> |
|||
<NButton type="error"> Error </NButton> |
|||
</NSpace> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="card-box mt-5 p-5"> |
|||
<div class="mb-3"> |
|||
<span class="text-lg font-semibold">卡片</span> |
|||
</div> |
|||
<div> |
|||
<NCard title="卡片"> 卡片内容 </NCard> |
|||
</div> |
|||
</div> |
|||
<div class="card-box mt-5 p-5"> |
|||
<div class="mb-3"> |
|||
<span class="text-lg font-semibold">信息 Message </span> |
|||
</div> |
|||
<div class="flex gap-3"> |
|||
<NButton type="error" @click="error"> 错误 </NButton> |
|||
<NButton type="warning" @click="warning"> 警告 </NButton> |
|||
<NButton type="success" @click="success"> 成功 </NButton> |
|||
<NButton type="primary" @click="loading"> 加载中 </NButton> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="card-box mt-5 p-5"> |
|||
<div class="mb-3"> |
|||
<span class="text-lg font-semibold">通知 Notification </span> |
|||
</div> |
|||
<div class="flex gap-3"> |
|||
<NButton type="error" @click="notify('error')"> 错误 </NButton> |
|||
<NButton type="warning" @click="notify('warning')"> 警告 </NButton> |
|||
<NButton type="success" @click="notify('success')"> 成功 </NButton> |
|||
<NButton type="primary" @click="notify('info')"> 加载中 </NButton> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1 @@ |
|||
export { default } from '@vben/tailwind-config'; |
|||
@ -0,0 +1,12 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web-app.json", |
|||
"compilerOptions": { |
|||
"baseUrl": ".", |
|||
"paths": { |
|||
"#/*": ["./src/*"] |
|||
} |
|||
}, |
|||
"references": [{ "path": "./tsconfig.node.json" }], |
|||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/node.json", |
|||
"compilerOptions": { |
|||
"composite": true, |
|||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", |
|||
"noEmit": false |
|||
}, |
|||
"include": ["vite.config.mts"] |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
import { defineConfig } from '@vben/vite-config'; |
|||
|
|||
export default defineConfig(async () => { |
|||
return { |
|||
application: {}, |
|||
vite: { |
|||
server: { |
|||
proxy: { |
|||
'/api': { |
|||
changeOrigin: true, |
|||
rewrite: (path) => path.replace(/^\/api/, ''), |
|||
// mock代理目标地址 |
|||
target: 'http://localhost:5320/api', |
|||
ws: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
}); |
|||
Loading…
Reference in new issue