feat(admin): enhance admin dashboard, user management, and content editor UX

- Dashboard: add stats API with content status distribution, recent notifications,
  and recent content updates; display active users, pending reviews, unread counts
- User management: full CRUD with role assignment, search, pagination, delete dialog
- Notification center: list with unread filter, mark as read, mark all as read,
  pagination, and auto-refresh unread count
- Content editor: form validation (required fields, slug format, blur-triggered
  errors), auto-save with 3s debounce and status indicator, publish confirmation
  dialog, unsaved changes warning on leave
- Admin layout: add navigation links for user management, roles, and notifications
- admin-api: make request() method public for custom API calls
- gitignore: add reports/mutation/ to exclude mutation test output
This commit is contained in:
2026-07-31 23:05:24 +08:00
parent a995f40eae
commit c480772aec
9 changed files with 1942 additions and 120 deletions
+82
View File
@@ -0,0 +1,82 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { authenticateRequest } from '@/lib/auth';
import { success, unauthorized, internalError } from '@/lib/api-response';
// GET /api/admin/stats - 获取仪表盘统计数据
export async function GET(request: NextRequest) {
const user = authenticateRequest(request);
if (!user) return unauthorized();
try {
const [modelCount, itemCount, zoneCount, userCount, pendingReviewCount, unreadCount] = await Promise.all([
prisma.contentModel.count(),
prisma.contentItem.count(),
prisma.contentZone.count(),
prisma.user.count({ where: { status: 1 } }),
prisma.contentItem.count({ where: { status: 'review' } }),
prisma.notification.count({ where: { userId: user.userId, read: false } }),
]);
// 获取每个内容模型的条目数
const modelItemCounts = await prisma.contentItem.groupBy({
by: ['modelCode'],
_count: { modelCode: true },
});
// 获取不同状态的条目数
const statusCounts = await prisma.contentItem.groupBy({
by: ['status'],
_count: { status: true },
});
// 获取最近 5 条通知
const recentNotifications = await prisma.notification.findMany({
where: { userId: user.userId },
orderBy: { createdAt: 'desc' },
take: 5,
select: {
id: true,
type: true,
title: true,
read: true,
createdAt: true,
},
});
// 获取最近 5 条更新的内容
const recentItems = await prisma.contentItem.findMany({
orderBy: { updatedAt: 'desc' },
take: 5,
select: {
id: true,
title: true,
modelCode: true,
status: true,
updatedAt: true,
},
});
return success({
models: modelCount,
items: itemCount,
zones: zoneCount,
activeUsers: userCount,
pendingReviews: pendingReviewCount,
unreadNotifications: unreadCount,
modelItemCounts: modelItemCounts.map((m) => ({
modelCode: m.modelCode,
count: m._count.modelCode,
})),
statusCounts: statusCounts.map((s) => ({
status: s.status,
count: s._count.status,
})),
recentNotifications,
recentItems,
});
} catch (error) {
console.error('Get stats error:', error);
return internalError();
}
}
+263
View File
@@ -0,0 +1,263 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { authenticateRequest, hashPassword } from '@/lib/auth';
import { success, unauthorized, forbidden, internalError, validationError } from '@/lib/api-response';
// GET /api/admin/users - 获取用户列表
export async function GET(request: NextRequest) {
const user = authenticateRequest(request);
if (!user) return unauthorized();
const userRoles = await prisma.userRole.findMany({ where: { userId: user.userId } });
const roleCodes = userRoles.map((ur) => ur.roleCode);
if (!roleCodes.includes('super_admin') && !roleCodes.includes('content_admin')) {
return forbidden('仅管理员可查看用户列表');
}
try {
const { searchParams } = new URL(request.url);
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
const pageSize = Math.min(100, Math.max(1, parseInt(searchParams.get('pageSize') || '20', 10)));
const search = searchParams.get('search') || '';
const where = search
? {
OR: [
{ username: { contains: search } },
{ nickname: { contains: search } },
{ email: { contains: search } },
],
}
: {};
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
select: {
id: true,
username: true,
nickname: true,
email: true,
phone: true,
status: true,
createdAt: true,
updatedAt: true,
},
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
}),
prisma.user.count({ where }),
]);
// 获取每个用户的角色
const usersWithRoles = await Promise.all(
users.map(async (u) => {
const roles = await prisma.userRole.findMany({
where: { userId: u.id },
include: { role: { select: { code: true, name: true } } },
});
return {
...u,
roles: roles.map((r) => ({ code: r.role.code, name: r.role.name })),
// 保留向后兼容
role: roles[0]?.role?.code || 'readonly',
};
}),
);
return success({
users: usersWithRoles,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
});
} catch (error) {
console.error('Get users error:', error);
return internalError();
}
}
// POST /api/admin/users - 创建用户
export async function POST(request: NextRequest) {
const user = authenticateRequest(request);
if (!user) return unauthorized();
const userRoles = await prisma.userRole.findMany({ where: { userId: user.userId } });
const roleCodes = userRoles.map((ur) => ur.roleCode);
if (!roleCodes.includes('super_admin') && !roleCodes.includes('content_admin')) {
return forbidden('仅管理员可创建用户');
}
try {
const body = await request.json() as {
username: string;
password: string;
nickname?: string;
email?: string;
phone?: string;
roleCodes?: string[];
};
if (!body.username || !body.password) {
return validationError('用户名和密码必填');
}
if (body.username.length < 3 || body.username.length > 32) {
return validationError('用户名长度需在 3-32 字符之间');
}
if (body.password.length < 6) {
return validationError('密码长度不能少于 6 位');
}
// 检查用户名是否已存在
const existing = await prisma.user.findUnique({ where: { username: body.username } });
if (existing) {
return validationError('用户名已存在');
}
const hashedPassword = await hashPassword(body.password);
const newUser = await prisma.user.create({
data: {
username: body.username,
password: hashedPassword,
nickname: body.nickname || '',
email: body.email || '',
phone: body.phone || '',
status: 1,
},
});
// 分配角色
const rolesToAssign = body.roleCodes?.length ? body.roleCodes : ['readonly'];
for (const roleCode of rolesToAssign) {
const role = await prisma.role.findUnique({ where: { code: roleCode } });
if (role) {
await prisma.userRole.create({
data: { userId: newUser.id, roleCode },
});
}
}
return success({
id: newUser.id,
username: newUser.username,
nickname: newUser.nickname,
email: newUser.email,
phone: newUser.phone,
roles: rolesToAssign,
}, 201);
} catch (error) {
console.error('Create user error:', error);
return internalError();
}
}
// PUT /api/admin/users - 更新用户
export async function PUT(request: NextRequest) {
const currentUser = authenticateRequest(request);
if (!currentUser) return unauthorized();
const userRoles = await prisma.userRole.findMany({ where: { userId: currentUser.userId } });
const roleCodes = userRoles.map((ur) => ur.roleCode);
if (!roleCodes.includes('super_admin') && !roleCodes.includes('content_admin')) {
return forbidden('仅管理员可编辑用户');
}
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get('id');
if (!userId) return validationError('用户 ID 必填');
const body = await request.json() as {
nickname?: string;
email?: string;
phone?: string;
password?: string;
status?: number;
roleCodes?: string[];
};
const updateData: Record<string, unknown> = {};
if (body.nickname !== undefined) updateData.nickname = body.nickname;
if (body.email !== undefined) updateData.email = body.email;
if (body.phone !== undefined) updateData.phone = body.phone;
if (body.status !== undefined) updateData.status = body.status;
if (body.password) {
updateData.password = await hashPassword(body.password);
}
await prisma.user.update({
where: { id: userId },
data: updateData,
});
// 更新角色分配
if (body.roleCodes) {
// 防止将自己从 super_admin 移除
if (userId === currentUser.userId) {
const currentRoles = await prisma.userRole.findMany({ where: { userId } });
const currentIsSuperAdmin = currentRoles.some((r) => r.roleCode === 'super_admin');
const stillHasSuperAdmin = body.roleCodes.includes('super_admin');
if (currentIsSuperAdmin && !stillHasSuperAdmin) {
return forbidden('不能移除自己的超级管理员角色');
}
}
await prisma.userRole.deleteMany({ where: { userId } });
for (const roleCode of body.roleCodes) {
const role = await prisma.role.findUnique({ where: { code: roleCode } });
if (role) {
await prisma.userRole.create({ data: { userId, roleCode } });
}
}
}
return success({ id: userId });
} catch (error) {
console.error('Update user error:', error);
return internalError();
}
}
// DELETE /api/admin/users - 删除用户
export async function DELETE(request: NextRequest) {
const currentUser = authenticateRequest(request);
if (!currentUser) return unauthorized();
const userRoles = await prisma.userRole.findMany({ where: { userId: currentUser.userId } });
const roleCodes = userRoles.map((ur) => ur.roleCode);
if (!roleCodes.includes('super_admin')) {
return forbidden('仅超级管理员可删除用户');
}
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get('id');
if (!userId) return validationError('用户 ID 必填');
if (userId === currentUser.userId) {
return forbidden('不能删除自己的账号');
}
// 检查是否是最后一个 super_admin
const targetRoles = await prisma.userRole.findMany({ where: { userId } });
const isTargetSuperAdmin = targetRoles.some((r) => r.roleCode === 'super_admin');
if (isTargetSuperAdmin) {
const superAdminCount = await prisma.userRole.count({ where: { roleCode: 'super_admin' } });
if (superAdminCount <= 1) {
return forbidden('不能删除最后一个超级管理员');
}
}
await prisma.userRole.deleteMany({ where: { userId } });
await prisma.user.delete({ where: { id: userId } });
return success({ deleted: true });
} catch (error) {
console.error('Delete user error:', error);
return internalError();
}
}