feat(admin): 补全 admin 模块核心功能与产品体验优化

修复 v4 报告中的 13 个产品体验问题:新增用户管理列表页和系统设置页,重组导航菜单并补充缺失入口,增加角色切换机制,Dashboard 增加快捷操作和 recharts 趋势图表,考勤增加统计概览,排课增加课表网格视图,统一 Toast 操作反馈,同步更新架构文档
This commit is contained in:
SpecialX
2026-06-22 13:38:07 +08:00
parent 978d9a8309
commit c45b3488c5
23 changed files with 3112 additions and 213 deletions

View File

@@ -1,7 +1,7 @@
import "server-only"
import { cache } from "react"
import { and, count, desc, eq, gt, inArray } from "drizzle-orm"
import { and, count, desc, eq, gt, ilike, inArray, or } from "drizzle-orm"
import { auth } from "@/auth"
import { db } from "@/shared/db"
@@ -304,3 +304,97 @@ export const getUserIdsByGradeId = cache(
return rows.map((r) => r.id)
}
)
/** Returns all user IDs (used for school-wide announcement notifications). */
export const getAllUserIds = cache(async (): Promise<string[]> => {
const rows = await db.select({ id: users.id }).from(users)
return rows.map((r) => r.id)
})
export type AdminUserListItem = {
id: string
name: string | null
email: string
roles: string[]
phone: string | null
createdAt: Date
}
export type AdminUserListResult = {
items: AdminUserListItem[]
total: number
page: number
pageSize: number
totalPages: number
}
export async function getAdminUsers(params: {
page?: number
pageSize?: number
search?: string
role?: string
}): Promise<AdminUserListResult> {
const page = Math.max(1, params.page ?? 1)
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20))
const offset = (page - 1) * pageSize
const conditions = []
if (params.search) {
const search = `%${params.search}%`
conditions.push(
or(ilike(users.name, search), ilike(users.email, search))
)
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined
const [userRows, countRow] = await Promise.all([
db
.select()
.from(users)
.where(whereClause)
.orderBy(desc(users.createdAt))
.limit(pageSize)
.offset(offset),
db.select({ value: count() }).from(users).where(whereClause),
])
const userIds = userRows.map((u) => u.id)
const roleRows = userIds.length
? await db
.select({ userId: usersToRoles.userId, roleName: roles.name })
.from(usersToRoles)
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
.where(inArray(usersToRoles.userId, userIds))
: []
const rolesByUserId = new Map<string, string[]>()
for (const row of roleRows) {
const list = rolesByUserId.get(row.userId) ?? []
list.push(row.roleName)
rolesByUserId.set(row.userId, list)
}
const items = userRows.map((u) => ({
id: u.id,
name: u.name,
email: u.email,
roles: rolesByUserId.get(u.id) ?? [],
phone: u.phone,
createdAt: u.createdAt,
}))
const total = Number(countRow[0]?.value ?? 0)
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
}
}
export async function getAdminUserRoles(): Promise<string[]> {
const rows = await db.select({ name: roles.name }).from(roles)
return rows.map((r) => r.name)
}