first commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
export const USER_STATUS = {
|
||||
ENABLED: 1,
|
||||
DISABLED: 0,
|
||||
} as const
|
||||
|
||||
export type UserStatus = (typeof USER_STATUS)[keyof typeof USER_STATUS]
|
||||
|
||||
export interface MockUser {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
mobile: string
|
||||
email: string
|
||||
status: UserStatus
|
||||
deptName: string
|
||||
roleNames: string[]
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export const users: MockUser[] = Array.from({ length: 36 }, (_, index) => {
|
||||
const id = index + 1
|
||||
return {
|
||||
id,
|
||||
username: `user${String(id).padStart(3, '0')}`,
|
||||
nickname: `测试用户${id}`,
|
||||
mobile: `13${String(100000000 + id).slice(0, 9)}`,
|
||||
email: `user${id}@example.com`,
|
||||
status: id % 5 === 0 ? USER_STATUS.DISABLED : USER_STATUS.ENABLED,
|
||||
deptName: id % 3 === 0 ? '运营部' : '技术部',
|
||||
roleNames: id % 4 === 0 ? ['管理员'] : ['普通用户'],
|
||||
createTime: new Date(Date.now() - id * 86400000).toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
export const menuRoutes = [
|
||||
{
|
||||
path: '/system',
|
||||
component: 'Layout',
|
||||
redirect: '/system/user',
|
||||
meta: { title: '系统管理', icon: 'Setting', hidden: false },
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
component: 'system/user/index',
|
||||
name: 'User',
|
||||
meta: { title: '用户管理', icon: 'User', hidden: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const dashboardSummary = {
|
||||
userTotal: 1024,
|
||||
orderTotal: 8672,
|
||||
gmv: 1289340.55,
|
||||
pendingApproval: 12,
|
||||
trend: [
|
||||
{ date: '10-01', value: 120 },
|
||||
{ date: '10-02', value: 200 },
|
||||
{ date: '10-03', value: 150 },
|
||||
{ date: '10-04', value: 320 },
|
||||
{ date: '10-05', value: 280 },
|
||||
{ date: '10-06', value: 410 },
|
||||
{ date: '10-07', value: 365 },
|
||||
],
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { dashboardSummary, menuRoutes, USER_STATUS, users } from './data'
|
||||
|
||||
export interface MockContext {
|
||||
url: string
|
||||
params: string[]
|
||||
query: Record<string, string>
|
||||
body: Record<string, any>
|
||||
}
|
||||
|
||||
export interface MockRule {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
pattern: RegExp
|
||||
handler: (ctx: MockContext) => unknown
|
||||
}
|
||||
|
||||
const toNumber = (value: unknown, fallback = 0) => {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? n : fallback
|
||||
}
|
||||
|
||||
export const mockRules: MockRule[] = [
|
||||
{
|
||||
method: 'POST',
|
||||
pattern: /^\/api\/v1\/auth\/login$/,
|
||||
handler: (ctx) => {
|
||||
const { username, password } = ctx.body
|
||||
if (!username || !password) {
|
||||
return { code: 'A0120', msg: '用户名和密码不能为空' }
|
||||
}
|
||||
return {
|
||||
accessToken: 'mock-access-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 7200,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
pattern: /^\/api\/v1\/auth\/logout$/,
|
||||
handler: () => true,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
pattern: /^\/api\/v1\/auth\/me$/,
|
||||
handler: () => ({
|
||||
userId: 1,
|
||||
username: 'admin',
|
||||
nickname: '超级管理员',
|
||||
avatar: '',
|
||||
roles: ['ADMIN'],
|
||||
permissions: ['sys:user:add', 'sys:user:edit', 'sys:user:delete', 'sys:user:query'],
|
||||
}),
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
pattern: /^\/api\/v1\/auth\/routes$/,
|
||||
handler: () => menuRoutes,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
pattern: /^\/api\/v1\/users\/page$/,
|
||||
handler: (ctx) => {
|
||||
const { username, status, pageNum = '1', pageSize = '10' } = ctx.query
|
||||
const filtered = users.filter((item) => {
|
||||
const matchName = !username || item.username.includes(username)
|
||||
const matchStatus = !status || String(item.status) === status
|
||||
return matchName && matchStatus
|
||||
})
|
||||
const start = (toNumber(pageNum, 1) - 1) * toNumber(pageSize, 10)
|
||||
return {
|
||||
list: filtered.slice(start, start + toNumber(pageSize, 10)),
|
||||
total: filtered.length,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
pattern: /^\/api\/v1\/users\/(\d+)$/,
|
||||
handler: (ctx) => users.find((item) => item.id === toNumber(ctx.params[0])) ?? null,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
pattern: /^\/api\/v1\/users$/,
|
||||
handler: (ctx) => {
|
||||
const id = users.length + 1
|
||||
users.unshift({
|
||||
id,
|
||||
username: ctx.body.username ?? `user${id}`,
|
||||
nickname: ctx.body.nickname ?? '',
|
||||
mobile: ctx.body.mobile ?? '',
|
||||
email: ctx.body.email ?? '',
|
||||
status: ctx.body.status ?? USER_STATUS.ENABLED,
|
||||
deptName: ctx.body.deptName ?? '技术部',
|
||||
roleNames: ctx.body.roleNames ?? ['普通用户'],
|
||||
createTime: new Date().toISOString(),
|
||||
})
|
||||
return id
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
pattern: /^\/api\/v1\/users\/(\d+)$/,
|
||||
handler: (ctx) => {
|
||||
const target = users.find((item) => item.id === toNumber(ctx.params[0]))
|
||||
if (!target) return false
|
||||
Object.assign(target, ctx.body)
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
pattern: /^\/api\/v1\/users\/([\d,]+)$/,
|
||||
handler: (ctx) => {
|
||||
const ids = ctx.params[0].split(',').map((id) => toNumber(id))
|
||||
ids.forEach((id) => {
|
||||
const index = users.findIndex((item) => item.id === id)
|
||||
if (index > -1) users.splice(index, 1)
|
||||
})
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
pattern: /^\/api\/v1\/dashboard\/summary$/,
|
||||
handler: () => dashboardSummary,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import { mockRules, type MockContext } from './index'
|
||||
|
||||
const MOCK_PREFIX = '/api/v1'
|
||||
|
||||
function readBody(req: NodeJS.ReadableStream): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
let raw = ''
|
||||
req.on('data', (chunk) => (raw += chunk))
|
||||
req.on('end', () => resolve(raw))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地 mock 中间件:仅在 dev 且 VITE_USE_MOCK=true 时拦截 /api/v1 请求。
|
||||
* 不引入三方 mock 依赖,避免污染生产依赖树。
|
||||
*/
|
||||
export function mockPlugin(enabled = true): Plugin {
|
||||
return {
|
||||
name: 'portal-mock',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
if (!enabled) return
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (!req.url?.startsWith(MOCK_PREFIX)) return next()
|
||||
|
||||
const requestUrl = new URL(req.url, 'http://localhost')
|
||||
void (async () => {
|
||||
const raw = req.method === 'GET' ? '' : await readBody(req)
|
||||
const rule = mockRules.find(
|
||||
(item) =>
|
||||
item.method === (req.method ?? 'GET').toUpperCase() &&
|
||||
item.pattern.test(requestUrl.pathname),
|
||||
)
|
||||
|
||||
if (!rule) return next()
|
||||
|
||||
const matched = requestUrl.pathname.match(rule.pattern)
|
||||
const ctx: MockContext = {
|
||||
url: requestUrl.pathname,
|
||||
params: matched ? matched.slice(1) : [],
|
||||
query: Object.fromEntries(requestUrl.searchParams.entries()),
|
||||
body: raw ? JSON.parse(raw) : {},
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json;charset=utf-8')
|
||||
res.end(JSON.stringify({ code: '00000', data: rule.handler(ctx) ?? null, msg: 'success' }))
|
||||
})()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user