permission

This commit is contained in:
2026-10-26 23:17:06 +08:00
parent c654b89647
commit d2dfb4069d
42 changed files with 3113 additions and 346 deletions
+57 -32
View File
@@ -7,28 +7,55 @@ export type UserStatus = (typeof USER_STATUS)[keyof typeof USER_STATUS]
export interface MockUser {
id: number
username: string
mini_id: number
openid: string
union_id: string
nickname: string
mobile: string
email: string
status: UserStatus
deptName: string
roleNames: string[]
createTime: string
avatar: string
status: 0 | 1
created_at: string
updated_at: string
}
export const users: MockUser[] = Array.from({ length: 36 }, (_, index) => {
export const users: MockUser[] = Array.from({ length: 15 }, (_, 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(),
mini_id: (id % 3) + 1,
openid: `oABC${String(100000 + id)}`,
union_id: id % 2 === 0 ? `uXYZ${id}` : '',
nickname: `用户${id}`,
avatar: `https://picsum.photos/seed/${id}/80`,
status: id % 4 === 0 ? 0 : 1,
created_at: new Date(Date.now() - id * 86400000).toISOString(),
updated_at: new Date(Date.now() - id * 3600000).toISOString(),
}
})
export interface MockMiniApp {
id: number
name: string
description: string
app_id: string
app_secret: string
status: 0 | 1
ext: string
created_at: string
updated_at: string
}
export const miniApps: MockMiniApp[] = Array.from({ length: 12 }, (_, index) => {
const id = index + 1
return {
id,
name: `小程序${id}`,
description: `用于演示的第 ${id} 个小程序`,
app_id: `wx${String(1000000000 + id * 137)}`,
app_secret: `secret-${id}`,
status: id % 3 === 0 ? 0 : 1,
ext: '{}',
created_at: new Date(Date.now() - id * 86400000).toISOString(),
updated_at: new Date(Date.now() - id * 3600000).toISOString(),
}
})
@@ -47,20 +74,18 @@ export const menuRoutes = [
},
],
},
{
path: '/miniapp',
component: 'Layout',
redirect: '/miniapp/list',
meta: { title: '小程序管理', icon: 'Menu', hidden: false },
children: [
{
path: 'list',
component: 'miniapp/index',
name: 'Miniapp',
meta: { title: '小程序管理', icon: 'Menu', 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 },
],
}
+178 -34
View File
@@ -1,4 +1,10 @@
import { dashboardSummary, menuRoutes, USER_STATUS, users, type MockUser } from './data'
import {
menuRoutes,
miniApps,
users,
USER_STATUS,
type MockMiniApp,
} from './data'
export interface MockContext {
url: string
@@ -11,6 +17,8 @@ export interface MockRule {
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
pattern: RegExp
handler: (ctx: MockContext) => unknown
/** 全局 mock 关闭时仍生效(用于单独 mock 新增接口) */
alwaysMock?: boolean
}
const toNumber = (value: unknown, fallback = 0) => {
@@ -46,7 +54,16 @@ export const mockRules: MockRule[] = [
nickname: '超级管理员',
avatar: '',
roles: ['ADMIN'],
permissions: ['sys:user:add', 'sys:user:edit', 'sys:user:delete', 'sys:user:query'],
permissions: [
'sys:user:add',
'sys:user:edit',
'sys:user:delete',
'sys:user:query',
'miniapp:add',
'miniapp:edit',
'miniapp:delete',
'miniapp:query',
],
}),
},
{
@@ -56,74 +73,201 @@ export const mockRules: MockRule[] = [
},
{
method: 'GET',
pattern: /^\/api\/v1\/users\/page$/,
pattern: /^\/api\/v1\/api\/admin\/users$/,
handler: (ctx) => {
const { username, status } = ctx.query
const pageNum = toNumber(ctx.query.pageNum, 1)
const pageSize = toNumber(ctx.query.pageSize, 10)
const page = toNumber(ctx.query.page, 1)
const size = toNumber(ctx.query.size, 20)
const miniId = ctx.query.mini_id ? toNumber(ctx.query.mini_id) : 0
const keyword = (ctx.query.keyword as string) || ''
const filtered = users.filter((item) => {
const matchName = !username || item.username.includes(username)
const matchStatus = !status || String(item.status) === status
return matchName && matchStatus
if (miniId && item.mini_id !== miniId) return false
if (keyword && !item.nickname.includes(keyword) && !item.openid.includes(keyword)) return false
return true
})
const start = (pageNum - 1) * pageSize
return { list: filtered.slice(start, start + pageSize), total: filtered.length }
const start = (page - 1) * size
return { list: filtered.slice(start, start + size), total: filtered.length }
},
},
{
method: 'GET',
pattern: /^\/api\/v1\/users\/(\d+)$/,
pattern: /^\/api\/v1\/api\/admin\/users\/(\d+)$/,
handler: (ctx) => users.find((item) => item.id === toNumber(ctx.params[0])) ?? null,
},
{
method: 'POST',
pattern: /^\/api\/v1\/users$/,
method: 'PUT',
pattern: /^\/api\/v1\/api\/admin\/users\/(\d+)$/,
handler: (ctx) => {
const id = users.length + 1
const created: MockUser = {
const target = users.find((item) => item.id === toNumber(ctx.params[0]))
if (!target) return false
const body = ctx.body as Record<string, unknown>
if (typeof body.nickname === 'string') target.nickname = body.nickname
if (typeof body.avatar === 'string') target.avatar = body.avatar
if (typeof body.status !== 'undefined') target.status = toNumber(body.status) === 0 ? 0 : 1
target.updated_at = new Date().toISOString()
return target
},
},
{
method: 'POST',
pattern: /^\/api\/v1\/api\/admin\/users\/(\d+)\/status$/,
handler: (ctx) => {
const target = users.find((item) => item.id === toNumber(ctx.params[0]))
if (!target) return false
const body = ctx.body as { status?: number }
target.status = toNumber(body.status) === 0 ? 0 : 1
target.updated_at = new Date().toISOString()
return target
},
},
{
method: 'GET',
pattern: /^\/api\/v1\/api\/admin\/miniapps$/,
handler: (ctx) => {
const { status } = ctx.query
const pageNum = toNumber(ctx.query.pageNum, 1)
const pageSize = toNumber(ctx.query.pageSize, 10)
const filtered = miniApps.filter((item) => !status || String(item.status) === status)
const start = (pageNum - 1) * pageSize
const list = filtered.slice(start, start + pageSize).map((item) => {
const { app_secret: _app_secret, ...rest } = item
return rest
})
return { list, total: filtered.length }
},
},
{
method: 'GET',
pattern: /^\/api\/v1\/api\/admin\/miniapps\/(\d+)$/,
handler: (ctx) => {
const target = miniApps.find((item) => item.id === toNumber(ctx.params[0]))
if (!target) return null
const { app_secret: _app_secret, ...rest } = target
return rest
},
},
{
method: 'POST',
pattern: /^\/api\/v1\/api\/admin\/miniapps$/,
handler: (ctx) => {
const id = miniApps.length + 1
const created: MockMiniApp = {
id,
username: pickString(ctx.body.username, `user${id}`),
nickname: pickString(ctx.body.nickname),
mobile: pickString(ctx.body.mobile),
email: pickString(ctx.body.email),
name: pickString(ctx.body.name),
description: pickString(ctx.body.description),
app_id: pickString(ctx.body.app_id),
app_secret: pickString(ctx.body.app_secret),
status: toNumber(ctx.body.status, USER_STATUS.ENABLED) === 0 ? 0 : 1,
deptName: pickString(ctx.body.deptName, '技术部'),
roleNames: Array.isArray(ctx.body.roleNames)
? (ctx.body.roleNames as string[])
: ['普通用户'],
createTime: new Date().toISOString(),
ext: pickString(ctx.body.ext, '{}'),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}
users.unshift(created)
miniApps.unshift(created)
return id
},
},
{
method: 'PUT',
pattern: /^\/api\/v1\/users\/(\d+)$/,
pattern: /^\/api\/v1\/api\/admin\/miniapps\/(\d+)$/,
handler: (ctx) => {
const target = users.find((item) => item.id === toNumber(ctx.params[0]))
const target = miniApps.find((item) => item.id === toNumber(ctx.params[0]))
if (!target) return false
Object.assign(target, ctx.body)
const body = ctx.body as Record<string, unknown>
if (typeof body.name === 'string') target.name = body.name
if (typeof body.description === 'string') target.description = body.description
if (typeof body.app_id === 'string') target.app_id = body.app_id
if (typeof body.app_secret === 'string' && body.app_secret) target.app_secret = body.app_secret
if (typeof body.status !== 'undefined') target.status = toNumber(body.status) === 0 ? 0 : 1
if (typeof body.ext === 'string') target.ext = body.ext
target.updated_at = new Date().toISOString()
return true
},
},
{
method: 'DELETE',
pattern: /^\/api\/v1\/users\/([\d,]+)$/,
pattern: /^\/api\/v1\/api\/admin\/miniapps\/([\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)
const index = miniApps.findIndex((item) => item.id === id)
if (index > -1) miniApps.splice(index, 1)
})
return true
},
},
{
// 统计每个小程序的用户数(有真实用户的取真实值,其余返回稳定模拟值)
method: 'GET',
pattern: /^\/api\/v1\/dashboard\/summary$/,
handler: () => dashboardSummary,
pattern: /^\/api\/v1\/api\/admin\/dashboard\/user-count-by-miniapp$/,
alwaysMock: true,
handler: () =>
miniApps.map((app) => {
const real = users.filter((item) => item.mini_id === app.id).length
return {
miniAppId: app.id,
miniAppName: app.name,
userCount: real > 0 ? real : ((app.id * 37) % 200) + 8,
}
}),
},
{
method: 'GET',
pattern: /^\/api\/v1\/api\/admin\/monitor\/server-info$/,
alwaysMock: true,
handler: () => ({
hostname: 'portal-server-01',
os: 'Linux',
osVersion: 'Ubuntu 22.04.3 LTS',
arch: 'amd64',
kernel: '5.15.0-91-generic',
cpuModel: 'Intel(R) Xeon(R) Platinum 8369B CPU @ 2.70GHz',
cpuCores: 8,
runtimeVersion: 'go1.21.5',
uptime: 1234560,
}),
},
{
// 实时状态:每次请求生成浮动指标,模拟监控采集中
method: 'GET',
pattern: /^\/api\/v1\/api\/admin\/monitor\/server-status$/,
alwaysMock: true,
handler: () => {
const cpuUsage = Math.round((20 + Math.random() * 60) * 10) / 10
const memTotal = 16 * 1024 ** 3
const memUsed = Math.round(memTotal * (0.4 + Math.random() * 0.3))
const loadBase = (cpuUsage / 100) * 8
const gb = 1024 ** 3
const disks = [
{ mount: '/', filesystem: '/dev/vda1', total: 100 * gb, used: 62 * gb, free: 38 * gb },
{ mount: '/data', filesystem: '/dev/vdb1', total: 500 * gb, used: 311 * gb, free: 189 * gb },
{ mount: '/logs', filesystem: '/dev/vdc1', total: 50 * gb, used: 9 * gb, free: 41 * gb },
].map((disk) => ({
...disk,
usagePercent: Math.round((disk.used / disk.total) * 1000) / 10,
}))
return {
cpu: {
usagePercent: cpuUsage,
cores: 8,
model: 'Intel(R) Xeon(R) Platinum 8369B CPU @ 2.70GHz',
loadAverage: [
Math.round((loadBase + Math.random()) * 100) / 100,
Math.round((loadBase + Math.random()) * 100) / 100,
Math.round((loadBase + Math.random()) * 100) / 100,
],
},
memory: {
total: memTotal,
used: memUsed,
free: memTotal - memUsed,
usagePercent: Math.round((memUsed / memTotal) * 1000) / 10,
},
disks,
timestamp: Date.now(),
}
},
},
]
+5 -4
View File
@@ -12,15 +12,15 @@ function readBody(req: NodeJS.ReadableStream): Promise<string> {
}
/**
* 本地 mock 中间件:仅在 dev 且 VITE_USE_MOCK=true 时拦截 /api/v1 请求。
* 本地 mock 中间件:始终挂载于 dev,但仅命中「全局 mock 开启」或标记 alwaysMock 的规则。
* 这样新增页面的接口可被单独 mock,而项目其余接口仍走真实后端代理。
* 不引入三方 mock 依赖,避免污染生产依赖树。
*/
export function mockPlugin(enabled = true): Plugin {
export function mockPlugin(globalMock = false): 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()
@@ -30,7 +30,8 @@ export function mockPlugin(enabled = true): Plugin {
const rule = mockRules.find(
(item) =>
item.method === (req.method ?? 'GET').toUpperCase() &&
item.pattern.test(requestUrl.pathname),
item.pattern.test(requestUrl.pathname) &&
(globalMock || item.alwaysMock === true),
)
if (!rule) return next()