You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

10 KiB

Users Management Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 重新实现 admin/users 模块,提供多条件筛选、批量操作、详情抽屉编辑功能

Architecture: 后端扩展用户 API 支持状态/角色筛选、排序、批量操作;前端重写页面组件,采用筛选栏 + 多选表格 + 批量操作栏 + 详情抽屉的布局

Tech Stack: Nuxt 3, TypeScript, Tailwind (CSS variables from DESIGN.md), useHttpFetch composable


File Structure

server/api/users/
  index.get.ts        — 修改:增加 status, role, sortBy, sortOrder 筛选
  [id].put.ts         — 新增:更新单个用户
  batch.post.ts       — 新增:批量操作(启用/禁用/删除)

app/pages/admin/users/
  index.vue           — 重写:完整用户管理界面

Task 1: Extend GET /api/users with filtering & sorting

Files:

  • Modify: server/api/users/index.get.ts

  • Step 1: Read current implementation

Review server/api/users/index.get.ts (lines 1-50 already read)

  • Step 2: Add query parameters for status, role, sortBy, sortOrder
// Add after line 9 (after search)
const status = query.status as string | undefined;
const role = query.role as string | undefined;
const sortBy = query.sortBy as string | undefined;
const sortOrder = query.sortOrder as "asc" | "desc" | undefined;
  • Step 3: Build where clause
// Add after line 11 (after searchPattern)
const conditions = searchPattern
  ? or(
      like(users.username, searchPattern),
      like(users.email, searchPattern),
      like(users.nickname, searchPattern),
    )
  : undefined;

if (status && status !== "all") {
  conditions && conditions.length
    ? (conditions.push(eq(users.status, status as "active" | "disabled")))
    : undefined;
}

if (role && role !== "all") {
  conditions
    ? undefined
    : undefined;
}

Actually, use drizzle's and helper:

import { and, eq } from "drizzle-orm";

// Replace the where clause building (lines 30-38) with:
const whereConditions = [];

if (searchPattern) {
  whereConditions.push(
    or(
      like(users.username, searchPattern),
      like(users.email, searchPattern),
      like(users.nickname, searchPattern),
    )
  );
}

if (status && status !== "all") {
  whereConditions.push(eq(users.status, status as "active" | "disabled"));
}

if (role && role !== "all") {
  whereConditions.push(eq(users.role, role as "admin" | "user"));
}

const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined;
  • Step 4: Add sorting logic
// Add before dbGlobal.select (before line 18)
const orderColumn = sortBy === "username" ? users.username
  : sortBy === "createdAt" ? users.createdAt
  : users.createdAt;
const orderDirection = sortOrder === "asc" ? orderColumn : desc(orderColumn);
  • Step 5: Update query to use whereClause and orderBy

Replace .where(searchPattern ? or(...) : undefined) with .where(whereClause) Replace .orderBy(desc(users.createdAt)) with .orderBy(orderDirection)

  • Step 6: Commit
git add server/api/users/index.get.ts
git commit -m "feat(users): add status, role filtering and sorting to GET /api/users"

Task 2: Create PUT /api/users/:id endpoint

Files:

  • Create: server/api/users/[id].put.ts

  • Step 1: Create the file

import { dbGlobal } from "drizzle-pkg/lib/db";
import { users } from "drizzle-pkg/lib/schema/auth";
import { eq } from "drizzle-orm";
import log4js from "logger";

const logger = log4js.getLogger("USERS");

export default defineWrappedResponseHandler(async (event) => {
  const id = Number(event.context.params?.id);
  const body = await readBody(event);

  if (!id || isNaN(id)) {
    throw createError({
      statusCode: 400,
      statusMessage: "无效的用户ID",
    });
  }

  if (body.email !== undefined && body.email !== "" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
    throw createError({
      statusCode: 400,
      statusMessage: "邮箱格式不正确",
    });
  }

  if (body.role && !["admin", "user"].includes(body.role)) {
    throw createError({
      statusCode: 400,
      statusMessage: "无效的角色",
    });
  }

  if (body.status && !["active", "disabled"].includes(body.status)) {
    throw createError({
      statusCode: 400,
      statusMessage: "无效的状态",
    });
  }

  const [existing] = await dbGlobal
    .select({ id: users.id })
    .from(users)
    .where(eq(users.id, id));

  if (!existing) {
    throw createError({
      statusCode: 404,
      statusMessage: "用户不存在",
    });
  }

  const updateData: Partial<{
    nickname: string | null;
    email: string | null;
    role: "admin" | "user";
    status: "active" | "disabled";
  }> = {};

  if (body.nickname !== undefined) updateData.nickname = body.nickname || null;
  if (body.email !== undefined) updateData.email = body.email || null;
  if (body.role !== undefined) updateData.role = body.role;
  if (body.status !== undefined) updateData.status = body.status;

  const [updated] = await dbGlobal
    .update(users)
    .set(updateData)
    .where(eq(users.id, id))
    .returning({
      id: users.id,
      username: users.username,
      email: users.email,
      nickname: users.nickname,
      avatar: users.avatar,
      role: users.role,
      status: users.status,
      createdAt: users.createdAt,
    });

  logger.info("user updated by admin: %s (id: %d)", updated.username, id);
  return R.success(updated);
});
  • Step 2: Commit
