后台指南

SmartCrudPage 作战手册

TL;DR(3 步生成一页 CRUD)

  1. 建模型 → 2) createCrudActions 生成 Actions → 3) 在页面引入 SmartCrudPage + fieldsConfig

最小示例(可直接运行在你的模块中):

// app/(admin)/actions/demo/crud-action.demo.js
'use server'
import { createCrudActions } from '@/lib/core/crud-helper'
export const { getList, getDetail, create, update, delete: del } = createCrudActions({
  modelName: 'demo', primaryKey: 'id', softDelete: true,
  fields: { creatable: ['name'], updatable: ['name'], searchable: ['name'] },
})
// app/(admin)/admin/demo/demos/page.js
'use client'
import dynamic from 'next/dynamic'
import * as actions from '@/app/(admin)/actions/demo/crud-action.demo'
const SmartCrudPage = dynamic(() => import('@/components/admin/smart-crud-page'), { ssr: false })
const fieldsConfig = [{ key: 'name', title: '名称', type: 'text', search: { mode: 'like' }, form: { required: true } }]
export default () => <SmartCrudPage title="示例管理" fieldsConfig={fieldsConfig} actions={actions} enableCreate />

配置驱动 CRUD 的全量说明

组件清单 · 配置规范 · 页面参数 · CRUD 实操


🎯 概述

SmartCrudPage + SmartForm + createCrudActions 组成了后台的「三件套」:一份 fieldsConfig 同时驱动表格、表单、搜索、详情,配合 Server Actions 自动处理权限、验证、日志,10-30 分钟即可落一页 CRUD。


🧩 组件清单

名称位置作用关键点
SmartCrudPagecomponents/admin/smart-crud-page.jsx生成表格/搜索/详情/操作内置分页、排序、批量操作、详情抽屉、序号列、hook 回调
SmartForm / SmartModalForm / SmartDrawerFormcomponents/admin/smart-form生成表单支持分组、动态联动、校验、抽屉/弹窗形态
fieldsConfig 生成器lib/crud/field-generator.js · lib/crud/field-types.js将配置转成表格/表单/搜索支持 20+ 字段类型,兼容 Ant Design 官方 fieldProps
createCrudActionslib/core/crud-helper.jsCRUD 工厂自动命名、权限检查、日志、批量操作
wrapActionlib/core/action-wrapper.jsAction 包装器pub/auth/sys/_ 前缀自动鉴权 + 操作日志
BaseDAOapp/(admin)/actions/dao/base.jsPrisma 封装搜索、筛选、软删、连表、钩子、验证

📐 配置规范

基础结构(fieldsConfig)

{
  key: 'fieldName',      // 必填,保持与 Prisma 字段一致
  title: '显示名称',      // 必填
  type: 'text',          // 必填,见类型速查
  options: [],           // select / radio / checkbox / tree-select 可用,支持函数或 Action 名
  table: { ... },        // 表格配置,false 或 hideInTable 隐藏
  form: { ... },         // 表单配置,false 或 hideInForm 隐藏
  search: { ... },       // 搜索配置,false 或 hideInSearch 隐藏
  detail: { ... },       // 详情配置,false 或 hideInDetail 隐藏
  createOnly: true,      // 仅创建时展示;editOnly 反之
  showRule: 'status === "rejected"',  // 动态显隐(支持字符串/函数)
  disabled: (values) => values.locked // 动态禁用
}

类型速查

  • 文本:text · textarea · markdown
  • 数字:number · rate · slider
  • 选择:select · radio · checkbox · tree-select · cascader
  • 布尔/状态:switch
  • 时间:date · datetime · daterange · datetimerange · time
  • 媒体/文件:image · images · avatar · file · upload
  • 结构化:json · array · group(分组布局)
  • 其他:icon · color

所有表单组件的原生属性直接写到 form.fieldProps,选项也可写 valueEnum(支持 text/status/color)。

表格/表单/搜索要点

  • 表格:table.width/fixed/ellipsis/copyable/aligntable.formatter 纯函数或 table.render JSX;table.valueEnum 渲染彩色 Tag。
  • 表单:form.rules 兼容 Ant Design 规则;form.tips(tooltip)与 form.extraform.colSpan 控制一行宽度;form.action: 'sysGetXxx' 会自动调用对应 Action 作为选项。
  • 搜索:search.mode 支持 like/exact/in/range/gt/gte/lt/ltesearch.lazyLoad 在搜索折叠时隐藏该项;search.fieldProps 透传 ProForm 属性。
  • 详情:默认复用表格渲染,可用 detail.render 自定义。
  • 数据源:options/data 可为数组、函数(接收 formData),或 Action 名(通过 form.action 自动请求)。

搜索条件与 Prisma 映射

