后台指南
SmartCrudPage 作战手册
TL;DR(3 步生成一页 CRUD)
- 建模型 → 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 />
🎯 概述
SmartCrudPage + SmartForm + createCrudActions 组成了后台的「三件套」:一份 fieldsConfig 同时驱动表格、表单、搜索、详情,配合 Server Actions 自动处理权限、验证、日志,10-30 分钟即可落一页 CRUD。
🧩 组件清单
| 名称 | 位置 | 作用 | 关键点 |
|---|---|---|---|
SmartCrudPage | components/admin/smart-crud-page.jsx | 生成表格/搜索/详情/操作 | 内置分页、排序、批量操作、详情抽屉、序号列、hook 回调 |
SmartForm / SmartModalForm / SmartDrawerForm | components/admin/smart-form | 生成表单 | 支持分组、动态联动、校验、抽屉/弹窗形态 |
fieldsConfig 生成器 | lib/crud/field-generator.js · lib/crud/field-types.js | 将配置转成表格/表单/搜索 | 支持 20+ 字段类型,兼容 Ant Design 官方 fieldProps |
createCrudActions | lib/core/crud-helper.js | CRUD 工厂 | 自动命名、权限检查、日志、批量操作 |
wrapAction | lib/core/action-wrapper.js | Action 包装器 | pub/auth/sys/_ 前缀自动鉴权 + 操作日志 |
BaseDAO | app/(admin)/actions/dao/base.js | Prisma 封装 | 搜索、筛选、软删、连表、钩子、验证 |
📐 配置规范
基础结构(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/align;table.formatter纯函数或table.renderJSX;table.valueEnum渲染彩色 Tag。 - 表单:
form.rules兼容 Ant Design 规则;form.tips(tooltip)与form.extra;form.colSpan控制一行宽度;form.action: 'sysGetXxx'会自动调用对应 Action 作为选项。 - 搜索:
search.mode支持like/exact/in/range/gt/gte/lt/lte;search.lazyLoad在搜索折叠时隐藏该项;search.fieldProps透传 ProForm 属性。 - 详情:默认复用表格渲染,可用
detail.render自定义。 - 数据源:
options/data可为数组、函数(接收 formData),或 Action 名(通过form.action自动请求)。
搜索条件与 Prisma 映射
| mode | 映射 | 适用 |
|---|---|---|
like | { contains, mode: 'insensitive' } | 文本模糊 |
exact | value | 精确匹配 |
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_coupon2) 创建 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/菜单示例。
完成后,进入后台即可看到全新的「优惠券管理」页,表格/搜索/创建/编辑/删除/详情全部开箱即用。