git add server/api/users/[id].put.ts
git commit -m "feat(users): add PUT /api/users/:id endpoint"

Task 3: Create POST /api/users/batch endpoint

Files:

  • Create: server/api/users/batch.post.ts

  • Step 1: Create the file

import { dbGlobal } from "drizzle-pkg/lib/db";
import { users } from "drizzle-pkg/lib/schema/auth";
import { eq, inArray } from "drizzle-orm";
import log4js from "logger";

const logger = log4js.getLogger("USERS");

export default defineWrappedResponseHandler(async (event) => {
  const body = await readBody(event);

  if (!body?.ids || !Array.isArray(body.ids) || body.ids.length === 0) {
    throw createError({
      statusCode: 400,
      statusMessage: "请选择要操作的用户",
    });
  }

  if (!body?.action || !["enable", "disable", "delete"].includes(body.action)) {
    throw createError({
      statusCode: 400,
      statusMessage: "无效的操作类型",
    });
  }

  const ids = body.ids.map(Number).filter(n => !isNaN(n));
  if (ids.length === 0) {
    throw createError({
      statusCode: 400,
      statusMessage: "无效的用户ID列表",
    });
  }

  if (body.action === "delete") {
    await dbGlobal.delete(users).where(inArray(users.id, ids));
    logger.info("users batch deleted by admin: count=%d", ids.length);
    return R.success({ message: `已删除 ${ids.length} 个用户` });
  }

  const newStatus = body.action === "enable" ? "active" : "disabled";
  await dbGlobal
    .update(users)
    .set({ status: newStatus })
    .where(inArray(users.id, ids));

  logger.info("users batch %s by admin: ids=%s", body.action, ids.join(","));
  return R.success({
    message: `已${body.action === "enable" ? "启用" : "禁用"} ${ids.length} 个用户`
  });
});
  • Step 2: Commit
git add server/api/users/batch.post.ts
git commit -m "feat(users): add POST /api/users/batch for batch operations"

Task 4: Rewrite users/index.vue page

Files:

  • Modify: app/pages/admin/users/index.vue

This is the main frontend task. Follow the spec exactly:

  • Filter bar with status tabs, role dropdown, search

  • Stats cards (total, admin, disabled)

  • Sortable table with checkboxes

  • Batch action bar

  • Detail drawer

  • Create modal

  • Confirm dialog

  • Step 1: Read current implementation

Already read (320 lines) — use as reference for structure

  • Step 2: Write new implementation

Complete rewrite with all features from spec:

  • State tabs (全部/正常/禁用) using pill-style buttons
  • Role dropdown select
  • Search input with icon
  • Sortable table columns (username, createdAt)
  • Checkbox multi-select with select-all
  • Stats cards row
  • Batch bar slides in when selection > 0
  • Detail drawer from right side (480px wide)
  • Create modal centered
  • Confirm dialog for batch delete

Use DESIGN.md tokens:

  • var(--color-primary) for coral buttons

  • var(--color-accent-teal) for enable actions

  • var(--color-error) for delete actions

  • var(--color-surface-card) for cards

  • var(--color-hairline) for borders

  • var(--rounded-md) (8px) for buttons/inputs

  • var(--rounded-lg) (12px) for cards

  • Step 3: Test the page

Start dev server npm run dev and verify:

  • Filter by status works

  • Filter by role works

  • Search works

  • Table sorting works (click username/createdAt headers)

  • Checkbox selection works

  • Batch bar appears when items selected

  • Batch enable/disable works

  • Batch delete shows confirm dialog

  • Create modal works

  • Edit drawer opens and saves

  • Step 4: Commit

git add app/pages/admin/users/index.vue
git commit -m "feat(users): complete rewrite with filtering, sorting, batch operations"

Verification Checklist

  • GET /api/users supports status, role, sortBy, sortOrder params
  • PUT /api/users/:id updates nickname, email, role, status
  • POST /api/users/batch handles enable/disable/delete
  • Page shows filter bar with status tabs + role dropdown + search
  • Page shows stats cards (total/admin/disabled)
  • Table columns are sortable (username, createdAt)
  • Checkbox multi-select works with select-all
  • Batch action bar shows enable/disable/delete buttons
  • Detail drawer opens on edit and saves changes
  • Create modal adds new user
  • Batch delete shows confirm dialog