64 changed files with 2957 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
# 应用标题 |
|||
VITE_APP_TITLE=Vben Admin Antd |
|||
|
|||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 |
|||
VITE_APP_NAMESPACE=vben-web-antd |
|||
|
|||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 |
|||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key |
|||
@ -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=5666 |
|||
|
|||
VITE_BASE=/ |
|||
|
|||
# 接口地址 |
|||
VITE_GLOB_API_URL=/api |
|||
|
|||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭 |
|||
VITE_NITRO_MOCK=false |
|||
|
|||
# 是否打开 devtools,true 为打开,false 为关闭 |
|||
VITE_DEVTOOLS=false |
|||
|
|||
# 是否注入全局loading |
|||
VITE_INJECT_APP_LOADING=true |
|||
@ -0,0 +1,19 @@ |
|||
VITE_BASE=/ |
|||
|
|||
# 接口地址 |
|||
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api |
|||
|
|||
# 是否开启压缩,可以设置为 none, brotli, gzip |
|||
VITE_COMPRESS=none |
|||
|
|||
# 是否开启 PWA |
|||
VITE_PWA=false |
|||
|
|||
# vue-router 的模式 |
|||
VITE_ROUTER_HISTORY=hash |
|||
|
|||
# 是否注入全局loading |
|||
VITE_INJECT_APP_LOADING=true |
|||
|
|||
# 打包后是否生成dist.zip |
|||
VITE_ARCHIVER=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?b38e689f40558f20a9a686d7f6f33edf'; |
|||
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,50 @@ |
|||
{ |
|||
"name": "@vben/web-tdesign", |
|||
"version": "5.5.9", |
|||
"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/common-ui": "workspace:*", |
|||
"@vben/constants": "workspace:*", |
|||
"@vben/hooks": "workspace:*", |
|||
"@vben/icons": "workspace:*", |
|||
"@vben/layouts": "workspace:*", |
|||
"@vben/locales": "workspace:*", |
|||
"@vben/plugins": "workspace:*", |
|||
"@vben/preferences": "workspace:*", |
|||
"@vben/request": "workspace:*", |
|||
"@vben/stores": "workspace:*", |
|||
"@vben/styles": "workspace:*", |
|||
"@vben/types": "workspace:*", |
|||
"@vben/utils": "workspace:*", |
|||
"@vueuse/core": "catalog:", |
|||
"dayjs": "catalog:", |
|||
"pinia": "catalog:", |
|||
"tdesign-vue-next": "^1.17.1", |
|||
"vue": "catalog:", |
|||
"vue-router": "catalog:" |
|||
}, |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export { default } from '@vben/tailwind-config/postcss'; |
|||
|
After Width: | Height: | Size: 5.3 KiB |
@ -0,0 +1,212 @@ |
|||
import type { Component } from 'vue'; |
|||
|
|||
import type { BaseFormComponentType } from '@vben/common-ui'; |
|||
import type { Recordable } from '@vben/types'; |
|||
|
|||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; |
|||
|
|||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { notification } from 'ant-design-vue'; |
|||
/** |
|||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用 |
|||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用, |
|||
*/ |
|||
|
|||
const AutoComplete = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/auto-complete'), |
|||
); |
|||
const Button = defineAsyncComponent(() => import('tdesign-vue-next/es/button')); |
|||
const Checkbox = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/checkbox'), |
|||
); |
|||
const CheckboxGroup = defineAsyncComponent(() => |
|||
import('tdesign-vue-next/es/checkbox').then((res) => res.CheckboxGroup), |
|||
); |
|||
const DatePicker = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/date-picker'), |
|||
); |
|||
const Divider = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/divider'), |
|||
); |
|||
const Input = defineAsyncComponent(() => import('tdesign-vue-next/es/input')); |
|||
const InputNumber = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/input-number'), |
|||
); |
|||
// const InputPassword = defineAsyncComponent(() =>
|
|||
// import('tdesign-vue-next/es/input').then((res) => res.InputPassword),
|
|||
// );
|
|||
// const Mentions = defineAsyncComponent(
|
|||
// () => import('tdesign-vue-next/es/mentions'),
|
|||
// );
|
|||
const Radio = defineAsyncComponent(() => import('tdesign-vue-next/es/radio')); |
|||
const RadioGroup = defineAsyncComponent(() => |
|||
import('tdesign-vue-next/es/radio').then((res) => res.RadioGroup), |
|||
); |
|||
const RangePicker = defineAsyncComponent(() => |
|||
import('tdesign-vue-next/es/date-picker').then((res) => res.DateRangePicker), |
|||
); |
|||
const Rate = defineAsyncComponent(() => import('tdesign-vue-next/es/rate')); |
|||
const Select = defineAsyncComponent(() => import('tdesign-vue-next/es/select')); |
|||
const Space = defineAsyncComponent(() => import('tdesign-vue-next/es/space')); |
|||
const Switch = defineAsyncComponent(() => import('tdesign-vue-next/es/switch')); |
|||
const Textarea = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/textarea'), |
|||
); |
|||
const TimePicker = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/time-picker'), |
|||
); |
|||
const TreeSelect = defineAsyncComponent( |
|||
() => import('tdesign-vue-next/es/tree-select'), |
|||
); |
|||
const Upload = defineAsyncComponent(() => import('tdesign-vue-next/es/upload')); |
|||
|
|||
const withDefaultPlaceholder = <T extends Component>( |
|||
component: T, |
|||
type: 'input' | 'select', |
|||
componentProps: Recordable<any> = {}, |
|||
) => { |
|||
return defineComponent({ |
|||
name: component.name, |
|||
inheritAttrs: false, |
|||
setup: (props: any, { attrs, expose, slots }) => { |
|||
const placeholder = |
|||
props?.placeholder || |
|||
attrs?.placeholder || |
|||
$t(`ui.placeholder.${type}`); |
|||
// 透传组件暴露的方法
|
|||
const innerRef = ref(); |
|||
expose( |
|||
new Proxy( |
|||
{}, |
|||
{ |
|||
get: (_target, key) => innerRef.value?.[key], |
|||
has: (_target, key) => key in (innerRef.value || {}), |
|||
}, |
|||
), |
|||
); |
|||
return () => |
|||
h( |
|||
component, |
|||
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, |
|||
slots, |
|||
); |
|||
}, |
|||
}); |
|||
}; |
|||
|
|||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
|||
export type ComponentType = |
|||
| 'ApiSelect' |
|||
| 'ApiTreeSelect' |
|||
| 'AutoComplete' |
|||
| 'Checkbox' |
|||
| 'CheckboxGroup' |
|||
| 'DatePicker' |
|||
| 'DefaultButton' |
|||
| 'Divider' |
|||
| 'IconPicker' |
|||
| 'Input' |
|||
| 'InputNumber' |
|||
// | 'InputPassword'
|
|||
// | 'Mentions'
|
|||
| 'PrimaryButton' |
|||
| 'Radio' |
|||
| 'RadioGroup' |
|||
| 'RangePicker' |
|||
| 'Rate' |
|||
| 'Select' |
|||
| 'Space' |
|||
| 'Switch' |
|||
| 'Textarea' |
|||
| 'TimePicker' |
|||
| 'TreeSelect' |
|||
| 'Upload' |
|||
| BaseFormComponentType; |
|||
|
|||
async function initComponentAdapter() { |
|||
const components: Partial<Record<ComponentType, Component>> = { |
|||
// 如果你的组件体积比较大,可以使用异步加载
|
|||
// Button: () =>
|
|||
// import('xxx').then((res) => res.Button),
|
|||
ApiSelect: withDefaultPlaceholder( |
|||
{ |
|||
...ApiComponent, |
|||
name: 'ApiSelect', |
|||
}, |
|||
'select', |
|||
{ |
|||
component: Select, |
|||
loadingSlot: 'suffixIcon', |
|||
visibleEvent: 'onDropdownVisibleChange', |
|||
modelPropName: 'value', |
|||
}, |
|||
), |
|||
ApiTreeSelect: withDefaultPlaceholder( |
|||
{ |
|||
...ApiComponent, |
|||
name: 'ApiTreeSelect', |
|||
}, |
|||
'select', |
|||
{ |
|||
component: TreeSelect, |
|||
fieldNames: { label: 'label', value: 'value', children: 'children' }, |
|||
loadingSlot: 'suffixIcon', |
|||
modelPropName: 'value', |
|||
optionsPropName: 'treeData', |
|||
visibleEvent: 'onVisibleChange', |
|||
}, |
|||
), |
|||
AutoComplete, |
|||
Checkbox, |
|||
CheckboxGroup, |
|||
DatePicker, |
|||
// 自定义默认按钮
|
|||
DefaultButton: (props, { attrs, slots }) => { |
|||
return h(Button, { ...props, attrs, theme: 'default' }, slots); |
|||
}, |
|||
Divider, |
|||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', { |
|||
iconSlot: 'addonAfter', |
|||
inputComponent: Input, |
|||
modelValueProp: 'value', |
|||
}), |
|||
Input: withDefaultPlaceholder(Input, 'input'), |
|||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'), |
|||
// InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
|||
// Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
|||
// 自定义主要按钮
|
|||
PrimaryButton: (props, { attrs, slots }) => { |
|||
return h(Button, { ...props, attrs, theme: 'primary' }, slots); |
|||
}, |
|||
Radio, |
|||
RadioGroup, |
|||
RangePicker, |
|||
Rate, |
|||
Select: withDefaultPlaceholder(Select, 'select'), |
|||
Space, |
|||
Switch, |
|||
Textarea: withDefaultPlaceholder(Textarea, 'input'), |
|||
TimePicker, |
|||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), |
|||
Upload, |
|||
}; |
|||
|
|||
// 将组件注册到全局共享状态中
|
|||
globalShareState.setComponents(components); |
|||
|
|||
// 定义全局共享状态中的消息提示
|
|||
globalShareState.defineMessage({ |
|||
// 复制成功消息提示
|
|||
copyPreferencesSuccess: (title, content) => { |
|||
notification.success({ |
|||
description: content, |
|||
message: title, |
|||
placement: 'bottomRight', |
|||
}); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
export { initComponentAdapter }; |
|||
@ -0,0 +1,49 @@ |
|||
import type { |
|||
VbenFormSchema as FormSchema, |
|||
VbenFormProps, |
|||
} from '@vben/common-ui'; |
|||
|
|||
import type { ComponentType } from './component'; |
|||
|
|||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
async function initSetupVbenForm() { |
|||
setupVbenForm<ComponentType>({ |
|||
config: { |
|||
// ant design vue组件库默认都是 v-model:value
|
|||
baseModelPropName: 'value', |
|||
|
|||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
|||
modelPropNameMap: { |
|||
Checkbox: 'checked', |
|||
Radio: 'checked', |
|||
Switch: 'checked', |
|||
Upload: 'fileList', |
|||
}, |
|||
}, |
|||
defineRules: { |
|||
// 输入项目必填国际化适配
|
|||
required: (value, _params, ctx) => { |
|||
if (value === undefined || value === null || value.length === 0) { |
|||
return $t('ui.formRules.required', [ctx.label]); |
|||
} |
|||
return true; |
|||
}, |
|||
// 选择项目必填国际化适配
|
|||
selectRequired: (value, _params, ctx) => { |
|||
if (value === undefined || value === null) { |
|||
return $t('ui.formRules.selectRequired', [ctx.label]); |
|||
} |
|||
return true; |
|||
}, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
const useVbenForm = useForm<ComponentType>; |
|||
|
|||
export { initSetupVbenForm, useVbenForm, z }; |
|||
|
|||
export type VbenFormSchema = FormSchema<ComponentType>; |
|||
export type { VbenFormProps }; |
|||
@ -0,0 +1,69 @@ |
|||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; |
|||
|
|||
import { h } from 'vue'; |
|||
|
|||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; |
|||
|
|||
import { Button, Image } from 'ant-design-vue'; |
|||
|
|||
import { useVbenForm } from './form'; |
|||
|
|||
setupVbenVxeTable({ |
|||
configVxeTable: (vxeUI) => { |
|||
vxeUI.setConfig({ |
|||
grid: { |
|||
align: 'center', |
|||
border: false, |
|||
columnConfig: { |
|||
resizable: true, |
|||
}, |
|||
minHeight: 180, |
|||
formConfig: { |
|||
// 全局禁用vxe-table的表单配置,使用formOptions
|
|||
enabled: false, |
|||
}, |
|||
proxyConfig: { |
|||
autoLoad: true, |
|||
response: { |
|||
result: 'items', |
|||
total: 'total', |
|||
list: 'items', |
|||
}, |
|||
showActiveMsg: true, |
|||
showResponseMsg: false, |
|||
}, |
|||
round: true, |
|||
showOverflow: true, |
|||
size: 'small', |
|||
} as VxeTableGridOptions, |
|||
}); |
|||
|
|||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
|||
vxeUI.renderer.add('CellImage', { |
|||
renderTableDefault(_renderOpts, params) { |
|||
const { column, row } = params; |
|||
return h(Image, { src: row[column.field] }); |
|||
}, |
|||
}); |
|||
|
|||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
|||
vxeUI.renderer.add('CellLink', { |
|||
renderTableDefault(renderOpts) { |
|||
const { props } = renderOpts; |
|||
return h( |
|||
Button, |
|||
{ size: 'small', type: 'link' }, |
|||
{ default: () => props?.text }, |
|||
); |
|||
}, |
|||
}); |
|||
|
|||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
|||
// vxeUI.formats.add
|
|||
}, |
|||
useVbenForm, |
|||
}); |
|||
|
|||
export { useVbenVxeGrid }; |
|||
|
|||
export type * from '@vben/plugins/vxe-table'; |
|||
@ -0,0 +1,51 @@ |
|||
import { baseRequestClient, requestClient } from '#/api/request'; |
|||
|
|||
export namespace AuthApi { |
|||
/** 登录接口参数 */ |
|||
export interface LoginParams { |
|||
password?: string; |
|||
username?: string; |
|||
} |
|||
|
|||
/** 登录接口返回值 */ |
|||
export interface LoginResult { |
|||
accessToken: string; |
|||
} |
|||
|
|||
export interface RefreshTokenResult { |
|||
data: string; |
|||
status: number; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 登录 |
|||
*/ |
|||
export async function loginApi(data: AuthApi.LoginParams) { |
|||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data); |
|||
} |
|||
|
|||
/** |
|||
* 刷新accessToken |
|||
*/ |
|||
export async function refreshTokenApi() { |
|||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', { |
|||
withCredentials: true, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 退出登录 |
|||
*/ |
|||
export async function logoutApi() { |
|||
return baseRequestClient.post('/auth/logout', { |
|||
withCredentials: true, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取用户权限码 |
|||
*/ |
|||
export async function getAccessCodesApi() { |
|||
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 getAllMenusApi() { |
|||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all'); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
import type { UserInfo } from '@vben/types'; |
|||
|
|||
import { requestClient } from '#/api/request'; |
|||
|
|||
/** |
|||
* 获取用户信息 |
|||
*/ |
|||
export async function getUserInfoApi() { |
|||
return requestClient.get<UserInfo>('/user/info'); |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './core'; |
|||
@ -0,0 +1,113 @@ |
|||
/** |
|||
* 该文件可自行根据业务逻辑进行调整 |
|||
*/ |
|||
import type { RequestClientOptions } from '@vben/request'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { |
|||
authenticateResponseInterceptor, |
|||
defaultResponseInterceptor, |
|||
errorMessageResponseInterceptor, |
|||
RequestClient, |
|||
} from '@vben/request'; |
|||
import { useAccessStore } from '@vben/stores'; |
|||
|
|||
import { message } from 'ant-design-vue'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
import { refreshTokenApi } from './core'; |
|||
|
|||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
function createRequestClient(baseURL: string, options?: RequestClientOptions) { |
|||
const client = new RequestClient({ |
|||
...options, |
|||
baseURL, |
|||
}); |
|||
|
|||
/** |
|||
* 重新认证逻辑 |
|||
*/ |
|||
async function doReAuthenticate() { |
|||
console.warn('Access token or refresh token is invalid or expired. '); |
|||
const accessStore = useAccessStore(); |
|||
const authStore = useAuthStore(); |
|||
accessStore.setAccessToken(null); |
|||
if ( |
|||
preferences.app.loginExpiredMode === 'modal' && |
|||
accessStore.isAccessChecked |
|||
) { |
|||
accessStore.setLoginExpired(true); |
|||
} else { |
|||
await authStore.logout(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 刷新token逻辑 |
|||
*/ |
|||
async function doRefreshToken() { |
|||
const accessStore = useAccessStore(); |
|||
const resp = await refreshTokenApi(); |
|||
const newToken = resp.data; |
|||
accessStore.setAccessToken(newToken); |
|||
return newToken; |
|||
} |
|||
|
|||
function formatToken(token: null | string) { |
|||
return token ? `Bearer ${token}` : null; |
|||
} |
|||
|
|||
// 请求头处理
|
|||
client.addRequestInterceptor({ |
|||
fulfilled: async (config) => { |
|||
const accessStore = useAccessStore(); |
|||
|
|||
config.headers.Authorization = formatToken(accessStore.accessToken); |
|||
config.headers['Accept-Language'] = preferences.app.locale; |
|||
return config; |
|||
}, |
|||
}); |
|||
|
|||
// 处理返回的响应数据格式
|
|||
client.addResponseInterceptor( |
|||
defaultResponseInterceptor({ |
|||
codeField: 'code', |
|||
dataField: 'data', |
|||
successCode: 0, |
|||
}), |
|||
); |
|||
|
|||
// token过期的处理
|
|||
client.addResponseInterceptor( |
|||
authenticateResponseInterceptor({ |
|||
client, |
|||
doReAuthenticate, |
|||
doRefreshToken, |
|||
enableRefreshToken: preferences.app.enableRefreshToken, |
|||
formatToken, |
|||
}), |
|||
); |
|||
|
|||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
|||
client.addResponseInterceptor( |
|||
errorMessageResponseInterceptor((msg: string, error) => { |
|||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
|||
// 当前mock接口返回的错误字段是 error 或者 message
|
|||
const responseData = error?.response?.data ?? {}; |
|||
const errorMessage = responseData?.error ?? responseData?.message ?? ''; |
|||
// 如果没有错误信息,则会根据状态码进行提示
|
|||
message.error(errorMessage || msg); |
|||
}), |
|||
); |
|||
|
|||
return client; |
|||
} |
|||
|
|||
export const requestClient = createRequestClient(apiURL, { |
|||
responseReturn: 'data', |
|||
}); |
|||
|
|||
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); |
|||
@ -0,0 +1,39 @@ |
|||
<script lang="ts" setup> |
|||
import { computed } from 'vue'; |
|||
|
|||
import { useAntdDesignTokens } from '@vben/hooks'; |
|||
import { preferences, usePreferences } from '@vben/preferences'; |
|||
|
|||
import { App, ConfigProvider, theme } from 'ant-design-vue'; |
|||
|
|||
import { antdLocale } from '#/locales'; |
|||
|
|||
defineOptions({ name: 'App' }); |
|||
|
|||
const { isDark } = usePreferences(); |
|||
const { tokens } = useAntdDesignTokens(); |
|||
|
|||
const tokenTheme = computed(() => { |
|||
const algorithm = isDark.value |
|||
? [theme.darkAlgorithm] |
|||
: [theme.defaultAlgorithm]; |
|||
|
|||
// antd 紧凑模式算法 |
|||
if (preferences.app.compact) { |
|||
algorithm.push(theme.compactAlgorithm); |
|||
} |
|||
|
|||
return { |
|||
algorithm, |
|||
token: tokens, |
|||
}; |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme"> |
|||
<App> |
|||
<RouterView /> |
|||
</App> |
|||
</ConfigProvider> |
|||
</template> |
|||
@ -0,0 +1,78 @@ |
|||
import { createApp, watchEffect } from 'vue'; |
|||
|
|||
import { registerAccessDirective } from '@vben/access'; |
|||
import { registerLoadingDirective } from '@vben/common-ui/es/loading'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { initStores } from '@vben/stores'; |
|||
import '@vben/styles'; |
|||
import '@vben/styles/antd'; |
|||
|
|||
import { useTitle } from '@vueuse/core'; |
|||
|
|||
import { $t, setupI18n } from '#/locales'; |
|||
|
|||
import { initComponentAdapter } from './adapter/component'; |
|||
import { initSetupVbenForm } from './adapter/form'; |
|||
import App from './app.vue'; |
|||
import { router } from './router'; |
|||
|
|||
// 引入组件库的少量全局样式变量
|
|||
|
|||
async function bootstrap(namespace: string) { |
|||
// 初始化组件适配器
|
|||
await initComponentAdapter(); |
|||
|
|||
// 初始化表单组件
|
|||
await initSetupVbenForm(); |
|||
|
|||
// // 设置弹窗的默认配置
|
|||
// setDefaultModalProps({
|
|||
// fullscreenButton: false,
|
|||
// });
|
|||
// // 设置抽屉的默认配置
|
|||
// setDefaultDrawerProps({
|
|||
// zIndex: 1020,
|
|||
// });
|
|||
|
|||
const app = createApp(App); |
|||
|
|||
// 注册v-loading指令
|
|||
registerLoadingDirective(app, { |
|||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
|||
spinning: 'spinning', |
|||
}); |
|||
|
|||
// 国际化 i18n 配置
|
|||
await setupI18n(app); |
|||
|
|||
// 配置 pinia-tore
|
|||
await initStores(app, { namespace }); |
|||
|
|||
// 安装权限指令
|
|||
registerAccessDirective(app); |
|||
|
|||
// 初始化 tippy
|
|||
const { initTippy } = await import('@vben/common-ui/es/tippy'); |
|||
initTippy(app); |
|||
|
|||
// 配置路由及路由守卫
|
|||
app.use(router); |
|||
|
|||
// 配置Motion插件
|
|||
const { MotionPlugin } = await import('@vben/plugins/motion'); |
|||
app.use(MotionPlugin); |
|||
|
|||
// 动态更新标题
|
|||
watchEffect(() => { |
|||
if (preferences.app.dynamicTitle) { |
|||
const routeTitle = router.currentRoute.value.meta?.title; |
|||
const pageTitle = |
|||
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name; |
|||
useTitle(pageTitle); |
|||
} |
|||
}); |
|||
|
|||
app.mount('#app'); |
|||
} |
|||
|
|||
export { bootstrap }; |
|||
@ -0,0 +1,23 @@ |
|||
<script lang="ts" setup> |
|||
import { computed } from 'vue'; |
|||
|
|||
import { AuthPageLayout } from '@vben/layouts'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { $t } from '#/locales'; |
|||
|
|||
const appName = computed(() => preferences.app.name); |
|||
const logo = computed(() => preferences.logo.source); |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthPageLayout |
|||
:app-name="appName" |
|||
:logo="logo" |
|||
:page-description="$t('authentication.pageDesc')" |
|||
:page-title="$t('authentication.pageTitle')" |
|||
> |
|||
<!-- 自定义工具栏 --> |
|||
<!-- <template #toolbar></template> --> |
|||
</AuthPageLayout> |
|||
</template> |
|||
@ -0,0 +1,162 @@ |
|||
<script lang="ts" setup> |
|||
import type { NotificationItem } from '@vben/layouts'; |
|||
|
|||
import { computed, ref, watch } from 'vue'; |
|||
|
|||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; |
|||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; |
|||
import { useWatermark } from '@vben/hooks'; |
|||
import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons'; |
|||
import { |
|||
BasicLayout, |
|||
LockScreen, |
|||
Notification, |
|||
UserDropdown, |
|||
} from '@vben/layouts'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useAccessStore, useUserStore } from '@vben/stores'; |
|||
import { openWindow } from '@vben/utils'; |
|||
|
|||
import { $t } from '#/locales'; |
|||
import { useAuthStore } from '#/store'; |
|||
import LoginForm from '#/views/_core/authentication/login.vue'; |
|||
|
|||
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 { destroyWatermark, updateWatermark } = useWatermark(); |
|||
const showDot = computed(() => |
|||
notifications.value.some((item) => !item.isRead), |
|||
); |
|||
|
|||
const menus = computed(() => [ |
|||
{ |
|||
handler: () => { |
|||
openWindow(VBEN_DOC_URL, { |
|||
target: '_blank', |
|||
}); |
|||
}, |
|||
icon: BookOpenText, |
|||
text: $t('ui.widgets.document'), |
|||
}, |
|||
{ |
|||
handler: () => { |
|||
openWindow(VBEN_GITHUB_URL, { |
|||
target: '_blank', |
|||
}); |
|||
}, |
|||
icon: SvgGithubIcon, |
|||
text: 'GitHub', |
|||
}, |
|||
{ |
|||
handler: () => { |
|||
openWindow(`${VBEN_GITHUB_URL}/issues`, { |
|||
target: '_blank', |
|||
}); |
|||
}, |
|||
icon: CircleHelp, |
|||
text: $t('ui.widgets.qa'), |
|||
}, |
|||
]); |
|||
|
|||
const avatar = computed(() => { |
|||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; |
|||
}); |
|||
|
|||
async function handleLogout() { |
|||
await authStore.logout(false); |
|||
} |
|||
|
|||
function handleNoticeClear() { |
|||
notifications.value = []; |
|||
} |
|||
|
|||
function handleMakeAll() { |
|||
notifications.value.forEach((item) => (item.isRead = true)); |
|||
} |
|||
watch( |
|||
() => ({ |
|||
enable: preferences.app.watermark, |
|||
content: preferences.app.watermarkContent, |
|||
}), |
|||
async ({ enable, content }) => { |
|||
if (enable) { |
|||
await updateWatermark({ |
|||
content: |
|||
content || |
|||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, |
|||
}); |
|||
} else { |
|||
destroyWatermark(); |
|||
} |
|||
}, |
|||
{ |
|||
immediate: 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 |
|||
> |
|||
<LoginForm /> |
|||
</AuthenticationLoginExpiredModal> |
|||
</template> |
|||
<template #lock-screen> |
|||
<LockScreen :avatar @to-login="handleLogout" /> |
|||
</template> |
|||
</BasicLayout> |
|||
</template> |
|||
@ -0,0 +1,6 @@ |
|||
const BasicLayout = () => import('./basic.vue'); |
|||
const AuthPageLayout = () => import('./auth.vue'); |
|||
|
|||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView); |
|||
|
|||
export { AuthPageLayout, BasicLayout, IFrameView }; |
|||
@ -0,0 +1,3 @@ |
|||
# locale |
|||
|
|||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。 |
|||
@ -0,0 +1,102 @@ |
|||
import type { Locale } from 'ant-design-vue/es/locale'; |
|||
|
|||
import type { App } from 'vue'; |
|||
|
|||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales'; |
|||
|
|||
import { ref } from 'vue'; |
|||
|
|||
import { |
|||
$t, |
|||
setupI18n as coreSetup, |
|||
loadLocalesMapFromDir, |
|||
} from '@vben/locales'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import antdEnLocale from 'ant-design-vue/es/locale/en_US'; |
|||
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN'; |
|||
import dayjs from 'dayjs'; |
|||
|
|||
const antdLocale = ref<Locale>(antdDefaultLocale); |
|||
|
|||
const modules = import.meta.glob('./langs/**/*.json'); |
|||
|
|||
const localesMap = loadLocalesMapFromDir( |
|||
/\.\/langs\/([^/]+)\/(.*)\.json$/, |
|||
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([loadAntdLocale(lang), loadDayjsLocale(lang)]); |
|||
} |
|||
|
|||
/** |
|||
* 加载dayjs的语言包 |
|||
* @param lang |
|||
*/ |
|||
async function loadDayjsLocale(lang: SupportedLanguagesType) { |
|||
let locale; |
|||
switch (lang) { |
|||
case 'en-US': { |
|||
locale = await import('dayjs/locale/en'); |
|||
break; |
|||
} |
|||
case 'zh-CN': { |
|||
locale = await import('dayjs/locale/zh-cn'); |
|||
break; |
|||
} |
|||
// 默认使用英语
|
|||
default: { |
|||
locale = await import('dayjs/locale/en'); |
|||
} |
|||
} |
|||
if (locale) { |
|||
dayjs.locale(locale); |
|||
} else { |
|||
console.error(`Failed to load dayjs locale for ${lang}`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 加载antd的语言包 |
|||
* @param lang |
|||
*/ |
|||
async function loadAntdLocale(lang: SupportedLanguagesType) { |
|||
switch (lang) { |
|||
case 'en-US': { |
|||
antdLocale.value = antdEnLocale; |
|||
break; |
|||
} |
|||
case 'zh-CN': { |
|||
antdLocale.value = antdDefaultLocale; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) { |
|||
await coreSetup(app, { |
|||
defaultLocale: preferences.app.locale, |
|||
loadMessages, |
|||
missingWarn: !import.meta.env.PROD, |
|||
...options, |
|||
}); |
|||
} |
|||
|
|||
export { $t, antdLocale, setupI18n }; |
|||
@ -0,0 +1,12 @@ |
|||
{ |
|||
"title": "Demos", |
|||
"antd": "Ant Design Vue", |
|||
"vben": { |
|||
"title": "Project", |
|||
"about": "About", |
|||
"document": "Document", |
|||
"antdv": "Ant Design Vue Version", |
|||
"naive-ui": "Naive UI Version", |
|||
"element-plus": "Element Plus Version" |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"auth": { |
|||
"login": "Login", |
|||
"register": "Register", |
|||
"codeLogin": "Code Login", |
|||
"qrcodeLogin": "Qr Code Login", |
|||
"forgetPassword": "Forget Password" |
|||
}, |
|||
"dashboard": { |
|||
"title": "Dashboard", |
|||
"analytics": "Analytics", |
|||
"workspace": "Workspace" |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
{ |
|||
"title": "演示", |
|||
"antd": "Ant Design Vue", |
|||
"vben": { |
|||
"title": "项目", |
|||
"about": "关于", |
|||
"document": "文档", |
|||
"antdv": "Ant Design Vue 版本", |
|||
"naive-ui": "Naive UI 版本", |
|||
"element-plus": "Element Plus 版本" |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"auth": { |
|||
"login": "登录", |
|||
"register": "注册", |
|||
"codeLogin": "验证码登录", |
|||
"qrcodeLogin": "二维码登录", |
|||
"forgetPassword": "忘记密码" |
|||
}, |
|||
"dashboard": { |
|||
"title": "概览", |
|||
"analytics": "分析页", |
|||
"workspace": "工作台" |
|||
} |
|||
} |
|||
@ -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,13 @@ |
|||
import { defineOverridesPreferences } from '@vben/preferences'; |
|||
|
|||
/** |
|||
* @description 项目配置文件 |
|||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置 |
|||
* !!! 更改配置后请清空缓存,否则可能不生效 |
|||
*/ |
|||
export const overridesPreferences = defineOverridesPreferences({ |
|||
// overrides
|
|||
app: { |
|||
name: import.meta.env.VITE_APP_TITLE, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,42 @@ |
|||
import type { |
|||
ComponentRecordType, |
|||
GenerateMenuAndRoutesOptions, |
|||
} from '@vben/types'; |
|||
|
|||
import { generateAccessible } from '@vben/access'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { message } from 'ant-design-vue'; |
|||
|
|||
import { getAllMenusApi } 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 () => { |
|||
message.loading({ |
|||
content: `${$t('common.loadingMenu')}...`, |
|||
duration: 1.5, |
|||
}); |
|||
return await getAllMenusApi(); |
|||
}, |
|||
// 可以指定没有权限跳转403页面
|
|||
forbiddenComponent, |
|||
// 如果 route.meta.menuVisibleWithForbidden = true
|
|||
layoutMap, |
|||
pageMap, |
|||
}); |
|||
} |
|||
|
|||
export { generateAccess }; |
|||
@ -0,0 +1,133 @@ |
|||
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 { accessRoutes, coreRouteNames } from '#/router/routes'; |
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
import { generateAccess } from './access'; |
|||
|
|||
/** |
|||
* 通用守卫配置 |
|||
* @param router |
|||
*/ |
|||
function setupCommonGuard(router: Router) { |
|||
// 记录已经加载的页面
|
|||
const loadedPaths = new Set<string>(); |
|||
|
|||
router.beforeEach((to) => { |
|||
to.meta.loaded = loadedPaths.has(to.path); |
|||
|
|||
// 页面加载进度条
|
|||
if (!to.meta.loaded && preferences.transition.progress) { |
|||
startProgress(); |
|||
} |
|||
return true; |
|||
}); |
|||
|
|||
router.afterEach((to) => { |
|||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
|||
|
|||
loadedPaths.add(to.path); |
|||
|
|||
// 关闭页面加载进度条
|
|||
if (preferences.transition.progress) { |
|||
stopProgress(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 权限访问守卫配置 |
|||
* @param router |
|||
*/ |
|||
function setupAccessGuard(router: Router) { |
|||
router.beforeEach(async (to, from) => { |
|||
const accessStore = useAccessStore(); |
|||
const userStore = useUserStore(); |
|||
const authStore = useAuthStore(); |
|||
|
|||
// 基本路由,这些路由不需要进入权限拦截
|
|||
if (coreRouteNames.includes(to.name as string)) { |
|||
if (to.path === LOGIN_PATH && accessStore.accessToken) { |
|||
return decodeURIComponent( |
|||
(to.query?.redirect as string) || |
|||
userStore.userInfo?.homePath || |
|||
preferences.app.defaultHomePath, |
|||
); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
// accessToken 检查
|
|||
if (!accessStore.accessToken) { |
|||
// 明确声明忽略权限访问权限,则可以访问
|
|||
if (to.meta.ignoreAccess) { |
|||
return true; |
|||
} |
|||
|
|||
// 没有访问权限,跳转登录页面
|
|||
if (to.fullPath !== LOGIN_PATH) { |
|||
return { |
|||
path: LOGIN_PATH, |
|||
// 如不需要,直接删除 query
|
|||
query: |
|||
to.fullPath === preferences.app.defaultHomePath |
|||
? {} |
|||
: { redirect: encodeURIComponent(to.fullPath) }, |
|||
// 携带当前跳转的页面,登录后重新跳转该页面
|
|||
replace: true, |
|||
}; |
|||
} |
|||
return to; |
|||
} |
|||
|
|||
// 是否已经生成过动态路由
|
|||
if (accessStore.isAccessChecked) { |
|||
return true; |
|||
} |
|||
|
|||
// 生成路由表
|
|||
// 当前登录用户拥有的角色标识列表
|
|||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); |
|||
const userRoles = userInfo.roles ?? []; |
|||
|
|||
// 生成菜单和路由
|
|||
const { accessibleMenus, accessibleRoutes } = await generateAccess({ |
|||
roles: userRoles, |
|||
router, |
|||
// 则会在菜单中显示,但是访问会被重定向到403
|
|||
routes: accessRoutes, |
|||
}); |
|||
|
|||
// 保存菜单信息和路由信息
|
|||
accessStore.setAccessMenus(accessibleMenus); |
|||
accessStore.setAccessRoutes(accessibleRoutes); |
|||
accessStore.setIsAccessChecked(true); |
|||
const redirectPath = (from.query.redirect ?? |
|||
(to.path === preferences.app.defaultHomePath |
|||
? userInfo.homePath || preferences.app.defaultHomePath |
|||
: to.fullPath)) as string; |
|||
|
|||
return { |
|||
...router.resolve(decodeURIComponent(redirectPath)), |
|||
replace: true, |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 项目守卫配置 |
|||
* @param router |
|||
*/ |
|||
function createRouterGuard(router: Router) { |
|||
/** 通用 */ |
|||
setupCommonGuard(router); |
|||
/** 权限访问 */ |
|||
setupAccessGuard(router); |
|||
} |
|||
|
|||
export { createRouterGuard }; |
|||
@ -0,0 +1,37 @@ |
|||
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: (to, _from, savedPosition) => { |
|||
if (savedPosition) { |
|||
return savedPosition; |
|||
} |
|||
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 }; |
|||
}, |
|||
// 是否应该禁止尾部斜杠。
|
|||
// strict: true,
|
|||
}); |
|||
|
|||
const resetRoutes = () => resetStaticRoutes(router, routes); |
|||
|
|||
// 创建路由守卫
|
|||
createRouterGuard(router); |
|||
|
|||
export { resetRoutes, router }; |
|||
@ -0,0 +1,97 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { $t } from '#/locales'; |
|||
|
|||
const BasicLayout = () => import('#/layouts/basic.vue'); |
|||
const AuthPageLayout = () => import('#/layouts/auth.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[] = [ |
|||
/** |
|||
* 根路由 |
|||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。 |
|||
* 此路由必须存在,且不应修改 |
|||
*/ |
|||
{ |
|||
component: BasicLayout, |
|||
meta: { |
|||
hideInBreadcrumb: true, |
|||
title: 'Root', |
|||
}, |
|||
name: 'Root', |
|||
path: '/', |
|||
redirect: preferences.app.defaultHomePath, |
|||
children: [], |
|||
}, |
|||
{ |
|||
component: AuthPageLayout, |
|||
meta: { |
|||
hideInTab: true, |
|||
title: 'Authentication', |
|||
}, |
|||
name: 'Authentication', |
|||
path: '/auth', |
|||
redirect: LOGIN_PATH, |
|||
children: [ |
|||
{ |
|||
name: 'Login', |
|||
path: 'login', |
|||
component: () => import('#/views/_core/authentication/login.vue'), |
|||
meta: { |
|||
title: $t('page.auth.login'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'CodeLogin', |
|||
path: 'code-login', |
|||
component: () => import('#/views/_core/authentication/code-login.vue'), |
|||
meta: { |
|||
title: $t('page.auth.codeLogin'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'QrCodeLogin', |
|||
path: 'qrcode-login', |
|||
component: () => |
|||
import('#/views/_core/authentication/qrcode-login.vue'), |
|||
meta: { |
|||
title: $t('page.auth.qrcodeLogin'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'ForgetPassword', |
|||
path: 'forget-password', |
|||
component: () => |
|||
import('#/views/_core/authentication/forget-password.vue'), |
|||
meta: { |
|||
title: $t('page.auth.forgetPassword'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'Register', |
|||
path: 'register', |
|||
component: () => import('#/views/_core/authentication/register.vue'), |
|||
meta: { |
|||
title: $t('page.auth.register'), |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export { coreRoutes, fallbackNotFoundRoute }; |
|||
@ -0,0 +1,37 @@ |
|||
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 externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
|||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
|||
|
|||
/** 动态路由 */ |
|||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); |
|||
|
|||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */ |
|||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
|||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
|||
const staticRoutes: RouteRecordRaw[] = []; |
|||
const externalRoutes: RouteRecordRaw[] = []; |
|||
|
|||
/** 路由列表,由基本路由、外部路由和404兜底路由组成 |
|||
* 无需走权限验证(会一直显示在菜单中) */ |
|||
const routes: RouteRecordRaw[] = [ |
|||
...coreRoutes, |
|||
...externalRoutes, |
|||
fallbackNotFoundRoute, |
|||
]; |
|||
|
|||
/** 基本路由列表,这些路由不需要进入权限拦截 */ |
|||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name); |
|||
|
|||
/** 有权限校验的路由列表,包含动态路由和静态路由 */ |
|||
const accessRoutes = [...dynamicRoutes, ...staticRoutes]; |
|||
export { accessRoutes, coreRouteNames, routes }; |
|||
@ -0,0 +1,38 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { $t } from '#/locales'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
meta: { |
|||
icon: 'lucide:layout-dashboard', |
|||
order: -1, |
|||
title: $t('page.dashboard.title'), |
|||
}, |
|||
name: 'Dashboard', |
|||
path: '/dashboard', |
|||
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: { |
|||
icon: 'carbon:workspace', |
|||
title: $t('page.dashboard.workspace'), |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -0,0 +1,28 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { $t } from '#/locales'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
meta: { |
|||
icon: 'ic:baseline-view-in-ar', |
|||
keepAlive: true, |
|||
order: 1000, |
|||
title: $t('demos.title'), |
|||
}, |
|||
name: 'Demos', |
|||
path: '/demos', |
|||
children: [ |
|||
{ |
|||
meta: { |
|||
title: $t('demos.antd'), |
|||
}, |
|||
name: 'AntDesignDemos', |
|||
path: '/demos/ant-design', |
|||
component: () => import('#/views/demos/antd/index.vue'), |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -0,0 +1,81 @@ |
|||
import type { RouteRecordRaw } from 'vue-router'; |
|||
|
|||
import { |
|||
VBEN_DOC_URL, |
|||
VBEN_ELE_PREVIEW_URL, |
|||
VBEN_GITHUB_URL, |
|||
VBEN_LOGO_URL, |
|||
VBEN_NAIVE_PREVIEW_URL, |
|||
} from '@vben/constants'; |
|||
|
|||
import { IFrameView } from '#/layouts'; |
|||
import { $t } from '#/locales'; |
|||
|
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
meta: { |
|||
badgeType: 'dot', |
|||
icon: VBEN_LOGO_URL, |
|||
order: 9998, |
|||
title: $t('demos.vben.title'), |
|||
}, |
|||
name: 'VbenProject', |
|||
path: '/vben-admin', |
|||
children: [ |
|||
{ |
|||
name: 'VbenDocument', |
|||
path: '/vben-admin/document', |
|||
component: IFrameView, |
|||
meta: { |
|||
icon: 'lucide:book-open-text', |
|||
link: VBEN_DOC_URL, |
|||
title: $t('demos.vben.document'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'VbenGithub', |
|||
path: '/vben-admin/github', |
|||
component: IFrameView, |
|||
meta: { |
|||
icon: 'mdi:github', |
|||
link: VBEN_GITHUB_URL, |
|||
title: 'Github', |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'VbenNaive', |
|||
path: '/vben-admin/naive', |
|||
component: IFrameView, |
|||
meta: { |
|||
badgeType: 'dot', |
|||
icon: 'logos:naiveui', |
|||
link: VBEN_NAIVE_PREVIEW_URL, |
|||
title: $t('demos.vben.naive-ui'), |
|||
}, |
|||
}, |
|||
{ |
|||
name: 'VbenElementPlus', |
|||
path: '/vben-admin/ele', |
|||
component: IFrameView, |
|||
meta: { |
|||
badgeType: 'dot', |
|||
icon: 'logos:element', |
|||
link: VBEN_ELE_PREVIEW_URL, |
|||
title: $t('demos.vben.element-plus'), |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
name: 'VbenAbout', |
|||
path: '/vben-admin/about', |
|||
component: () => import('#/views/_core/about/index.vue'), |
|||
meta: { |
|||
icon: 'lucide:copyright', |
|||
title: $t('demos.vben.about'), |
|||
order: 9999, |
|||
}, |
|||
}, |
|||
]; |
|||
|
|||
export default routes; |
|||
@ -0,0 +1,117 @@ |
|||
import type { Recordable, UserInfo } from '@vben/types'; |
|||
|
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; |
|||
|
|||
import { notification } from 'ant-design-vue'; |
|||
import { defineStore } from 'pinia'; |
|||
|
|||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } 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: Recordable<any>, |
|||
onSuccess?: () => Promise<void> | void, |
|||
) { |
|||
// 异步处理用户登录操作并获取 accessToken
|
|||
const userInfo: null | UserInfo = null; |
|||
try { |
|||
loginLoading.value = true; |
|||
const { accessToken } = await loginApi(params); |
|||
|
|||
// 如果成功获取到 accessToken
|
|||
if (accessToken) { |
|||
accessStore.setAccessToken(accessToken); |
|||
// 获取用户信息并存储到 accessStore 中
|
|||
const [fetchUserInfoResult, accessCodes] = await Promise.all([ |
|||
fetchUserInfo(), |
|||
getAccessCodesApi(), |
|||
]); |
|||
|
|||
userInfo = fetchUserInfoResult; |
|||
|
|||
userStore.setUserInfo(userInfo); |
|||
accessStore.setAccessCodes(accessCodes); |
|||
|
|||
if (accessStore.loginExpired) { |
|||
accessStore.setLoginExpired(false); |
|||
} else { |
|||
onSuccess |
|||
? await onSuccess?.() |
|||
: await router.push( |
|||
userInfo.homePath || preferences.app.defaultHomePath, |
|||
); |
|||
} |
|||
|
|||
if (userInfo?.realName) { |
|||
notification.success({ |
|||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, |
|||
duration: 3, |
|||
message: $t('authentication.loginSuccess'), |
|||
}); |
|||
} |
|||
} |
|||
} finally { |
|||
loginLoading.value = false; |
|||
} |
|||
|
|||
return { |
|||
userInfo, |
|||
}; |
|||
} |
|||
|
|||
async function logout(redirect: boolean = true) { |
|||
try { |
|||
await logoutApi(); |
|||
} catch { |
|||
// 不做任何处理
|
|||
} |
|||
resetAllStores(); |
|||
accessStore.setLoginExpired(false); |
|||
|
|||
// 回登录页带上当前路由地址
|
|||
await router.replace({ |
|||
path: LOGIN_PATH, |
|||
query: redirect |
|||
? { |
|||
redirect: encodeURIComponent(router.currentRoute.value.fullPath), |
|||
} |
|||
: {}, |
|||
}); |
|||
} |
|||
|
|||
async function fetchUserInfo() { |
|||
let userInfo: null | UserInfo = null; |
|||
userInfo = await getUserInfoApi(); |
|||
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,9 @@ |
|||
<script lang="ts" setup> |
|||
import { About } from '@vben/common-ui'; |
|||
|
|||
defineOptions({ name: 'About' }); |
|||
</script> |
|||
|
|||
<template> |
|||
<About /> |
|||
</template> |
|||
@ -0,0 +1,69 @@ |
|||
<script lang="ts" setup> |
|||
import type { VbenFormSchema } from '@vben/common-ui'; |
|||
import type { Recordable } from '@vben/types'; |
|||
|
|||
import { computed, ref } from 'vue'; |
|||
|
|||
import { AuthenticationCodeLogin, z } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
defineOptions({ name: 'CodeLogin' }); |
|||
|
|||
const loading = ref(false); |
|||
const CODE_LENGTH = 6; |
|||
|
|||
const formSchema = computed((): VbenFormSchema[] => { |
|||
return [ |
|||
{ |
|||
component: 'VbenInput', |
|||
componentProps: { |
|||
placeholder: $t('authentication.mobile'), |
|||
}, |
|||
fieldName: 'phoneNumber', |
|||
label: $t('authentication.mobile'), |
|||
rules: z |
|||
.string() |
|||
.min(1, { message: $t('authentication.mobileTip') }) |
|||
.refine((v) => /^\d{11}$/.test(v), { |
|||
message: $t('authentication.mobileErrortip'), |
|||
}), |
|||
}, |
|||
{ |
|||
component: 'VbenPinInput', |
|||
componentProps: { |
|||
codeLength: CODE_LENGTH, |
|||
createText: (countdown: number) => { |
|||
const text = |
|||
countdown > 0 |
|||
? $t('authentication.sendText', [countdown]) |
|||
: $t('authentication.sendCode'); |
|||
return text; |
|||
}, |
|||
placeholder: $t('authentication.code'), |
|||
}, |
|||
fieldName: 'code', |
|||
label: $t('authentication.code'), |
|||
rules: z.string().length(CODE_LENGTH, { |
|||
message: $t('authentication.codeTip', [CODE_LENGTH]), |
|||
}), |
|||
}, |
|||
]; |
|||
}); |
|||
/** |
|||
* 异步处理登录操作 |
|||
* Asynchronously handle the login process |
|||
* @param values 登录表单数据 |
|||
*/ |
|||
async function handleLogin(values: Recordable<any>) { |
|||
// eslint-disable-next-line no-console |
|||
console.log(values); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationCodeLogin |
|||
:form-schema="formSchema" |
|||
:loading="loading" |
|||
@submit="handleLogin" |
|||
/> |
|||
</template> |
|||
@ -0,0 +1,43 @@ |
|||
<script lang="ts" setup> |
|||
import type { VbenFormSchema } from '@vben/common-ui'; |
|||
import type { Recordable } from '@vben/types'; |
|||
|
|||
import { computed, ref } from 'vue'; |
|||
|
|||
import { AuthenticationForgetPassword, z } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
defineOptions({ name: 'ForgetPassword' }); |
|||
|
|||
const loading = ref(false); |
|||
|
|||
const formSchema = computed((): VbenFormSchema[] => { |
|||
return [ |
|||
{ |
|||
component: 'VbenInput', |
|||
componentProps: { |
|||
placeholder: 'example@example.com', |
|||
}, |
|||
fieldName: 'email', |
|||
label: $t('authentication.email'), |
|||
rules: z |
|||
.string() |
|||
.min(1, { message: $t('authentication.emailTip') }) |
|||
.email($t('authentication.emailValidErrorTip')), |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
function handleSubmit(value: Recordable<any>) { |
|||
// eslint-disable-next-line no-console |
|||
console.log('reset email:', value); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationForgetPassword |
|||
:form-schema="formSchema" |
|||
:loading="loading" |
|||
@submit="handleSubmit" |
|||
/> |
|||
</template> |
|||
@ -0,0 +1,99 @@ |
|||
<script lang="ts" setup> |
|||
import type { VbenFormSchema } from '@vben/common-ui'; |
|||
import type { BasicOption } from '@vben/types'; |
|||
|
|||
import { computed, markRaw } from 'vue'; |
|||
|
|||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
defineOptions({ name: 'Login' }); |
|||
|
|||
const authStore = useAuthStore(); |
|||
|
|||
const MOCK_USER_OPTIONS: BasicOption[] = [ |
|||
{ |
|||
label: 'Super', |
|||
value: 'vben', |
|||
}, |
|||
{ |
|||
label: 'Admin', |
|||
value: 'admin', |
|||
}, |
|||
{ |
|||
label: 'User', |
|||
value: 'jack', |
|||
}, |
|||
]; |
|||
|
|||
const formSchema = computed((): VbenFormSchema[] => { |
|||
return [ |
|||
{ |
|||
component: 'VbenSelect', |
|||
componentProps: { |
|||
options: MOCK_USER_OPTIONS, |
|||
placeholder: $t('authentication.selectAccount'), |
|||
}, |
|||
fieldName: 'selectAccount', |
|||
label: $t('authentication.selectAccount'), |
|||
rules: z |
|||
.string() |
|||
.min(1, { message: $t('authentication.selectAccount') }) |
|||
.optional() |
|||
.default('vben'), |
|||
}, |
|||
{ |
|||
component: 'VbenInput', |
|||
componentProps: { |
|||
placeholder: $t('authentication.usernameTip'), |
|||
}, |
|||
dependencies: { |
|||
trigger(values, form) { |
|||
if (values.selectAccount) { |
|||
const findUser = MOCK_USER_OPTIONS.find( |
|||
(item) => item.value === values.selectAccount, |
|||
); |
|||
if (findUser) { |
|||
form.setValues({ |
|||
password: '123456', |
|||
username: findUser.value, |
|||
}); |
|||
} |
|||
} |
|||
}, |
|||
triggerFields: ['selectAccount'], |
|||
}, |
|||
fieldName: 'username', |
|||
label: $t('authentication.username'), |
|||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }), |
|||
}, |
|||
{ |
|||
component: 'VbenInputPassword', |
|||
componentProps: { |
|||
placeholder: $t('authentication.password'), |
|||
}, |
|||
fieldName: 'password', |
|||
label: $t('authentication.password'), |
|||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }), |
|||
}, |
|||
|
|||
{ |
|||
component: markRaw(SliderCaptcha), |
|||
fieldName: 'captcha', |
|||
rules: z.boolean().refine((value) => value, { |
|||
message: $t('authentication.verifyRequiredTip'), |
|||
}), |
|||
}, |
|||
]; |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationLogin |
|||
:form-schema="formSchema" |
|||
:loading="authStore.loginLoading" |
|||
@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,96 @@ |
|||
<script lang="ts" setup> |
|||
import type { VbenFormSchema } from '@vben/common-ui'; |
|||
import type { Recordable } from '@vben/types'; |
|||
|
|||
import { computed, h, ref } from 'vue'; |
|||
|
|||
import { AuthenticationRegister, z } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
defineOptions({ name: 'Register' }); |
|||
|
|||
const loading = ref(false); |
|||
|
|||
const formSchema = computed((): VbenFormSchema[] => { |
|||
return [ |
|||
{ |
|||
component: 'VbenInput', |
|||
componentProps: { |
|||
placeholder: $t('authentication.usernameTip'), |
|||
}, |
|||
fieldName: 'username', |
|||
label: $t('authentication.username'), |
|||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }), |
|||
}, |
|||
{ |
|||
component: 'VbenInputPassword', |
|||
componentProps: { |
|||
passwordStrength: true, |
|||
placeholder: $t('authentication.password'), |
|||
}, |
|||
fieldName: 'password', |
|||
label: $t('authentication.password'), |
|||
renderComponentContent() { |
|||
return { |
|||
strengthText: () => $t('authentication.passwordStrength'), |
|||
}; |
|||
}, |
|||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }), |
|||
}, |
|||
{ |
|||
component: 'VbenInputPassword', |
|||
componentProps: { |
|||
placeholder: $t('authentication.confirmPassword'), |
|||
}, |
|||
dependencies: { |
|||
rules(values) { |
|||
const { password } = values; |
|||
return z |
|||
.string({ required_error: $t('authentication.passwordTip') }) |
|||
.min(1, { message: $t('authentication.passwordTip') }) |
|||
.refine((value) => value === password, { |
|||
message: $t('authentication.confirmPasswordTip'), |
|||
}); |
|||
}, |
|||
triggerFields: ['password'], |
|||
}, |
|||
fieldName: 'confirmPassword', |
|||
label: $t('authentication.confirmPassword'), |
|||
}, |
|||
{ |
|||
component: 'VbenCheckbox', |
|||
fieldName: 'agreePolicy', |
|||
renderComponentContent: () => ({ |
|||
default: () => |
|||
h('span', [ |
|||
$t('authentication.agree'), |
|||
h( |
|||
'a', |
|||
{ |
|||
class: 'vben-link ml-1 ', |
|||
href: '', |
|||
}, |
|||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`, |
|||
), |
|||
]), |
|||
}), |
|||
rules: z.boolean().refine((value) => !!value, { |
|||
message: $t('authentication.agreeTip'), |
|||
}), |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
async function handleSubmit(value: Recordable<any>) { |
|||
// eslint-disable-next-line no-console |
|||
console.log('register submit:', value); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationRegister |
|||
:form-schema="formSchema" |
|||
:loading="loading" |
|||
@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,98 @@ |
|||
<script lang="ts" setup> |
|||
import type { EchartsUIType } from '@vben/plugins/echarts'; |
|||
|
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
|||
|
|||
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', |
|||
// }, |
|||
xAxis: { |
|||
axisTick: { |
|||
show: false, |
|||
}, |
|||
boundaryGap: false, |
|||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`), |
|||
splitLine: { |
|||
lineStyle: { |
|||
type: 'solid', |
|||
width: 1, |
|||
}, |
|||
show: true, |
|||
}, |
|||
type: 'category', |
|||
}, |
|||
yAxis: [ |
|||
{ |
|||
axisTick: { |
|||
show: false, |
|||
}, |
|||
max: 80_000, |
|||
splitArea: { |
|||
show: true, |
|||
}, |
|||
splitNumber: 4, |
|||
type: 'value', |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<EchartsUI ref="chartRef" /> |
|||
</template> |
|||
@ -0,0 +1,82 @@ |
|||
<script lang="ts" setup> |
|||
import type { EchartsUIType } from '@vben/plugins/echarts'; |
|||
|
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
|||
|
|||
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,46 @@ |
|||
<script lang="ts" setup> |
|||
import type { EchartsUIType } from '@vben/plugins/echarts'; |
|||
|
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
|||
|
|||
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,65 @@ |
|||
<script lang="ts" setup> |
|||
import type { EchartsUIType } from '@vben/plugins/echarts'; |
|||
|
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
|||
|
|||
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,55 @@ |
|||
<script lang="ts" setup> |
|||
import type { EchartsUIType } from '@vben/plugins/echarts'; |
|||
|
|||
import { onMounted, ref } from 'vue'; |
|||
|
|||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
|||
|
|||
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 AnalyticsVisitsData from './analytics-visits-data.vue'; |
|||
import AnalyticsVisitsSales from './analytics-visits-sales.vue'; |
|||
import AnalyticsVisitsSource from './analytics-visits-source.vue'; |
|||
import AnalyticsVisits from './analytics-visits.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,266 @@ |
|||
<script lang="ts" setup> |
|||
import type { |
|||
WorkbenchProjectItem, |
|||
WorkbenchQuickNavItem, |
|||
WorkbenchTodoItem, |
|||
WorkbenchTrendItem, |
|||
} from '@vben/common-ui'; |
|||
|
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { |
|||
AnalysisChartCard, |
|||
WorkbenchHeader, |
|||
WorkbenchProject, |
|||
WorkbenchQuickNav, |
|||
WorkbenchTodo, |
|||
WorkbenchTrends, |
|||
} from '@vben/common-ui'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
import { openWindow } from '@vben/utils'; |
|||
|
|||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue'; |
|||
|
|||
const userStore = useUserStore(); |
|||
|
|||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整 |
|||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转 |
|||
// 例如:url: /dashboard/workspace |
|||
const projectItems: WorkbenchProjectItem[] = [ |
|||
{ |
|||
color: '', |
|||
content: '不要等待机会,而要创造机会。', |
|||
date: '2021-04-01', |
|||
group: '开源组', |
|||
icon: 'carbon:logo-github', |
|||
title: 'Github', |
|||
url: 'https://github.com', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
content: '现在的你决定将来的你。', |
|||
date: '2021-04-01', |
|||
group: '算法组', |
|||
icon: 'ion:logo-vue', |
|||
title: 'Vue', |
|||
url: 'https://vuejs.org', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
content: '没有什么才能比努力更重要。', |
|||
date: '2021-04-01', |
|||
group: '上班摸鱼', |
|||
icon: 'ion:logo-html5', |
|||
title: 'Html5', |
|||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
content: '热情和欲望可以突破一切难关。', |
|||
date: '2021-04-01', |
|||
group: 'UI', |
|||
icon: 'ion:logo-angular', |
|||
title: 'Angular', |
|||
url: 'https://angular.io', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
content: '健康的身体是实现目标的基石。', |
|||
date: '2021-04-01', |
|||
group: '技术牛', |
|||
icon: 'bx:bxl-react', |
|||
title: 'React', |
|||
url: 'https://reactjs.org', |
|||
}, |
|||
{ |
|||
color: '#EBD94E', |
|||
content: '路是走出来的,而不是空想出来的。', |
|||
date: '2021-04-01', |
|||
group: '架构组', |
|||
icon: 'ion:logo-javascript', |
|||
title: 'Js', |
|||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript', |
|||
}, |
|||
]; |
|||
|
|||
// 同样,这里的 url 也可以使用以 http 开头的外部链接 |
|||
const quickNavItems: WorkbenchQuickNavItem[] = [ |
|||
{ |
|||
color: '#1fdaca', |
|||
icon: 'ion:home-outline', |
|||
title: '首页', |
|||
url: '/', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
icon: 'ion:grid-outline', |
|||
title: '仪表盘', |
|||
url: '/dashboard', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
icon: 'ion:layers-outline', |
|||
title: '组件', |
|||
url: '/demos/features/icons', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
icon: 'ion:settings-outline', |
|||
title: '系统管理', |
|||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整 |
|||
}, |
|||
{ |
|||
color: '#4daf1bc9', |
|||
icon: 'ion:key-outline', |
|||
title: '权限管理', |
|||
url: '/demos/access/page-control', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
icon: 'ion:bar-chart-outline', |
|||
title: '图表', |
|||
url: '/analytics', |
|||
}, |
|||
]; |
|||
|
|||
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', |
|||
}, |
|||
]; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整 |
|||
// This is a sample method, adjust according to the actual project requirements |
|||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { |
|||
if (nav.url?.startsWith('http')) { |
|||
openWindow(nav.url); |
|||
return; |
|||
} |
|||
if (nav.url?.startsWith('/')) { |
|||
router.push(nav.url).catch((error) => { |
|||
console.error('Navigation failed:', error); |
|||
}); |
|||
} else { |
|||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`); |
|||
} |
|||
} |
|||
</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="项目" @click="navTo" /> |
|||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" /> |
|||
</div> |
|||
<div class="w-full lg:w-2/5"> |
|||
<WorkbenchQuickNav |
|||
:items="quickNavItems" |
|||
class="mt-5 lg:mt-0" |
|||
title="快捷导航" |
|||
@click="navTo" |
|||
/> |
|||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" /> |
|||
<AnalysisChartCard class="mt-5" title="访问来源"> |
|||
<AnalyticsVisitsSource /> |
|||
</AnalysisChartCard> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,66 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
|
|||
import { Button, Card, message, notification, Space } from 'ant-design-vue'; |
|||
|
|||
type NotificationType = 'error' | 'info' | 'success' | 'warning'; |
|||
|
|||
function info() { |
|||
message.info('How many roads must a man walk down'); |
|||
} |
|||
|
|||
function error() { |
|||
message.error({ |
|||
content: 'Once upon a time you dressed so fine', |
|||
duration: 2500, |
|||
}); |
|||
} |
|||
|
|||
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 notify(type: NotificationType) { |
|||
notification[type]({ |
|||
duration: 2500, |
|||
message: '说点啥呢', |
|||
type, |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Page |
|||
description="支持多语言,主题功能集成切换等" |
|||
title="Ant Design Vue组件使用演示" |
|||
> |
|||
<Card class="mb-5" title="按钮"> |
|||
<Space> |
|||
<Button>Default</Button> |
|||
<Button type="primary"> Primary </Button> |
|||
<Button> Info </Button> |
|||
<Button danger> Error </Button> |
|||
</Space> |
|||
</Card> |
|||
<Card class="mb-5" title="Message"> |
|||
<Space> |
|||
<Button @click="info"> 信息 </Button> |
|||
<Button danger @click="error"> 错误 </Button> |
|||
<Button @click="warning"> 警告 </Button> |
|||
<Button @click="success"> 成功 </Button> |
|||
</Space> |
|||
</Card> |
|||
|
|||
<Card class="mb-5" title="Notification"> |
|||
<Space> |
|||
<Button @click="notify('info')"> 信息 </Button> |
|||
<Button danger @click="notify('error')"> 错误 </Button> |
|||
<Button @click="notify('warning')"> 警告 </Button> |
|||
<Button @click="notify('success')"> 成功 </Button> |
|||
</Space> |
|||
</Card> |
|||
</Page> |
|||
</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