mode映射适用
like{ contains, mode: 'insensitive' }文本模糊
exactvalue精确匹配
in{ in: [] }多选
range{ gte, lte }时间/数字范围
gt/gte/lt/lte同名操作符数字/时间比较

🛠️ 页面参数(SmartCrudPage)

<SmartCrudPage
  title="示例管理"
  fieldsConfig={fieldsConfig}
  actions={actions}              // getList 必填,其余按需
  enableCreate={true}            // 默认 false
  enableEdit={true}              // 默认 true
  enableDelete={true}            // 默认 true
  enableDetail={true}            // 默认 true
  enableBatchDelete={true}
  enableIndexColumn={true}
  baseQuery={{ enable: true }}   // 强制过滤条件
  customToolbarButtons={[<Button key="export" onClick={exportData}>导出</Button>]}
  customRowActions={[
    { key: 'publish', label: '发布', onClick: onPublish, confirm: { title: '确认发布?' }, showCondition: r => r.status === 'draft' },
  ]}
  batchActions={[
    { key: 'enable', label: '批量启用', onClick: rows => batchEnable(rows.map(r => r.id)) },
  ]}
  beforeCreate={(values) => ({ ...values, code: values.code?.toUpperCase?.() })}
  beforeEdit={(record) => record.enable !== false}   // 返回 false 阻止编辑
  beforeDelete={(record) => !record.locked}
  tableApiRef={tableApiRef}          // 外部可调用 refresh/reset/search
  refreshTrigger={refreshVersion}    // 数值变化即自动刷新
/>

要点:

  • actions 支持 getList/getDetail/create/update/delete/batchUpdate/batchDelete;缺失的能力自动隐藏 UI。
  • tableApiRef.current 暴露 refresh/reloadAndRest/resetSearch/submitSearch/getSelectedRows 等。
  • dataSource + loading 可直接接管数据(跳过 request)。
  • 序号列:enableIndexColumn,按分页计算全局序号。
  • 详情抽屉:自动使用 fieldsConfig 渲染,可通过 renderDetailHeader 定制头部。

🔧 Server Actions / DAO 配置

createCrudActions 配置关键字

const config = {
  modelName: 'coupon',          // Prisma 模型名(小写)
  tableName: 'coupons',         // selects 连表必填
  primaryKey: 'id',
  softDelete: true,             // 自动过滤 deletedAt
  requireAdmin: false,          // true 仅 admin 角色

  fields: {
    creatable: ['name', 'code', 'type', 'discount', 'enable'],
    updatable: ['name', 'type', 'discount', 'enable', 'remark'],
    searchable: ['name', 'code'],
  },

  query: {
    defaultSort: { createdAt: 'desc' },
    defaultPageSize: 20,
    baseFilter: { enable: true },
    include: { author: true },    // Prisma include
    foreignDB: [/* selects 连表 */],
  },

  validation: {                  // 通过 auto-schema 校验
    name: { required: true, minLength: 2, maxLength: 100 },
    code: { required: true, pattern: /^[A-Z0-9_-]+$/ },
    discount: { type: 'number', min: 0 },
    enable: { type: 'boolean', default: true },
  },

  uniqueFields: ['code'],        // 自动唯一性检查

  hooks: {
    beforeCreate: async (data, ctx) => ({ ...data, creatorId: ctx.userId }),
    afterCreate: async (record) => record,
    beforeUpdate: async (id, data) => data,
    beforeDelete: async (id, ctx) => true,
  },

  transforms: {
    input: (data) => ({ ...data, code: data.code?.toUpperCase?.() }),
    output: (record) => ({ ...record, discountLabel: `${record.discount}%` }),
  },
}

export const {
  getList: sysGetCouponList,
  getDetail: sysGetCouponDetail,
  create: sysCreateCoupon,
  update: sysUpdateCoupon,
  delete: sysDeleteCoupon,
  batchDelete: sysBatchDeleteCoupon,
} = createCrudActions(config)

约定与规范:

  • Action 名字用 sys 前缀即可自动校验后台登录 + RBAC + 操作日志;auth 要求登录,pub 公开。
  • baseFilter 永远附着在所有查询;foreignDB/include 让列表与详情同时连表。
  • hooks 返回 false 阻断操作,抛出 BusinessError 会记录日志但不打印堆栈。
  • tableName 缺失会导致 selects 查询报错,连表时务必填写。

🧪 CRUD 实操(10 分钟落一页优惠券)

1) 建模

prisma/schema.prisma 添加模型:

model Coupon {
  id          String   @id @default(uuid())
  name        String
  code        String   @unique
  type        String   @default("percentage") // percentage | fixed
  discount    Decimal  @db.Decimal(10, 2)
  startDate   DateTime?
  endDate     DateTime?
  usageLimit  Int      @default(0)
  enable      Boolean  @default(true)
  remark      String?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  deletedAt   DateTime?

  @@index([code])
  @@index([enable])
  @@index([deletedAt])
  @@map("coupons")
}

