mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-04-08 00:43:15 +08:00
refactor: 移除ele和naive目录
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
- VxeTable升级V4.10.0
|
- VxeTable升级V4.10.0
|
||||||
- 移除`@deprecated` `apps/web-antd/src/adapter/vxe-table.ts`的`tableCheckboxEvent`方法
|
- 移除`@deprecated` `apps/web-antd/src/adapter/vxe-table.ts`的`tableCheckboxEvent`方法
|
||||||
- 移除`由于更新方案弃用的` `apps/web-antd/src/adapter/vxe-table.ts`的`vxeSortEvent`方法
|
- 移除`由于更新方案弃用的` `apps/web-antd/src/adapter/vxe-table.ts`的`vxeSortEvent`方法
|
||||||
|
- 移除apps下的ele和naive目录
|
||||||
|
|
||||||
# 1.1.3
|
# 1.1.3
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# 应用标题
|
|
||||||
VITE_APP_TITLE=Vben Admin Ele
|
|
||||||
|
|
||||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
|
||||||
VITE_APP_NAMESPACE=vben-web-ele
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# public path
|
|
||||||
VITE_BASE=/
|
|
||||||
|
|
||||||
# Basic interface address SPA
|
|
||||||
VITE_GLOB_API_URL=/api
|
|
||||||
|
|
||||||
VITE_VISUALIZER=true
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# 端口号
|
|
||||||
VITE_PORT=5777
|
|
||||||
|
|
||||||
VITE_BASE=/
|
|
||||||
|
|
||||||
# 接口地址
|
|
||||||
VITE_GLOB_API_URL=/api
|
|
||||||
|
|
||||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
|
||||||
VITE_NITRO_MOCK=true
|
|
||||||
|
|
||||||
# 是否打开 devtools,true 为打开,false 为关闭
|
|
||||||
VITE_DEVTOOLS=false
|
|
||||||
|
|
||||||
# 是否注入全局loading
|
|
||||||
VITE_INJECT_APP_LOADING=true
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<!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?97352b16ed2df8c3860cf5a1a65fb4dd';
|
|
||||||
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>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@vben/web-ele",
|
|
||||||
"version": "5.5.2",
|
|
||||||
"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-ele"
|
|
||||||
},
|
|
||||||
"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:",
|
|
||||||
"element-plus": "catalog:",
|
|
||||||
"pinia": "catalog:",
|
|
||||||
"vue": "catalog:",
|
|
||||||
"vue-router": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"unplugin-element-plus": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@vben/tailwind-config/postcss';
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB |
@@ -1,232 +0,0 @@
|
|||||||
/**
|
|
||||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
|
||||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
|
||||||
import type { Recordable } from '@vben/types';
|
|
||||||
import type { Component, SetupContext } from 'vue';
|
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
|
||||||
import { $t } from '@vben/locales';
|
|
||||||
import {
|
|
||||||
ElButton,
|
|
||||||
ElCheckbox,
|
|
||||||
ElCheckboxButton,
|
|
||||||
ElCheckboxGroup,
|
|
||||||
ElDatePicker,
|
|
||||||
ElDivider,
|
|
||||||
ElInput,
|
|
||||||
ElInputNumber,
|
|
||||||
ElNotification,
|
|
||||||
ElRadio,
|
|
||||||
ElRadioButton,
|
|
||||||
ElRadioGroup,
|
|
||||||
ElSelectV2,
|
|
||||||
ElSpace,
|
|
||||||
ElSwitch,
|
|
||||||
ElTimePicker,
|
|
||||||
ElTreeSelect,
|
|
||||||
ElUpload,
|
|
||||||
} from 'element-plus';
|
|
||||||
import { h } from 'vue';
|
|
||||||
|
|
||||||
const withDefaultPlaceholder = <T extends Component>(
|
|
||||||
component: T,
|
|
||||||
type: 'input' | 'select',
|
|
||||||
) => {
|
|
||||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
|
||||||
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
|
|
||||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
|
||||||
export type ComponentType =
|
|
||||||
| 'ApiSelect'
|
|
||||||
| 'ApiTreeSelect'
|
|
||||||
| 'Checkbox'
|
|
||||||
| 'CheckboxGroup'
|
|
||||||
| 'DatePicker'
|
|
||||||
| 'Divider'
|
|
||||||
| 'IconPicker'
|
|
||||||
| 'Input'
|
|
||||||
| 'InputNumber'
|
|
||||||
| 'RadioGroup'
|
|
||||||
| 'Select'
|
|
||||||
| 'Space'
|
|
||||||
| 'Switch'
|
|
||||||
| 'TimePicker'
|
|
||||||
| 'TreeSelect'
|
|
||||||
| 'Upload'
|
|
||||||
| BaseFormComponentType;
|
|
||||||
|
|
||||||
async function initComponentAdapter() {
|
|
||||||
const components: Partial<Record<ComponentType, Component>> = {
|
|
||||||
// 如果你的组件体积比较大,可以使用异步加载
|
|
||||||
// Button: () =>
|
|
||||||
// import('xxx').then((res) => res.Button),
|
|
||||||
ApiSelect: (props, { attrs, slots }) => {
|
|
||||||
return h(
|
|
||||||
ApiComponent,
|
|
||||||
{
|
|
||||||
placeholder: $t('ui.placeholder.select'),
|
|
||||||
...props,
|
|
||||||
...attrs,
|
|
||||||
component: ElSelectV2,
|
|
||||||
loadingSlot: 'loading',
|
|
||||||
visibleEvent: 'onVisibleChange',
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
|
||||||
return h(
|
|
||||||
ApiComponent,
|
|
||||||
{
|
|
||||||
placeholder: $t('ui.placeholder.select'),
|
|
||||||
...props,
|
|
||||||
...attrs,
|
|
||||||
component: ElTreeSelect,
|
|
||||||
props: { label: 'label', children: 'children' },
|
|
||||||
nodeKey: 'value',
|
|
||||||
loadingSlot: 'loading',
|
|
||||||
optionsPropName: 'data',
|
|
||||||
visibleEvent: 'onVisibleChange',
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Checkbox: ElCheckbox,
|
|
||||||
CheckboxGroup: (props, { attrs, slots }) => {
|
|
||||||
let defaultSlot;
|
|
||||||
if (Reflect.has(slots, 'default')) {
|
|
||||||
defaultSlot = slots.default;
|
|
||||||
} else {
|
|
||||||
const { options, isButton } = attrs;
|
|
||||||
if (Array.isArray(options)) {
|
|
||||||
defaultSlot = () =>
|
|
||||||
options.map((option) =>
|
|
||||||
h(isButton ? ElCheckboxButton : ElCheckbox, option),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h(
|
|
||||||
ElCheckboxGroup,
|
|
||||||
{ ...props, ...attrs },
|
|
||||||
{ ...slots, default: defaultSlot },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// 自定义默认按钮
|
|
||||||
DefaultButton: (props, { attrs, slots }) => {
|
|
||||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
|
||||||
},
|
|
||||||
// 自定义主要按钮
|
|
||||||
PrimaryButton: (props, { attrs, slots }) => {
|
|
||||||
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
|
||||||
},
|
|
||||||
Divider: ElDivider,
|
|
||||||
IconPicker: (props, { attrs, slots }) => {
|
|
||||||
return h(
|
|
||||||
IconPicker,
|
|
||||||
{
|
|
||||||
iconSlot: 'append',
|
|
||||||
modelValueProp: 'model-value',
|
|
||||||
inputComponent: ElInput,
|
|
||||||
...props,
|
|
||||||
...attrs,
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Input: withDefaultPlaceholder(ElInput, 'input'),
|
|
||||||
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
|
|
||||||
RadioGroup: (props, { attrs, slots }) => {
|
|
||||||
let defaultSlot;
|
|
||||||
if (Reflect.has(slots, 'default')) {
|
|
||||||
defaultSlot = slots.default;
|
|
||||||
} else {
|
|
||||||
const { options } = attrs;
|
|
||||||
if (Array.isArray(options)) {
|
|
||||||
defaultSlot = () =>
|
|
||||||
options.map((option) =>
|
|
||||||
h(attrs.isButton ? ElRadioButton : ElRadio, option),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h(
|
|
||||||
ElRadioGroup,
|
|
||||||
{ ...props, ...attrs },
|
|
||||||
{ ...slots, default: defaultSlot },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Select: (props, { attrs, slots }) => {
|
|
||||||
return h(ElSelectV2, { ...props, attrs }, slots);
|
|
||||||
},
|
|
||||||
Space: ElSpace,
|
|
||||||
Switch: ElSwitch,
|
|
||||||
TimePicker: (props, { attrs, slots }) => {
|
|
||||||
const { name, id, isRange } = props;
|
|
||||||
const extraProps: Recordable<any> = {};
|
|
||||||
if (isRange) {
|
|
||||||
if (name && !Array.isArray(name)) {
|
|
||||||
extraProps.name = [name, `${name}_end`];
|
|
||||||
}
|
|
||||||
if (id && !Array.isArray(id)) {
|
|
||||||
extraProps.id = [id, `${id}_end`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h(
|
|
||||||
ElTimePicker,
|
|
||||||
{
|
|
||||||
...props,
|
|
||||||
...attrs,
|
|
||||||
...extraProps,
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
DatePicker: (props, { attrs, slots }) => {
|
|
||||||
const { name, id, type } = props;
|
|
||||||
const extraProps: Recordable<any> = {};
|
|
||||||
if (type && type.includes('range')) {
|
|
||||||
if (name && !Array.isArray(name)) {
|
|
||||||
extraProps.name = [name, `${name}_end`];
|
|
||||||
}
|
|
||||||
if (id && !Array.isArray(id)) {
|
|
||||||
extraProps.id = [id, `${id}_end`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h(
|
|
||||||
ElDatePicker,
|
|
||||||
{
|
|
||||||
...props,
|
|
||||||
...attrs,
|
|
||||||
...extraProps,
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
|
|
||||||
Upload: ElUpload,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将组件注册到全局共享状态中
|
|
||||||
globalShareState.setComponents(components);
|
|
||||||
|
|
||||||
// 定义全局共享状态中的消息提示
|
|
||||||
globalShareState.defineMessage({
|
|
||||||
// 复制成功消息提示
|
|
||||||
copyPreferencesSuccess: (title, content) => {
|
|
||||||
ElNotification({
|
|
||||||
title,
|
|
||||||
message: content,
|
|
||||||
position: 'bottom-right',
|
|
||||||
duration: 0,
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { initComponentAdapter };
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
setupVbenForm<ComponentType>({
|
|
||||||
config: {
|
|
||||||
modelPropNameMap: {
|
|
||||||
Upload: 'fileList',
|
|
||||||
CheckboxGroup: 'model-value',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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 { useVbenForm, z };
|
|
||||||
|
|
||||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
|
||||||
export type { VbenFormProps };
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { h } from 'vue';
|
|
||||||
|
|
||||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
|
||||||
|
|
||||||
import { ElButton, ElImage } from 'element-plus';
|
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
|
||||||
vxeUI.renderer.add('CellImage', {
|
|
||||||
renderTableDefault(_renderOpts, params) {
|
|
||||||
const { column, row } = params;
|
|
||||||
const src = row[column.field];
|
|
||||||
return h(ElImage, { src, previewSrcList: [src] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
|
||||||
vxeUI.renderer.add('CellLink', {
|
|
||||||
renderTableDefault(renderOpts) {
|
|
||||||
const { props } = renderOpts;
|
|
||||||
return h(
|
|
||||||
ElButton,
|
|
||||||
{ size: 'small', link: true },
|
|
||||||
{ default: () => props?.text },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
|
||||||
// vxeUI.formats.add
|
|
||||||
},
|
|
||||||
useVbenForm,
|
|
||||||
});
|
|
||||||
|
|
||||||
export { useVbenVxeGrid };
|
|
||||||
|
|
||||||
export type * from '@vben/plugins/vxe-table';
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './auth';
|
|
||||||
export * from './menu';
|
|
||||||
export * from './user';
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { RouteRecordStringComponent } from '@vben/types';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户所有菜单
|
|
||||||
*/
|
|
||||||
export async function getAllMenusApi() {
|
|
||||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { UserInfo } from '@vben/types';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息
|
|
||||||
*/
|
|
||||||
export async function getUserInfoApi() {
|
|
||||||
return requestClient.get<UserInfo>('/user/info');
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './core';
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/**
|
|
||||||
* 该文件可自行根据业务逻辑进行调整
|
|
||||||
*/
|
|
||||||
import type { RequestClientOptions } from '@vben/request';
|
|
||||||
|
|
||||||
import { useAppConfig } from '@vben/hooks';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import {
|
|
||||||
authenticateResponseInterceptor,
|
|
||||||
errorMessageResponseInterceptor,
|
|
||||||
RequestClient,
|
|
||||||
} from '@vben/request';
|
|
||||||
import { useAccessStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 ?? '';
|
|
||||||
// 如果没有错误信息,则会根据状态码进行提示
|
|
||||||
ElMessage.error(errorMessage || msg);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const requestClient = createRequestClient(apiURL, {
|
|
||||||
responseReturn: 'data',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { useElementPlusDesignTokens } from '@vben/hooks';
|
|
||||||
|
|
||||||
import { ElConfigProvider } from 'element-plus';
|
|
||||||
|
|
||||||
import { elementLocale } from '#/locales';
|
|
||||||
|
|
||||||
defineOptions({ name: 'App' });
|
|
||||||
|
|
||||||
useElementPlusDesignTokens();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElConfigProvider :locale="elementLocale">
|
|
||||||
<RouterView />
|
|
||||||
</ElConfigProvider>
|
|
||||||
</template>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { createApp, watchEffect } from 'vue';
|
|
||||||
|
|
||||||
import { registerAccessDirective } from '@vben/access';
|
|
||||||
import { initTippy } from '@vben/common-ui';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import { initStores } from '@vben/stores';
|
|
||||||
import '@vben/styles';
|
|
||||||
import '@vben/styles/ele';
|
|
||||||
|
|
||||||
import { useTitle } from '@vueuse/core';
|
|
||||||
import { ElLoading } from 'element-plus';
|
|
||||||
|
|
||||||
import { $t, setupI18n } from '#/locales';
|
|
||||||
|
|
||||||
import { initComponentAdapter } from './adapter/component';
|
|
||||||
import App from './app.vue';
|
|
||||||
import { router } from './router';
|
|
||||||
|
|
||||||
async function bootstrap(namespace: string) {
|
|
||||||
// 初始化组件适配器
|
|
||||||
await initComponentAdapter();
|
|
||||||
// // 设置弹窗的默认配置
|
|
||||||
// setDefaultModalProps({
|
|
||||||
// fullscreenButton: false,
|
|
||||||
// });
|
|
||||||
// // 设置抽屉的默认配置
|
|
||||||
// setDefaultDrawerProps({
|
|
||||||
// zIndex: 2000,
|
|
||||||
// });
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
// 注册Element Plus提供的v-loading指令
|
|
||||||
app.directive('loading', ElLoading.directive);
|
|
||||||
|
|
||||||
// 国际化 i18n 配置
|
|
||||||
await setupI18n(app);
|
|
||||||
|
|
||||||
// 配置 pinia-tore
|
|
||||||
await initStores(app, { namespace });
|
|
||||||
|
|
||||||
// 安装权限指令
|
|
||||||
registerAccessDirective(app);
|
|
||||||
|
|
||||||
// 初始化 tippy
|
|
||||||
initTippy(app);
|
|
||||||
|
|
||||||
// 配置路由及路由守卫
|
|
||||||
app.use(router);
|
|
||||||
|
|
||||||
// 动态更新标题
|
|
||||||
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 };
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
<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, MdiGithub } 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: MdiGithub,
|
|
||||||
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(
|
|
||||||
() => preferences.app.watermark,
|
|
||||||
async (enable) => {
|
|
||||||
if (enable) {
|
|
||||||
await updateWatermark({
|
|
||||||
content: `${userStore.userInfo?.username}`,
|
|
||||||
});
|
|
||||||
} 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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const BasicLayout = () => import('./basic.vue');
|
|
||||||
const AuthPageLayout = () => import('./auth.vue');
|
|
||||||
|
|
||||||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
|
||||||
|
|
||||||
export { AuthPageLayout, BasicLayout, IFrameView };
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# locale
|
|
||||||
|
|
||||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
|
||||||
import type { Language } from 'element-plus/es/locale';
|
|
||||||
import type { App } from 'vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
$t,
|
|
||||||
setupI18n as coreSetup,
|
|
||||||
loadLocalesMapFromDir,
|
|
||||||
} from '@vben/locales';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import enLocale from 'element-plus/es/locale/lang/en';
|
|
||||||
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const elementLocale = ref<Language>(defaultLocale);
|
|
||||||
|
|
||||||
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([loadElementLocale(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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载element-plus的语言包
|
|
||||||
* @param lang
|
|
||||||
*/
|
|
||||||
async function loadElementLocale(lang: SupportedLanguagesType) {
|
|
||||||
switch (lang) {
|
|
||||||
case 'en-US': {
|
|
||||||
elementLocale.value = enLocale;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'zh-CN': {
|
|
||||||
elementLocale.value = defaultLocale;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
|
||||||
await coreSetup(app, {
|
|
||||||
defaultLocale: preferences.app.locale,
|
|
||||||
loadMessages,
|
|
||||||
missingWarn: !import.meta.env.PROD,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { $t, elementLocale, setupI18n };
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Demos",
|
|
||||||
"elementPlus": "Element Plus",
|
|
||||||
"form": "Form",
|
|
||||||
"vben": {
|
|
||||||
"title": "Project",
|
|
||||||
"about": "About",
|
|
||||||
"document": "Document",
|
|
||||||
"antdv": "Ant Design Vue Version",
|
|
||||||
"naive-ui": "Naive UI Version",
|
|
||||||
"element-plus": "Element Plus Version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"auth": {
|
|
||||||
"login": "Login",
|
|
||||||
"register": "Register",
|
|
||||||
"codeLogin": "Code Login",
|
|
||||||
"qrcodeLogin": "Qr Code Login",
|
|
||||||
"forgetPassword": "Forget Password"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "Dashboard",
|
|
||||||
"analytics": "Analytics",
|
|
||||||
"workspace": "Workspace"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "演示",
|
|
||||||
"elementPlus": "Element Plus",
|
|
||||||
"form": "表单演示",
|
|
||||||
"vben": {
|
|
||||||
"title": "项目",
|
|
||||||
"about": "关于",
|
|
||||||
"document": "文档",
|
|
||||||
"antdv": "Ant Design Vue 版本",
|
|
||||||
"naive-ui": "Naive UI 版本",
|
|
||||||
"element-plus": "Element Plus 版本"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"auth": {
|
|
||||||
"login": "登录",
|
|
||||||
"register": "注册",
|
|
||||||
"codeLogin": "验证码登录",
|
|
||||||
"qrcodeLogin": "二维码登录",
|
|
||||||
"forgetPassword": "忘记密码"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "概览",
|
|
||||||
"analytics": "分析页",
|
|
||||||
"workspace": "工作台"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { defineOverridesPreferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 项目配置文件
|
|
||||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
|
||||||
* !!! 更改配置后请清空缓存,否则可能不生效
|
|
||||||
*/
|
|
||||||
export const overridesPreferences = defineOverridesPreferences({
|
|
||||||
// overrides
|
|
||||||
app: {
|
|
||||||
name: import.meta.env.VITE_APP_TITLE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import type {
|
|
||||||
ComponentRecordType,
|
|
||||||
GenerateMenuAndRoutesOptions,
|
|
||||||
} from '@vben/types';
|
|
||||||
|
|
||||||
import { generateAccessible } from '@vben/access';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
ElMessage({
|
|
||||||
duration: 1500,
|
|
||||||
message: `${$t('common.loadingMenu')}...`,
|
|
||||||
});
|
|
||||||
return await getAllMenusApi();
|
|
||||||
},
|
|
||||||
// 可以指定没有权限跳转403页面
|
|
||||||
forbiddenComponent,
|
|
||||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
|
||||||
layoutMap,
|
|
||||||
pageMap,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { generateAccess };
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import type { Router } from 'vue-router';
|
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, 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(async (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 ||
|
|
||||||
DEFAULT_HOME_PATH,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 === DEFAULT_HOME_PATH
|
|
||||||
? {}
|
|
||||||
: { 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 === DEFAULT_HOME_PATH
|
|
||||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
|
||||||
: to.fullPath)) as string;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...router.resolve(decodeURIComponent(redirectPath)),
|
|
||||||
replace: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目守卫配置
|
|
||||||
* @param router
|
|
||||||
*/
|
|
||||||
function createRouterGuard(router: Router) {
|
|
||||||
/** 通用 */
|
|
||||||
setupCommonGuard(router);
|
|
||||||
/** 权限访问 */
|
|
||||||
setupAccessGuard(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { createRouterGuard };
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
|
||||||
|
|
||||||
import { AuthPageLayout, BasicLayout } from '#/layouts';
|
|
||||||
import { $t } from '#/locales';
|
|
||||||
import Login from '#/views/_core/authentication/login.vue';
|
|
||||||
|
|
||||||
/** 全局404页面 */
|
|
||||||
const fallbackNotFoundRoute: RouteRecordRaw = {
|
|
||||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
|
||||||
meta: {
|
|
||||||
hideInBreadcrumb: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
hideInTab: true,
|
|
||||||
title: '404',
|
|
||||||
},
|
|
||||||
name: 'FallbackNotFound',
|
|
||||||
path: '/:path(.*)*',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 基本路由,这些路由是必须存在的 */
|
|
||||||
const coreRoutes: RouteRecordRaw[] = [
|
|
||||||
/**
|
|
||||||
* 根路由
|
|
||||||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
|
||||||
* 此路由必须存在,且不应修改
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
component: BasicLayout,
|
|
||||||
meta: {
|
|
||||||
hideInBreadcrumb: true,
|
|
||||||
title: 'Root',
|
|
||||||
},
|
|
||||||
name: 'Root',
|
|
||||||
path: '/',
|
|
||||||
redirect: DEFAULT_HOME_PATH,
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: AuthPageLayout,
|
|
||||||
meta: {
|
|
||||||
hideInTab: true,
|
|
||||||
title: 'Authentication',
|
|
||||||
},
|
|
||||||
name: 'Authentication',
|
|
||||||
path: '/auth',
|
|
||||||
redirect: LOGIN_PATH,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'Login',
|
|
||||||
path: 'login',
|
|
||||||
component: Login,
|
|
||||||
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 };
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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.elementPlus'),
|
|
||||||
},
|
|
||||||
name: 'NaiveDemos',
|
|
||||||
path: '/demos/element',
|
|
||||||
component: () => import('#/views/demos/element/index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
title: $t('demos.form'),
|
|
||||||
},
|
|
||||||
name: 'BasicForm',
|
|
||||||
path: '/demos/form',
|
|
||||||
component: () => import('#/views/demos/form/basic.vue'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default routes;
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
|
||||||
|
|
||||||
import {
|
|
||||||
VBEN_ANT_PREVIEW_URL,
|
|
||||||
VBEN_DOC_URL,
|
|
||||||
VBEN_GITHUB_URL,
|
|
||||||
VBEN_LOGO_URL,
|
|
||||||
VBEN_NAIVE_PREVIEW_URL,
|
|
||||||
} from '@vben/constants';
|
|
||||||
import { SvgAntdvLogoIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
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: 'VbenAntd',
|
|
||||||
path: '/vben-admin/antd',
|
|
||||||
component: IFrameView,
|
|
||||||
meta: {
|
|
||||||
badgeType: 'dot',
|
|
||||||
icon: SvgAntdvLogoIcon,
|
|
||||||
link: VBEN_ANT_PREVIEW_URL,
|
|
||||||
title: $t('demos.vben.antdv'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import type { Recordable, UserInfo } from '@vben/types';
|
|
||||||
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
|
||||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import { ElNotification } from 'element-plus';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
import { 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
|
|
||||||
let userInfo: null | UserInfo = null;
|
|
||||||
try {
|
|
||||||
loginLoading.value = true;
|
|
||||||
const { accessToken } = await loginApi(params);
|
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
|
||||||
if (accessToken) {
|
|
||||||
// 将 accessToken 存储到 accessStore 中
|
|
||||||
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 || DEFAULT_HOME_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userInfo?.realName) {
|
|
||||||
ElNotification({
|
|
||||||
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
|
||||||
title: $t('authentication.loginSuccess'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loginLoading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
userInfo,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout(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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './auth';
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# \_core
|
|
||||||
|
|
||||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { About } from '@vben/common-ui';
|
|
||||||
|
|
||||||
defineOptions({ name: 'About' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<About />
|
|
||||||
</template>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<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>) {
|
|
||||||
console.log(values);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AuthenticationCodeLogin
|
|
||||||
:form-schema="formSchema"
|
|
||||||
:loading="loading"
|
|
||||||
@submit="handleLogin"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<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>) {
|
|
||||||
console.log('reset email:', value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AuthenticationForgetPassword
|
|
||||||
:form-schema="formSchema"
|
|
||||||
:loading="loading"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<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'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleSubmit(value: Recordable<any>) {
|
|
||||||
console.log('register submit:', value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AuthenticationRegister
|
|
||||||
:form-schema="formSchema"
|
|
||||||
:loading="loading"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Fallback } from '@vben/common-ui';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Fallback status="coming-soon" />
|
|
||||||
</template>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Fallback } from '@vben/common-ui';
|
|
||||||
|
|
||||||
defineOptions({ name: 'Fallback403Demo' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Fallback status="403" />
|
|
||||||
</template>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Fallback } from '@vben/common-ui';
|
|
||||||
|
|
||||||
defineOptions({ name: 'Fallback500Demo' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Fallback status="500" />
|
|
||||||
</template>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Fallback } from '@vben/common-ui';
|
|
||||||
|
|
||||||
defineOptions({ name: 'Fallback404Demo' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Fallback status="404" />
|
|
||||||
</template>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Fallback } from '@vben/common-ui';
|
|
||||||
|
|
||||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Fallback status="offline" />
|
|
||||||
</template>
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
||||||
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
||||||
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
||||||
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
||||||
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
||||||
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
|
||||||
import type { TabOption } from '@vben/types';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AnalysisChartCard,
|
|
||||||
AnalysisChartsTabs,
|
|
||||||
AnalysisOverview,
|
|
||||||
} from '@vben/common-ui';
|
|
||||||
import {
|
|
||||||
SvgBellIcon,
|
|
||||||
SvgCakeIcon,
|
|
||||||
SvgCardIcon,
|
|
||||||
SvgDownloadIcon,
|
|
||||||
} from '@vben/icons';
|
|
||||||
|
|
||||||
import AnalyticsTrends from './analytics-trends.vue';
|
|
||||||
import AnalyticsVisits from './analytics-visits.vue';
|
|
||||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
|
||||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
|
||||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
|
||||||
|
|
||||||
const overviewItems: AnalysisOverviewItem[] = [
|
|
||||||
{
|
|
||||||
icon: SvgCardIcon,
|
|
||||||
title: '用户量',
|
|
||||||
totalTitle: '总用户量',
|
|
||||||
totalValue: 120_000,
|
|
||||||
value: 2000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: SvgCakeIcon,
|
|
||||||
title: '访问量',
|
|
||||||
totalTitle: '总访问量',
|
|
||||||
totalValue: 500_000,
|
|
||||||
value: 20_000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: SvgDownloadIcon,
|
|
||||||
title: '下载量',
|
|
||||||
totalTitle: '总下载量',
|
|
||||||
totalValue: 120_000,
|
|
||||||
value: 8000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: SvgBellIcon,
|
|
||||||
title: '使用量',
|
|
||||||
totalTitle: '总使用量',
|
|
||||||
totalValue: 50_000,
|
|
||||||
value: 5000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const chartTabs: TabOption[] = [
|
|
||||||
{
|
|
||||||
label: '流量趋势',
|
|
||||||
value: 'trends',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '月访问量',
|
|
||||||
value: 'visits',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-5">
|
|
||||||
<AnalysisOverview :items="overviewItems" />
|
|
||||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
|
||||||
<template #trends>
|
|
||||||
<AnalyticsTrends />
|
|
||||||
</template>
|
|
||||||
<template #visits>
|
|
||||||
<AnalyticsVisits />
|
|
||||||
</template>
|
|
||||||
</AnalysisChartsTabs>
|
|
||||||
|
|
||||||
<div class="mt-5 w-full md:flex">
|
|
||||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
|
||||||
<AnalyticsVisitsData />
|
|
||||||
</AnalysisChartCard>
|
|
||||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
|
||||||
<AnalyticsVisitsSource />
|
|
||||||
</AnalysisChartCard>
|
|
||||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
|
||||||
<AnalyticsVisitsSales />
|
|
||||||
</AnalysisChartCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ElButton,
|
|
||||||
ElCard,
|
|
||||||
ElMessage,
|
|
||||||
ElNotification,
|
|
||||||
ElSegmented,
|
|
||||||
ElSpace,
|
|
||||||
ElTable,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
|
||||||
|
|
||||||
function info() {
|
|
||||||
ElMessage.info('How many roads must a man walk down');
|
|
||||||
}
|
|
||||||
|
|
||||||
function error() {
|
|
||||||
ElMessage.error({
|
|
||||||
duration: 2500,
|
|
||||||
message: 'Once upon a time you dressed so fine',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function warning() {
|
|
||||||
ElMessage.warning('How many roads must a man walk down');
|
|
||||||
}
|
|
||||||
function success() {
|
|
||||||
ElMessage.success(
|
|
||||||
'Cause you walked hand in hand With another man in my place',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notify(type: NotificationType) {
|
|
||||||
ElNotification({
|
|
||||||
duration: 2500,
|
|
||||||
message: '说点啥呢',
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const tableData = [
|
|
||||||
{ prop1: '1', prop2: 'A' },
|
|
||||||
{ prop1: '2', prop2: 'B' },
|
|
||||||
{ prop1: '3', prop2: 'C' },
|
|
||||||
{ prop1: '4', prop2: 'D' },
|
|
||||||
{ prop1: '5', prop2: 'E' },
|
|
||||||
{ prop1: '6', prop2: 'F' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const segmentedValue = ref('Mon');
|
|
||||||
|
|
||||||
const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Page
|
|
||||||
description="支持多语言,主题功能集成切换等"
|
|
||||||
title="Element Plus组件使用演示"
|
|
||||||
>
|
|
||||||
<div class="flex flex-wrap gap-5">
|
|
||||||
<ElCard class="mb-5 w-auto">
|
|
||||||
<template #header> 按钮 </template>
|
|
||||||
<ElSpace>
|
|
||||||
<ElButton text>Text</ElButton>
|
|
||||||
<ElButton>Default</ElButton>
|
|
||||||
<ElButton type="primary"> Primary </ElButton>
|
|
||||||
<ElButton type="info"> Info </ElButton>
|
|
||||||
<ElButton type="success"> Success </ElButton>
|
|
||||||
<ElButton type="warning"> Warning </ElButton>
|
|
||||||
<ElButton type="danger"> Error </ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard class="mb-5 w-80">
|
|
||||||
<template #header> Message </template>
|
|
||||||
<ElSpace>
|
|
||||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
|
||||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
|
||||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
|
||||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard class="mb-5 w-80">
|
|
||||||
<template #header> Notification </template>
|
|
||||||
<ElSpace>
|
|
||||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
|
||||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
|
||||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
|
||||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard class="mb-5 w-auto">
|
|
||||||
<template #header> Segmented </template>
|
|
||||||
<ElSegmented
|
|
||||||
v-model="segmentedValue"
|
|
||||||
:options="segmentedOptions"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard class="mb-5 w-80">
|
|
||||||
<template #header> V-Loading </template>
|
|
||||||
<div class="flex size-72 items-center justify-center" v-loading="true">
|
|
||||||
一些演示的内容
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard class="mb-5 w-80">
|
|
||||||
<ElTable :data="tableData" stripe>
|
|
||||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
|
||||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
|
||||||
</ElTable>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { h } from 'vue';
|
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
|
||||||
import { getAllMenusApi } from '#/api';
|
|
||||||
|
|
||||||
const [Form, formApi] = useVbenForm({
|
|
||||||
commonConfig: {
|
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
|
||||||
class: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layout: 'horizontal',
|
|
||||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
||||||
handleSubmit: (values) => {
|
|
||||||
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
|
|
||||||
},
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
|
||||||
component: 'ApiSelect',
|
|
||||||
// 对应组件的参数
|
|
||||||
componentProps: {
|
|
||||||
// 菜单接口转options格式
|
|
||||||
afterFetch: (data: { name: string; path: string }[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.path,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
// 菜单接口
|
|
||||||
api: getAllMenusApi,
|
|
||||||
},
|
|
||||||
// 字段名
|
|
||||||
fieldName: 'api',
|
|
||||||
// 界面显示的label
|
|
||||||
label: 'ApiSelect',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'ApiTreeSelect',
|
|
||||||
// 对应组件的参数
|
|
||||||
componentProps: {
|
|
||||||
// 菜单接口
|
|
||||||
api: getAllMenusApi,
|
|
||||||
childrenField: 'children',
|
|
||||||
// 菜单接口转options格式
|
|
||||||
labelField: 'name',
|
|
||||||
valueField: 'path',
|
|
||||||
},
|
|
||||||
// 字段名
|
|
||||||
fieldName: 'apiTree',
|
|
||||||
// 界面显示的label
|
|
||||||
label: 'ApiTreeSelect',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'string',
|
|
||||||
label: 'String',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'InputNumber',
|
|
||||||
fieldName: 'number',
|
|
||||||
label: 'Number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'RadioGroup',
|
|
||||||
fieldName: 'radio',
|
|
||||||
label: 'Radio',
|
|
||||||
componentProps: {
|
|
||||||
options: [
|
|
||||||
{ value: 'A', label: 'A' },
|
|
||||||
{ value: 'B', label: 'B' },
|
|
||||||
{ value: 'C', label: 'C' },
|
|
||||||
{ value: 'D', label: 'D' },
|
|
||||||
{ value: 'E', label: 'E' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'RadioGroup',
|
|
||||||
fieldName: 'radioButton',
|
|
||||||
label: 'RadioButton',
|
|
||||||
componentProps: {
|
|
||||||
isButton: true,
|
|
||||||
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
|
|
||||||
value: v,
|
|
||||||
label: `选项${v}`,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'CheckboxGroup',
|
|
||||||
fieldName: 'checkbox',
|
|
||||||
label: 'Checkbox',
|
|
||||||
componentProps: {
|
|
||||||
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'CheckboxGroup',
|
|
||||||
fieldName: 'checkbox1',
|
|
||||||
label: 'Checkbox1',
|
|
||||||
renderComponentContent: () => {
|
|
||||||
return {
|
|
||||||
default: () => {
|
|
||||||
return ['A', 'B', 'C', 'D'].map((v) =>
|
|
||||||
h(ElCheckbox, { label: v, value: v }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'CheckboxGroup',
|
|
||||||
fieldName: 'checkbotton',
|
|
||||||
label: 'CheckBotton',
|
|
||||||
componentProps: {
|
|
||||||
isButton: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'A', label: '选项A' },
|
|
||||||
{ value: 'B', label: '选项B' },
|
|
||||||
{ value: 'C', label: '选项C' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'DatePicker',
|
|
||||||
fieldName: 'date',
|
|
||||||
label: 'Date',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'Select',
|
|
||||||
fieldName: 'select',
|
|
||||||
label: 'Select',
|
|
||||||
componentProps: {
|
|
||||||
filterable: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'A', label: '选项A' },
|
|
||||||
{ value: 'B', label: '选项B' },
|
|
||||||
{ value: 'C', label: '选项C' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
function setFormValues() {
|
|
||||||
formApi.setValues({
|
|
||||||
string: 'string',
|
|
||||||
number: 123,
|
|
||||||
radio: 'B',
|
|
||||||
radioButton: 'C',
|
|
||||||
checkbox: ['A', 'C'],
|
|
||||||
checkbotton: ['B', 'C'],
|
|
||||||
checkbox1: ['A', 'B'],
|
|
||||||
date: new Date(),
|
|
||||||
select: 'B',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<Page
|
|
||||||
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
|
|
||||||
title="表单演示"
|
|
||||||
>
|
|
||||||
<ElCard>
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="flex-auto">基础表单演示</span>
|
|
||||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<Form />
|
|
||||||
</ElCard>
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@vben/tailwind-config';
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { defineConfig } from '@vben/vite-config';
|
|
||||||
|
|
||||||
import ElementPlus from 'unplugin-element-plus/vite';
|
|
||||||
|
|
||||||
export default defineConfig(async () => {
|
|
||||||
return {
|
|
||||||
application: {},
|
|
||||||
vite: {
|
|
||||||
plugins: [
|
|
||||||
ElementPlus({
|
|
||||||
format: 'esm',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
|
||||||
// mock代理目标地址
|
|
||||||
target: 'http://localhost:5320/api',
|
|
||||||
ws: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# 应用标题
|
|
||||||
VITE_APP_TITLE=Vben Admin Naive
|
|
||||||
|
|
||||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
|
||||||
VITE_APP_NAMESPACE=vben-web-naive
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# public path
|
|
||||||
VITE_BASE=/
|
|
||||||
|
|
||||||
# Basic interface address SPA
|
|
||||||
VITE_GLOB_API_URL=/api
|
|
||||||
|
|
||||||
VITE_VISUALIZER=true
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# 端口号
|
|
||||||
VITE_PORT=5888
|
|
||||||
|
|
||||||
VITE_BASE=/
|
|
||||||
|
|
||||||
# 接口地址
|
|
||||||
VITE_GLOB_API_URL=/api
|
|
||||||
|
|
||||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
|
||||||
VITE_NITRO_MOCK=true
|
|
||||||
|
|
||||||
# 是否打开 devtools,true 为打开,false 为关闭
|
|
||||||
VITE_DEVTOOLS=false
|
|
||||||
|
|
||||||
# 是否注入全局loading
|
|
||||||
VITE_INJECT_APP_LOADING=true
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<!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?24bb3eb91dfe4ebfcbcee6952a107cb6';
|
|
||||||
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>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@vben/web-naive",
|
|
||||||
"version": "5.5.2",
|
|
||||||
"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:",
|
|
||||||
"naive-ui": "catalog:",
|
|
||||||
"pinia": "catalog:",
|
|
||||||
"vue": "catalog:",
|
|
||||||
"vue-router": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@vben/tailwind-config/postcss';
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB |
@@ -1,179 +0,0 @@
|
|||||||
/**
|
|
||||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
|
||||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
|
||||||
import type { Component, SetupContext } from 'vue';
|
|
||||||
|
|
||||||
import { message } from '#/adapter/naive';
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
|
||||||
import { $t } from '@vben/locales';
|
|
||||||
import {
|
|
||||||
NButton,
|
|
||||||
NCheckbox,
|
|
||||||
NCheckboxGroup,
|
|
||||||
NDatePicker,
|
|
||||||
NDivider,
|
|
||||||
NInput,
|
|
||||||
NInputNumber,
|
|
||||||
NRadio,
|
|
||||||
NRadioButton,
|
|
||||||
NRadioGroup,
|
|
||||||
NSelect,
|
|
||||||
NSpace,
|
|
||||||
NSwitch,
|
|
||||||
NTimePicker,
|
|
||||||
NTreeSelect,
|
|
||||||
NUpload,
|
|
||||||
} from 'naive-ui';
|
|
||||||
import { h } from 'vue';
|
|
||||||
|
|
||||||
const withDefaultPlaceholder = <T extends Component>(
|
|
||||||
component: T,
|
|
||||||
type: 'input' | 'select',
|
|
||||||
) => {
|
|
||||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
|
||||||
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
|
|
||||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
|
||||||
export type ComponentType =
|
|
||||||
| 'ApiSelect'
|
|
||||||
| 'ApiTreeSelect'
|
|
||||||
| 'Checkbox'
|
|
||||||
| 'CheckboxGroup'
|
|
||||||
| 'DatePicker'
|
|
||||||
| 'Divider'
|
|
||||||
| 'IconPicker'
|
|
||||||
| 'Input'
|
|
||||||
| 'InputNumber'
|
|
||||||
| 'RadioGroup'
|
|
||||||
| 'Select'
|
|
||||||
| 'Space'
|
|
||||||
| 'Switch'
|
|
||||||
| 'TimePicker'
|
|
||||||
| 'TreeSelect'
|
|
||||||
| 'Upload'
|
|
||||||
| BaseFormComponentType;
|
|
||||||
|
|
||||||
async function initComponentAdapter() {
|
|
||||||
const components: Partial<Record<ComponentType, Component>> = {
|
|
||||||
// 如果你的组件体积比较大,可以使用异步加载
|
|
||||||
// Button: () =>
|
|
||||||
// import('xxx').then((res) => res.Button),
|
|
||||||
|
|
||||||
ApiSelect: (props, { attrs, slots }) => {
|
|
||||||
return h(
|
|
||||||
ApiComponent,
|
|
||||||
{
|
|
||||||
placeholder: $t('ui.placeholder.select'),
|
|
||||||
...props,
|
|
||||||
...attrs,
|
|
||||||
component: NSelect,
|
|
||||||
modelPropName: 'value',
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
|
||||||
return h(
|
|
||||||
ApiComponent,
|
|
||||||
{
|
|
||||||
placeholder: $t('ui.placeholder.select'),
|
|
||||||
...props,
|
|
||||||
...attrs,
|
|
||||||
component: NTreeSelect,
|
|
||||||
nodeKey: 'value',
|
|
||||||
loadingSlot: 'arrow',
|
|
||||||
keyField: 'value',
|
|
||||||
modelPropName: 'value',
|
|
||||||
optionsPropName: 'options',
|
|
||||||
visibleEvent: 'onVisibleChange',
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Checkbox: NCheckbox,
|
|
||||||
CheckboxGroup: (props, { attrs, slots }) => {
|
|
||||||
let defaultSlot;
|
|
||||||
if (Reflect.has(slots, 'default')) {
|
|
||||||
defaultSlot = slots.default;
|
|
||||||
} else {
|
|
||||||
const { options } = attrs;
|
|
||||||
if (Array.isArray(options)) {
|
|
||||||
defaultSlot = () => options.map((option) => h(NCheckbox, option));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h(
|
|
||||||
NCheckboxGroup,
|
|
||||||
{ ...props, ...attrs },
|
|
||||||
{ default: defaultSlot },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
DatePicker: NDatePicker,
|
|
||||||
// 自定义默认按钮
|
|
||||||
DefaultButton: (props, { attrs, slots }) => {
|
|
||||||
return h(NButton, { ...props, attrs, type: 'default' }, slots);
|
|
||||||
},
|
|
||||||
// 自定义主要按钮
|
|
||||||
PrimaryButton: (props, { attrs, slots }) => {
|
|
||||||
return h(NButton, { ...props, attrs, type: 'primary' }, slots);
|
|
||||||
},
|
|
||||||
Divider: NDivider,
|
|
||||||
IconPicker: (props, { attrs, slots }) => {
|
|
||||||
return h(
|
|
||||||
IconPicker,
|
|
||||||
{ iconSlot: 'suffix', inputComponent: NInput, ...props, ...attrs },
|
|
||||||
slots,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Input: withDefaultPlaceholder(NInput, 'input'),
|
|
||||||
InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
|
|
||||||
RadioGroup: (props, { attrs, slots }) => {
|
|
||||||
let defaultSlot;
|
|
||||||
if (Reflect.has(slots, 'default')) {
|
|
||||||
defaultSlot = slots.default;
|
|
||||||
} else {
|
|
||||||
const { options } = attrs;
|
|
||||||
if (Array.isArray(options)) {
|
|
||||||
defaultSlot = () =>
|
|
||||||
options.map((option) =>
|
|
||||||
h(attrs.isButton ? NRadioButton : NRadio, option),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const groupRender = h(
|
|
||||||
NRadioGroup,
|
|
||||||
{ ...props, ...attrs },
|
|
||||||
{ default: defaultSlot },
|
|
||||||
);
|
|
||||||
return attrs.isButton
|
|
||||||
? h(NSpace, { vertical: true }, () => groupRender)
|
|
||||||
: groupRender;
|
|
||||||
},
|
|
||||||
Select: withDefaultPlaceholder(NSelect, 'select'),
|
|
||||||
Space: NSpace,
|
|
||||||
Switch: NSwitch,
|
|
||||||
TimePicker: NTimePicker,
|
|
||||||
TreeSelect: withDefaultPlaceholder(NTreeSelect, 'select'),
|
|
||||||
Upload: NUpload,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将组件注册到全局共享状态中
|
|
||||||
globalShareState.setComponents(components);
|
|
||||||
|
|
||||||
// 定义全局共享状态中的消息提示
|
|
||||||
globalShareState.defineMessage({
|
|
||||||
// 复制成功消息提示
|
|
||||||
copyPreferencesSuccess: (title, content) => {
|
|
||||||
message.success(content || title, {
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { initComponentAdapter };
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
setupVbenForm<ComponentType>({
|
|
||||||
config: {
|
|
||||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
|
||||||
emptyStateValue: null,
|
|
||||||
baseModelPropName: 'value',
|
|
||||||
modelPropNameMap: {
|
|
||||||
Checkbox: 'checked',
|
|
||||||
Radio: '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 { useVbenForm, z };
|
|
||||||
|
|
||||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
|
||||||
export type { VbenFormProps };
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import '@vben/styles';
|
|
||||||
|
|
||||||
import { createDiscreteApi, darkTheme, lightTheme } from 'naive-ui';
|
|
||||||
|
|
||||||
const themeOverridesProviderProps = computed(() => ({
|
|
||||||
themeOverrides: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const themeProviderProps = computed(() => ({
|
|
||||||
theme: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const { dialog, loadingBar, message, modal, notification } =
|
|
||||||
createDiscreteApi(
|
|
||||||
['message', 'dialog', 'notification', 'loadingBar', 'modal'],
|
|
||||||
{
|
|
||||||
configProviderProps: themeProviderProps,
|
|
||||||
loadingBarProviderProps: themeOverridesProviderProps,
|
|
||||||
messageProviderProps: themeOverridesProviderProps,
|
|
||||||
notificationProviderProps: themeOverridesProviderProps,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { h } from 'vue';
|
|
||||||
|
|
||||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
|
||||||
|
|
||||||
import { NButton, NImage } from 'naive-ui';
|
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
|
||||||
vxeUI.renderer.add('CellImage', {
|
|
||||||
renderTableDefault(_renderOpts, params) {
|
|
||||||
const { column, row } = params;
|
|
||||||
return h(NImage, { src: row[column.field] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
|
||||||
vxeUI.renderer.add('CellLink', {
|
|
||||||
renderTableDefault(renderOpts) {
|
|
||||||
const { props } = renderOpts;
|
|
||||||
return h(
|
|
||||||
NButton,
|
|
||||||
{ size: 'small', type: 'primary', quaternary: true },
|
|
||||||
{ default: () => props?.text },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
|
||||||
// vxeUI.formats.add
|
|
||||||
},
|
|
||||||
useVbenForm,
|
|
||||||
});
|
|
||||||
|
|
||||||
export { useVbenVxeGrid };
|
|
||||||
|
|
||||||
export type * from '@vben/plugins/vxe-table';
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './auth';
|
|
||||||
export * from './menu';
|
|
||||||
export * from './user';
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { RouteRecordStringComponent } from '@vben/types';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户所有菜单
|
|
||||||
*/
|
|
||||||
export async function getAllMenusApi() {
|
|
||||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { UserInfo } from '@vben/types';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息
|
|
||||||
*/
|
|
||||||
export async function getUserInfoApi() {
|
|
||||||
return requestClient.get<UserInfo>('/user/info');
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './core';
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
/**
|
|
||||||
* 该文件可自行根据业务逻辑进行调整
|
|
||||||
*/
|
|
||||||
import type { RequestClientOptions } from '@vben/request';
|
|
||||||
|
|
||||||
import { useAppConfig } from '@vben/hooks';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import {
|
|
||||||
authenticateResponseInterceptor,
|
|
||||||
errorMessageResponseInterceptor,
|
|
||||||
RequestClient,
|
|
||||||
} from '@vben/request';
|
|
||||||
import { useAccessStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import { message } from '#/adapter/naive';
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 });
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
|
||||||
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import { useNaiveDesignTokens } from '@vben/hooks';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
import {
|
|
||||||
darkTheme,
|
|
||||||
dateEnUS,
|
|
||||||
dateZhCN,
|
|
||||||
enUS,
|
|
||||||
lightTheme,
|
|
||||||
NConfigProvider,
|
|
||||||
NMessageProvider,
|
|
||||||
NNotificationProvider,
|
|
||||||
zhCN,
|
|
||||||
} from 'naive-ui';
|
|
||||||
|
|
||||||
defineOptions({ name: 'App' });
|
|
||||||
|
|
||||||
const { commonTokens } = useNaiveDesignTokens();
|
|
||||||
|
|
||||||
const tokenLocale = computed(() =>
|
|
||||||
preferences.app.locale === 'zh-CN' ? zhCN : enUS,
|
|
||||||
);
|
|
||||||
const tokenDateLocale = computed(() =>
|
|
||||||
preferences.app.locale === 'zh-CN' ? dateZhCN : dateEnUS,
|
|
||||||
);
|
|
||||||
const tokenTheme = computed(() =>
|
|
||||||
preferences.theme.mode === 'dark' ? darkTheme : lightTheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
const themeOverrides = computed((): GlobalThemeOverrides => {
|
|
||||||
return {
|
|
||||||
common: commonTokens,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NConfigProvider
|
|
||||||
:date-locale="tokenDateLocale"
|
|
||||||
:locale="tokenLocale"
|
|
||||||
:theme="tokenTheme"
|
|
||||||
:theme-overrides="themeOverrides"
|
|
||||||
class="h-full"
|
|
||||||
>
|
|
||||||
<NNotificationProvider>
|
|
||||||
<NMessageProvider>
|
|
||||||
<RouterView />
|
|
||||||
</NMessageProvider>
|
|
||||||
</NNotificationProvider>
|
|
||||||
</NConfigProvider>
|
|
||||||
</template>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { createApp, watchEffect } from 'vue';
|
|
||||||
|
|
||||||
import { registerAccessDirective } from '@vben/access';
|
|
||||||
import { initTippy } from '@vben/common-ui';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import { initStores } from '@vben/stores';
|
|
||||||
import '@vben/styles';
|
|
||||||
import '@vben/styles/naive';
|
|
||||||
|
|
||||||
import { useTitle } from '@vueuse/core';
|
|
||||||
|
|
||||||
import { $t, setupI18n } from '#/locales';
|
|
||||||
|
|
||||||
import { initComponentAdapter } from './adapter/component';
|
|
||||||
import App from './app.vue';
|
|
||||||
import { router } from './router';
|
|
||||||
|
|
||||||
async function bootstrap(namespace: string) {
|
|
||||||
// 初始化组件适配器
|
|
||||||
initComponentAdapter();
|
|
||||||
|
|
||||||
// // 设置弹窗的默认配置
|
|
||||||
// setDefaultModalProps({
|
|
||||||
// fullscreenButton: false,
|
|
||||||
// });
|
|
||||||
// // 设置抽屉的默认配置
|
|
||||||
// setDefaultDrawerProps({
|
|
||||||
// // zIndex: 2000,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
// 国际化 i18n 配置
|
|
||||||
await setupI18n(app);
|
|
||||||
|
|
||||||
// 配置 pinia-tore
|
|
||||||
await initStores(app, { namespace });
|
|
||||||
|
|
||||||
// 安装权限指令
|
|
||||||
registerAccessDirective(app);
|
|
||||||
|
|
||||||
// 初始化 tippy
|
|
||||||
initTippy(app);
|
|
||||||
|
|
||||||
// 配置路由及路由守卫
|
|
||||||
app.use(router);
|
|
||||||
|
|
||||||
// 动态更新标题
|
|
||||||
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 };
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<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, MdiGithub } 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: MdiGithub,
|
|
||||||
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(
|
|
||||||
() => preferences.app.watermark,
|
|
||||||
async (enable) => {
|
|
||||||
if (enable) {
|
|
||||||
await updateWatermark({
|
|
||||||
content: `${userStore.userInfo?.username}`,
|
|
||||||
});
|
|
||||||
} 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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const BasicLayout = () => import('./basic.vue');
|
|
||||||
const AuthPageLayout = () => import('./auth.vue');
|
|
||||||
|
|
||||||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
|
||||||
|
|
||||||
export { AuthPageLayout, BasicLayout, IFrameView };
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# locale
|
|
||||||
|
|
||||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
|
||||||
import type { App } from 'vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
$t,
|
|
||||||
setupI18n as coreSetup,
|
|
||||||
loadLocalesMapFromDir,
|
|
||||||
} from '@vben/locales';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
const modules = import.meta.glob('./langs/**/*.json');
|
|
||||||
|
|
||||||
const localesMap = loadLocalesMapFromDir(
|
|
||||||
/\.\/langs\/([^/]+)\/(.*)\.json$/,
|
|
||||||
modules,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载应用特有的语言包
|
|
||||||
* 这里也可以改造为从服务端获取翻译数据
|
|
||||||
* @param lang
|
|
||||||
*/
|
|
||||||
async function loadMessages(lang: SupportedLanguagesType) {
|
|
||||||
const appLocaleMessages = await localesMap[lang]?.();
|
|
||||||
return appLocaleMessages?.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
|
||||||
await coreSetup(app, {
|
|
||||||
defaultLocale: preferences.app.locale,
|
|
||||||
loadMessages,
|
|
||||||
missingWarn: !import.meta.env.PROD,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { $t, setupI18n };
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Demos",
|
|
||||||
"naive": "Naive UI",
|
|
||||||
"table": "Table",
|
|
||||||
"form": "Form",
|
|
||||||
"vben": {
|
|
||||||
"title": "Project",
|
|
||||||
"about": "About",
|
|
||||||
"document": "Document",
|
|
||||||
"antdv": "Ant Design Vue Version",
|
|
||||||
"naive-ui": "Naive UI Version",
|
|
||||||
"element-plus": "Element Plus Version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"auth": {
|
|
||||||
"login": "Login",
|
|
||||||
"register": "Register",
|
|
||||||
"codeLogin": "Code Login",
|
|
||||||
"qrcodeLogin": "Qr Code Login",
|
|
||||||
"forgetPassword": "Forget Password",
|
|
||||||
"oauthLogin": "OAuth Login"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "Dashboard",
|
|
||||||
"analytics": "Analytics",
|
|
||||||
"workspace": "Workspace"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "演示",
|
|
||||||
"naive": "Naive UI",
|
|
||||||
"table": "Table",
|
|
||||||
"form": "表单",
|
|
||||||
"vben": {
|
|
||||||
"title": "项目",
|
|
||||||
"about": "关于",
|
|
||||||
"document": "文档",
|
|
||||||
"antdv": "Ant Design Vue 版本",
|
|
||||||
"naive-ui": "Naive UI 版本",
|
|
||||||
"element-plus": "Element Plus 版本"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"auth": {
|
|
||||||
"login": "登录",
|
|
||||||
"register": "注册",
|
|
||||||
"codeLogin": "验证码登陆",
|
|
||||||
"qrcodeLogin": "二维码登陆",
|
|
||||||
"forgetPassword": "忘记密码",
|
|
||||||
"oauthLogin": "第三方登录"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "概览",
|
|
||||||
"analytics": "分析页",
|
|
||||||
"workspace": "工作台"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { defineOverridesPreferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 项目配置文件
|
|
||||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
|
||||||
* !!! 更改配置后请清空缓存,否则可能不生效
|
|
||||||
*/
|
|
||||||
export const overridesPreferences = defineOverridesPreferences({
|
|
||||||
// overrides
|
|
||||||
app: {
|
|
||||||
name: import.meta.env.VITE_APP_TITLE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import type {
|
|
||||||
ComponentRecordType,
|
|
||||||
GenerateMenuAndRoutesOptions,
|
|
||||||
} from '@vben/types';
|
|
||||||
|
|
||||||
import { generateAccessible } from '@vben/access';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
import { message } from '#/adapter/naive';
|
|
||||||
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(`${$t('common.loadingMenu')}...`, {
|
|
||||||
duration: 1.5,
|
|
||||||
});
|
|
||||||
return await getAllMenusApi();
|
|
||||||
},
|
|
||||||
// 可以指定没有权限跳转403页面
|
|
||||||
forbiddenComponent,
|
|
||||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
|
||||||
layoutMap,
|
|
||||||
pageMap,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { generateAccess };
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import type { Router } from 'vue-router';
|
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, 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(async (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 ||
|
|
||||||
DEFAULT_HOME_PATH,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 === DEFAULT_HOME_PATH
|
|
||||||
? {}
|
|
||||||
: { 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 === DEFAULT_HOME_PATH
|
|
||||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
|
||||||
: to.fullPath)) as string;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...router.resolve(decodeURIComponent(redirectPath)),
|
|
||||||
replace: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目守卫配置
|
|
||||||
* @param router
|
|
||||||
*/
|
|
||||||
function createRouterGuard(router: Router) {
|
|
||||||
/** 通用 */
|
|
||||||
setupCommonGuard(router);
|
|
||||||
/** 权限访问 */
|
|
||||||
setupAccessGuard(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { createRouterGuard };
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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 };
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user