feat(backend-mock): 新增后端模拟服务基础功能
添加后端模拟服务的基础功能,包括用户认证、菜单管理、部门管理、角色管理等模块。提供模拟数据支持前端开发,包含以下主要功能: - 用户登录、登出及令牌刷新 - 菜单权限管理 - 部门树形结构管理 - 角色权限配置 - 时区设置功能 - 表格数据模拟 - 错误处理中间件 - 跨域支持配置 同时添加了相关文档说明和测试接口,便于前端开发时使用。
Showing
44 changed files
with
1349 additions
and
23 deletions
apps/backend-mock/.env
0 → 100644
apps/backend-mock/README.md
0 → 100644
| 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 | +``` |
apps/backend-mock/api/auth/codes.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/auth/login.post.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/auth/logout.post.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/auth/refresh.post.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/demo/bigint.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/menu/all.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/status.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/system/dept/.post.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/system/dept/list.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/system/menu/list.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/system/role/list.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/table/list.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/test.get.ts
0 → 100644
apps/backend-mock/api/test.post.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/upload.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/api/user/info.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/error.ts
0 → 100644
apps/backend-mock/middleware/1.api.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/nitro.config.ts
0 → 100644
| 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 | +}); |
apps/backend-mock/package.json
0 → 100644
| 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 | +} |
apps/backend-mock/routes/[...].ts
0 → 100644
| 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 | +}); |
apps/backend-mock/tsconfig.build.json
0 → 100644
apps/backend-mock/tsconfig.json
0 → 100644
apps/backend-mock/utils/cookie-utils.ts
0 → 100644
| 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 | +} |
apps/backend-mock/utils/jwt-utils.ts
0 → 100644
| 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 | +} |
apps/backend-mock/utils/mock-data.ts
0 → 100644
| 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 | +]; |
apps/backend-mock/utils/response.ts
0 → 100644
| 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 | +} |
apps/backend-mock/utils/timezone-utils.ts
0 → 100644
| ... | @@ -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> | ... | ... |
-
Please register or login to post a comment