执行迁移(任选包管理器):

bunx prisma migrate dev --name add_coupon

2) 创建 Actions

app/(admin)/actions/cms/crud-action.coupon.js

'use server'
import { createCrudActions } from '@/lib/core/crud-helper'

const config = {
  modelName: 'coupon',
  tableName: 'coupons',
  primaryKey: 'id',
  softDelete: true,
  fields: {
    creatable: ['name', 'code', 'type', 'discount', 'startDate', 'endDate', 'usageLimit', 'enable', 'remark'],
    updatable: ['name', 'type', 'discount', 'startDate', 'endDate', 'usageLimit', 'enable', 'remark'],
    searchable: ['name', 'code'],
  },
  query: { defaultSort: { createdAt: 'desc' }, defaultPageSize: 20 },
  validation: {
    name: { required: true, minLength: 2, maxLength: 100 },
    code: { required: true, pattern: /^[A-Z0-9_-]{3,20}$/ },
    type: { enum: ['percentage', 'fixed'], default: 'percentage' },
    discount: { type: 'number', min: 0 },
    usageLimit: { type: 'number', min: 0, default: 0 },
  },
  uniqueFields: ['code'],
  transforms: {
    input: (data) => ({ ...data, code: data.code?.toUpperCase?.() }),
  },
}

export const {
  getList: sysGetCouponList,
  getDetail: sysGetCouponDetail,
  create: sysCreateCoupon,
  update: sysUpdateCoupon,
  delete: sysDeleteCoupon,
  batchDelete: sysBatchDeleteCoupon,
} = createCrudActions(config)

3) 编写页面

app/(admin)/admin/cms/coupons/page.js

'use client'
import dynamic from 'next/dynamic'
import { Tag } from 'antd'
import * as actions from '@/app/(admin)/actions/cms/crud-action.coupon'

const SmartCrudPage = dynamic(() => import('@/components/admin/smart-crud-page'), { ssr: false })

const fieldsConfig = [
  { key: 'id', title: 'ID', type: 'text', table: { width: 140, copyable: true }, form: { hidden: true }, search: false },
  { key: 'name', title: '名称', type: 'text', table: { ellipsis: true }, form: { required: true }, search: { mode: 'like' } },
  { key: 'code', title: '优惠码', type: 'text', table: { width: 140, formatter: (v) => v || '-' }, form: { required: true } },
  {
    key: 'type',
    title: '类型',
    type: 'select',
    options: [
      { label: '百分比', value: 'percentage' },
      { label: '固定金额', value: 'fixed' },
    ],
    table: {
      width: 120,
      valueEnum: {
        percentage: { text: '百分比', status: 'Processing' },
        fixed: { text: '固定金额', status: 'Default' },
      },
    },
    form: { required: true, defaultValue: 'percentage' },
  },
  { key: 'discount', title: '折扣值', type: 'number', table: { width: 100 }, form: { required: true, min: 0 }, search: { mode: 'range' } },
  { key: 'usageLimit', title: '可用次数', type: 'number', table: { width: 100 }, form: { min: 0, defaultValue: 0 } },
  {
    key: 'enable',
    title: '状态',
    type: 'switch',
    table: {
      width: 100,
      render: (v) => <Tag color={v ? 'green' : 'red'}>{v ? '启用' : '停用'}</Tag>,
    },
    form: { defaultValue: true },
    search: { mode: 'exact' },
  },
  { key: 'startDate', title: '开始时间', type: 'datetime', table: { width: 170 }, search: { mode: 'range' } },
  { key: 'endDate', title: '结束时间', type: 'datetime', table: { width: 170 }, search: { mode: 'range' } },
  { key: 'remark', title: '备注', type: 'textarea', table: { hidden: true }, form: { rows: 3, colSpan: 24 } },
  { key: 'createdAt', title: '创建时间', type: 'datetime', table: { width: 180, sorter: true }, form: { hidden: true }, search: { mode: 'range' } },
]

export default function CouponPage() {
  return (
    <SmartCrudPage
      title="优惠券管理"
      fieldsConfig={fieldsConfig}
      actions={actions}
      enableCreate
      enableDelete
      enableBatchDelete
      enableIndexColumn
    />
  )
}

4) 权限与菜单

  • Actions 已自动命名为 sysGetCouponList/sysCreateCoupon/...,在 RBAC → 权限中添加同名 Action 即可生效。
  • 在「菜单管理」添加一条菜单指向 /admin/cms/coupons,绑定上面的权限,立刻受控。
  • 需要种子数据时可参考 templates/crud/PERMISSIONS.md 的 Action/菜单示例。

完成后,进入后台即可看到全新的「优惠券管理」页,表格/搜索/创建/编辑/删除/详情全部开箱即用。


📚 相关文档