hookehuyr

feat(backend-mock): 新增后端模拟服务基础功能

添加后端模拟服务的基础功能,包括用户认证、菜单管理、部门管理、角色管理等模块。提供模拟数据支持前端开发,包含以下主要功能:

- 用户登录、登出及令牌刷新
- 菜单权限管理
- 部门树形结构管理
- 角色权限配置
- 时区设置功能
- 表格数据模拟
- 错误处理中间件
- 跨域支持配置

同时添加了相关文档说明和测试接口,便于前端开发时使用。
Showing 44 changed files with 1349 additions and 23 deletions
1 +PORT=5320
2 +ACCESS_TOKEN_SECRET=access_token_secret
3 +REFRESH_TOKEN_SECRET=refresh_token_secret
1 +# @vben/backend-mock
2 +
3 +## Description
4 +
5 +Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
6 +
7 +## Running the app
8 +
9 +```bash
10 +# development
11 +$ pnpm run start
12 +
13 +# production mode
14 +$ pnpm run build
15 +```
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { MOCK_CODES } from '~/utils/mock-data';
4 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
5 +
6 +export default eventHandler((event) => {
7 + const userinfo = verifyAccessToken(event);
8 + if (!userinfo) {
9 + return unAuthorizedResponse(event);
10 + }
11 +
12 + const codes =
13 + MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
14 +
15 + return useResponseSuccess(codes);
16 +});
1 +import { defineEventHandler, readBody, setResponseStatus } from 'h3';
2 +import {
3 + clearRefreshTokenCookie,
4 + setRefreshTokenCookie,
5 +} from '~/utils/cookie-utils';
6 +import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
7 +import { MOCK_USERS } from '~/utils/mock-data';
8 +import {
9 + forbiddenResponse,
10 + useResponseError,
11 + useResponseSuccess,
12 +} from '~/utils/response';
13 +
14 +export default defineEventHandler(async (event) => {
15 + const { password, username } = await readBody(event);
16 + if (!password || !username) {
17 + setResponseStatus(event, 400);
18 + return useResponseError(
19 + 'BadRequestException',
20 + 'Username and password are required',
21 + );
22 + }
23 +
24 + const findUser = MOCK_USERS.find(
25 + (item) => item.username === username && item.password === password,
26 + );
27 +
28 + if (!findUser) {
29 + clearRefreshTokenCookie(event);
30 + return forbiddenResponse(event, 'Username or password is incorrect.');
31 + }
32 +
33 + const accessToken = generateAccessToken(findUser);
34 + const refreshToken = generateRefreshToken(findUser);
35 +
36 + setRefreshTokenCookie(event, refreshToken);
37 +
38 + return useResponseSuccess({
39 + ...findUser,
40 + accessToken,
41 + });
42 +});
1 +import { defineEventHandler } from 'h3';
2 +import {
3 + clearRefreshTokenCookie,
4 + getRefreshTokenFromCookie,
5 +} from '~/utils/cookie-utils';
6 +import { useResponseSuccess } from '~/utils/response';
7 +
8 +export default defineEventHandler(async (event) => {
9 + const refreshToken = getRefreshTokenFromCookie(event);
10 + if (!refreshToken) {
11 + return useResponseSuccess('');
12 + }
13 +
14 + clearRefreshTokenCookie(event);
15 +
16 + return useResponseSuccess('');
17 +});
1 +import { defineEventHandler } from 'h3';
2 +import {
3 + clearRefreshTokenCookie,
4 + getRefreshTokenFromCookie,
5 + setRefreshTokenCookie,
6 +} from '~/utils/cookie-utils';
7 +import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
8 +import { MOCK_USERS } from '~/utils/mock-data';
9 +import { forbiddenResponse } from '~/utils/response';
10 +
11 +export default defineEventHandler(async (event) => {
12 + const refreshToken = getRefreshTokenFromCookie(event);
13 + if (!refreshToken) {
14 + return forbiddenResponse(event);
15 + }
16 +
17 + clearRefreshTokenCookie(event);
18 +
19 + const userinfo = verifyRefreshToken(refreshToken);
20 + if (!userinfo) {
21 + return forbiddenResponse(event);
22 + }
23 +
24 + const findUser = MOCK_USERS.find(
25 + (item) => item.username === userinfo.username,
26 + );
27 + if (!findUser) {
28 + return forbiddenResponse(event);
29 + }
30 + const accessToken = generateAccessToken(findUser);
31 +
32 + setRefreshTokenCookie(event, refreshToken);
33 +
34 + return accessToken;
35 +});
1 +import { eventHandler, setHeader } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { unAuthorizedResponse } from '~/utils/response';
4 +
5 +export default eventHandler(async (event) => {
6 + const userinfo = verifyAccessToken(event);
7 + if (!userinfo) {
8 + return unAuthorizedResponse(event);
9 + }
10 + const data = `
11 + {
12 + "code": 0,
13 + "message": "success",
14 + "data": [
15 + {
16 + "id": 123456789012345678901234567890123456789012345678901234567890,
17 + "name": "John Doe",
18 + "age": 30,
19 + "email": "john-doe@demo.com"
20 + },
21 + {
22 + "id": 987654321098765432109876543210987654321098765432109876543210,
23 + "name": "Jane Smith",
24 + "age": 25,
25 + "email": "jane@demo.com"
26 + }
27 + ]
28 + }
29 + `;
30 + setHeader(event, 'Content-Type', 'application/json');
31 + return data;
32 +});
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { MOCK_MENUS } from '~/utils/mock-data';
4 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
5 +
6 +export default eventHandler(async (event) => {
7 + const userinfo = verifyAccessToken(event);
8 + if (!userinfo) {
9 + return unAuthorizedResponse(event);
10 + }
11 +
12 + const menus =
13 + MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
14 + return useResponseSuccess(menus);
15 +});
1 +import { eventHandler, getQuery, setResponseStatus } from 'h3';
2 +import { useResponseError } from '~/utils/response';
3 +
4 +export default eventHandler((event) => {
5 + const { status } = getQuery(event);
6 + setResponseStatus(event, Number(status));
7 + return useResponseError(`${status}`);
8 +});
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import {
4 + sleep,
5 + unAuthorizedResponse,
6 + useResponseSuccess,
7 +} from '~/utils/response';
8 +
9 +export default eventHandler(async (event) => {
10 + const userinfo = verifyAccessToken(event);
11 + if (!userinfo) {
12 + return unAuthorizedResponse(event);
13 + }
14 + await sleep(600);
15 + return useResponseSuccess(null);
16 +});
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import {
4 + sleep,
5 + unAuthorizedResponse,
6 + useResponseSuccess,
7 +} from '~/utils/response';
8 +
9 +export default eventHandler(async (event) => {
10 + const userinfo = verifyAccessToken(event);
11 + if (!userinfo) {
12 + return unAuthorizedResponse(event);
13 + }
14 + await sleep(1000);
15 + return useResponseSuccess(null);
16 +});
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import {
4 + sleep,
5 + unAuthorizedResponse,
6 + useResponseSuccess,
7 +} from '~/utils/response';
8 +
9 +export default eventHandler(async (event) => {
10 + const userinfo = verifyAccessToken(event);
11 + if (!userinfo) {
12 + return unAuthorizedResponse(event);
13 + }
14 + await sleep(2000);
15 + return useResponseSuccess(null);
16 +});
1 +import { faker } from '@faker-js/faker';
2 +import { eventHandler } from 'h3';
3 +import { verifyAccessToken } from '~/utils/jwt-utils';
4 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
5 +
6 +const formatterCN = new Intl.DateTimeFormat('zh-CN', {
7 + timeZone: 'Asia/Shanghai',
8 + year: 'numeric',
9 + month: '2-digit',
10 + day: '2-digit',
11 + hour: '2-digit',
12 + minute: '2-digit',
13 + second: '2-digit',
14 +});
15 +
16 +function generateMockDataList(count: number) {
17 + const dataList = [];
18 +
19 + for (let i = 0; i < count; i++) {
20 + const dataItem: Record<string, any> = {
21 + id: faker.string.uuid(),
22 + pid: 0,
23 + name: faker.commerce.department(),
24 + status: faker.helpers.arrayElement([0, 1]),
25 + createTime: formatterCN.format(
26 + faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
27 + ),
28 + remark: faker.lorem.sentence(),
29 + };
30 + if (faker.datatype.boolean()) {
31 + dataItem.children = Array.from(
32 + { length: faker.number.int({ min: 1, max: 5 }) },
33 + () => ({
34 + id: faker.string.uuid(),
35 + pid: dataItem.id,
36 + name: faker.commerce.department(),
37 + status: faker.helpers.arrayElement([0, 1]),
38 + createTime: formatterCN.format(
39 + faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
40 + ),
41 + remark: faker.lorem.sentence(),
42 + }),
43 + );
44 + }
45 + dataList.push(dataItem);
46 + }
47 +
48 + return dataList;
49 +}
50 +
51 +const mockData = generateMockDataList(10);
52 +
53 +export default eventHandler(async (event) => {
54 + const userinfo = verifyAccessToken(event);
55 + if (!userinfo) {
56 + return unAuthorizedResponse(event);
57 + }
58 +
59 + const listData = structuredClone(mockData);
60 +
61 + return useResponseSuccess(listData);
62 +});
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { MOCK_MENU_LIST } from '~/utils/mock-data';
4 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
5 +
6 +export default eventHandler(async (event) => {
7 + const userinfo = verifyAccessToken(event);
8 + if (!userinfo) {
9 + return unAuthorizedResponse(event);
10 + }
11 +
12 + return useResponseSuccess(MOCK_MENU_LIST);
13 +});
1 +import { eventHandler, getQuery } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { MOCK_MENU_LIST } from '~/utils/mock-data';
4 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
5 +
6 +const namesMap: Record<string, any> = {};
7 +
8 +function getNames(menus: any[]) {
9 + menus.forEach((menu) => {
10 + namesMap[menu.name] = String(menu.id);
11 + if (menu.children) {
12 + getNames(menu.children);
13 + }
14 + });
15 +}
16 +getNames(MOCK_MENU_LIST);
17 +
18 +export default eventHandler(async (event) => {
19 + const userinfo = verifyAccessToken(event);
20 + if (!userinfo) {
21 + return unAuthorizedResponse(event);
22 + }
23 + const { id, name } = getQuery(event);
24 +
25 + return (name as string) in namesMap &&
26 + (!id || namesMap[name as string] !== String(id))
27 + ? useResponseSuccess(true)
28 + : useResponseSuccess(false);
29 +});
1 +import { eventHandler, getQuery } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { MOCK_MENU_LIST } from '~/utils/mock-data';
4 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
5 +
6 +const pathMap: Record<string, any> = { '/': 0 };
7 +
8 +function getPaths(menus: any[]) {
9 + menus.forEach((menu) => {
10 + pathMap[menu.path] = String(menu.id);
11 + if (menu.children) {
12 + getPaths(menu.children);
13 + }
14 + });
15 +}
16 +getPaths(MOCK_MENU_LIST);
17 +
18 +export default eventHandler(async (event) => {
19 + const userinfo = verifyAccessToken(event);
20 + if (!userinfo) {
21 + return unAuthorizedResponse(event);
22 + }
23 + const { id, path } = getQuery(event);
24 +
25 + return (path as string) in pathMap &&
26 + (!id || pathMap[path as string] !== String(id))
27 + ? useResponseSuccess(true)
28 + : useResponseSuccess(false);
29 +});
1 +import { faker } from '@faker-js/faker';
2 +import { eventHandler, getQuery } from 'h3';
3 +import { verifyAccessToken } from '~/utils/jwt-utils';
4 +import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
5 +import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
6 +
7 +const formatterCN = new Intl.DateTimeFormat('zh-CN', {
8 + timeZone: 'Asia/Shanghai',
9 + year: 'numeric',
10 + month: '2-digit',
11 + day: '2-digit',
12 + hour: '2-digit',
13 + minute: '2-digit',
14 + second: '2-digit',
15 +});
16 +
17 +const menuIds = getMenuIds(MOCK_MENU_LIST);
18 +
19 +function generateMockDataList(count: number) {
20 + const dataList = [];
21 +
22 + for (let i = 0; i < count; i++) {
23 + const dataItem: Record<string, any> = {
24 + id: faker.string.uuid(),
25 + name: faker.commerce.product(),
26 + status: faker.helpers.arrayElement([0, 1]),
27 + createTime: formatterCN.format(
28 + faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
29 + ),
30 + permissions: faker.helpers.arrayElements(menuIds),
31 + remark: faker.lorem.sentence(),
32 + };
33 +
34 + dataList.push(dataItem);
35 + }
36 +
37 + return dataList;
38 +}
39 +
40 +const mockData = generateMockDataList(100);
41 +
42 +export default eventHandler(async (event) => {
43 + const userinfo = verifyAccessToken(event);
44 + if (!userinfo) {
45 + return unAuthorizedResponse(event);
46 + }
47 +
48 + const {
49 + page = 1,
50 + pageSize = 20,
51 + name,
52 + id,
53 + remark,
54 + startTime,
55 + endTime,
56 + status,
57 + } = getQuery(event);
58 + let listData = structuredClone(mockData);
59 + if (name) {
60 + listData = listData.filter((item) =>
61 + item.name.toLowerCase().includes(String(name).toLowerCase()),
62 + );
63 + }
64 + if (id) {
65 + listData = listData.filter((item) =>
66 + item.id.toLowerCase().includes(String(id).toLowerCase()),
67 + );
68 + }
69 + if (remark) {
70 + listData = listData.filter((item) =>
71 + item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
72 + );
73 + }
74 + if (startTime) {
75 + listData = listData.filter((item) => item.createTime >= startTime);
76 + }
77 + if (endTime) {
78 + listData = listData.filter((item) => item.createTime <= endTime);
79 + }
80 + if (['0', '1'].includes(status as string)) {
81 + listData = listData.filter((item) => item.status === Number(status));
82 + }
83 + return usePageResponseSuccess(page as string, pageSize as string, listData);
84 +});
1 +import { faker } from '@faker-js/faker';
2 +import { eventHandler, getQuery } from 'h3';
3 +import { verifyAccessToken } from '~/utils/jwt-utils';
4 +import {
5 + sleep,
6 + unAuthorizedResponse,
7 + usePageResponseSuccess,
8 +} from '~/utils/response';
9 +
10 +function generateMockDataList(count: number) {
11 + const dataList = [];
12 +
13 + for (let i = 0; i < count; i++) {
14 + const dataItem = {
15 + id: faker.string.uuid(),
16 + imageUrl: faker.image.avatar(),
17 + imageUrl2: faker.image.avatar(),
18 + open: faker.datatype.boolean(),
19 + status: faker.helpers.arrayElement(['success', 'error', 'warning']),
20 + productName: faker.commerce.productName(),
21 + price: faker.commerce.price(),
22 + currency: faker.finance.currencyCode(),
23 + quantity: faker.number.int({ min: 1, max: 100 }),
24 + available: faker.datatype.boolean(),
25 + category: faker.commerce.department(),
26 + releaseDate: faker.date.past(),
27 + rating: faker.number.float({ min: 1, max: 5 }),
28 + description: faker.commerce.productDescription(),
29 + weight: faker.number.float({ min: 0.1, max: 10 }),
30 + color: faker.color.human(),
31 + inProduction: faker.datatype.boolean(),
32 + tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
33 + };
34 +
35 + dataList.push(dataItem);
36 + }
37 +
38 + return dataList;
39 +}
40 +
41 +const mockData = generateMockDataList(100);
42 +
43 +export default eventHandler(async (event) => {
44 + const userinfo = verifyAccessToken(event);
45 + if (!userinfo) {
46 + return unAuthorizedResponse(event);
47 + }
48 +
49 + await sleep(600);
50 +
51 + const { page, pageSize, sortBy, sortOrder } = getQuery(event);
52 + // 规范化分页参数,处理 string[]
53 + const pageRaw = Array.isArray(page) ? page[0] : page;
54 + const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
55 + const pageNumber = Math.max(
56 + 1,
57 + Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
58 + );
59 + const pageSizeNumber = Math.min(
60 + 100,
61 + Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
62 + );
63 + const listData = structuredClone(mockData);
64 +
65 + // 规范化 query 入参,兼容 string[]
66 + const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
67 + const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
68 + // 检查 sortBy 是否是 listData 元素的合法属性键
69 + if (
70 + typeof sortKeyRaw === 'string' &&
71 + listData[0] &&
72 + Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
73 + ) {
74 + // 定义数组元素的类型
75 + type ItemType = (typeof listData)[0];
76 + const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
77 + const isDesc = sortOrderRaw === 'desc';
78 + listData.sort((a, b) => {
79 + const aValue = a[sortKey] as unknown;
80 + const bValue = b[sortKey] as unknown;
81 +
82 + let result = 0;
83 +
84 + if (typeof aValue === 'number' && typeof bValue === 'number') {
85 + result = aValue - bValue;
86 + } else if (aValue instanceof Date && bValue instanceof Date) {
87 + result = aValue.getTime() - bValue.getTime();
88 + } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
89 + if (aValue === bValue) {
90 + result = 0;
91 + } else {
92 + result = aValue ? 1 : -1;
93 + }
94 + } else {
95 + const aStr = String(aValue);
96 + const bStr = String(bValue);
97 + const aNum = Number(aStr);
98 + const bNum = Number(bStr);
99 + result =
100 + Number.isFinite(aNum) && Number.isFinite(bNum)
101 + ? aNum - bNum
102 + : aStr.localeCompare(bStr, undefined, {
103 + numeric: true,
104 + sensitivity: 'base',
105 + });
106 + }
107 +
108 + return isDesc ? -result : result;
109 + });
110 + }
111 +
112 + return usePageResponseSuccess(
113 + String(pageNumber),
114 + String(pageSizeNumber),
115 + listData,
116 + );
117 +});
1 +import { defineEventHandler } from 'h3';
2 +
3 +export default defineEventHandler(() => 'Test get handler');
1 +import { defineEventHandler } from 'h3';
2 +
3 +export default defineEventHandler(() => 'Test post handler');
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
4 +import { getTimezone } from '~/utils/timezone-utils';
5 +
6 +export default eventHandler((event) => {
7 + const userinfo = verifyAccessToken(event);
8 + if (!userinfo) {
9 + return unAuthorizedResponse(event);
10 + }
11 + return useResponseSuccess(getTimezone());
12 +});
1 +import { eventHandler } from 'h3';
2 +import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
3 +import { useResponseSuccess } from '~/utils/response';
4 +
5 +export default eventHandler(() => {
6 + const data = TIME_ZONE_OPTIONS.map((o) => ({
7 + label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
8 + value: o.timezone,
9 + }));
10 + return useResponseSuccess(data);
11 +});
1 +import { eventHandler, readBody } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
4 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
5 +import { setTimezone } from '~/utils/timezone-utils';
6 +
7 +export default eventHandler(async (event) => {
8 + const userinfo = verifyAccessToken(event);
9 + if (!userinfo) {
10 + return unAuthorizedResponse(event);
11 + }
12 + const body = await readBody<{ timezone?: unknown }>(event);
13 + const timezone =
14 + typeof body?.timezone === 'string' ? body.timezone : undefined;
15 + const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
16 + if (!timezone || !allowed) {
17 + setResponseStatus(event, 400);
18 + return useResponseError('Bad Request', 'Invalid timezone');
19 + }
20 + setTimezone(timezone);
21 + return useResponseSuccess({});
22 +});
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
4 +
5 +export default eventHandler((event) => {
6 + const userinfo = verifyAccessToken(event);
7 + if (!userinfo) {
8 + return unAuthorizedResponse(event);
9 + }
10 + return useResponseSuccess({
11 + url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
12 + });
13 + // return useResponseError("test")
14 +});
1 +import { eventHandler } from 'h3';
2 +import { verifyAccessToken } from '~/utils/jwt-utils';
3 +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
4 +
5 +export default eventHandler((event) => {
6 + const userinfo = verifyAccessToken(event);
7 + if (!userinfo) {
8 + return unAuthorizedResponse(event);
9 + }
10 + return useResponseSuccess(userinfo);
11 +});
1 +import type { NitroErrorHandler } from 'nitropack';
2 +
3 +const errorHandler: NitroErrorHandler = function (error, event) {
4 + event.node.res.end(`[Error Handler] ${error.stack}`);
5 +};
6 +
7 +export default errorHandler;
1 +import { defineEventHandler } from 'h3';
2 +import { forbiddenResponse, sleep } from '~/utils/response';
3 +
4 +export default defineEventHandler(async (event) => {
5 + event.node.res.setHeader(
6 + 'Access-Control-Allow-Origin',
7 + event.headers.get('Origin') ?? '*',
8 + );
9 + if (event.method === 'OPTIONS') {
10 + event.node.res.statusCode = 204;
11 + event.node.res.statusMessage = 'No Content.';
12 + return 'OK';
13 + } else if (
14 + ['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
15 + event.path.startsWith('/api/system/')
16 + ) {
17 + await sleep(Math.floor(Math.random() * 2000));
18 + return forbiddenResponse(event, '演示环境,禁止修改');
19 + }
20 +});
1 +import errorHandler from './error';
2 +
3 +process.env.COMPATIBILITY_DATE = new Date().toISOString();
4 +export default defineNitroConfig({
5 + devErrorHandler: errorHandler,
6 + errorHandler: '~/error',
7 + routeRules: {
8 + '/api/**': {
9 + cors: true,
10 + headers: {
11 + 'Access-Control-Allow-Credentials': 'true',
12 + 'Access-Control-Allow-Headers':
13 + 'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
14 + 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
15 + 'Access-Control-Allow-Origin': '*',
16 + 'Access-Control-Expose-Headers': '*',
17 + },
18 + },
19 + },
20 +});
1 +{
2 + "name": "@vben/backend-mock",
3 + "version": "0.0.1",
4 + "description": "",
5 + "private": true,
6 + "license": "MIT",
7 + "author": "",
8 + "scripts": {
9 + "build": "nitro build",
10 + "start": "nitro dev"
11 + },
12 + "dependencies": {
13 + "@faker-js/faker": "catalog:",
14 + "jsonwebtoken": "catalog:",
15 + "nitropack": "catalog:"
16 + },
17 + "devDependencies": {
18 + "@types/jsonwebtoken": "catalog:",
19 + "h3": "catalog:"
20 + }
21 +}
1 +import { defineEventHandler } from 'h3';
2 +
3 +export default defineEventHandler(() => {
4 + return `
5 +<h1>Hello Vben Admin</h1>
6 +<h2>Mock service is starting</h2>
7 +<ul>
8 +<li><a href="/api/user">/api/user/info</a></li>
9 +<li><a href="/api/menu">/api/menu/all</a></li>
10 +<li><a href="/api/auth/codes">/api/auth/codes</a></li>
11 +<li><a href="/api/auth/login">/api/auth/login</a></li>
12 +<li><a href="/api/upload">/api/upload</a></li>
13 +</ul>
14 +`;
15 +});
1 +{
2 + "extends": "./tsconfig.json",
3 + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 +}
1 +{
2 + "extends": "./.nitro/types/tsconfig.json"
3 +}
1 +import type { EventHandlerRequest, H3Event } from 'h3';
2 +
3 +import { deleteCookie, getCookie, setCookie } from 'h3';
4 +
5 +export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
6 + deleteCookie(event, 'jwt', {
7 + httpOnly: true,
8 + sameSite: 'none',
9 + secure: true,
10 + });
11 +}
12 +
13 +export function setRefreshTokenCookie(
14 + event: H3Event<EventHandlerRequest>,
15 + refreshToken: string,
16 +) {
17 + setCookie(event, 'jwt', refreshToken, {
18 + httpOnly: true,
19 + maxAge: 24 * 60 * 60, // unit: seconds
20 + sameSite: 'none',
21 + secure: true,
22 + });
23 +}
24 +
25 +export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
26 + const refreshToken = getCookie(event, 'jwt');
27 + return refreshToken;
28 +}
1 +import type { EventHandlerRequest, H3Event } from 'h3';
2 +
3 +import type { UserInfo } from './mock-data';
4 +
5 +import { getHeader } from 'h3';
6 +import jwt from 'jsonwebtoken';
7 +
8 +import { MOCK_USERS } from './mock-data';
9 +
10 +// TODO: Replace with your own secret key
11 +const ACCESS_TOKEN_SECRET = 'access_token_secret';
12 +const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
13 +
14 +export interface UserPayload extends UserInfo {
15 + iat: number;
16 + exp: number;
17 +}
18 +
19 +export function generateAccessToken(user: UserInfo) {
20 + return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
21 +}
22 +
23 +export function generateRefreshToken(user: UserInfo) {
24 + return jwt.sign(user, REFRESH_TOKEN_SECRET, {
25 + expiresIn: '30d',
26 + });
27 +}
28 +
29 +export function verifyAccessToken(
30 + event: H3Event<EventHandlerRequest>,
31 +): null | Omit<UserInfo, 'password'> {
32 + const authHeader = getHeader(event, 'Authorization');
33 + if (!authHeader?.startsWith('Bearer')) {
34 + return null;
35 + }
36 +
37 + const tokenParts = authHeader.split(' ');
38 + if (tokenParts.length !== 2) {
39 + return null;
40 + }
41 + const token = tokenParts[1] as string;
42 + try {
43 + const decoded = jwt.verify(
44 + token,
45 + ACCESS_TOKEN_SECRET,
46 + ) as unknown as UserPayload;
47 +
48 + const username = decoded.username;
49 + const user = MOCK_USERS.find((item) => item.username === username);
50 + if (!user) {
51 + return null;
52 + }
53 + const { password: _pwd, ...userinfo } = user;
54 + return userinfo;
55 + } catch {
56 + return null;
57 + }
58 +}
59 +
60 +export function verifyRefreshToken(
61 + token: string,
62 +): null | Omit<UserInfo, 'password'> {
63 + try {
64 + const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
65 + const username = decoded.username;
66 + const user = MOCK_USERS.find(
67 + (item) => item.username === username,
68 + ) as UserInfo;
69 + if (!user) {
70 + return null;
71 + }
72 + const { password: _pwd, ...userinfo } = user;
73 + return userinfo;
74 + } catch {
75 + return null;
76 + }
77 +}
1 +export interface UserInfo {
2 + id: number;
3 + password: string;
4 + realName: string;
5 + roles: string[];
6 + username: string;
7 + homePath?: string;
8 +}
9 +
10 +export interface TimezoneOption {
11 + offset: number;
12 + timezone: string;
13 +}
14 +
15 +export const MOCK_USERS: UserInfo[] = [
16 + {
17 + id: 0,
18 + password: '123456',
19 + realName: 'Vben',
20 + roles: ['super'],
21 + username: 'vben',
22 + },
23 + {
24 + id: 1,
25 + password: '123456',
26 + realName: 'Admin',
27 + roles: ['admin'],
28 + username: 'admin',
29 + homePath: '/workspace',
30 + },
31 + {
32 + id: 2,
33 + password: '123456',
34 + realName: 'Jack',
35 + roles: ['user'],
36 + username: 'jack',
37 + homePath: '/analytics',
38 + },
39 +];
40 +
41 +export const MOCK_CODES = [
42 + // super
43 + {
44 + codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
45 + username: 'vben',
46 + },
47 + {
48 + // admin
49 + codes: ['AC_100010', 'AC_100020', 'AC_100030'],
50 + username: 'admin',
51 + },
52 + {
53 + // user
54 + codes: ['AC_1000001', 'AC_1000002'],
55 + username: 'jack',
56 + },
57 +];
58 +
59 +const dashboardMenus = [
60 + {
61 + meta: {
62 + order: -1,
63 + title: 'page.dashboard.title',
64 + },
65 + name: 'Dashboard',
66 + path: '/dashboard',
67 + redirect: '/analytics',
68 + children: [
69 + {
70 + name: 'Analytics',
71 + path: '/analytics',
72 + component: '/dashboard/analytics/index',
73 + meta: {
74 + affixTab: true,
75 + title: 'page.dashboard.analytics',
76 + },
77 + },
78 + {
79 + name: 'Workspace',
80 + path: '/workspace',
81 + component: '/dashboard/workspace/index',
82 + meta: {
83 + title: 'page.dashboard.workspace',
84 + },
85 + },
86 + ],
87 + },
88 +];
89 +
90 +const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
91 + const roleWithMenus = {
92 + admin: {
93 + component: '/demos/access/admin-visible',
94 + meta: {
95 + icon: 'mdi:button-cursor',
96 + title: 'demos.access.adminVisible',
97 + },
98 + name: 'AccessAdminVisibleDemo',
99 + path: '/demos/access/admin-visible',
100 + },
101 + super: {
102 + component: '/demos/access/super-visible',
103 + meta: {
104 + icon: 'mdi:button-cursor',
105 + title: 'demos.access.superVisible',
106 + },
107 + name: 'AccessSuperVisibleDemo',
108 + path: '/demos/access/super-visible',
109 + },
110 + user: {
111 + component: '/demos/access/user-visible',
112 + meta: {
113 + icon: 'mdi:button-cursor',
114 + title: 'demos.access.userVisible',
115 + },
116 + name: 'AccessUserVisibleDemo',
117 + path: '/demos/access/user-visible',
118 + },
119 + };
120 +
121 + return [
122 + {
123 + meta: {
124 + icon: 'ic:baseline-view-in-ar',
125 + keepAlive: true,
126 + order: 1000,
127 + title: 'demos.title',
128 + },
129 + name: 'Demos',
130 + path: '/demos',
131 + redirect: '/demos/access',
132 + children: [
133 + {
134 + name: 'AccessDemos',
135 + path: '/demosaccess',
136 + meta: {
137 + icon: 'mdi:cloud-key-outline',
138 + title: 'demos.access.backendPermissions',
139 + },
140 + redirect: '/demos/access/page-control',
141 + children: [
142 + {
143 + name: 'AccessPageControlDemo',
144 + path: '/demos/access/page-control',
145 + component: '/demos/access/index',
146 + meta: {
147 + icon: 'mdi:page-previous-outline',
148 + title: 'demos.access.pageAccess',
149 + },
150 + },
151 + {
152 + name: 'AccessButtonControlDemo',
153 + path: '/demos/access/button-control',
154 + component: '/demos/access/button-control',
155 + meta: {
156 + icon: 'mdi:button-cursor',
157 + title: 'demos.access.buttonControl',
158 + },
159 + },
160 + {
161 + name: 'AccessMenuVisible403Demo',
162 + path: '/demos/access/menu-visible-403',
163 + component: '/demos/access/menu-visible-403',
164 + meta: {
165 + authority: ['no-body'],
166 + icon: 'mdi:button-cursor',
167 + menuVisibleWithForbidden: true,
168 + title: 'demos.access.menuVisible403',
169 + },
170 + },
171 + roleWithMenus[role],
172 + ],
173 + },
174 + ],
175 + },
176 + ];
177 +};
178 +
179 +export const MOCK_MENUS = [
180 + {
181 + menus: [...dashboardMenus, ...createDemosMenus('super')],
182 + username: 'vben',
183 + },
184 + {
185 + menus: [...dashboardMenus, ...createDemosMenus('admin')],
186 + username: 'admin',
187 + },
188 + {
189 + menus: [...dashboardMenus, ...createDemosMenus('user')],
190 + username: 'jack',
191 + },
192 +];
193 +
194 +export const MOCK_MENU_LIST = [
195 + {
196 + id: 1,
197 + name: 'Workspace',
198 + status: 1,
199 + type: 'menu',
200 + icon: 'mdi:dashboard',
201 + path: '/workspace',
202 + component: '/dashboard/workspace/index',
203 + meta: {
204 + icon: 'carbon:workspace',
205 + title: 'page.dashboard.workspace',
206 + affixTab: true,
207 + order: 0,
208 + },
209 + },
210 + {
211 + id: 2,
212 + meta: {
213 + icon: 'carbon:settings',
214 + order: 9997,
215 + title: 'system.title',
216 + badge: 'new',
217 + badgeType: 'normal',
218 + badgeVariants: 'primary',
219 + },
220 + status: 1,
221 + type: 'catalog',
222 + name: 'System',
223 + path: '/system',
224 + children: [
225 + {
226 + id: 201,
227 + pid: 2,
228 + path: '/system/menu',
229 + name: 'SystemMenu',
230 + authCode: 'System:Menu:List',
231 + status: 1,
232 + type: 'menu',
233 + meta: {
234 + icon: 'carbon:menu',
235 + title: 'system.menu.title',
236 + },
237 + component: '/system/menu/list',
238 + children: [
239 + {
240 + id: 20_101,
241 + pid: 201,
242 + name: 'SystemMenuCreate',
243 + status: 1,
244 + type: 'button',
245 + authCode: 'System:Menu:Create',
246 + meta: { title: 'common.create' },
247 + },
248 + {
249 + id: 20_102,
250 + pid: 201,
251 + name: 'SystemMenuEdit',
252 + status: 1,
253 + type: 'button',
254 + authCode: 'System:Menu:Edit',
255 + meta: { title: 'common.edit' },
256 + },
257 + {
258 + id: 20_103,
259 + pid: 201,
260 + name: 'SystemMenuDelete',
261 + status: 1,
262 + type: 'button',
263 + authCode: 'System:Menu:Delete',
264 + meta: { title: 'common.delete' },
265 + },
266 + ],
267 + },
268 + {
269 + id: 202,
270 + pid: 2,
271 + path: '/system/dept',
272 + name: 'SystemDept',
273 + status: 1,
274 + type: 'menu',
275 + authCode: 'System:Dept:List',
276 + meta: {
277 + icon: 'carbon:container-services',
278 + title: 'system.dept.title',
279 + },
280 + component: '/system/dept/list',
281 + children: [
282 + {
283 + id: 20_401,
284 + pid: 202,
285 + name: 'SystemDeptCreate',
286 + status: 1,
287 + type: 'button',
288 + authCode: 'System:Dept:Create',
289 + meta: { title: 'common.create' },
290 + },
291 + {
292 + id: 20_402,
293 + pid: 202,
294 + name: 'SystemDeptEdit',
295 + status: 1,
296 + type: 'button',
297 + authCode: 'System:Dept:Edit',
298 + meta: { title: 'common.edit' },
299 + },
300 + {
301 + id: 20_403,
302 + pid: 202,
303 + name: 'SystemDeptDelete',
304 + status: 1,
305 + type: 'button',
306 + authCode: 'System:Dept:Delete',
307 + meta: { title: 'common.delete' },
308 + },
309 + ],
310 + },
311 + ],
312 + },
313 + {
314 + id: 9,
315 + meta: {
316 + badgeType: 'dot',
317 + order: 9998,
318 + title: 'demos.vben.title',
319 + icon: 'carbon:data-center',
320 + },
321 + name: 'Project',
322 + path: '/vben-admin',
323 + type: 'catalog',
324 + status: 1,
325 + children: [
326 + {
327 + id: 901,
328 + pid: 9,
329 + name: 'VbenDocument',
330 + path: '/vben-admin/document',
331 + component: 'IFrameView',
332 + type: 'embedded',
333 + status: 1,
334 + meta: {
335 + icon: 'carbon:book',
336 + iframeSrc: 'https://doc.vben.pro',
337 + title: 'demos.vben.document',
338 + },
339 + },
340 + {
341 + id: 902,
342 + pid: 9,
343 + name: 'VbenGithub',
344 + path: '/vben-admin/github',
345 + component: 'IFrameView',
346 + type: 'link',
347 + status: 1,
348 + meta: {
349 + icon: 'carbon:logo-github',
350 + link: 'https://github.com/vbenjs/vue-vben-admin',
351 + title: 'Github',
352 + },
353 + },
354 + {
355 + id: 903,
356 + pid: 9,
357 + name: 'VbenAntdv',
358 + path: '/vben-admin/antdv',
359 + component: 'IFrameView',
360 + type: 'link',
361 + status: 0,
362 + meta: {
363 + icon: 'carbon:hexagon-vertical-solid',
364 + badgeType: 'dot',
365 + link: 'https://ant.vben.pro',
366 + title: 'demos.vben.antdv',
367 + },
368 + },
369 + ],
370 + },
371 + {
372 + id: 10,
373 + component: '_core/about/index',
374 + type: 'menu',
375 + status: 1,
376 + meta: {
377 + icon: 'lucide:copyright',
378 + order: 9999,
379 + title: 'demos.vben.about',
380 + },
381 + name: 'About',
382 + path: '/about',
383 + },
384 +];
385 +
386 +export function getMenuIds(menus: any[]) {
387 + const ids: number[] = [];
388 + menus.forEach((item) => {
389 + ids.push(item.id);
390 + if (item.children && item.children.length > 0) {
391 + ids.push(...getMenuIds(item.children));
392 + }
393 + });
394 + return ids;
395 +}
396 +
397 +/**
398 + * 时区选项
399 + */
400 +export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
401 + {
402 + offset: -5,
403 + timezone: 'America/New_York',
404 + },
405 + {
406 + offset: 0,
407 + timezone: 'Europe/London',
408 + },
409 + {
410 + offset: 8,
411 + timezone: 'Asia/Shanghai',
412 + },
413 + {
414 + offset: 9,
415 + timezone: 'Asia/Tokyo',
416 + },
417 + {
418 + offset: 9,
419 + timezone: 'Asia/Seoul',
420 + },
421 +];
1 +import type { EventHandlerRequest, H3Event } from 'h3';
2 +
3 +import { setResponseStatus } from 'h3';
4 +
5 +export function useResponseSuccess<T = any>(data: T) {
6 + return {
7 + code: 0,
8 + data,
9 + error: null,
10 + message: 'ok',
11 + };
12 +}
13 +
14 +export function usePageResponseSuccess<T = any>(
15 + page: number | string,
16 + pageSize: number | string,
17 + list: T[],
18 + { message = 'ok' } = {},
19 +) {
20 + const pageData = pagination(
21 + Number.parseInt(`${page}`),
22 + Number.parseInt(`${pageSize}`),
23 + list,
24 + );
25 +
26 + return {
27 + ...useResponseSuccess({
28 + items: pageData,
29 + total: list.length,
30 + }),
31 + message,
32 + };
33 +}
34 +
35 +export function useResponseError(message: string, error: any = null) {
36 + return {
37 + code: -1,
38 + data: null,
39 + error,
40 + message,
41 + };
42 +}
43 +
44 +export function forbiddenResponse(
45 + event: H3Event<EventHandlerRequest>,
46 + message = 'Forbidden Exception',
47 +) {
48 + setResponseStatus(event, 403);
49 + return useResponseError(message, message);
50 +}
51 +
52 +export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
53 + setResponseStatus(event, 401);
54 + return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
55 +}
56 +
57 +export function sleep(ms: number) {
58 + return new Promise((resolve) => setTimeout(resolve, ms));
59 +}
60 +
61 +export function pagination<T = any>(
62 + pageNo: number,
63 + pageSize: number,
64 + array: T[],
65 +): T[] {
66 + const offset = (pageNo - 1) * Number(pageSize);
67 + return offset + Number(pageSize) >= array.length
68 + ? array.slice(offset)
69 + : array.slice(offset, offset + Number(pageSize));
70 +}
1 +let mockTimeZone: null | string = null;
2 +
3 +export const setTimezone = (timeZone: string) => {
4 + mockTimeZone = timeZone;
5 +};
6 +
7 +export const getTimezone = () => {
8 + return mockTimeZone;
9 +};
...@@ -6,7 +6,6 @@ import path, { relative } from 'node:path'; ...@@ -6,7 +6,6 @@ import path, { relative } from 'node:path';
6 6
7 import { findMonorepoRoot } from '@vben/node-utils'; 7 import { findMonorepoRoot } from '@vben/node-utils';
8 8
9 -import { NodePackageImporter } from 'sass';
10 import { defineConfig, loadEnv, mergeConfig } from 'vite'; 9 import { defineConfig, loadEnv, mergeConfig } from 'vite';
11 10
12 import { defaultImportmapOptions, getDefaultPwaOptions } from '../options'; 11 import { defaultImportmapOptions, getDefaultPwaOptions } from '../options';
...@@ -114,8 +113,6 @@ function createCssOptions(injectGlobalScss = true): CSSOptions { ...@@ -114,8 +113,6 @@ function createCssOptions(injectGlobalScss = true): CSSOptions {
114 } 113 }
115 return content; 114 return content;
116 }, 115 },
117 - // api: 'modern',
118 - importers: [new NodePackageImporter()],
119 }, 116 },
120 } 117 }
121 : {}, 118 : {},
......
...@@ -29,9 +29,8 @@ describe('useSortable', () => { ...@@ -29,9 +29,8 @@ describe('useSortable', () => {
29 await initializeSortable(); 29 await initializeSortable();
30 30
31 // Import sortablejs to access the mocked create function 31 // Import sortablejs to access the mocked create function
32 - const Sortable = await import( 32 + const Sortable =
33 - 'sortablejs/modular/sortable.complete.esm.js' 33 + await import('sortablejs/modular/sortable.complete.esm.js');
34 - );
35 34
36 // Verify that Sortable.create was called with the correct parameters 35 // Verify that Sortable.create was called with the correct parameters
37 expect(Sortable.default.create).toHaveBeenCalledTimes(1); 36 expect(Sortable.default.create).toHaveBeenCalledTimes(1);
......
...@@ -352,7 +352,7 @@ export interface VbenFormProps< ...@@ -352,7 +352,7 @@ export interface VbenFormProps<
352 > extends Omit< 352 > extends Omit<
353 FormRenderProps<T>, 353 FormRenderProps<T>,
354 'componentBindEventMap' | 'componentMap' | 'form' 354 'componentBindEventMap' | 'componentMap' | 'form'
355 - > { 355 +> {
356 /** 356 /**
357 * 操作按钮是否反转(提交按钮前置) 357 * 操作按钮是否反转(提交按钮前置)
358 */ 358 */
......
...@@ -27,8 +27,10 @@ export type CustomRenderType = (() => Component | string) | string; ...@@ -27,8 +27,10 @@ export type CustomRenderType = (() => Component | string) | string;
27 27
28 export type ValueType = boolean | number | string; 28 export type ValueType = boolean | number | string;
29 29
30 -export interface VbenButtonGroupProps 30 +export interface VbenButtonGroupProps extends Pick<
31 - extends Pick<VbenButtonProps, 'disabled'> { 31 + VbenButtonProps,
32 + 'disabled'
33 +> {
32 /** 单选模式下允许清除选中 */ 34 /** 单选模式下允许清除选中 */
33 allowClear?: boolean; 35 allowClear?: boolean;
34 /** 值改变前的回调 */ 36 /** 值改变前的回调 */
......
...@@ -54,8 +54,7 @@ export interface PointSelectionCaptchaCardProps { ...@@ -54,8 +54,7 @@ export interface PointSelectionCaptchaCardProps {
54 width?: number | string; 54 width?: number | string;
55 } 55 }
56 56
57 -export interface PointSelectionCaptchaProps 57 +export interface PointSelectionCaptchaProps extends PointSelectionCaptchaCardProps {
58 - extends PointSelectionCaptchaCardProps {
59 /** 58 /**
60 * 是否展示确定按钮 59 * 是否展示确定按钮
61 * @default false 60 * @default false
......
...@@ -157,9 +157,7 @@ const routes: RouteRecordRaw[] = [ ...@@ -157,9 +157,7 @@ const routes: RouteRecordRaw[] = [
157 name: 'HideChildrenInMenuDemo', 157 name: 'HideChildrenInMenuDemo',
158 path: '', 158 path: '',
159 component: () => 159 component: () =>
160 - import( 160 + import('#/views/demos/features/hide-menu-children/parent.vue'),
161 - '#/views/demos/features/hide-menu-children/parent.vue'
162 - ),
163 meta: { 161 meta: {
164 // hideInMenu: true, 162 // hideInMenu: true,
165 title: $t('demos.features.hideChildrenInMenu'), 163 title: $t('demos.features.hideChildrenInMenu'),
...@@ -169,9 +167,7 @@ const routes: RouteRecordRaw[] = [ ...@@ -169,9 +167,7 @@ const routes: RouteRecordRaw[] = [
169 name: 'HideChildrenInMenuChildrenDemo', 167 name: 'HideChildrenInMenuChildrenDemo',
170 path: '/demos/features/hide-menu-children/children', 168 path: '/demos/features/hide-menu-children/children',
171 component: () => 169 component: () =>
172 - import( 170 + import('#/views/demos/features/hide-menu-children/children.vue'),
173 - '#/views/demos/features/hide-menu-children/children.vue'
174 - ),
175 meta: { 171 meta: {
176 activePath: '/demos/features/hide-menu-children', 172 activePath: '/demos/features/hide-menu-children',
177 title: $t('demos.features.hideChildrenInMenu'), 173 title: $t('demos.features.hideChildrenInMenu'),
...@@ -247,9 +243,7 @@ const routes: RouteRecordRaw[] = [ ...@@ -247,9 +243,7 @@ const routes: RouteRecordRaw[] = [
247 name: 'RequestParamsSerializerDemo', 243 name: 'RequestParamsSerializerDemo',
248 path: '/demos/features/request-params-serializer', 244 path: '/demos/features/request-params-serializer',
249 component: () => 245 component: () =>
250 - import( 246 + import('#/views/demos/features/request-params-serializer/index.vue'),
251 - '#/views/demos/features/request-params-serializer/index.vue'
252 - ),
253 meta: { 247 meta: {
254 icon: 'lucide:git-pull-request-arrow', 248 icon: 'lucide:git-pull-request-arrow',
255 title: $t('demos.features.requestParamsSerializer'), 249 title: $t('demos.features.requestParamsSerializer'),
......
...@@ -43,15 +43,14 @@ const contextMenus = () => { ...@@ -43,15 +43,14 @@ const contextMenus = () => {
43 }, 43 },
44 ]; 44 ];
45 }; 45 };
46 -
47 </script> 46 </script>
48 47
49 <template> 48 <template>
50 <Page title="Context Menu 上下文菜单"> 49 <Page title="Context Menu 上下文菜单">
51 <Card title="基本使用"> 50 <Card title="基本使用">
52 <div>一共四个菜单(刷新、关闭当前、关闭其他、关闭所有)</div> 51 <div>一共四个菜单(刷新、关闭当前、关闭其他、关闭所有)</div>
53 - <br/> 52 + <br />
54 - <br/> 53 + <br />
55 <VbenContextMenu :menus="contextMenus" :modal="true" item-class="pr-6"> 54 <VbenContextMenu :menus="contextMenus" :modal="true" item-class="pr-6">
56 <Button> 右键点击我打开上下文菜单(有隐藏项) </Button> 55 <Button> 右键点击我打开上下文菜单(有隐藏项) </Button>
57 </VbenContextMenu> 56 </VbenContextMenu>
......