ワンクリックで
frontend-crud
// 为 mfish-nocode-pro 项目前端(Vue3 + TypeScript)生成标准增删改查页面代码,包括 Model、API、data、index.vue、Modal、ViewModal 六个文件。当用户说"帮我生成前端增删改查"、"新增前端页面"、"生成前端CRUD"时使用此 skill。
// 为 mfish-nocode-pro 项目前端(Vue3 + TypeScript)生成标准增删改查页面代码,包括 Model、API、data、index.vue、Modal、ViewModal 六个文件。当用户说"帮我生成前端增删改查"、"新增前端页面"、"生成前端CRUD"时使用此 skill。
| name | frontend-crud |
| description | 为 mfish-nocode-pro 项目前端(Vue3 + TypeScript)生成标准增删改查页面代码,包括 Model、API、data、index.vue、Modal、ViewModal 六个文件。当用户说"帮我生成前端增删改查"、"新增前端页面"、"生成前端CRUD"时使用此 skill。 |
mfish-nocode-view/src/
├── api/
│ └── {apiPrefix}/ # 如:sys、demo、nocode
│ ├── model/
│ │ └── {类名}Model.ts # 接口类型定义
│ └── {类名}.ts # API 请求函数
└── views/
└── {apiPrefix}/
└── {entity-kebab-case}/ # 如:demo-order、sys-dict
├── {变量名}.data.ts # 表格列、搜索表单、表单Schema、详情Schema
├── index.vue # 列表页主页面
├── {类名}Modal.vue # 新增/编辑弹窗
└── {类名}ViewModal.vue # 详情查看弹窗
<script lang="ts" setup>defHttp(来自 @mfish/core/utils/http/axios)BasicTable + useTable(来自 @mfish/core/components/Table)BasicModal + useModal / useModalInner(来自 @mfish/core/components/Modal)BasicForm + useForm(来自 @mfish/core/components/Form)Description + useDescription(来自 @mfish/core/components/Description)buildDictTag + getDictProps(来自 @mfish/core/components/DictTag)v-auth="'{apiPrefix}:{entityName}:操作'"(insert/update/delete/query/export)BaseEntity<string>、PageResult<T>、ReqPage(来自 @mfish/core/api)string,数值型主键时为 numberAI 经常犯这个错误,必须严格遵守以下规则:
// 严重错误!不要这样做!
{
icon: 'ant-design:cloud-upload-outlined',
onClick: handlePublish.bind(null, record), // ❌ 第一次调用
auth: 'workflow:flowManage:publish',
color: 'warning',
tooltip: '发布',
popConfirm: {
title: '是否确认发布',
confirm: handlePublish.bind(null, record) // ❌ 第二次调用
}
}
使用 popConfirm(需要二次确认):
{
icon: 'ant-design:cloud-upload-outlined',
// 注意:没有 onClick
auth: 'workflow:flowManage:publish',
color: 'warning',
tooltip: '发布',
ifShow: () => record.released !== 1,
popConfirm: {
title: '是否确认发布',
placement: 'left',
confirm: handlePublish.bind(null, record)
}
}
使用 onClick(不需要确认):
{
icon: 'ant-design:info-circle-outlined',
onClick: handleQuery.bind(null, record),
// 注意:没有 popConfirm
auth: 'workflow:flowManage:query',
color: 'success',
tooltip: '查看'
}
popConfirm,是否有 onClick?如果有,立即删除onClick,是否有 popConfirm?如果有,立即删除使用 popConfirm(需要二次确认):
使用 onClick(不需要确认):
这是最重要的规则,必须严格遵守:
用户要求:“已发布的流程不允许修改和删除”
AI 错误做法(绝对不要这样做):
// ❌ 错误:擅自修改了权限配置
{
icon: 'ant-design:cloud-upload-outlined',
auth: 'workflow:flowManage:publish', // ❌ 用户没让你改这个!
color: 'warning',
tooltip: '发布',
ifShow: () => record.released !== 1
}
用户要求什么,就只做什么:
// ✅ 正确:只添加 ifShow,其他保持不变
{
icon: 'ant-design:cloud-upload-outlined',
auth: 'workflow:flowManage:update', // 保持用户的原配置不动
color: 'warning',
tooltip: '发布',
ifShow: () => record.released !== 1 // 只添加用户要求的这一行
}
用户已经配置好权限:
workflow:flowManage:update 权限workflow:flowManage:update 权限AI 自作主张改成:
workflow:flowManage:publish 权限workflow:flowManage:unpublish 权限结果:用户的权限配置被覆盖,导致功能异常!
记住:AI 的工作是帮助用户,不是替用户做决定!
询问用户(如未提供):
demo、sys、nocode)DemoOrder)string/number/boolean)、中文描述、是否可选Input / ApiSelect+字典编码 / DatePicker)按以下顺序生成,所有文件路径基于 mfish-nocode-view/src/ 目录。
api/{apiPrefix}/model/{类名}Model.ts)import { BaseEntity, PageResult, ReqPage } from "@mfish/core/api";
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.4.0
*/
export interface {类名} extends BaseEntity<string> {
//{字段注释}
{字段名}?: {TS类型};
// ... 更多字段
}
export interface Req{类名} extends ReqPage {
//{搜索字段注释}
{搜索字段名}?: {TS类型};
// ... 更多搜索字段
}
//分页结果集
export type {类名}PageModel = PageResult<{类名}>;
字段类型映射规则:
| Java/DB 类型 | TS 类型 |
|---|---|
String、Date | string |
Integer、Long、Short、Double、BigDecimal | number |
Boolean | boolean |
api/{apiPrefix}/{类名}.ts)import { defHttp } from "@mfish/core/utils/http/axios";
import { {类名}, Req{类名}, {类名}PageModel } from "@/api/{apiPrefix}/model/{类名}Model";
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.4.0
*/
enum Api {
{类名} = "/{apiPrefix}/{变量名}"
}
/**
* 分页列表查询
*/
export const get{类名}List = (req{类名}?: Req{类名}) => {
return defHttp.get<{类名}PageModel>({ url: Api.{类名}, params: req{类名} });
};
/**
* 通过id查询
*/
export function get{类名}ById(id: string) {
return defHttp.get<{类名}>({ url: `${Api.{类名}}/${id}` });
}
/**
* 导出{中文名称}
*/
export function export{类名}(req{类名}?: Req{类名}) {
return defHttp.download({ url: `${Api.{类名}}/export`, params: req{类名} });
}
/**
* 新增{中文名称}
*/
export function insert{类名}({变量名}: {类名}) {
return defHttp.post<{类名}>({ url: Api.{类名}, params: {变量名} }, { successMessageMode: "message" });
}
/**
* 修改{中文名称}
*/
export function update{类名}({变量名}: {类名}) {
return defHttp.put<{类名}>({ url: Api.{类名}, params: {变量名} }, { successMessageMode: "message" });
}
/**
* 删除{中文名称}
*/
export function delete{类名}(id: string) {
return defHttp.delete<boolean>({ url: `${Api.{类名}}/${id}` }, { successMessageMode: "message" });
}
/**
* 批量删除{中文名称}
*/
export function deleteBatch{类名}(ids: string) {
return defHttp.delete<boolean>({ url: `${Api.{类名}}/batch/${ids}` }, { successMessageMode: "message" });
}
若 ID 类型为数值型(
number),delete{类名}参数类型改为number。
views/{apiPrefix}/{entity-kebab-case}/{变量名}.data.ts)import { BasicColumn, FormSchema } from "@mfish/core/components/Table";
import { DescItem } from "@mfish/core/components/Description";
// 有字典字段时引入(无字典字段则删除)
import { buildDictTag, getDictProps } from "@mfish/core/components/DictTag";
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.4.0
*/
// ========== 表格列定义 ==========
export const columns: BasicColumn[] = [
// 普通字段
{
title: "{字段中文名}",
dataIndex: "{字段名}",
width: 120
},
// 字典字段(有字典时使用 customRender)
{
customRender: ({ record }) => {
return buildDictTag("{字典编码}", record.{字段名});
},
title: "{字段中文名}",
dataIndex: "{字段名}",
width: 120
}
];
// ========== 搜索表单 Schema ==========
export const searchFormSchema: FormSchema[] = [
// 普通输入框
{
field: "{字段名}",
label: "{字段中文名}",
component: "Input",
colProps: { xl: 5, md: 6 }
},
// 字典下拉(单选)
{
field: "{字段名}",
label: "{字段中文名}",
component: "ApiSelect",
componentProps: getDictProps("{字典编码}"),
colProps: { xl: 5, md: 6 }
},
// 字典下拉(多选)
{
field: "{字段名}",
label: "{字段中文名}",
component: "ApiSelect",
componentProps: { ...getDictProps("{字典编码}"), mode: "multiple" },
colProps: { xl: 5, md: 6 }
}
];
// ========== 新增/编辑表单 Schema ==========
export const {变量名}FormSchema: FormSchema[] = [
{
field: "id",
label: "唯一ID",
component: "Input",
show: false
},
// 文本输入
{
field: "{字段名}",
label: "{字段中文名}",
component: "Input",
required: true // 必填时加上
},
// 数值输入
{
field: "{字段名}",
label: "{字段中文名}",
component: "InputNumber"
},
// 字典下拉
{
field: "{字段名}",
label: "{字段中文名}",
component: "ApiSelect",
componentProps: getDictProps("{字典编码}")
},
// 日期(仅日期)
{
field: "{字段名}",
label: "{字段中文名}",
component: "DatePicker",
componentProps: {
valueFormat: "YYYY-MM-DD",
format: "YYYY-MM-DD",
getPopupContainer: () => document.body
}
},
// 日期时间
{
field: "{字段名}",
label: "{字段中文名}",
component: "DatePicker",
componentProps: {
valueFormat: "YYYY-MM-DD HH:mm:ss",
format: "YYYY-MM-DD HH:mm:ss",
showTime: { format: "HH:mm:ss" },
getPopupContainer: () => document.body
}
}
];
// ========== 详情查看 Schema ==========
export class {类名}Desc {
viewSchema: DescItem[] = [
{
label: "id",
field: "id",
show: () => false
},
// 普通字段
{
field: "{字段名}",
label: "{字段中文名}"
},
// 字典字段
{
render: (val) => {
if (val === undefined) return;
return buildDictTag("{字典编码}", val);
},
field: "{字段名}",
label: "{字段中文名}"
}
];
}
组件选择规则:
| 字段类型 | 表单组件 |
|---|---|
string(普通文本) | Input |
number(整数/小数) | InputNumber |
string(日期) | DatePicker(dateFormat: YYYY-MM-DD) |
string(日期时间) | DatePicker(showTime) |
| 有字典 | ApiSelect + getDictProps("{字典编码}") |
views/{apiPrefix}/{entity-kebab-case}/index.vue)<!--
@description: {中文名称}
@author: mfish
@date: {当前日期}
@version: V2.4.0
-->
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<AButton type="primary" @click="handleCreate" v-auth="'{apiPrefix}:{变量名}:insert'">新增</AButton>
<AButton color="warning" @click="handleExport" v-auth="'{apiPrefix}:{变量名}:export'">导出</AButton>
<AButton color="error" @click="handleBatchDelete" v-auth="'{apiPrefix}:{变量名}:delete'">批量删除</AButton>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{
icon: 'ant-design:info-circle-outlined',
onClick: handleQuery.bind(null, record),
auth: '{apiPrefix}:{变量名}:query',
color: 'success',
tooltip: '查看'
},
{
icon: 'ant-design:edit-outlined',
onClick: handleEdit.bind(null, record),
auth: '{apiPrefix}:{变量名}:update',
tooltip: '修改'
},
{
icon: 'ant-design:delete-outlined',
color: 'error',
popConfirm: {
title: '是否确认删除',
placement: 'left',
confirm: handleDelete.bind(null, record)
},
auth: '{apiPrefix}:{变量名}:delete',
tooltip: '删除'
}
]"
/>
</template>
</template>
</BasicTable>
<{类名}Modal @register="registerModal" @success="handleSuccess" />
<{类名}ViewModal @register="registerViewModal" />
</div>
</template>
<script lang="ts" setup>
import { BasicTable, useTable, TableAction } from "@mfish/core/components/Table";
import { useModal } from "@mfish/core/components/Modal";
import { Button as AButton } from "@mfish/core/components/Button";
import { deleteBatch{类名}, delete{类名}, export{类名}, get{类名}List } from "@/api/{apiPrefix}/{类名}";
import {类名}Modal from "./{类名}Modal.vue";
import {类名}ViewModal from "./{类名}ViewModal.vue";
import { columns, searchFormSchema } from "./{变量名}.data";
import { {类名} } from "@/api/{apiPrefix}/model/{类名}Model";
import { ref } from "vue";
import { useMessage } from "@mfish/core/hooks";
defineOptions({ name: "{类名}Management" });
const [registerModal, { openModal }] = useModal();
const [registerViewModal, { openModal: openViewModal }] = useModal();
const selectedRowKeys = ref<any[]>([]);
const [registerTable, { reload, getForm }] = useTable({
title: "{中文名称}列表",
api: get{类名}List,
rowKey: "id",
columns,
formConfig: {
name: "search_form_item",
labelWidth: 100,
schemas: searchFormSchema,
autoSubmitOnEnter: true
},
useSearchForm: true,
showTableSetting: true,
bordered: true,
showIndexColumn: false,
rowSelection: {
onChange: (rowKeys: any[]) => {
selectedRowKeys.value = rowKeys;
}
},
actionColumn: {
width: 120,
title: "操作",
dataIndex: "action"
}
});
const { createMessage } = useMessage();
function handleCreate() {
openModal(true, { isUpdate: false });
}
function handleExport() {
export{类名}({ ...getForm().getFieldsValue(), pageNum: 1, pageSize: 1000 });
}
function handleQuery({变量名}: {类名}) {
openViewModal(true, { record: {变量名} });
}
function handleEdit({变量名}: {类名}) {
openModal(true, { record: {变量名}, isUpdate: true });
}
function handleDelete({变量名}: {类名}) {
if ({变量名}.id) {
delete{类名}({变量名}.id).then(() => {
handleSuccess();
});
}
}
function handleBatchDelete() {
if (selectedRowKeys.value.length > 0) {
deleteBatch{类名}(selectedRowKeys.value.join(",")).then(() => {
handleSuccess();
});
} else {
createMessage.warning("请勾选要删除的数据");
}
}
function handleSuccess() {
reload();
}
</script>
views/{apiPrefix}/{entity-kebab-case}/{类名}Modal.vue)<!--
@description: {中文名称}
@author: mfish
@date: {当前日期}
@version: V2.4.0
-->
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm" @submit="handleSubmit" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from "vue";
import { BasicForm, useForm } from "@mfish/core/components/Form";
import { {变量名}FormSchema } from "./{变量名}.data";
import { BasicModal, useModalInner } from "@mfish/core/components/Modal";
import { insert{类名}, update{类名} } from "@/api/{apiPrefix}/{类名}";
defineOptions({ name: "{类名}Modal" });
const emit = defineEmits(["success", "register"]);
const isUpdate = ref(true);
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
name: "model_form_item",
labelWidth: 100,
baseColProps: { span: 12 },
schemas: {变量名}FormSchema,
showActionButtonGroup: false,
autoSubmitOnEnter: true
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields().then();
setModalProps({ confirmLoading: false, width: "800px" });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
setFieldsValue({ ...data.record }).then();
}
});
const getTitle = computed(() => (unref(isUpdate) ? "编辑{中文名称}" : "新增{中文名称}"));
async function handleSubmit() {
const values = await validate();
setModalProps({ confirmLoading: true });
if (unref(isUpdate)) {
save{类名}(update{类名}, values);
} else {
save{类名}(insert{类名}, values);
}
}
function save{类名}(save, values) {
save(values)
.then(() => {
emit("success");
closeModal();
})
.finally(() => {
setModalProps({ confirmLoading: false });
});
}
</script>
views/{apiPrefix}/{entity-kebab-case}/{类名}ViewModal.vue)<!--
@description: {中文名称}查看
@author: mfish
@date: {当前日期}
@version: V2.4.0
-->
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="{中文名称}信息">
<Description @register="registerDesc" />
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from "@mfish/core/components/Modal";
import { Description, useDescription } from "@mfish/core/components/Description";
import { ref } from "vue";
import { {类名}Desc } from "./{变量名}.data";
defineOptions({ name: "{类名}ViewModal" });
const {变量名}Data = ref();
const {变量名}Desc = new {类名}Desc();
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
setModalProps({
confirmLoading: false,
width: "800px",
cancelText: "关闭",
showOkBtn: false
});
{变量名}Data.value = data.record;
});
const [registerDesc] = useDescription({
data: {变量名}Data,
schema: {变量名}Desc.viewSchema,
column: 2
});
</script>
| 占位符 | 说明 | 示例 |
|---|---|---|
{类名} | PascalCase 类名 | DemoOrder |
{变量名} | camelCase 变量名 | demoOrder |
{apiPrefix} | 模块路径(小写) | demo |
{entity-kebab-case} | kebab-case 目录名 | demo-order |
columns 和 viewSchema 中使用 buildDictTag,searchFormSchema 和 FormSchema 中使用 getDictProps;只要有字典字段就需要在文件顶部引入 buildDictTag 和 getDictPropssearchFormSchema 可以为空数组 [],useSearchForm 设为 falsedelete{类名} 参数类型改为 number,Model 中 BaseEntity<number>handleExport、export{类名} 的引入和 toolbar 中的导出按钮handleBatchDelete、deleteBatch{类名} 的引入、rowSelection 配置和 toolbar 中的批量删除按钮{apiPrefix}/{变量名} 对应后端 Controller 的 @RequestMapping 路径auth 属性进行权限控制ifShow 属性可以根据记录状态动态显示/隐藏按钮,避免无效操作confirm 属性已经绑定了处理函数,不需要再使用 onClickonClick当后端增加了新的接口(如发布、审核等),需要在前端列表页添加对应按钮时,按以下步骤操作:
/**
* 发布流程
*
* @param id 唯一ID
* @return 返回结果
*/
export function publishFlowManage(id: string) {
return defHttp.put<boolean>({ url: `${Api.FlowManage}/publish/${id}` }, { successMessageMode: "message" });
}
要点:
get/post/put/delete)${Api.{类名}}/{action}/{id}successMessageMode: "message" 显示成功提示boolean/void/具体类型)import { deleteFlowManage, getFlowManageList, publishFlowManage } from "@/api/workflow/FlowManage";
在 index.vue 的 TableAction 中添加:
重要原则:避免重复定义方法
按钮配置规则:
规则 1:使用 popConfirm 时,不要同时使用 onClick
// ✅ 正确:只使用 popConfirm
{
icon: 'ant-design:cloud-upload-outlined',
auth: 'workflow:flowManage:publish',
color: 'warning',
tooltip: '发布',
ifShow: () => record.released !== 1,
popConfirm: {
title: '是否确认发布',
placement: 'left',
confirm: handlePublish.bind(null, record)
}
}
// ❌ 错误:同时使用 onClick 和 popConfirm(会导致重复调用)
{
icon: 'ant-design:cloud-upload-outlined',
onClick: handlePublish.bind(null, record), // 不要这样!
auth: 'workflow:flowManage:publish',
color: 'warning',
tooltip: '发布',
popConfirm: {
title: '是否确认发布',
confirm: handlePublish.bind(null, record) // 重复调用!
}
}
规则 2:重要操作必须使用 popConfirm
以下操作需要二次确认:
// 需要确认的操作
{
icon: 'ant-design:cloud-upload-outlined',
auth: 'workflow:flowManage:publish',
color: 'warning',
tooltip: '发布',
ifShow: () => record.released !== 1,
popConfirm: {
title: '是否确认发布',
placement: 'left',
confirm: handlePublish.bind(null, record)
}
}
// 不需要确认的操作(如查看、编辑)
{
icon: 'ant-design:info-circle-outlined',
onClick: handleQuery.bind(null, record),
auth: 'workflow:flowManage:query',
color: 'success',
tooltip: '查看'
}
按钮配置项说明:
| 配置项 | 说明 | 示例值 |
|---|---|---|
icon | 图标(ant-design 图标名) | 'ant-design:cloud-upload-outlined' |
onClick | 点击事件处理函数 | handlePublish.bind(null, record) |
auth | 权限标识 | 'workflow:flowManage:publish' |
color | 按钮颜色 | 'success'/'warning'/'error'/'primary' |
tooltip | 鼠标悬停提示 | '发布' 或根据状态动态显示 |
ifShow | 条件显示(布尔值或函数) | record.status === 0 |
popConfirm | 二次确认对话框 | { title: '是否确认操作', confirm: handler } |
常用图标:
ant-design:cloud-upload-outlinedant-design:audit-outlinedant-design:import-outlinedant-design:export-outlinedant-design:check-circle-outlined / ant-design:stop-outlined/**
* 发布
* @param flowManage 流程管理对象
*/
function handlePublish(flowManage: FlowManage) {
if (flowManage.id) {
publishFlowManage(flowManage.id).then(() => {
handleSuccess();
});
}
}
要点:
handleSuccess() 刷新列表场景 1:根据状态显示不同文案
tooltip: record.released === 1 ? '重新发布' : '发布'
场景 2:根据状态显示/隐藏按钮
ifShow: record.released !== 1 // 未发布时显示
场景 3:根据状态改变按钮颜色
color: record.status === 0 ? 'warning' : 'success'
场景 4:需要二次确认的操作
{
icon: 'ant-design:delete-outlined',
color: 'error',
popConfirm: {
title: '是否确认删除',
placement: 'left',
confirm: handleDelete.bind(null, record)
},
auth: 'workflow:flowManage:delete',
tooltip: '删除'
}
场景 5:发布操作(需要确认)
{
icon: 'ant-design:cloud-upload-outlined',
auth: 'workflow:flowManage:publish',
color: 'warning',
tooltip: '发布',
ifShow: () => record.released !== 1,
popConfirm: {
title: '是否确认发布',
placement: 'left',
confirm: handlePublish.bind(null, record)
}
}
场景 6:撤回发布操作(需要确认)
{
icon: 'ant-design:rollback-outlined',
auth: 'workflow:flowManage:unpublish',
color: 'error',
tooltip: '撤回发布',
ifShow: () => record.released === 1,
popConfirm: {
title: '是否确认撤回发布',
placement: 'left',
confirm: handleUnpublish.bind(null, record)
}
}
在 data.ts 的 columns 中添加状态列:
{
title: '发布状态',
dataIndex: 'released',
width: 120,
customRender: ({ record }) => {
return YNTag_Name(record.released === 1, '已发布', '未发布');
}
}
要点:
YNTag_Name 渲染是/否状态标签buildDictTag 渲染字典标签