Browse Source
* chore: init project * chore: install element-plus * chore: locale config * fix: eslint error * chore: merge from main * fix: lint * chore: finish todo * chore: update comments * chore: update * fix: lint error * chore: add unplugin-element-plus * chore: add useElementPlusDesignTokens * chore: configure some colorpull/3993/head
committed by
GitHub
64 changed files with 3161 additions and 1207 deletions
@ -0,0 +1,5 @@ |
|||||
|
# 应用标题 |
||||
|
VITE_APP_TITLE=Vben Admin |
||||
|
|
||||
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 |
||||
|
VITE_APP_NAMESPACE=vben-web-element |
||||
@ -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=5555 |
||||
|
|
||||
|
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,53 @@ |
|||||
|
{ |
||||
|
"name": "@vben/web-ele", |
||||
|
"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-antd" |
||||
|
}, |
||||
|
"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", |
||||
|
"dayjs": "^1.11.12", |
||||
|
"element-plus": "^2.7.6", |
||||
|
"pinia": "2.1.7", |
||||
|
"vue": "^3.4.34", |
||||
|
"vue-router": "^4.4.0" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"unplugin-element-plus": "^0.8.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 './status'; |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { requestClient } from '#/api/request'; |
||||
|
|
||||
|
/** |
||||
|
* 模拟任意状态码 |
||||
|
*/ |
||||
|
async function getMockStatus(status: string) { |
||||
|
return requestClient.get('/status', { params: { status } }); |
||||
|
} |
||||
|
|
||||
|
export { getMockStatus }; |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './core'; |
||||
|
export * from './demos'; |
||||
@ -0,0 +1,67 @@ |
|||||
|
/** |
||||
|
* 该文件可自行根据业务逻辑进行调整 |
||||
|
*/ |
||||
|
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 { ElMessage } from 'element-plus'; |
||||
|
|
||||
|
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) => ElMessage.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,17 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useElementPlusDesignTokens } from '@vben/hooks'; |
||||
|
|
||||
|
import { ElConfigProvider } from 'element-plus'; |
||||
|
|
||||
|
import { elementLocale } from '#/locales'; |
||||
|
|
||||
|
defineOptions({ name: 'App' }); |
||||
|
|
||||
|
useElementPlusDesignTokens(); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<ElConfigProvider :locale="elementLocale"> |
||||
|
<RouterView /> |
||||
|
</ElConfigProvider> |
||||
|
</template> |
||||
@ -0,0 +1,31 @@ |
|||||
|
import { createApp } from 'vue'; |
||||
|
|
||||
|
import { registerAccessDirective } from '@vben/access'; |
||||
|
import { initStores } from '@vben/stores'; |
||||
|
import '@vben/styles'; |
||||
|
import '@vben/styles/antd'; |
||||
|
|
||||
|
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.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,91 @@ |
|||||
|
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales'; |
||||
|
|
||||
|
import type { App } from 'vue'; |
||||
|
import { ref } from 'vue'; |
||||
|
|
||||
|
import { $t, setupI18n as coreSetup, loadLocalesMap } from '@vben/locales'; |
||||
|
import { preferences } from '@vben/preferences'; |
||||
|
|
||||
|
import dayjs from 'dayjs'; |
||||
|
import { Language } from 'element-plus/es/locale'; |
||||
|
import defaultLocale from 'element-plus/es/locale/lang/zh-cn'; |
||||
|
|
||||
|
const elementLocale = ref<Language>(defaultLocale); |
||||
|
|
||||
|
const modules = import.meta.glob('./langs/*.json'); |
||||
|
|
||||
|
const localesMap = loadLocalesMap(modules); |
||||
|
|
||||
|
/** |
||||
|
* 加载应用特有的语言包 |
||||
|
* 这里也可以改造为从服务端获取翻译数据 |
||||
|
* @param lang |
||||
|
*/ |
||||
|
async function loadMessages(lang: SupportedLanguagesType) { |
||||
|
const [appLocaleMessages] = await Promise.all([ |
||||
|
localesMap[lang](), |
||||
|
loadThirdPartyMessage(lang), |
||||
|
]); |
||||
|
return appLocaleMessages.default; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 加载第三方组件库的语言包 |
||||
|
* @param lang |
||||
|
*/ |
||||
|
async function loadThirdPartyMessage(lang: SupportedLanguagesType) { |
||||
|
await Promise.all([loadElementLocale(lang), loadDayjsLocale(lang)]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 加载dayjs的语言包 |
||||
|
* @param lang |
||||
|
*/ |
||||
|
async function loadDayjsLocale(lang: SupportedLanguagesType) { |
||||
|
let locale; |
||||
|
switch (lang) { |
||||
|
case 'zh-CN': { |
||||
|
locale = await import('dayjs/locale/zh-cn'); |
||||
|
break; |
||||
|
} |
||||
|
case 'en-US': { |
||||
|
locale = await import('dayjs/locale/en'); |
||||
|
break; |
||||
|
} |
||||
|
// 默认使用英语
|
||||
|
default: { |
||||
|
locale = await import('dayjs/locale/en'); |
||||
|
} |
||||
|
} |
||||
|
dayjs.locale(locale); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 加载element-plus的语言包 |
||||
|
* @param lang |
||||
|
*/ |
||||
|
async function loadElementLocale(lang: SupportedLanguagesType) { |
||||
|
switch (lang) { |
||||
|
case 'zh-CN': { |
||||
|
elementLocale.value = defaultLocale; |
||||
|
break; |
||||
|
} |
||||
|
case 'en-US': { |
||||
|
elementLocale.value = (await import( |
||||
|
'element-plus/es/locale/lang/en' |
||||
|
)) as unknown as Language; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function setupI18n(app: App, options: LocaleSetupOptions = {}) { |
||||
|
await coreSetup(app, { |
||||
|
defaultLocale: preferences.app.locale, |
||||
|
loadMessages, |
||||
|
missingWarn: !import.meta.env.PROD, |
||||
|
...options, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export { $t, elementLocale, loadMessages, setupI18n }; |
||||
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"page": { |
||||
|
"demos": { |
||||
|
"title": "Demos", |
||||
|
"element-plus": "Element Plus" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"page": { |
||||
|
"demos": { |
||||
|
"title": "演示", |
||||
|
"element-plus": "Element Plus" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,9 @@ |
|||||
|
import { defineOverridesPreferences } from '@vben/preferences'; |
||||
|
|
||||
|
/** |
||||
|
* @description 项目配置文件 |
||||
|
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置 |
||||
|
*/ |
||||
|
export const overridesPreferences = defineOverridesPreferences({ |
||||
|
// overrides
|
||||
|
}); |
||||
@ -0,0 +1,42 @@ |
|||||
|
import type { |
||||
|
ComponentRecordType, |
||||
|
GenerateMenuAndRoutesOptions, |
||||
|
} from '@vben/types'; |
||||
|
|
||||
|
import { generateAccessible } from '@vben/access'; |
||||
|
import { preferences } from '@vben/preferences'; |
||||
|
|
||||
|
import { ElMessage } from 'element-plus'; |
||||
|
|
||||
|
import { getAllMenus } from '#/api'; |
||||
|
import { BasicLayout, IFrameView } from '#/layouts'; |
||||
|
import { $t } from '#/locales'; |
||||
|
|
||||
|
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 () => { |
||||
|
ElMessage({ |
||||
|
duration: 1500, |
||||
|
message: `${$t('common.loadingMenu')}...`, |
||||
|
}); |
||||
|
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,31 @@ |
|||||
|
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.element-plus'), |
||||
|
}, |
||||
|
name: 'NaiveDemos', |
||||
|
path: '/demos/element', |
||||
|
component: () => import('#/views/demos/element/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 { ElNotification } from 'element-plus'; |
||||
|
import { defineStore } from 'pinia'; |
||||
|
|
||||
|
import { getAccessCodes, getUserInfo, login } from '#/api'; |
||||
|
import { $t } from '#/locales'; |
||||
|
|
||||
|
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) { |
||||
|
ElNotification({ |
||||
|
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, |
||||
|
title: $t('authentication.loginSuccess'), |
||||
|
type: 'success', |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} 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,88 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { |
||||
|
ElButton, |
||||
|
ElCard, |
||||
|
ElMessage, |
||||
|
ElNotification, |
||||
|
ElSpace, |
||||
|
} from 'element-plus'; |
||||
|
|
||||
|
type NotificationType = 'error' | 'info' | 'success' | 'warning'; |
||||
|
|
||||
|
function error() { |
||||
|
ElMessage.error('Once upon a time you dressed so fine'); |
||||
|
} |
||||
|
|
||||
|
function warning() { |
||||
|
ElMessage.warning('How many roads must a man walk down'); |
||||
|
} |
||||
|
function success() { |
||||
|
ElMessage.success( |
||||
|
"'Cause you walked hand in hand With another man in my place", |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function notify(type: NotificationType) { |
||||
|
ElNotification({ |
||||
|
duration: 2500, |
||||
|
message: '说点啥呢', |
||||
|
type, |
||||
|
}); |
||||
|
} |
||||
|
</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> |
||||
|
<ElSpace> |
||||
|
<ElButton>Default</ElButton> |
||||
|
<ElButton type="primary"> Primary </ElButton> |
||||
|
<ElButton type="info"> Info </ElButton> |
||||
|
<ElButton type="success"> Success </ElButton> |
||||
|
<ElButton type="warning"> Warning </ElButton> |
||||
|
<ElButton type="danger"> Error </ElButton> |
||||
|
</ElSpace> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-box mt-5 p-5"> |
||||
|
<div class="mb-3"> |
||||
|
<span class="text-lg font-semibold">卡片</span> |
||||
|
</div> |
||||
|
<div> |
||||
|
<ElCard title="卡片"> 卡片内容 </ElCard> |
||||
|
</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"> |
||||
|
<ElButton type="danger" @click="error"> 错误 </ElButton> |
||||
|
<ElButton type="warning" @click="warning"> 警告 </ElButton> |
||||
|
<ElButton type="success" @click="success"> 成功 </ElButton> |
||||
|
</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"> |
||||
|
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton> |
||||
|
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton> |
||||
|
<ElButton type="success" @click="notify('success')"> 成功 </ElButton> |
||||
|
<ElButton type="primary" @click="notify('info')"> 加载中 </ElButton> |
||||
|
</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,24 @@ |
|||||
|
import { defineConfig } from '@vben/vite-config'; |
||||
|
import ElementPlus from 'unplugin-element-plus/vite' |
||||
|
|
||||
|
export default defineConfig(async () => { |
||||
|
return { |
||||
|
application: {}, |
||||
|
vite: { |
||||
|
plugins: [ElementPlus({ |
||||
|
format:"esm" |
||||
|
})], |
||||
|
server: { |
||||
|
proxy: { |
||||
|
'/api': { |
||||
|
changeOrigin: true, |
||||
|
rewrite: (path) => path.replace(/^\/api/, ''), |
||||
|
// mock代理目标地址 |
||||
|
target: 'http://localhost:5320/api', |
||||
|
ws: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
}); |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue