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