提交
This commit is contained in:
5
playground/.env
Normal file
5
playground/.env
Normal file
@@ -0,0 +1,5 @@
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=Vben Admin
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-play
|
||||
7
playground/.env.analyze
Normal file
7
playground/.env.analyze
Normal file
@@ -0,0 +1,7 @@
|
||||
# public path
|
||||
VITE_BASE=/
|
||||
|
||||
# Basic interface address SPA
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
VITE_VISUALIZER=true
|
||||
16
playground/.env.development
Normal file
16
playground/.env.development
Normal file
@@ -0,0 +1,16 @@
|
||||
# 端口号
|
||||
VITE_PORT=5555
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||
VITE_NITRO_MOCK=false
|
||||
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
20
playground/.env.production
Normal file
20
playground/.env.production
Normal file
@@ -0,0 +1,20 @@
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
# VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||
VITE_GLOB_API_URL=http://127.0.0.1:5000/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
|
||||
20
playground/__tests__/e2e/auth-login.spec.ts
Normal file
20
playground/__tests__/e2e/auth-login.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { authLogin } from './common/auth';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Auth Login Page Tests', () => {
|
||||
test('check title and page elements', async ({ page }) => {
|
||||
// 获取页面标题并断言标题包含 'Vben Admin'
|
||||
const title = await page.title();
|
||||
expect(title).toContain('Vben Admin');
|
||||
});
|
||||
|
||||
// 测试用例: 成功登录
|
||||
test('should successfully login with valid credentials', async ({ page }) => {
|
||||
await authLogin(page);
|
||||
});
|
||||
});
|
||||
46
playground/__tests__/e2e/common/auth.ts
Normal file
46
playground/__tests__/e2e/common/auth.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
export async function authLogin(page: Page) {
|
||||
// 确保登录表单正常
|
||||
const usernameInput = await page.locator(`input[name='username']`);
|
||||
await expect(usernameInput).toBeVisible();
|
||||
|
||||
const passwordInput = await page.locator(`input[name='password']`);
|
||||
await expect(passwordInput).toBeVisible();
|
||||
|
||||
const sliderCaptcha = await page.locator(`div[name='captcha']`);
|
||||
const sliderCaptchaAction = await page.locator(`div[name='captcha-action']`);
|
||||
await expect(sliderCaptcha).toBeVisible();
|
||||
await expect(sliderCaptchaAction).toBeVisible();
|
||||
|
||||
// 拖动验证码滑块
|
||||
// 获取拖动按钮的位置
|
||||
const sliderCaptchaBox = await sliderCaptcha.boundingBox();
|
||||
if (!sliderCaptchaBox) throw new Error('滑块未找到');
|
||||
|
||||
const actionBoundingBox = await sliderCaptchaAction.boundingBox();
|
||||
if (!actionBoundingBox) throw new Error('要拖动的按钮未找到');
|
||||
|
||||
// 计算起始位置和目标位置
|
||||
const startX = actionBoundingBox.x + actionBoundingBox.width / 2; // div 中心的 x 坐标
|
||||
const startY = actionBoundingBox.y + actionBoundingBox.height / 2; // div 中心的 y 坐标
|
||||
|
||||
const targetX = startX + sliderCaptchaBox.width + actionBoundingBox.width; // 向右拖动容器的宽度
|
||||
const targetY = startY; // y 坐标保持不变
|
||||
|
||||
// 模拟鼠标拖动
|
||||
await page.mouse.move(startX, startY); // 移动到 action 的中心
|
||||
await page.mouse.down(); // 按下鼠标
|
||||
await page.mouse.move(targetX, targetY, { steps: 20 }); // 拖动到目标位置
|
||||
await page.mouse.up(); // 松开鼠标
|
||||
|
||||
// 在拖动后进行断言,检查action是否在预期位置,
|
||||
const newActionBoundingBox = await sliderCaptchaAction.boundingBox();
|
||||
expect(newActionBoundingBox?.x).toBeGreaterThan(actionBoundingBox.x);
|
||||
|
||||
// 到这里已经校验成功,点击进行登录
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByRole('button', { name: 'login' }).click();
|
||||
}
|
||||
35
playground/index.html
Normal file
35
playground/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta name="description" content="A Modern Back-end Management System" />
|
||||
<meta name="keywords" content="Vben Admin Vue3 Vite" />
|
||||
<meta name="author" content="Vben" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||
/>
|
||||
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||
<title><%= VITE_APP_TITLE %></title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script>
|
||||
// 生产环境下注入百度统计
|
||||
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement('script');
|
||||
hm.src =
|
||||
'https://hm.baidu.com/hm.js?d20a01273820422b6aa2ee41b6c9414d';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
55
playground/package.json
Normal file
55
playground/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@vben/playground",
|
||||
"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": "playground"
|
||||
},
|
||||
"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",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e-ui": "playwright test --ui",
|
||||
"test:e2e-codegen": "playwright codegen"
|
||||
},
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/vue-query": "catalog:",
|
||||
"@vben-core/menu-ui": "workspace:*",
|
||||
"@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:",
|
||||
"ant-design-vue": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
}
|
||||
}
|
||||
108
playground/playwright.config.ts
Normal file
108
playground/playwright.config.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
import { devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: 'node_modules/.e2e/test-results/',
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: {
|
||||
// ...devices['Desktop Firefox'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: {
|
||||
// ...devices['Desktop Safari'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', { outputFolder: 'node_modules/.e2e/test-results' }],
|
||||
],
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
testDir: './__tests__/e2e',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:5555',
|
||||
/* Only on CI systems run the tests headless */
|
||||
headless: !!process.env.CI,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: process.env.CI ? 'pnpm preview --port 5555' : 'pnpm dev',
|
||||
port: 5555,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
playground/postcss.config.mjs
Normal file
1
playground/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
BIN
playground/public/favicon.ico
Normal file
BIN
playground/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
170
playground/src/adapter/component/index.ts
Normal file
170
playground/src/adapter/component/index.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
notification,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Upload,
|
||||
Modal
|
||||
} from 'ant-design-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'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
| 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
IconPicker,
|
||||
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
description: content,
|
||||
message: title,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
45
playground/src/adapter/form.ts
Normal file
45
playground/src/adapter/form.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { useVbenForm, z };
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
68
playground/src/adapter/vxe-table.ts
Normal file
68
playground/src/adapter/vxe-table.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
minHeight: 180,
|
||||
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(Image, { src: row[column.field] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
51
playground/src/api/core/auth.ts
Normal file
51
playground/src/api/core/auth.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
password?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新accessToken
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return baseRequestClient.post('/auth/logout', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
export async function getAccessCodesApi() {
|
||||
return requestClient.get<string[]>('/auth/codes');
|
||||
}
|
||||
3
playground/src/api/core/index.ts
Normal file
3
playground/src/api/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
10
playground/src/api/core/menu.ts
Normal file
10
playground/src/api/core/menu.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||
}
|
||||
10
playground/src/api/core/user.ts
Normal file
10
playground/src/api/core/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { UserInfo } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/auth/userinfo');
|
||||
}
|
||||
2
playground/src/api/examples/index.ts
Normal file
2
playground/src/api/examples/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './status';
|
||||
export * from './table';
|
||||
10
playground/src/api/examples/status.ts
Normal file
10
playground/src/api/examples/status.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 模拟任意状态码
|
||||
*/
|
||||
async function getMockStatusApi(status: string) {
|
||||
return requestClient.get('/status', { params: { status } });
|
||||
}
|
||||
|
||||
export { getMockStatusApi };
|
||||
18
playground/src/api/examples/table.ts
Normal file
18
playground/src/api/examples/table.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace DemoTableApi {
|
||||
export interface PageFetchParams {
|
||||
[key: string]: any;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取示例表格数据
|
||||
*/
|
||||
async function getExampleTableApi(params: DemoTableApi.PageFetchParams) {
|
||||
return requestClient.get('/table/list', { params });
|
||||
}
|
||||
|
||||
export { getExampleTableApi };
|
||||
2
playground/src/api/index.ts
Normal file
2
playground/src/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './core';
|
||||
export * from './examples';
|
||||
114
playground/src/api/request.ts
Normal file
114
playground/src/api/request.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } 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 'ant-design-vue';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
function createRequestClient(baseURL: string) {
|
||||
const client = new RequestClient({
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
const { code, data } = responseData;
|
||||
|
||||
if (status >= 200 && status < 400 && code === 0) {
|
||||
return data;
|
||||
}
|
||||
throw Object.assign({}, response, { response });
|
||||
},
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
39
playground/src/app.vue
Normal file
39
playground/src/app.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useAntdDesignTokens } from '@vben/hooks';
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
|
||||
import { App, ConfigProvider, theme } from 'ant-design-vue';
|
||||
|
||||
import { antdLocale } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
const { tokens } = useAntdDesignTokens();
|
||||
|
||||
const tokenTheme = computed(() => {
|
||||
const algorithm = isDark.value
|
||||
? [theme.darkAlgorithm]
|
||||
: [theme.defaultAlgorithm];
|
||||
|
||||
// antd 紧凑模式算法
|
||||
if (preferences.app.compact) {
|
||||
algorithm.push(theme.compactAlgorithm);
|
||||
}
|
||||
|
||||
return {
|
||||
algorithm,
|
||||
token: tokens,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
||||
<App>
|
||||
<RouterView />
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
52
playground/src/bootstrap.ts
Normal file
52
playground/src/bootstrap.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
import '@vben/styles/antd';
|
||||
|
||||
import { VueQueryPlugin } from '@tanstack/vue-query';
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import App from './app.vue';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
// 配置 pinia-tore
|
||||
await initStores(app, { namespace });
|
||||
|
||||
// 安装权限指令
|
||||
registerAccessDirective(app);
|
||||
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
// 配置@tanstack/vue-query
|
||||
app.use(VueQueryPlugin);
|
||||
|
||||
// 动态更新标题
|
||||
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 };
|
||||
23
playground/src/layouts/auth.vue
Normal file
23
playground/src/layouts/auth.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthPageLayout } from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const appName = computed(() => preferences.app.name);
|
||||
const logo = computed(() => preferences.logo.source);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthPageLayout
|
||||
:app-name="appName"
|
||||
:logo="logo"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
>
|
||||
<!-- 自定义工具栏 -->
|
||||
<!-- <template #toolbar></template> -->
|
||||
</AuthPageLayout>
|
||||
</template>
|
||||
159
playground/src/layouts/basic.vue
Normal file
159
playground/src/layouts/basic.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<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"
|
||||
trigger="both"
|
||||
@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>
|
||||
6
playground/src/layouts/index.ts
Normal file
6
playground/src/layouts/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const BasicLayout = () => import('./basic.vue');
|
||||
const AuthPageLayout = () => import('./auth.vue');
|
||||
|
||||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||
|
||||
export { AuthPageLayout, BasicLayout, IFrameView };
|
||||
3
playground/src/locales/README.md
Normal file
3
playground/src/locales/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# locale
|
||||
|
||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||
100
playground/src/locales/index.ts
Normal file
100
playground/src/locales/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
import type { Locale } from 'ant-design-vue/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
$t,
|
||||
setupI18n as coreSetup,
|
||||
loadLocalesMapFromDir,
|
||||
} from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
|
||||
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const antdLocale = ref<Locale>(antdDefaultLocale);
|
||||
|
||||
const modules = import.meta.glob('./langs/**/*.json');
|
||||
|
||||
const localesMap = loadLocalesMapFromDir(
|
||||
/\.\/langs\/([^/]+)\/(.*)\.json$/,
|
||||
modules,
|
||||
);
|
||||
/**
|
||||
* 加载应用特有的语言包
|
||||
* 这里也可以改造为从服务端获取翻译数据
|
||||
* @param lang
|
||||
*/
|
||||
async function loadMessages(lang: SupportedLanguagesType) {
|
||||
const [appLocaleMessages] = await Promise.all([
|
||||
localesMap[lang]?.(),
|
||||
loadThirdPartyMessage(lang),
|
||||
]);
|
||||
return appLocaleMessages?.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载第三方组件库的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
|
||||
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载dayjs的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||
let locale;
|
||||
switch (lang) {
|
||||
case 'en-US': {
|
||||
locale = await import('dayjs/locale/en');
|
||||
break;
|
||||
}
|
||||
case 'zh-CN': {
|
||||
locale = await import('dayjs/locale/zh-cn');
|
||||
break;
|
||||
}
|
||||
// 默认使用英语
|
||||
default: {
|
||||
locale = await import('dayjs/locale/en');
|
||||
}
|
||||
}
|
||||
if (locale) {
|
||||
dayjs.locale(locale);
|
||||
} else {
|
||||
console.error(`Failed to load dayjs locale for ${lang}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载antd的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadAntdLocale(lang: SupportedLanguagesType) {
|
||||
switch (lang) {
|
||||
case 'en-US': {
|
||||
antdLocale.value = antdEnLocale;
|
||||
break;
|
||||
}
|
||||
case 'zh-CN': {
|
||||
antdLocale.value = antdDefaultLocale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||
await coreSetup(app, {
|
||||
defaultLocale: preferences.app.locale,
|
||||
loadMessages,
|
||||
missingWarn: !import.meta.env.PROD,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export { $t, antdLocale, setupI18n };
|
||||
70
playground/src/locales/langs/en-US/demos.json
Normal file
70
playground/src/locales/langs/en-US/demos.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"title": "Demos",
|
||||
"access": {
|
||||
"frontendPermissions": "Frontend Permissions",
|
||||
"backendPermissions": "Backend Permissions",
|
||||
"pageAccess": "Page Access",
|
||||
"buttonControl": "Button Control",
|
||||
"menuVisible403": "Menu Visible(403)",
|
||||
"superVisible": "Visible to Super",
|
||||
"adminVisible": "Visible to Admin",
|
||||
"userVisible": "Visible to User"
|
||||
},
|
||||
"nested": {
|
||||
"title": "Nested Menu",
|
||||
"menu1": "Menu 1",
|
||||
"menu2": "Menu 2",
|
||||
"menu2_1": "Menu 2-1",
|
||||
"menu3": "Menu 3",
|
||||
"menu3_1": "Menu 3-1",
|
||||
"menu3_2": "Menu 3-2",
|
||||
"menu3_2_1": "Menu 3-2-1"
|
||||
},
|
||||
"outside": {
|
||||
"title": "External Pages",
|
||||
"embedded": "Embedded",
|
||||
"externalLink": "External Link"
|
||||
},
|
||||
"badge": {
|
||||
"title": "Menu Badge",
|
||||
"dot": "Dot Badge",
|
||||
"text": "Text Badge",
|
||||
"color": "Badge Color"
|
||||
},
|
||||
"activeIcon": {
|
||||
"title": "Active Menu Icon",
|
||||
"children": "Children Active Icon"
|
||||
},
|
||||
"fallback": {
|
||||
"title": "Fallback Page"
|
||||
},
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"hideChildrenInMenu": "Hide Menu Children",
|
||||
"loginExpired": "Login Expired",
|
||||
"icons": "Icons",
|
||||
"watermark": "Watermark",
|
||||
"tabs": "Tabs",
|
||||
"tabDetail": "Tab Detail Page",
|
||||
"fullScreen": "FullScreen",
|
||||
"clipboard": "Clipboard",
|
||||
"menuWithQuery": "Menu With Query",
|
||||
"openInNewWindow": "Open in New Window",
|
||||
"fileDownload": "File Download"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"navigation": "Breadcrumb Navigation",
|
||||
"lateral": "Lateral Mode",
|
||||
"lateralDetail": "Lateral Mode Detail",
|
||||
"level": "Level Mode",
|
||||
"levelDetail": "Level Mode Detail"
|
||||
},
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
"document": "Document",
|
||||
"antdv": "Ant Design Vue Version",
|
||||
"naive-ui": "Naive UI Version",
|
||||
"element-plus": "Element Plus Version"
|
||||
}
|
||||
}
|
||||
66
playground/src/locales/langs/en-US/examples.json
Normal file
66
playground/src/locales/langs/en-US/examples.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"title": "Examples",
|
||||
"modal": {
|
||||
"title": "Modal"
|
||||
},
|
||||
"drawer": {
|
||||
"title": "Drawer"
|
||||
},
|
||||
"ellipsis": {
|
||||
"title": "EllipsisText"
|
||||
},
|
||||
"form": {
|
||||
"title": "Form",
|
||||
"basic": "Basic Form",
|
||||
"query": "Query Form",
|
||||
"rules": "Form Rules",
|
||||
"dynamic": "Dynamic Form",
|
||||
"custom": "Custom Component",
|
||||
"api": "Api",
|
||||
"merge": "Merge Form"
|
||||
},
|
||||
"vxeTable": {
|
||||
"title": "Vxe Table",
|
||||
"basic": "Basic Table",
|
||||
"remote": "Remote Load",
|
||||
"tree": "Tree Table",
|
||||
"fixed": "Fixed Header/Column",
|
||||
"virtual": "Virtual Scroll",
|
||||
"editCell": "Edit Cell",
|
||||
"editRow": "Edit Row",
|
||||
"custom-cell": "Custom Cell",
|
||||
"form": "Form Table"
|
||||
},
|
||||
"captcha": {
|
||||
"title": "Captcha",
|
||||
"pointSelection": "Point Selection Captcha",
|
||||
"sliderCaptcha": "Slider Captcha",
|
||||
"sliderRotateCaptcha": "Rotate Captcha",
|
||||
"captchaCardTitle": "Please complete the security verification",
|
||||
"pageDescription": "Verify user identity by clicking on specific locations in the image.",
|
||||
"pageTitle": "Captcha Component Example",
|
||||
"basic": "Basic Usage",
|
||||
"titlePlaceholder": "Captcha Title Text",
|
||||
"captchaImageUrlPlaceholder": "Captcha Image (supports img tag src attribute value)",
|
||||
"hintImage": "Hint Image",
|
||||
"hintText": "Hint Text",
|
||||
"hintImagePlaceholder": "Hint Image (supports img tag src attribute value)",
|
||||
"hintTextPlaceholder": "Hint Text",
|
||||
"showConfirm": "Show Confirm",
|
||||
"hideConfirm": "Hide Confirm",
|
||||
"widthPlaceholder": "Captcha Image Width Default 300px",
|
||||
"heightPlaceholder": "Captcha Image Height Default 220px",
|
||||
"paddingXPlaceholder": "Horizontal Padding Default 12px",
|
||||
"paddingYPlaceholder": "Vertical Padding Default 16px",
|
||||
"index": "Index:",
|
||||
"timestamp": "Timestamp:",
|
||||
"x": "x:",
|
||||
"y": "y:"
|
||||
},
|
||||
"resize": {
|
||||
"title": "Resize"
|
||||
},
|
||||
"layout": {
|
||||
"col-page": "ColPage Layout"
|
||||
}
|
||||
}
|
||||
16
playground/src/locales/langs/en-US/page.json
Normal file
16
playground/src/locales/langs/en-US/page.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password",
|
||||
"sendingCode": "SMS Code is sending...",
|
||||
"codeSentTo": "Code has been sent to {0}"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"analytics": "Analytics",
|
||||
"workspace": "Workspace"
|
||||
}
|
||||
}
|
||||
70
playground/src/locales/langs/zh-CN/demos.json
Normal file
70
playground/src/locales/langs/zh-CN/demos.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"title": "演示",
|
||||
"access": {
|
||||
"frontendPermissions": "前端权限",
|
||||
"backendPermissions": "后端权限",
|
||||
"pageAccess": "页面访问",
|
||||
"buttonControl": "按钮控制",
|
||||
"menuVisible403": "菜单可见(403)",
|
||||
"superVisible": "Super 可见",
|
||||
"adminVisible": "Admin 可见",
|
||||
"userVisible": "User 可见"
|
||||
},
|
||||
"nested": {
|
||||
"title": "嵌套菜单",
|
||||
"menu1": "菜单 1",
|
||||
"menu2": "菜单 2",
|
||||
"menu2_1": "菜单 2-1",
|
||||
"menu3": "菜单 3",
|
||||
"menu3_1": "菜单 3-1",
|
||||
"menu3_2": "菜单 3-2",
|
||||
"menu3_2_1": "菜单 3-2-1"
|
||||
},
|
||||
"outside": {
|
||||
"title": "外部页面",
|
||||
"embedded": "内嵌",
|
||||
"externalLink": "外链"
|
||||
},
|
||||
"badge": {
|
||||
"title": "菜单徽标",
|
||||
"dot": "点徽标",
|
||||
"text": "文本徽标",
|
||||
"color": "徽标颜色"
|
||||
},
|
||||
"activeIcon": {
|
||||
"title": "菜单激活图标",
|
||||
"children": "子级激活图标"
|
||||
},
|
||||
"fallback": {
|
||||
"title": "缺省页"
|
||||
},
|
||||
"features": {
|
||||
"title": "功能",
|
||||
"hideChildrenInMenu": "隐藏子菜单",
|
||||
"loginExpired": "登录过期",
|
||||
"icons": "图标",
|
||||
"watermark": "水印",
|
||||
"tabs": "标签页",
|
||||
"tabDetail": "标签详情页",
|
||||
"fullScreen": "全屏",
|
||||
"clipboard": "剪贴板",
|
||||
"menuWithQuery": "带参菜单",
|
||||
"openInNewWindow": "新窗口打开",
|
||||
"fileDownload": "文件下载"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"navigation": "面包屑导航",
|
||||
"lateral": "平级模式",
|
||||
"level": "层级模式",
|
||||
"levelDetail": "层级模式详情",
|
||||
"lateralDetail": "平级模式详情"
|
||||
},
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
"document": "文档",
|
||||
"antdv": "Ant Design Vue 版本",
|
||||
"naive-ui": "Naive UI 版本",
|
||||
"element-plus": "Element Plus 版本"
|
||||
}
|
||||
}
|
||||
66
playground/src/locales/langs/zh-CN/examples.json
Normal file
66
playground/src/locales/langs/zh-CN/examples.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"title": "示例",
|
||||
"modal": {
|
||||
"title": "弹窗"
|
||||
},
|
||||
"drawer": {
|
||||
"title": "抽屉"
|
||||
},
|
||||
"ellipsis": {
|
||||
"title": "文本省略"
|
||||
},
|
||||
"resize": {
|
||||
"title": "拖动调整"
|
||||
},
|
||||
"form": {
|
||||
"title": "表单",
|
||||
"basic": "基础表单",
|
||||
"query": "查询表单",
|
||||
"rules": "表单校验",
|
||||
"dynamic": "动态表单",
|
||||
"custom": "自定义组件",
|
||||
"api": "Api",
|
||||
"merge": "合并表单"
|
||||
},
|
||||
"vxeTable": {
|
||||
"title": "Vxe 表格",
|
||||
"basic": "基础表格",
|
||||
"remote": "远程加载",
|
||||
"tree": "树形表格",
|
||||
"fixed": "固定表头/列",
|
||||
"virtual": "虚拟滚动",
|
||||
"editCell": "单元格编辑",
|
||||
"editRow": "行编辑",
|
||||
"custom-cell": "自定义单元格",
|
||||
"form": "搜索表单"
|
||||
},
|
||||
"captcha": {
|
||||
"title": "验证码",
|
||||
"pointSelection": "点选验证",
|
||||
"sliderCaptcha": "滑块验证",
|
||||
"sliderRotateCaptcha": "旋转验证",
|
||||
"captchaCardTitle": "请完成安全验证",
|
||||
"pageDescription": "通过点击图片中的特定位置来验证用户身份。",
|
||||
"pageTitle": "验证码组件示例",
|
||||
"basic": "基本使用",
|
||||
"titlePlaceholder": "验证码标题文案",
|
||||
"captchaImageUrlPlaceholder": "验证码图片(支持img标签src属性值)",
|
||||
"hintImage": "提示图片",
|
||||
"hintText": "提示文本",
|
||||
"hintImagePlaceholder": "提示图片(支持img标签src属性值)",
|
||||
"hintTextPlaceholder": "提示文本",
|
||||
"showConfirm": "展示确认",
|
||||
"hideConfirm": "隐藏确认",
|
||||
"widthPlaceholder": "验证码图片宽度 默认300px",
|
||||
"heightPlaceholder": "验证码图片高度 默认220px",
|
||||
"paddingXPlaceholder": "水平内边距 默认12px",
|
||||
"paddingYPlaceholder": "垂直内边距 默认16px",
|
||||
"index": "索引:",
|
||||
"timestamp": "时间戳:",
|
||||
"x": "x:",
|
||||
"y": "y:"
|
||||
},
|
||||
"layout": {
|
||||
"col-page": "双列布局"
|
||||
}
|
||||
}
|
||||
16
playground/src/locales/langs/zh-CN/page.json
Normal file
16
playground/src/locales/langs/zh-CN/page.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码",
|
||||
"sendingCode": "正在发送验证码",
|
||||
"codeSentTo": "验证码已发送至{0}"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
"analytics": "分析页",
|
||||
"workspace": "工作台"
|
||||
}
|
||||
}
|
||||
31
playground/src/main.ts
Normal file
31
playground/src/main.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { initPreferences } from '@vben/preferences';
|
||||
import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
*/
|
||||
async function initApplication() {
|
||||
// name用于指定项目唯一标识
|
||||
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
||||
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION;
|
||||
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
|
||||
|
||||
// app偏好设置初始化
|
||||
await initPreferences({
|
||||
namespace,
|
||||
overrides: overridesPreferences,
|
||||
});
|
||||
|
||||
// 启动应用并挂载
|
||||
// vue应用主要逻辑及视图
|
||||
const { bootstrap } = await import('./bootstrap');
|
||||
await bootstrap(namespace);
|
||||
|
||||
// 移除并销毁loading
|
||||
unmountGlobalLoading();
|
||||
}
|
||||
|
||||
initApplication();
|
||||
13
playground/src/preferences.ts
Normal file
13
playground/src/preferences.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
|
||||
/**
|
||||
* @description 项目配置文件
|
||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||
*/
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
},
|
||||
});
|
||||
42
playground/src/router/access.ts
Normal file
42
playground/src/router/access.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
} from '@vben/types';
|
||||
|
||||
import { generateAccessible } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { getAllMenusApi } from '#/api';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
IFrameView,
|
||||
};
|
||||
|
||||
return await generateAccessible(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
message.loading({
|
||||
content: `${$t('common.loadingMenu')}...`,
|
||||
duration: 1.5,
|
||||
});
|
||||
return await getAllMenusApi();
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
forbiddenComponent,
|
||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
layoutMap,
|
||||
pageMap,
|
||||
});
|
||||
}
|
||||
|
||||
export { generateAccess };
|
||||
131
playground/src/router/guard.ts
Normal file
131
playground/src/router/guard.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
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 };
|
||||
37
playground/src/router/index.ts
Normal file
37
playground/src/router/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
createWebHistory,
|
||||
} from 'vue-router';
|
||||
|
||||
import { resetStaticRoutes } from '@vben/utils';
|
||||
|
||||
import { createRouterGuard } from './guard';
|
||||
import { routes } from './routes';
|
||||
|
||||
/**
|
||||
* @zh_CN 创建vue-router实例
|
||||
*/
|
||||
const router = createRouter({
|
||||
history:
|
||||
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
|
||||
? createWebHashHistory(import.meta.env.VITE_BASE)
|
||||
: createWebHistory(import.meta.env.VITE_BASE),
|
||||
// 应该添加到路由的初始路由列表。
|
||||
routes,
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
|
||||
},
|
||||
// 是否应该禁止尾部斜杠。
|
||||
// strict: true,
|
||||
});
|
||||
|
||||
const resetRoutes = () => resetStaticRoutes(router, routes);
|
||||
|
||||
// 创建路由守卫
|
||||
createRouterGuard(router);
|
||||
|
||||
export { resetRoutes, router };
|
||||
88
playground/src/router/routes/core.ts
Normal file
88
playground/src/router/routes/core.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
import { AuthPageLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
import Login from '#/views/_core/authentication/login.vue';
|
||||
|
||||
/** 全局404页面 */
|
||||
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
title: '404',
|
||||
},
|
||||
name: 'FallbackNotFound',
|
||||
path: '/:path(.*)*',
|
||||
};
|
||||
|
||||
/** 基本路由,这些路由是必须存在的 */
|
||||
const coreRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: DEFAULT_HOME_PATH,
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
meta: {
|
||||
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 };
|
||||
37
playground/src/router/routes/index.ts
Normal file
37
playground/src/router/routes/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||
|
||||
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||
|
||||
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||
|
||||
/** 动态路由 */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
fallbackNotFoundRoute,
|
||||
];
|
||||
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
40
playground/src/router/routes/modules/dashboard.ts
Normal file
40
playground/src/router/routes/modules/dashboard.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
path: '/analytics',
|
||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||
meta: {
|
||||
affixTab: true,
|
||||
icon: 'lucide:area-chart',
|
||||
title: $t('page.dashboard.analytics'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
path: '/workspace',
|
||||
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||
meta: {
|
||||
icon: 'carbon:workspace',
|
||||
title: $t('page.dashboard.workspace'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
572
playground/src/router/routes/modules/demos.ts
Normal file
572
playground/src/router/routes/modules/demos.ts
Normal file
@@ -0,0 +1,572 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: $t('demos.title'),
|
||||
},
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
children: [
|
||||
// 权限控制
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:shield-key-outline',
|
||||
title: $t('demos.access.frontendPermissions'),
|
||||
},
|
||||
name: 'AccessDemos',
|
||||
path: '/demos/access',
|
||||
children: [
|
||||
{
|
||||
name: 'AccessPageControlDemo',
|
||||
path: '/demos/access/page-control',
|
||||
component: () => import('#/views/demos/access/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:page-previous-outline',
|
||||
title: $t('demos.access.pageAccess'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessButtonControlDemo',
|
||||
path: '/demos/access/button-control',
|
||||
component: () => import('#/views/demos/access/button-control.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.buttonControl'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessMenuVisible403Demo',
|
||||
path: '/demos/access/menu-visible-403',
|
||||
component: () =>
|
||||
import('#/views/demos/access/menu-visible-403.vue'),
|
||||
meta: {
|
||||
authority: ['no-body'],
|
||||
icon: 'mdi:button-cursor',
|
||||
menuVisibleWithForbidden: true,
|
||||
title: $t('demos.access.menuVisible403'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessSuperVisibleDemo',
|
||||
path: '/demos/access/super-visible',
|
||||
component: () => import('#/views/demos/access/super-visible.vue'),
|
||||
meta: {
|
||||
authority: ['super'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.superVisible'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessAdminVisibleDemo',
|
||||
path: '/demos/access/admin-visible',
|
||||
component: () => import('#/views/demos/access/admin-visible.vue'),
|
||||
meta: {
|
||||
authority: ['admin'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.adminVisible'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessUserVisibleDemo',
|
||||
path: '/demos/access/user-visible',
|
||||
component: () => import('#/views/demos/access/user-visible.vue'),
|
||||
meta: {
|
||||
authority: ['user'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.userVisible'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 功能
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:feature-highlight',
|
||||
title: $t('demos.features.title'),
|
||||
},
|
||||
name: 'FeaturesDemos',
|
||||
path: '/demos/features',
|
||||
children: [
|
||||
{
|
||||
name: 'LoginExpiredDemo',
|
||||
path: '/demos/features/login-expired',
|
||||
component: () =>
|
||||
import('#/views/demos/features/login-expired/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:encryption-expiration',
|
||||
title: $t('demos.features.loginExpired'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IconsDemo',
|
||||
path: '/demos/features/icons',
|
||||
component: () => import('#/views/demos/features/icons/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:annoyed',
|
||||
title: $t('demos.features.icons'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WatermarkDemo',
|
||||
path: '/demos/features/watermark',
|
||||
component: () =>
|
||||
import('#/views/demos/features/watermark/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:tags',
|
||||
title: $t('demos.features.watermark'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FeatureTabsDemo',
|
||||
path: '/demos/features/tabs',
|
||||
component: () => import('#/views/demos/features/tabs/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:app-window',
|
||||
title: $t('demos.features.tabs'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FeatureTabDetailDemo',
|
||||
path: '/demos/features/tabs/detail/:id',
|
||||
component: () =>
|
||||
import('#/views/demos/features/tabs/tab-detail.vue'),
|
||||
meta: {
|
||||
activePath: '/demos/features/tabs',
|
||||
hideInMenu: true,
|
||||
maxNumOfOpenTab: 3,
|
||||
title: $t('demos.features.tabDetail'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'HideChildrenInMenuParentDemo',
|
||||
path: '/demos/features/hide-menu-children',
|
||||
component: () =>
|
||||
import('#/views/demos/features/hide-menu-children/parent.vue'),
|
||||
meta: {
|
||||
hideChildrenInMenu: true,
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.features.hideChildrenInMenu'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'HideChildrenInMenuDemo',
|
||||
path: '',
|
||||
component: () =>
|
||||
import(
|
||||
'#/views/demos/features/hide-menu-children/children.vue'
|
||||
),
|
||||
meta: {
|
||||
hideInMenu: true,
|
||||
title: $t('demos.features.hideChildrenInMenu'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'HideChildrenInMenuChildrenDemo',
|
||||
path: '/demos/features/hide-menu-children/children',
|
||||
component: () =>
|
||||
import(
|
||||
'#/views/demos/features/hide-menu-children/children.vue'
|
||||
),
|
||||
meta: { title: $t('demos.features.hideChildrenInMenu') },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'FullScreenDemo',
|
||||
path: '/demos/features/full-screen',
|
||||
component: () =>
|
||||
import('#/views/demos/features/full-screen/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:fullscreen',
|
||||
title: $t('demos.features.fullScreen'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FileDownloadDemo',
|
||||
path: '/demos/features/file-download',
|
||||
component: () =>
|
||||
import('#/views/demos/features/file-download/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:hard-drive-download',
|
||||
title: $t('demos.features.fileDownload'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ClipboardDemo',
|
||||
path: '/demos/features/clipboard',
|
||||
component: () =>
|
||||
import('#/views/demos/features/clipboard/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copy',
|
||||
title: $t('demos.features.clipboard'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MenuQueryDemo',
|
||||
path: '/demos/menu-query',
|
||||
component: () =>
|
||||
import('#/views/demos/features/menu-query/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:curly-braces',
|
||||
query: {
|
||||
id: 1,
|
||||
},
|
||||
title: $t('demos.features.menuWithQuery'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NewWindowDemo',
|
||||
path: '/demos/new-window',
|
||||
component: () =>
|
||||
import('#/views/demos/features/new-window/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:app-window',
|
||||
openInNewWindow: true,
|
||||
title: $t('demos.features.openInNewWindow'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VueQueryDemo',
|
||||
path: '/demos/features/vue-query',
|
||||
component: () =>
|
||||
import('#/views/demos/features/vue-query/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:git-pull-request-arrow',
|
||||
title: 'Tanstack Query',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 面包屑导航
|
||||
{
|
||||
name: 'BreadcrumbDemos',
|
||||
path: '/demos/breadcrumb',
|
||||
meta: {
|
||||
icon: 'lucide:navigation',
|
||||
title: $t('demos.breadcrumb.navigation'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'BreadcrumbLateralDemo',
|
||||
path: '/demos/breadcrumb/lateral',
|
||||
component: () => import('#/views/demos/breadcrumb/lateral.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:navigation',
|
||||
title: $t('demos.breadcrumb.lateral'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BreadcrumbLateralDetailDemo',
|
||||
path: '/demos/breadcrumb/lateral-detail',
|
||||
component: () =>
|
||||
import('#/views/demos/breadcrumb/lateral-detail.vue'),
|
||||
meta: {
|
||||
activePath: '/demos/breadcrumb/lateral',
|
||||
hideInMenu: true,
|
||||
title: $t('demos.breadcrumb.lateralDetail'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BreadcrumbLevelDemo',
|
||||
path: '/demos/breadcrumb/level',
|
||||
meta: {
|
||||
icon: 'lucide:navigation',
|
||||
title: $t('demos.breadcrumb.level'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'BreadcrumbLevelDetailDemo',
|
||||
path: '/demos/breadcrumb/level/detail',
|
||||
component: () =>
|
||||
import('#/views/demos/breadcrumb/level-detail.vue'),
|
||||
meta: {
|
||||
title: $t('demos.breadcrumb.levelDetail'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 缺省页
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:lightbulb-error-outline',
|
||||
title: $t('demos.fallback.title'),
|
||||
},
|
||||
name: 'FallbackDemos',
|
||||
path: '/demos/fallback',
|
||||
children: [
|
||||
{
|
||||
name: 'Fallback403Demo',
|
||||
path: '/demos/fallback/403',
|
||||
component: () => import('#/views/_core/fallback/forbidden.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:do-not-disturb-alt',
|
||||
title: '403',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Fallback404Demo',
|
||||
path: '/demos/fallback/404',
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:table-off',
|
||||
title: '404',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Fallback500Demo',
|
||||
path: '/demos/fallback/500',
|
||||
component: () =>
|
||||
import('#/views/_core/fallback/internal-error.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:server-network-off',
|
||||
title: '500',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FallbackOfflineDemo',
|
||||
path: '/demos/fallback/offline',
|
||||
component: () => import('#/views/_core/fallback/offline.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:offline',
|
||||
title: $t('ui.fallback.offline'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 菜单徽标
|
||||
{
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
badgeVariants: 'destructive',
|
||||
icon: 'lucide:circle-dot',
|
||||
title: $t('demos.badge.title'),
|
||||
},
|
||||
name: 'BadgeDemos',
|
||||
path: '/demos/badge',
|
||||
children: [
|
||||
{
|
||||
name: 'BadgeDotDemo',
|
||||
component: () => import('#/views/demos/badge/index.vue'),
|
||||
path: '/demos/badge/dot',
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: 'lucide:square-dot',
|
||||
title: $t('demos.badge.dot'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BadgeTextDemo',
|
||||
component: () => import('#/views/demos/badge/index.vue'),
|
||||
path: '/demos/badge/text',
|
||||
meta: {
|
||||
badge: '10',
|
||||
icon: 'lucide:square-dot',
|
||||
title: $t('demos.badge.text'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BadgeColorDemo',
|
||||
component: () => import('#/views/demos/badge/index.vue'),
|
||||
path: '/demos/badge/color',
|
||||
meta: {
|
||||
badge: 'Hot',
|
||||
badgeVariants: 'destructive',
|
||||
icon: 'lucide:square-dot',
|
||||
title: $t('demos.badge.color'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 菜单激活图标
|
||||
{
|
||||
meta: {
|
||||
activeIcon: 'fluent-emoji:radioactive',
|
||||
icon: 'bi:radioactive',
|
||||
title: $t('demos.activeIcon.title'),
|
||||
},
|
||||
name: 'ActiveIconDemos',
|
||||
path: '/demos/active-icon',
|
||||
children: [
|
||||
{
|
||||
name: 'ActiveIconDemo',
|
||||
component: () => import('#/views/demos/active-icon/index.vue'),
|
||||
path: '/demos/active-icon/children',
|
||||
meta: {
|
||||
activeIcon: 'fluent-emoji:radioactive',
|
||||
icon: 'bi:radioactive',
|
||||
title: $t('demos.activeIcon.children'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 外部链接
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:round-settings-input-composite',
|
||||
title: $t('demos.outside.title'),
|
||||
},
|
||||
name: 'OutsideDemos',
|
||||
path: '/demos/outside',
|
||||
children: [
|
||||
{
|
||||
name: 'IframeDemos',
|
||||
path: '/demos/outside/iframe',
|
||||
meta: {
|
||||
icon: 'mdi:newspaper-variant-outline',
|
||||
title: $t('demos.outside.embedded'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'VueDocumentDemo',
|
||||
path: '/demos/outside/iframe/vue-document',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'logos:vue',
|
||||
iframeSrc: 'https://cn.vuejs.org/',
|
||||
keepAlive: true,
|
||||
title: 'Vue',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TailwindcssDemo',
|
||||
path: '/demos/outside/iframe/tailwindcss',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'devicon:tailwindcss',
|
||||
iframeSrc: 'https://tailwindcss.com/',
|
||||
// keepAlive: true,
|
||||
title: 'Tailwindcss',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ExternalLinkDemos',
|
||||
path: '/demos/outside/external-link',
|
||||
meta: {
|
||||
icon: 'mdi:newspaper-variant-multiple-outline',
|
||||
title: $t('demos.outside.externalLink'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ViteDemo',
|
||||
path: '/demos/outside/external-link/vite',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'logos:vitejs',
|
||||
link: 'https://vitejs.dev/',
|
||||
title: 'Vite',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VueUseDemo',
|
||||
path: '/demos/outside/external-link/vue-use',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'logos:vueuse',
|
||||
link: 'https://vueuse.org',
|
||||
title: 'VueUse',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 嵌套菜单
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.nested.title'),
|
||||
},
|
||||
name: 'NestedDemos',
|
||||
path: '/demos/nested',
|
||||
children: [
|
||||
{
|
||||
name: 'Menu1Demo',
|
||||
path: '/demos/nested/menu1',
|
||||
component: () => import('#/views/demos/nested/menu-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu1'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Menu2Demo',
|
||||
path: '/demos/nested/menu2',
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu2'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Menu21Demo',
|
||||
path: '/demos/nested/menu2/menu2-1',
|
||||
component: () => import('#/views/demos/nested/menu-2-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu2_1'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Menu3Demo',
|
||||
path: '/demos/nested/menu3',
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.nested.menu3'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Menu31Demo',
|
||||
path: 'menu3-1',
|
||||
component: () => import('#/views/demos/nested/menu-3-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu3_1'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Menu32Demo',
|
||||
path: 'menu3-2',
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.nested.menu3_2'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Menu321Demo',
|
||||
path: '/demos/nested/menu3/menu3-2/menu3-2-1',
|
||||
component: () =>
|
||||
import('#/views/demos/nested/menu-3-2-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu3_2_1'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
255
playground/src/router/routes/modules/examples.ts
Normal file
255
playground/src/router/routes/modules/examples.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'ion:layers-outline',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: $t('examples.title'),
|
||||
},
|
||||
name: 'Examples',
|
||||
path: '/examples',
|
||||
children: [
|
||||
{
|
||||
name: 'FormExample',
|
||||
path: '/examples/form',
|
||||
meta: {
|
||||
icon: 'mdi:form-select',
|
||||
title: $t('examples.form.title'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'FormBasicExample',
|
||||
path: '/examples/form/basic',
|
||||
component: () => import('#/views/examples/form/basic.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.basic'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormQueryExample',
|
||||
path: '/examples/form/query',
|
||||
component: () => import('#/views/examples/form/query.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.query'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormRulesExample',
|
||||
path: '/examples/form/rules',
|
||||
component: () => import('#/views/examples/form/rules.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.rules'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormDynamicExample',
|
||||
path: '/examples/form/dynamic',
|
||||
component: () => import('#/views/examples/form/dynamic.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.dynamic'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormCustomExample',
|
||||
path: '/examples/form/custom',
|
||||
component: () => import('#/views/examples/form/custom.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.custom'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormApiExample',
|
||||
path: '/examples/form/api',
|
||||
component: () => import('#/views/examples/form/api.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.api'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormMergeExample',
|
||||
path: '/examples/form/merge',
|
||||
component: () => import('#/views/examples/form/merge.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.merge'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'VxeTableExample',
|
||||
path: '/examples/vxe-table',
|
||||
meta: {
|
||||
icon: 'lucide:table',
|
||||
title: $t('examples.vxeTable.title'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'VxeTableBasicExample',
|
||||
path: '/examples/vxe-table/basic',
|
||||
component: () => import('#/views/examples/vxe-table/basic.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.basic'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableRemoteExample',
|
||||
path: '/examples/vxe-table/remote',
|
||||
component: () => import('#/views/examples/vxe-table/remote.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.remote'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableTreeExample',
|
||||
path: '/examples/vxe-table/tree',
|
||||
component: () => import('#/views/examples/vxe-table/tree.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.tree'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableFixedExample',
|
||||
path: '/examples/vxe-table/fixed',
|
||||
component: () => import('#/views/examples/vxe-table/fixed.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.fixed'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableCustomCellExample',
|
||||
path: '/examples/vxe-table/custom-cell',
|
||||
component: () =>
|
||||
import('#/views/examples/vxe-table/custom-cell.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.custom-cell'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableFormExample',
|
||||
path: '/examples/vxe-table/form',
|
||||
component: () => import('#/views/examples/vxe-table/form.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.form'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableEditCellExample',
|
||||
path: '/examples/vxe-table/edit-cell',
|
||||
component: () => import('#/views/examples/vxe-table/edit-cell.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.editCell'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableEditRowExample',
|
||||
path: '/examples/vxe-table/edit-row',
|
||||
component: () => import('#/views/examples/vxe-table/edit-row.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.editRow'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableVirtualExample',
|
||||
path: '/examples/vxe-table/virtual',
|
||||
component: () => import('#/views/examples/vxe-table/virtual.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.virtual'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'CaptchaExample',
|
||||
path: '/examples/captcha',
|
||||
meta: {
|
||||
icon: 'logos:recaptcha',
|
||||
title: $t('examples.captcha.title'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'DragVerifyExample',
|
||||
path: '/examples/captcha/slider',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/slider-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.sliderCaptcha'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RotateVerifyExample',
|
||||
path: '/examples/captcha/slider-rotate',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/slider-rotate-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.sliderRotateCaptcha'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CaptchaPointSelectionExample',
|
||||
path: '/examples/captcha/point-selection',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/point-selection-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.pointSelection'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ModalExample',
|
||||
path: '/examples/modal',
|
||||
component: () => import('#/views/examples/modal/index.vue'),
|
||||
meta: {
|
||||
icon: 'system-uicons:window-content',
|
||||
title: $t('examples.modal.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'DrawerExample',
|
||||
path: '/examples/drawer',
|
||||
component: () => import('#/views/examples/drawer/index.vue'),
|
||||
meta: {
|
||||
icon: 'iconoir:drawer',
|
||||
title: $t('examples.drawer.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EllipsisExample',
|
||||
path: '/examples/ellipsis',
|
||||
component: () => import('#/views/examples/ellipsis/index.vue'),
|
||||
meta: {
|
||||
icon: 'ion:ellipsis-horizontal',
|
||||
title: $t('examples.ellipsis.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VueResizeDemo',
|
||||
path: '/demos/resize/basic',
|
||||
component: () => import('#/views/examples/resize/basic.vue'),
|
||||
meta: {
|
||||
icon: 'material-symbols:resize',
|
||||
title: $t('examples.resize.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ColPageDemo',
|
||||
path: '/examples/layout/col-page',
|
||||
component: () => import('#/views/examples/layout/col-page.vue'),
|
||||
meta: {
|
||||
badge: 'Alpha',
|
||||
badgeVariants: 'destructive',
|
||||
icon: 'material-symbols:horizontal-distribute',
|
||||
title: $t('examples.layout.col-page'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
94
playground/src/router/routes/modules/vben.ts
Normal file
94
playground/src/router/routes/modules/vben.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import {
|
||||
VBEN_ANT_PREVIEW_URL,
|
||||
VBEN_DOC_URL,
|
||||
VBEN_ELE_PREVIEW_URL,
|
||||
VBEN_GITHUB_URL,
|
||||
VBEN_LOGO_URL,
|
||||
VBEN_NAIVE_PREVIEW_URL,
|
||||
} from '@vben/constants';
|
||||
import { SvgAntdvLogoIcon } from '@vben/icons';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: VBEN_LOGO_URL,
|
||||
order: 9999,
|
||||
title: $t('demos.vben.title'),
|
||||
},
|
||||
name: 'VbenProject',
|
||||
path: '/vben-admin',
|
||||
children: [
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
},
|
||||
},
|
||||
{
|
||||
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: 'VbenAntdv',
|
||||
path: '/vben-admin/antdv',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: SvgAntdvLogoIcon,
|
||||
link: VBEN_ANT_PREVIEW_URL,
|
||||
title: $t('demos.vben.antdv'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenNaive',
|
||||
path: '/vben-admin/naive',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: 'logos:naiveui',
|
||||
link: VBEN_NAIVE_PREVIEW_URL,
|
||||
title: $t('demos.vben.naive-ui'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenElementPlus',
|
||||
path: '/vben-admin/ele',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: 'logos:element',
|
||||
link: VBEN_ELE_PREVIEW_URL,
|
||||
title: $t('demos.vben.element-plus'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
120
playground/src/store/auth.ts
Normal file
120
playground/src/store/auth.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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 { notification } from 'ant-design-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const loginLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param params 登录表单数据
|
||||
* @param onSuccess 成功之后的回调函数
|
||||
*/
|
||||
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);
|
||||
console.log(999999999999)
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
accessStore.setAccessToken(accessToken);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
fetchUserInfo(),
|
||||
getAccessCodesApi(),
|
||||
]);
|
||||
|
||||
console.log(99889999)
|
||||
|
||||
userInfo = fetchUserInfoResult;
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
notification.success({
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
duration: 3,
|
||||
message: $t('authentication.loginSuccess'),
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
async function logout(redirect: boolean = true) {
|
||||
try {
|
||||
await logoutApi();
|
||||
} catch {
|
||||
// 不做任何处理
|
||||
}
|
||||
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登录页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
let userInfo: null | UserInfo = null;
|
||||
userInfo = await getUserInfoApi();
|
||||
console.log(666,userInfo)
|
||||
userStore.setUserInfo(userInfo);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
authLogin,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
1
playground/src/store/index.ts
Normal file
1
playground/src/store/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth';
|
||||
3
playground/src/views/_core/README.md
Normal file
3
playground/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
9
playground/src/views/_core/about/index.vue
Normal file
9
playground/src/views/_core/about/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { About } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'About' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<About />
|
||||
</template>
|
||||
109
playground/src/views/_core/authentication/code-login.vue
Normal file
109
playground/src/views/_core/authentication/code-login.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
const loginRef =
|
||||
useTemplateRef<InstanceType<typeof AuthenticationCodeLogin>>('loginRef');
|
||||
function sendCodeApi(phoneNumber: string) {
|
||||
message.loading({
|
||||
content: $t('page.auth.sendingCode'),
|
||||
duration: 0,
|
||||
key: 'sending-code',
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
message.success({
|
||||
content: $t('page.auth.codeSentTo', [phoneNumber]),
|
||||
duration: 3,
|
||||
key: 'sending-code',
|
||||
});
|
||||
resolve({ code: '123456', phoneNumber });
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
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;
|
||||
},
|
||||
handleSendCode: async () => {
|
||||
// 模拟发送验证码
|
||||
// Simulate sending verification code
|
||||
loading.value = true;
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
loading.value = false;
|
||||
throw new Error('formApi is not ready');
|
||||
}
|
||||
await formApi.validateField('phoneNumber');
|
||||
const isPhoneReady = await formApi.isFieldValid('phoneNumber');
|
||||
if (!isPhoneReady) {
|
||||
loading.value = false;
|
||||
throw new Error('Phone number is not Ready');
|
||||
}
|
||||
const { phoneNumber } = await formApi.getValues();
|
||||
await sendCodeApi(phoneNumber);
|
||||
loading.value = false;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
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: Record<string, any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
115
playground/src/views/_core/authentication/login.vue
Normal file
115
playground/src/views/_core/authentication/login.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<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(_values, form) {
|
||||
// return {
|
||||
// 'onUpdate:modelValue': (value: string) => {
|
||||
// const findItem = MOCK_USER_OPTIONS.find(
|
||||
// (item) => item.value === value,
|
||||
// );
|
||||
// if (findItem) {
|
||||
// form.setValues({
|
||||
// password: '123456',
|
||||
// username: findItem.label,
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// options: MOCK_USER_OPTIONS,
|
||||
// placeholder: $t('authentication.selectAccount'),
|
||||
// };
|
||||
// },
|
||||
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>
|
||||
10
playground/src/views/_core/authentication/qrcode-login.vue
Normal file
10
playground/src/views/_core/authentication/qrcode-login.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
defineOptions({ name: 'QrCodeLogin' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||
</template>
|
||||
96
playground/src/views/_core/authentication/register.vue
Normal file
96
playground/src/views/_core/authentication/register.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
{
|
||||
component: 'VbenCheckbox',
|
||||
fieldName: 'agreePolicy',
|
||||
renderComponentContent: () => ({
|
||||
default: () =>
|
||||
h('span', [
|
||||
$t('authentication.agree'),
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'vben-link ml-1 ',
|
||||
href: '',
|
||||
},
|
||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
rules: z.boolean().refine((value) => !!value, {
|
||||
message: $t('authentication.agreeTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
7
playground/src/views/_core/fallback/coming-soon.vue
Normal file
7
playground/src/views/_core/fallback/coming-soon.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
9
playground/src/views/_core/fallback/forbidden.vue
Normal file
9
playground/src/views/_core/fallback/forbidden.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback403Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="403" />
|
||||
</template>
|
||||
9
playground/src/views/_core/fallback/internal-error.vue
Normal file
9
playground/src/views/_core/fallback/internal-error.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback500Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="500" />
|
||||
</template>
|
||||
9
playground/src/views/_core/fallback/not-found.vue
Normal file
9
playground/src/views/_core/fallback/not-found.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback404Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="404" />
|
||||
</template>
|
||||
9
playground/src/views/_core/fallback/offline.vue
Normal file
9
playground/src/views/_core/fallback/offline.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="offline" />
|
||||
</template>
|
||||
100
playground/src/views/dashboard/analytics/analytics-trends.vue
Normal file
100
playground/src/views/dashboard/analytics/analytics-trends.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#019680',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].sort((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
{ name: '联盟广告', value: 484 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
90
playground/src/views/dashboard/analytics/index.vue
Normal file
90
playground/src/views/dashboard/analytics/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 120_000,
|
||||
value: 8000,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 50_000,
|
||||
value: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
266
playground/src/views/dashboard/workspace/index.vue
Normal file
266
playground/src/views/dashboard/workspace/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
11
playground/src/views/demos/access/admin-visible.vue
Normal file
11
playground/src/views/demos/access/admin-visible.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 Admin 账号可见"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
157
playground/src/views/demos/access/button-control.vue
Normal file
157
playground/src/views/demos/access/button-control.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AccessControl, useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { resetAllStores, useUserStore } from '@vben/stores';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
const accounts: Record<string, Recordable<any>> = {
|
||||
admin: {
|
||||
password: '123456',
|
||||
username: 'admin',
|
||||
},
|
||||
super: {
|
||||
password: '123456',
|
||||
username: 'vben',
|
||||
},
|
||||
user: {
|
||||
password: '123456',
|
||||
username: 'jack',
|
||||
},
|
||||
};
|
||||
|
||||
const { accessMode, hasAccessByCodes } = useAccess();
|
||||
const authStore = useAuthStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
function roleButtonType(role: string) {
|
||||
return userStore.userRoles.includes(role) ? 'primary' : 'default';
|
||||
}
|
||||
|
||||
async function changeAccount(role: string) {
|
||||
if (userStore.userRoles.includes(role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = accounts[role];
|
||||
resetAllStores();
|
||||
if (account) {
|
||||
await authStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
:title="`${accessMode === 'frontend' ? '前端' : '后端'}按钮访问权限演示`"
|
||||
description="切换不同的账号,观察按钮变化。"
|
||||
>
|
||||
<Card class="mb-5">
|
||||
<template #title>
|
||||
<span class="font-semibold">当前角色:</span>
|
||||
<span class="text-primary mx-4 text-lg">
|
||||
{{ userStore.userRoles?.[0] }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<Button :type="roleButtonType('super')" @click="changeAccount('super')">
|
||||
切换为 Super 账号
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
:type="roleButtonType('admin')"
|
||||
class="mx-4"
|
||||
@click="changeAccount('admin')"
|
||||
>
|
||||
切换为 Admin 账号
|
||||
</Button>
|
||||
<Button :type="roleButtonType('user')" @click="changeAccount('user')">
|
||||
切换为 User 账号
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="组件形式控制 - 权限码">
|
||||
<AccessControl :codes="['AC_100100']" type="code">
|
||||
<Button class="mr-4"> Super 账号可见 ["AC_100100"] </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['AC_100030']" type="code">
|
||||
<Button class="mr-4"> Admin 账号可见 ["AC_100030"] </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['AC_1000001']" type="code">
|
||||
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['AC_100100', 'AC_100030']" type="code">
|
||||
<Button class="mr-4">
|
||||
Super & Admin 账号可见 ["AC_100100","AC_100030"]
|
||||
</Button>
|
||||
</AccessControl>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-if="accessMode === 'frontend'"
|
||||
class="mb-5"
|
||||
title="组件形式控制 - 角色"
|
||||
>
|
||||
<AccessControl :codes="['super']" type="role">
|
||||
<Button class="mr-4"> Super 角色可见 </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['admin']" type="role">
|
||||
<Button class="mr-4"> Admin 角色可见 </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['user']" type="role">
|
||||
<Button class="mr-4"> User 角色可见 </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['super', 'admin']" type="role">
|
||||
<Button class="mr-4"> Super & Admin 角色可见 </Button>
|
||||
</AccessControl>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="函数形式控制">
|
||||
<Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
|
||||
Super 账号可见 ["AC_100100"]
|
||||
</Button>
|
||||
<Button v-if="hasAccessByCodes(['AC_100030'])" class="mr-4">
|
||||
Admin 账号可见 ["AC_100030"]
|
||||
</Button>
|
||||
<Button v-if="hasAccessByCodes(['AC_1000001'])" class="mr-4">
|
||||
User 账号可见 ["AC_1000001"]
|
||||
</Button>
|
||||
<Button v-if="hasAccessByCodes(['AC_100100', 'AC_100030'])" class="mr-4">
|
||||
Super & Admin 账号可见 ["AC_100100","AC_100030"]
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="指令方式 - 权限码">
|
||||
<Button class="mr-4" v-access:code="['AC_100100']">
|
||||
Super 账号可见 ["AC_100100"]
|
||||
</Button>
|
||||
<Button class="mr-4" v-access:code="['AC_100030']">
|
||||
Admin 账号可见 ["AC_100030"]
|
||||
</Button>
|
||||
<Button class="mr-4" v-access:code="['AC_1000001']">
|
||||
User 账号可见 ["AC_1000001"]
|
||||
</Button>
|
||||
<Button class="mr-4" v-access:code="['AC_100100', 'AC_100030']">
|
||||
Super & Admin 账号可见 ["AC_100100","AC_100030"]
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card v-if="accessMode === 'frontend'" class="mb-5" title="指令方式 - 角色">
|
||||
<Button class="mr-4" v-access:role="['super']"> Super 角色可见 </Button>
|
||||
<Button class="mr-4" v-access:role="['admin']"> Admin 角色可见 </Button>
|
||||
<Button class="mr-4" v-access:role="['user']"> User 角色可见 </Button>
|
||||
<Button class="mr-4" v-access:role="['super', 'admin']">
|
||||
Super & Admin 角色可见
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
98
playground/src/views/demos/access/index.vue
Normal file
98
playground/src/views/demos/access/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { resetAllStores, useUserStore } from '@vben/stores';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
const accounts: Record<string, Recordable<any>> = {
|
||||
admin: {
|
||||
password: '123456',
|
||||
username: 'admin',
|
||||
},
|
||||
super: {
|
||||
password: '123456',
|
||||
username: 'vben',
|
||||
},
|
||||
user: {
|
||||
password: '123456',
|
||||
username: 'jack',
|
||||
},
|
||||
};
|
||||
|
||||
const { accessMode, toggleAccessMode } = useAccess();
|
||||
const userStore = useUserStore();
|
||||
const accessStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
function roleButtonType(role: string) {
|
||||
return userStore.userRoles.includes(role) ? 'primary' : 'default';
|
||||
}
|
||||
|
||||
async function changeAccount(role: string) {
|
||||
if (userStore.userRoles.includes(role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = accounts[role];
|
||||
resetAllStores();
|
||||
if (account) {
|
||||
await accessStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleAccessMode() {
|
||||
if (!accounts.super) {
|
||||
return;
|
||||
}
|
||||
await toggleAccessMode();
|
||||
resetAllStores();
|
||||
|
||||
await accessStore.authLogin(accounts.super, async () => {
|
||||
setTimeout(() => {
|
||||
router.go(0);
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
:title="`${accessMode === 'frontend' ? '前端' : '后端'}页面访问权限演示`"
|
||||
description="切换不同的账号,观察左侧菜单变化。"
|
||||
>
|
||||
<Card class="mb-5" title="权限模式">
|
||||
<span class="font-semibold">当前权限模式:</span>
|
||||
<span class="text-primary mx-4">{{
|
||||
accessMode === 'frontend' ? '前端权限控制' : '后端权限控制'
|
||||
}}</span>
|
||||
<Button type="primary" @click="handleToggleAccessMode">
|
||||
切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式
|
||||
</Button>
|
||||
</Card>
|
||||
<Card title="账号切换">
|
||||
<Button :type="roleButtonType('super')" @click="changeAccount('super')">
|
||||
切换为 Super 账号
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
:type="roleButtonType('admin')"
|
||||
class="mx-4"
|
||||
@click="changeAccount('admin')"
|
||||
>
|
||||
切换为 Admin 账号
|
||||
</Button>
|
||||
<Button :type="roleButtonType('user')" @click="changeAccount('user')">
|
||||
切换为 User 账号
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
11
playground/src/views/demos/access/menu-visible-403.vue
Normal file
11
playground/src/views/demos/access/menu-visible-403.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面用户不可见,会被重定向到403页面"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
11
playground/src/views/demos/access/super-visible.vue
Normal file
11
playground/src/views/demos/access/super-visible.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 Super 账号可见"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
11
playground/src/views/demos/access/user-visible.vue
Normal file
11
playground/src/views/demos/access/user-visible.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 User 账号可见"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
11
playground/src/views/demos/active-icon/index.vue
Normal file
11
playground/src/views/demos/active-icon/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="用于菜单激活显示不同的图标"
|
||||
status="coming-soon"
|
||||
title="激活图标示例"
|
||||
/>
|
||||
</template>
|
||||
116
playground/src/views/demos/badge/index.vue
Normal file
116
playground/src/views/demos/badge/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { MenuBadge } from '@vben-core/menu-ui';
|
||||
|
||||
import { Button, Card, Radio, RadioGroup } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const colors = [
|
||||
{ label: '预设:默认', value: 'default' },
|
||||
{ label: '预设:关键', value: 'destructive' },
|
||||
{ label: '预设:主要', value: 'primary' },
|
||||
{ label: '预设:成功', value: 'success' },
|
||||
{ label: '自定义', value: 'bg-gray-200 text-black' },
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
const accessStore = useAccessStore();
|
||||
const menu = accessStore.getMenuByPath(route.path);
|
||||
const badgeProps = reactive({
|
||||
badge: menu?.badge as string,
|
||||
badgeType: menu?.badge ? 'normal' : (menu?.badgeType as 'dot' | 'normal'),
|
||||
badgeVariants: menu?.badgeVariants as string,
|
||||
});
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
handleValuesChange(values) {
|
||||
badgeProps.badge = values.badge;
|
||||
badgeProps.badgeType = values.badgeType;
|
||||
badgeProps.badgeVariants = values.badgeVariants;
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '点徽标', value: 'dot' },
|
||||
{ label: '文字徽标', value: 'normal' },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: badgeProps.badgeType,
|
||||
fieldName: 'badgeType',
|
||||
label: '类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 4,
|
||||
placeholder: '请输入徽标内容',
|
||||
style: { width: '200px' },
|
||||
},
|
||||
defaultValue: badgeProps.badge,
|
||||
fieldName: 'badge',
|
||||
label: '徽标内容',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
defaultValue: badgeProps.badgeVariants,
|
||||
fieldName: 'badgeVariants',
|
||||
label: '颜色',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'action',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function updateMenuBadge() {
|
||||
if (menu) {
|
||||
menu.badge = badgeProps.badge;
|
||||
menu.badgeType = badgeProps.badgeType;
|
||||
menu.badgeVariants = badgeProps.badgeVariants;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="菜单项上可以显示徽标,这些徽标可以主动更新"
|
||||
title="菜单徽标"
|
||||
>
|
||||
<Card title="徽标更新">
|
||||
<Form>
|
||||
<template #badgeVariants="slotProps">
|
||||
<RadioGroup v-bind="slotProps">
|
||||
<Radio
|
||||
v-for="color in colors"
|
||||
:key="color.value"
|
||||
:value="color.value"
|
||||
>
|
||||
<div
|
||||
:title="color.label"
|
||||
class="flex h-[14px] w-[50px] items-center justify-start"
|
||||
>
|
||||
<MenuBadge
|
||||
v-bind="{ ...badgeProps, badgeVariants: color.value }"
|
||||
/>
|
||||
</div>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
<template #action>
|
||||
<Button type="primary" @click="updateMenuBadge">更新徽标</Button>
|
||||
</template>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
21
playground/src/views/demos/breadcrumb/lateral-detail.vue
Normal file
21
playground/src/views/demos/breadcrumb/lateral-detail.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="面包屑导航-平级模式-详情页"
|
||||
status="coming-soon"
|
||||
title="注意观察面包屑导航变化"
|
||||
>
|
||||
<template #action>
|
||||
<Button @click="router.go(-1)">返回</Button>
|
||||
</template>
|
||||
</Fallback>
|
||||
</template>
|
||||
25
playground/src/views/demos/breadcrumb/lateral.vue
Normal file
25
playground/src/views/demos/breadcrumb/lateral.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function details() {
|
||||
router.push({ name: 'BreadcrumbLateralDetailDemo' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="点击查看详情,并观察面包屑导航变化"
|
||||
status="coming-soon"
|
||||
title="面包屑导航-平级模式"
|
||||
>
|
||||
<template #action>
|
||||
<Button type="primary" @click="details">点击查看详情</Button>
|
||||
</template>
|
||||
</Fallback>
|
||||
</template>
|
||||
11
playground/src/views/demos/breadcrumb/level-detail.vue
Normal file
11
playground/src/views/demos/breadcrumb/level-detail.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="面包屑导航-层级模式-详情页"
|
||||
status="coming-soon"
|
||||
title="注意观察面包屑导航变化"
|
||||
/>
|
||||
</template>
|
||||
25
playground/src/views/demos/features/clipboard/index.vue
Normal file
25
playground/src/views/demos/features/clipboard/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Button, Card, Input } from 'ant-design-vue';
|
||||
|
||||
const source = ref('Hello');
|
||||
const { copy, text } = useClipboard({ legacy: true, source });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="剪切板示例">
|
||||
<Card title="基本使用">
|
||||
<p class="mb-3">
|
||||
Current copied: <code>{{ text || 'none' }}</code>
|
||||
</p>
|
||||
<div class="flex">
|
||||
<Input v-model:value="source" class="mr-3 flex w-[200px]" />
|
||||
<Button type="primary" @click="copy(source)"> Copy </Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
File diff suppressed because one or more lines are too long
74
playground/src/views/demos/features/file-download/index.vue
Normal file
74
playground/src/views/demos/features/file-download/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
downloadFileFromBase64,
|
||||
downloadFileFromBlobPart,
|
||||
downloadFileFromImageUrl,
|
||||
downloadFileFromUrl,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import imageBase64 from './base64';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="文件下载示例">
|
||||
<Card title="根据文件地址下载文件">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromUrl({
|
||||
source:
|
||||
'https://codeload.github.com/vbenjs/vue-vben-admin-doc/zip/main',
|
||||
target: '_self',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="my-5" title="根据地址下载图片">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromImageUrl({
|
||||
source:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
fileName: 'vben-logo.png',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="my-5" title="base64流下载">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromBase64({
|
||||
source: imageBase64,
|
||||
fileName: 'image.png',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download Image
|
||||
</Button>
|
||||
</Card>
|
||||
<Card class="my-5" title="文本下载">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromBlobPart({
|
||||
source: 'text content',
|
||||
fileName: 'test.txt',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download TxT
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
47
playground/src/views/demos/features/full-screen/index.vue
Normal file
47
playground/src/views/demos/features/full-screen/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
const domRef = ref<HTMLElement>();
|
||||
|
||||
const { enter, exit, isFullscreen, toggle } = useFullscreen();
|
||||
|
||||
const { isFullscreen: isDomFullscreen, toggle: toggleDom } =
|
||||
useFullscreen(domRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="全屏示例">
|
||||
<Card title="Window Full Screen">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<Button :disabled="isFullscreen" type="primary" @click="enter">
|
||||
Enter Window Full Screen
|
||||
</Button>
|
||||
<Button @click="toggle"> Toggle Window Full Screen </Button>
|
||||
|
||||
<Button :disabled="!isFullscreen" danger @click="exit">
|
||||
Exit Window Full Screen
|
||||
</Button>
|
||||
|
||||
<span class="text-nowrap"> Current State: {{ isFullscreen }} </span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mt-5" title="Dom Full Screen">
|
||||
<Button type="primary" @click="toggleDom"> Enter Dom Full Screen </Button>
|
||||
</Card>
|
||||
|
||||
<div
|
||||
ref="domRef"
|
||||
class="mx-auto mt-10 flex h-64 w-1/2 items-center justify-center rounded-md bg-yellow-400"
|
||||
>
|
||||
<Button class="mr-2" type="primary" @click="toggleDom">
|
||||
{{ isDomFullscreen ? 'Exit Dom Full Screen' : 'Enter Dom Full Screen' }}
|
||||
</Button>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>children</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前菜单的子菜单不可见"
|
||||
status="coming-soon"
|
||||
title="隐藏子菜单"
|
||||
/>
|
||||
</template>
|
||||
106
playground/src/views/demos/features/icons/index.vue
Normal file
106
playground/src/views/demos/features/icons/index.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, ref } from 'vue';
|
||||
|
||||
import { IconPicker, Page } from '@vben/common-ui';
|
||||
import {
|
||||
MdiGithub,
|
||||
MdiGoogle,
|
||||
MdiKeyboardEsc,
|
||||
MdiQqchat,
|
||||
MdiWechat,
|
||||
SvgAvatar1Icon,
|
||||
SvgAvatar2Icon,
|
||||
SvgAvatar3Icon,
|
||||
SvgAvatar4Icon,
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { Card, Input } from 'ant-design-vue';
|
||||
|
||||
const iconValue = ref('ant-design:trademark-outlined');
|
||||
|
||||
const inputComponent = h(Input);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="图标">
|
||||
<template #description>
|
||||
<div class="text-foreground/80 mt-2">
|
||||
图标可在
|
||||
<a
|
||||
class="text-primary"
|
||||
href="https://icon-sets.iconify.design/"
|
||||
target="_blank"
|
||||
>
|
||||
Iconify
|
||||
</a>
|
||||
中查找,支持多种图标库,如 Material Design, Font Awesome, Jam Icons 等。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card class="mb-5" title="Iconify">
|
||||
<div class="flex items-center gap-5">
|
||||
<MdiGithub class="size-8" />
|
||||
<MdiGoogle class="size-8 text-red-500" />
|
||||
<MdiQqchat class="size-8 text-green-500" />
|
||||
<MdiWechat class="size-8" />
|
||||
<MdiKeyboardEsc class="size-8" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="Svg Icons">
|
||||
<div class="flex items-center gap-5">
|
||||
<SvgAvatar1Icon class="size-8" />
|
||||
<SvgAvatar2Icon class="size-8 text-red-500" />
|
||||
<SvgAvatar3Icon class="size-8 text-green-500" />
|
||||
<SvgAvatar4Icon class="size-8" />
|
||||
<SvgCakeIcon class="size-8" />
|
||||
<SvgBellIcon class="size-8" />
|
||||
<SvgCardIcon class="size-8" />
|
||||
<SvgDownloadIcon class="size-8" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="Tailwind CSS">
|
||||
<div class="flex items-center gap-5 text-3xl">
|
||||
<span class="icon-[ant-design--alipay-circle-outlined]"></span>
|
||||
<span class="icon-[ant-design--account-book-filled]"></span>
|
||||
<span class="icon-[ant-design--container-outlined]"></span>
|
||||
<span class="icon-[svg-spinners--wind-toy]"></span>
|
||||
<span class="icon-[svg-spinners--blocks-wave]"></span>
|
||||
<span class="icon-[line-md--compass-filled-loop]"></span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="图标选择器">
|
||||
<div class="mb-5 flex items-center gap-5">
|
||||
<span>原始样式(Iconify):</span>
|
||||
<IconPicker class="w-[200px]" />
|
||||
</div>
|
||||
<div class="mb-5 flex items-center gap-5">
|
||||
<span>原始样式(svg):</span>
|
||||
<IconPicker class="w-[200px]" prefix="svg" />
|
||||
</div>
|
||||
<div class="mb-5 flex items-center gap-5">
|
||||
<span>使用Input:</span>
|
||||
<IconPicker :input-component="inputComponent" icon-slot="addonAfter" />
|
||||
</div>
|
||||
<div class="flex items-center gap-5">
|
||||
<span>可手动输入,只能点击图标打开弹窗:</span>
|
||||
<Input
|
||||
v-model:value="iconValue"
|
||||
allow-clear
|
||||
placeholder="点击这里选择图标"
|
||||
style="width: 300px"
|
||||
>
|
||||
<template #addonAfter>
|
||||
<IconPicker v-model="iconValue" class="w-[200px]" />
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
39
playground/src/views/demos/features/login-expired/index.vue
Normal file
39
playground/src/views/demos/features/login-expired/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginExpiredModeType } from '@vben/types';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { preferences, updatePreferences } from '@vben/preferences';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { getMockStatusApi } from '#/api';
|
||||
|
||||
async function handleClick(type: LoginExpiredModeType) {
|
||||
const loginExpiredMode = preferences.app.loginExpiredMode;
|
||||
|
||||
updatePreferences({ app: { loginExpiredMode: type } });
|
||||
await getMockStatusApi('401');
|
||||
updatePreferences({ app: { loginExpiredMode } });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="登录过期演示">
|
||||
<template #description>
|
||||
<div class="text-foreground/80 mt-2">
|
||||
接口请求遇到401状态码时,需要重新登录。有两种方式:
|
||||
<p>1.转到登录页,登录成功后跳转回原页面</p>
|
||||
<p>
|
||||
2.弹出重新登录弹窗,登录后关闭弹窗,不进行任何页面跳转(刷新后还是会跳转登录页面)
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card class="mb-5" title="跳转登录页面方式">
|
||||
<Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
|
||||
</Card>
|
||||
<Card class="mb-5" title="登录弹窗方式">
|
||||
<Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
11
playground/src/views/demos/features/menu-query/index.vue
Normal file
11
playground/src/views/demos/features/menu-query/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="点击菜单,将会带上参数"
|
||||
status="coming-soon"
|
||||
title="菜单带参示例"
|
||||
/>
|
||||
</template>
|
||||
11
playground/src/views/demos/features/new-window/index.vue
Normal file
11
playground/src/views/demos/features/new-window/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面已在新窗口内打开"
|
||||
status="coming-soon"
|
||||
title="新窗口打开页面"
|
||||
/>
|
||||
</template>
|
||||
105
playground/src/views/demos/features/tabs/index.vue
Normal file
105
playground/src/views/demos/features/tabs/index.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { Button, Card, Input } from 'ant-design-vue';
|
||||
|
||||
const router = useRouter();
|
||||
const newTabTitle = ref('');
|
||||
|
||||
const {
|
||||
closeAllTabs,
|
||||
closeCurrentTab,
|
||||
closeLeftTabs,
|
||||
closeOtherTabs,
|
||||
closeRightTabs,
|
||||
closeTabByKey,
|
||||
refreshTab,
|
||||
resetTabTitle,
|
||||
setTabTitle,
|
||||
} = useTabs();
|
||||
|
||||
function openTab() {
|
||||
// 这里就是路由跳转,也可以用path
|
||||
router.push({ name: 'VbenAbout' });
|
||||
}
|
||||
|
||||
function openTabWithParams(id: number) {
|
||||
// 这里就是路由跳转,也可以用path
|
||||
router.push({ name: 'FeatureTabDetailDemo', params: { id } });
|
||||
}
|
||||
|
||||
function reset() {
|
||||
newTabTitle.value = '';
|
||||
resetTabTitle();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page description="用于需要操作标签页的场景" title="标签页">
|
||||
<Card class="mb-5" title="打开/关闭标签页">
|
||||
<div class="text-foreground/80 mb-3">
|
||||
如果标签页存在,直接跳转切换。如果标签页不存在,则打开新的标签页。
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button type="primary" @click="openTab"> 打开 "关于" 标签页 </Button>
|
||||
<Button type="primary" @click="closeTabByKey('/vben-admin/about')">
|
||||
关闭 "关于" 标签页
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="标签页操作">
|
||||
<div class="text-foreground/80 mb-3">用于动态控制标签页的各种操作</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button type="primary" @click="closeCurrentTab()">
|
||||
关闭当前标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="closeLeftTabs()">
|
||||
关闭左侧标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="closeRightTabs()">
|
||||
关闭右侧标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="closeAllTabs()"> 关闭所有标签页 </Button>
|
||||
<Button type="primary" @click="closeOtherTabs()">
|
||||
关闭其他标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="refreshTab()"> 刷新当前标签页 </Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="动态标题">
|
||||
<div class="text-foreground/80 mb-3">
|
||||
该操作不会影响页面标题,仅修改Tab标题
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<Input
|
||||
v-model:value="newTabTitle"
|
||||
class="w-40"
|
||||
placeholder="请输入新标题"
|
||||
/>
|
||||
<Button type="primary" @click="() => setTabTitle(newTabTitle)">
|
||||
修改
|
||||
</Button>
|
||||
<Button @click="reset"> 重置 </Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="最大打开数量">
|
||||
<div class="text-foreground/80 mb-3">
|
||||
限制带参数的tab打开的最大数量,由 `route.meta.maxNumOfOpenTab` 控制
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<template v-for="item in 5" :key="item">
|
||||
<Button type="primary" @click="openTabWithParams(item)">
|
||||
打开{{ item }}详情页
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
23
playground/src/views/demos/features/tabs/tab-detail.vue
Normal file
23
playground/src/views/demos/features/tabs/tab-detail.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { setTabTitle } = useTabs();
|
||||
|
||||
const index = computed(() => {
|
||||
return route.params?.id ?? -1;
|
||||
});
|
||||
|
||||
setTabTitle(`No.${index.value} - 详情信息`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="`标签页${index}详情页`">
|
||||
<template #description> {{ index }} - 详情页内容在此 </template>
|
||||
</Page>
|
||||
</template>
|
||||
25
playground/src/views/demos/features/vue-query/index.vue
Normal file
25
playground/src/views/demos/features/vue-query/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card } from 'ant-design-vue';
|
||||
|
||||
import InfiniteQueries from './infinite-queries.vue';
|
||||
import PaginatedQueries from './paginated-queries.vue';
|
||||
import QueryRetries from './query-retries.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="Vue Query示例">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Card title="分页查询">
|
||||
<PaginatedQueries />
|
||||
</Card>
|
||||
<Card title="无限滚动">
|
||||
<InfiniteQueries class="h-[300px] overflow-auto" />
|
||||
</Card>
|
||||
<Card title="错误重试">
|
||||
<QueryRetries />
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { IProducts } from './typing';
|
||||
|
||||
import { useInfiniteQuery } from '@tanstack/vue-query';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const LIMIT = 10;
|
||||
const fetchProducts = async ({ pageParam = 0 }): Promise<IProducts> => {
|
||||
const res = await fetch(
|
||||
`https://dummyjson.com/products?limit=${LIMIT}&skip=${pageParam * LIMIT}`,
|
||||
);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isError,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
isPending,
|
||||
} = useInfiniteQuery({
|
||||
getNextPageParam: (current, allPages) => {
|
||||
const nextPage = allPages.length + 1;
|
||||
const lastPage = current.skip + current.limit;
|
||||
if (lastPage === current.total) return;
|
||||
return nextPage;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
queryFn: fetchProducts,
|
||||
queryKey: ['products'],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="isPending">加载...</span>
|
||||
<span v-else-if="isError">出错了: {{ error }}</span>
|
||||
<div v-else-if="data">
|
||||
<span v-if="isFetching && !isFetchingNextPage">Fetching...</span>
|
||||
<ul v-for="(group, index) in data.pages" :key="index">
|
||||
<li v-for="product in group.products" :key="product.id">
|
||||
{{ product.title }}
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
:disabled="!hasNextPage || isFetchingNextPage"
|
||||
@click="() => fetchNextPage()"
|
||||
>
|
||||
<span v-if="isFetchingNextPage">加载中...</span>
|
||||
<span v-else-if="hasNextPage">加载更多</span>
|
||||
<span v-else>没有更多了</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import type { IProducts } from './typing';
|
||||
|
||||
import { type Ref, ref } from 'vue';
|
||||
|
||||
import { keepPreviousData, useQuery } from '@tanstack/vue-query';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const LIMIT = 10;
|
||||
const fetcher = async (page: Ref<number>): Promise<IProducts> => {
|
||||
const res = await fetch(
|
||||
`https://dummyjson.com/products?limit=${LIMIT}&skip=${(page.value - 1) * LIMIT}`,
|
||||
);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const page = ref(1);
|
||||
const { data, error, isError, isPending, isPlaceholderData } = useQuery({
|
||||
// The data from the last successful fetch is available while new data is being requested.
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: () => fetcher(page),
|
||||
queryKey: ['products', page],
|
||||
});
|
||||
const prevPage = () => {
|
||||
page.value = Math.max(page.value - 1, 1);
|
||||
};
|
||||
const nextPage = () => {
|
||||
if (!isPlaceholderData.value) {
|
||||
page.value = page.value + 1;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<Button size="small" @click="prevPage">上一页</Button>
|
||||
<p>当前页: {{ page }}</p>
|
||||
<Button size="small" @click="nextPage">下一页</Button>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div v-if="isPending">加载中...</div>
|
||||
<div v-else-if="isError">出错了: {{ error }}</div>
|
||||
<div v-else-if="data">
|
||||
<ul>
|
||||
<li v-for="item in data.products" :key="item.id">
|
||||
{{ item.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const count = ref(-1);
|
||||
async function fetchApi() {
|
||||
count.value += 1;
|
||||
return new Promise((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('something went wrong!'));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const { error, isFetching, refetch } = useQuery({
|
||||
enabled: false, // Disable automatic refetching when the query mounts
|
||||
queryFn: fetchApi,
|
||||
queryKey: ['queryKey'],
|
||||
retry: 3, // Will retry failed requests 3 times before displaying an error
|
||||
});
|
||||
|
||||
const onClick = async () => {
|
||||
count.value = -1;
|
||||
await refetch();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button :loading="isFetching" @click="onClick"> 发起错误重试 </Button>
|
||||
<p v-if="count > 0" class="my-3">重试次数{{ count }}</p>
|
||||
<p>{{ error }}</p>
|
||||
</template>
|
||||
18
playground/src/views/demos/features/vue-query/typing.ts
Normal file
18
playground/src/views/demos/features/vue-query/typing.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface IProducts {
|
||||
limit: number;
|
||||
products: {
|
||||
brand: string;
|
||||
category: string;
|
||||
description: string;
|
||||
discountPercentage: string;
|
||||
id: string;
|
||||
images: string[];
|
||||
price: string;
|
||||
rating: string;
|
||||
stock: string;
|
||||
thumbnail: string;
|
||||
title: string;
|
||||
}[];
|
||||
skip: number;
|
||||
total: number;
|
||||
}
|
||||
86
playground/src/views/demos/features/watermark/index.vue
Normal file
86
playground/src/views/demos/features/watermark/index.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
const { destroyWatermark, updateWatermark, watermark } = useWatermark();
|
||||
|
||||
async function recreateWaterMark() {
|
||||
destroyWatermark();
|
||||
await updateWatermark({});
|
||||
}
|
||||
|
||||
async function createWaterMark() {
|
||||
await updateWatermark({
|
||||
advancedStyle: {
|
||||
colorStops: [
|
||||
{
|
||||
color: 'red',
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
offset: 1,
|
||||
},
|
||||
],
|
||||
type: 'linear',
|
||||
},
|
||||
content: `hello my watermark\n${new Date().toLocaleString()}`,
|
||||
globalAlpha: 0.5,
|
||||
gridLayoutOptions: {
|
||||
cols: 2,
|
||||
gap: [20, 20],
|
||||
matrix: [
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
],
|
||||
rows: 2,
|
||||
},
|
||||
height: 200,
|
||||
layout: 'grid',
|
||||
rotate: 22,
|
||||
width: 200,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="水印">
|
||||
<template #description>
|
||||
<div class="text-foreground/80 mt-2">
|
||||
水印使用了
|
||||
<a
|
||||
class="text-primary"
|
||||
href="https://zhensherlock.github.io/watermark-js-plus/"
|
||||
target="_blank"
|
||||
>
|
||||
watermark-js-plus
|
||||
</a>
|
||||
开源插件,详细配置可见插件配置。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card title="使用">
|
||||
<Button
|
||||
:disabled="!!watermark"
|
||||
class="mr-2"
|
||||
type="primary"
|
||||
@click="recreateWaterMark"
|
||||
>
|
||||
创建水印
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="!watermark"
|
||||
class="mr-2"
|
||||
type="primary"
|
||||
@click="createWaterMark"
|
||||
>
|
||||
更新水印
|
||||
</Button>
|
||||
<Button :disabled="!watermark" danger @click="destroyWatermark">
|
||||
移除水印
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
7
playground/src/views/demos/nested/menu-1.vue
Normal file
7
playground/src/views/demos/nested/menu-1.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
7
playground/src/views/demos/nested/menu-2-1.vue
Normal file
7
playground/src/views/demos/nested/menu-2-1.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
7
playground/src/views/demos/nested/menu-3-1.vue
Normal file
7
playground/src/views/demos/nested/menu-3-1.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user