本技能用于在 Vben Admin v2 (Vue3 + Vite + TypeScript) 项目中快速生成标准的 CRUD 管理页面。基于对项目的深度分析,提炼出了一套完整的开发规范和代码模板。
公共规范(格式、组件命名、API命名、API层组织、权限模式、CRUD架构、Lint流程)已在公司统一配置 §六 中定义,本文件不再重复。
| 技术 | 版本/说明 |
|---|---|
| Vue | 3.x (Composition API + Options API 混用,项目使用 defineComponent + setup()) |
| Vite | 构建工具 |
| TypeScript | 主要开发语言 |
| Ant Design Vue | UI 组件库 |
| Vben Admin v2 | 后台管理框架 |
| Windi CSS | 原子化 CSS 框架 |
| Pinia / Vuex | 状态管理 |
| axios | HTTP 请求 |
| i18n | 国际化 (vue-i18n) |
| ESLint + Prettier | 代码规范 |
/@/* → src/*
/#/* → types/*
每个业务模块页面放在 src/views/{module}/{feature}/ 下,通常包含三个核心文件:
src/views/{module}/{feature}/
├── index.vue # 主页面(列表页)
├── {Feature}Drawer.vue # 侧边抽屉表单(新增/编辑)
└── {feature}.data.ts # 数据配置(列定义、表单Schema、搜索Schema)
命名规则:
alarm/contacts/)index.vueDrawer 后缀(如 ContactDrawer.vue、DictDrawer.vue).data.ts 后缀(如 config.data.ts、dict.data.ts)Modal 后缀(如 AccountModal.vue)公共原则见公司统一配置 §6.4(一功能一文件、禁止拆散、禁止混合、公共接口单独文件、接口只写一次)。以下仅列出 Vben2 专属结构。
src/api/{module}/
├── {feature}.ts # 单个功能的全部 API 方法(增删改查等)
├── {feature2}.ts # 另一个功能的全部 API 方法
└── model/
└── {feature}Model.ts # 对应功能的 TypeScript 类型/接口定义
Vben2 专属规则:
index.ts 统一导出,各页面直接按功能路径导入alarmContact.ts、dataBoard.ts)Model 后缀(如 alarmContactModel.ts、menuModel.ts)导入示例:
// 直接按功能路径导入,不经过 index.ts 中转
import { getContactList, addContact, deleteContact } from '/@/api/alarm/alarmContact';
src/enums/
└── {feature}Enum.ts # camelCase + Enum 后缀
通用命名规范见公司统一配置 §6.2(组件PascalCase、目录kebab-case、页面index.vue)和 §6.3(API方法命名)。以下仅列出 Vben2 专属差异。
| 类型 | 规则 | 示例 |
|---|---|---|
| Drawer/Modal 组件 | PascalCase + 后缀 | ContactDrawer.vue, AccountModal.vue |
| 数据配置 | kebab-case + .data.ts |
config.data.ts, role.data.ts |
| Model 文件 | camelCase + Model.ts |
menuModel.ts |
| 枚举文件 | camelCase + Enum.ts |
roleEnum.ts |
| Hook 文件 | camelCase, use 前缀 |
useBatchDelete.ts |
| 工具文件 | camelCase | menuUtil.ts |
| 类型 | 规则 | 示例 |
|---|---|---|
| 导出常量(列/表单配置) | camelCase | columns, searchFormSchema, formSchema |
| 导出接口/类型 | PascalCase | ContactModal, MenuOperationParams |
| 导出枚举 | PascalCase | RoleEnum, KeysTypeEnum |
| setup 内函数 | camelCase | handleCreate, handleEdit, handleDelete, handleSuccess |
| setup 内 ref | camelCase | isUpdate, treeData, roleMenus |
| 事件处理函数 | handle 前缀 |
handleSubmit, handleCreate, handleEdit |
| 计算属性 | get 前缀 |
getTitle, getCanBatchDelete |
API 路径使用 PascalCase 枚举:
enum Api {
AccountList = '/user/page',
BaseUserUrl = '/user',
}
或
enum API {
alarmContact = '/alarm_contact',
}
{feature}.data.ts — 数据配置文件模板import { BasicColumn, FormSchema } from '/@/components/Table';
// 按需导入:
// import { h } from 'vue';
// import { Tag } from 'ant-design-vue';
// import { Icon } from '/@/components/Icon';
// import { useI18n } from '/@/hooks/web/useI18n';
// import { emailRule, phoneRule } from '/@/utils/rules';
// const { t } = useI18n();
// ==================== 表格列配置 ====================
export const columns: BasicColumn[] = [
{
title: '字段名称', // 或 t('i18n.key')
dataIndex: 'fieldName',
width: 120,
// align: 'left', // 默认居中,需要时指定
// slots: { customRender: 'slotName' }, // 自定义渲染插槽
// customRender: ({ record }) => { // 行内自定义渲染
// return h(Tag, { color: 'green' }, () => '文本');
// },
},
{
title: '状态',
dataIndex: 'status',
width: 100,
customRender: ({ record }) => {
const enable = record.status === 1;
const color = enable ? 'green' : 'red';
const text = enable ? '启用' : '停用';
return h(Tag, { color }, () => text);
},
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
},
];
// ==================== 搜索表单配置 ====================
export const searchFormSchema: FormSchema[] = [
{
field: 'keyword',
label: '关键字',
component: 'Input',
colProps: { span: 8 }, // 搜索表单常用 span: 6 或 8
componentProps: {
maxLength: 255,
placeholder: '请输入关键字',
},
},
{
field: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: [
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
],
},
colProps: { span: 8 },
},
];
// ==================== 新增/编辑表单配置 ====================
export const formSchema: FormSchema[] = [
{
field: 'name',
label: '名称',
required: true,
component: 'Input',
componentProps: {
maxLength: 255,
placeholder: '请输入名称',
},
},
{
field: 'status',
label: '状态',
component: 'RadioButtonGroup',
defaultValue: 1,
componentProps: {
options: [
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
],
},
},
{
field: 'remark',
label: '备注',
component: 'InputTextArea',
componentProps: {
maxLength: 255,
placeholder: '请输入备注',
},
dynamicRules: () => {
return [
{
required: false,
validator: (_, value) => {
if (String(value).length > 255) {
return Promise.reject('字数不超过255个字');
}
return Promise.resolve();
},
},
];
},
},
{
field: 'id',
label: '',
component: 'Input',
show: false, // 隐藏字段,用于传递id
},
];
FormSchema 常用 component 类型:
Input / InputTextArea — 文本输入InputNumber — 数字输入Select — 下拉选择RadioButtonGroup — 单选按钮组Switch — 开关DatePicker — 日期选择TreeSelect / ApiTreeSelect — 树形选择IconPicker — 图标选择Checkbox — 复选框FormSchema 常用属性:
ifShow: ({ values }) => boolean — 条件显示(根据表单其他值)dynamicRules — 动态校验规则rules — 静态校验规则(如 phoneRule, emailRule)slot — 自定义插槽show: false — 隐藏字段(用于传递隐藏值如 id)colProps: { lg: 12, md: 24 } — 栅格布局defaultValue — 默认值index.vue — 主列表页模板(标准 CRUD)<template>
<div>
<BasicTable
:clickToRowSelect="false"
@register="registerTable"
>
<template #toolbar>
<Authority value="api:yt:{module}:post">
<a-button type="primary" @click="handleCreate"> 新增{业务名} </a-button>
</Authority>
<Authority value="api:yt:{module}:delete">
<Popconfirm
title="您确定要批量删除数据"
ok-text="确定"
cancel-text="取消"
@confirm="handleDeleteOrBatchDelete(null)"
>
<a-button type="primary" color="error" :disabled="hasBatchDelete">
批量删除
</a-button>
</Popconfirm>
</Authority>
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
label: '编辑',
auth: 'api:yt:{module}:update',
icon: 'clarity:note-edit-line',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
auth: 'api:yt:{module}:delete',
icon: 'ant-design:delete-outlined',
color: 'error',
popConfirm: {
title: '是否确认删除',
confirm: handleDeleteOrBatchDelete.bind(null, record),
},
},
]"
/>
</template>
</BasicTable>
<{Feature}Drawer @register="registerDrawer" @success="handleSuccess" />
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useDrawer } from '/@/components/Drawer';
import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
import { Authority } from '/@/components/Authority';
import { Popconfirm } from 'ant-design-vue';
import {Feature}Drawer from './{Feature}Drawer.vue';
import { columns, searchFormSchema } from './{feature}.data';
import { get{Feature}List, delete{Feature} } from '/@/api/{module}/{feature}';
export default defineComponent({
name: '{Feature}Management',
components: {
BasicTable,
{Feature}Drawer,
TableAction,
Authority,
Popconfirm,
},
setup() {
const [registerDrawer, { openDrawer }] = useDrawer();
function handleSuccess() {
reload();
}
const [registerTable, { reload, setProps }] = useTable({
title: '{业务名}列表',
api: get{Feature}List,
columns,
formConfig: {
labelWidth: 120,
schemas: searchFormSchema,
},
useSearchForm: true,
showTableSetting: true,
bordered: true,
showIndexColumn: false,
rowKey: 'id',
actionColumn: {
width: 200,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: 'right',
},
});
const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
delete{Feature},
handleSuccess,
setProps
);
nextTick(() => {
setProps(selectionOptions);
});
function handleCreate() {
openDrawer(true, {
isUpdate: false,
});
}
function handleEdit(record: Recordable) {
openDrawer(true, {
record,
isUpdate: true,
});
}
return {
registerTable,
registerDrawer,
handleCreate,
handleEdit,
handleSuccess,
hasBatchDelete,
handleDeleteOrBatchDelete,
};
},
});
</script>
{Feature}Drawer.vue — Drawer 表单模板(标准新增/编辑)<template>
<BasicDrawer
v-bind="$attrs"
@register="registerDrawer"
showFooter
:title="getTitle"
width="500px"
@ok="handleSubmit"
>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts">
import { defineComponent, ref, computed, unref } from 'vue';
import { BasicForm, useForm } from '/@/components/Form';
import { formSchema } from './{feature}.data';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { saveOrEdit{Feature} } from '/@/api/{module}/{feature}';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
name: '{Feature}Drawer',
components: { BasicDrawer, BasicForm },
emits: ['success', 'register'],
setup(_, { emit }) {
const isUpdate = ref(true);
let id: string | undefined;
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100,
schemas: formSchema,
showActionButtonGroup: false,
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await resetFields();
setDrawerProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
id = data.record.id;
await setFieldsValue({
...data.record,
});
}
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增{业务名}' : '编辑{业务名}'));
async function handleSubmit() {
setDrawerProps({ confirmLoading: true });
try {
const { createMessage } = useMessage();
const values = await validate();
let successMessage = '添加成功';
if (unref(isUpdate)) {
successMessage = '修改成功';
Reflect.set(values, 'id', id);
}
await saveOrEdit{Feature}(values, unref(isUpdate)).then(() => {
closeDrawer();
emit('success');
createMessage.success(successMessage);
});
} finally {
setTimeout(() => {
setDrawerProps({ confirmLoading: false });
}, 300);
}
}
return {
registerDrawer,
registerForm,
getTitle,
handleSubmit,
};
},
});
</script>
{Feature}Modal.vue — Modal 表单模板(弹窗模式,替代 Drawer)<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:title="getTitle"
@ok="handleSubmit"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form';
import { formSchema } from './{feature}.data';
import { saveOrEdit{Feature} } from '/@/api/{module}/{feature}';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
name: '{Feature}Modal',
components: { BasicModal, BasicForm },
emits: ['success', 'register'],
setup(_, { emit }) {
const isUpdate = ref(true);
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100,
schemas: formSchema,
showActionButtonGroup: false,
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
await setFieldsValue({
...data.record,
});
}
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增{业务名}' : '编辑{业务名}'));
async function handleSubmit() {
setModalProps({ confirmLoading: true });
try {
const { createMessage } = useMessage();
const values = await validate();
await saveOrEdit{Feature}(values, unref(isUpdate));
closeModal();
emit('success');
createMessage.success(unref(isUpdate) ? '编辑成功' : '新增成功');
} finally {
setTimeout(() => {
setModalProps({ confirmLoading: false });
}, 300);
}
}
return {
registerModal,
registerForm,
getTitle,
handleSubmit,
};
},
});
</script>
import { defHttp } from '/@/utils/http/axios';
import type {
{Feature}PageParams,
{Feature}Modal,
{Feature}Params,
{Feature}Info,
} from './model/{feature}Model';
import { getPageData } from '../../base'; // 分页查询使用 getPageData
enum Api {
{FEATURE}_URL = '/{feature_path}',
UPDATE_URL = '/{feature_path}/update',
}
/**
* 分页查询{业务名}列表
* @param params 查询参数(含分页)
* @returns 分页数据
*/
export const get{Feature}List = (params: {Feature}PageParams) => {
return getPageData<{Feature}Modal>(params, Api.{FEATURE}_URL);
};
/**
* 新增{业务名}
* @param params 新增参数
*/
export const add{Feature} = (params: {Feature}Params) => {
return defHttp.post({
url: Api.{FEATURE}_URL,
data: params,
});
};
/**
* 更新{业务名}
* @param params 更新参数
*/
export const update{Feature} = (params: {Feature}Params) => {
return defHttp.post({
url: Api.UPDATE_URL,
data: params,
});
};
/**
* 保存或编辑{业务名}
* @param params 表单数据
* @param isUpdate 是否为编辑模式
*/
export const saveOrEdit{Feature} = (params: {Feature}Info, isUpdate: boolean) => {
if (isUpdate) return update{Feature}(params);
return add{Feature}(params);
};
/**
* 删除{业务名}
* @param ids 待删除的ID数组
*/
export const delete{Feature} = (ids: string[]) => {
return defHttp.delete({
url: Api.{FEATURE}_URL,
data: ids, // 方式一:直接传数组
// data: { ids }, // 方式二:包装为对象
});
};
/**
* 更新{业务名}状态
* @param id 记录ID
* @param status 目标状态值
*/
export const set{Feature}Status = (id: string, status: number) => {
return defHttp.put({ url: Api.{FEATURE}_URL + '/update_status/' + id + '/' + status });
};
API 设计模式说明:
getPageData 工具函数(基于 defHttp.get)defHttp.postdefHttp.put 或 defHttp.post(取决于后端设计)defHttp.delete,参数为 string[](ID数组)saveOrEdit 模式,通过 isUpdate 参数区分import { BasicPageParams } from '/@/api/model/baseModel';
// 分页查询参数
export type {Feature}PageParams = BasicPageParams & {
keyword?: string;
status?: number;
};
// 列表项数据模型
export interface {Feature}Modal {
id: string;
name: string;
status: number;
createTime: string;
updateTime: string;
remark?: string;
}
// 新增/编辑参数
export interface {Feature}Params {
name: string;
status: number;
remark?: string;
}
export type {Feature}Info = {Feature}Params;
// 通用分页返回模型(如使用 getPageData)
// 返回类型为 PageData<{Feature}Modal>,已包含 items + total
基础模型说明(src/api/model/baseModel.ts):
export interface BasicPageParams {
page: number;
pageSize: number;
}
export interface BasicFetchResult<T> {
items: T[];
total: number;
}
分页基础请求类(src/api/base.ts):
export class BaseQueryRequest implements BaseQueryParams {
constructor(page = 1, pageSize = 10) { ... }
}
export function getPageData<T>(params: BaseQueryParams, url: string): Promise<PageData<T>> { ... }
const [registerTable, { reload, setProps, getRowSelection, getSelectRowKeys, setSelectedRowKeys }] = useTable({
title: '列表标题',
api: getListApi, // 必须是返回 Promise 的函数
columns, // 从 .data.ts 导入
formConfig: { // 搜索表单配置(与 useSearchForm 配合)
labelWidth: 120,
schemas: searchFormSchema,
resetFunc: resetFn, // 重置回调
},
useSearchForm: true, // 是否显示搜索表单
showTableSetting: true, // 表格设置按钮
bordered: true, // 边框
showIndexColumn: false, // 序号列
rowKey: 'id', // 行唯一标识
canResize: false, // 是否可以自适应高度
pagination: true, // 分页(树表设为 false)
isTreeTable: false, // 是否树形表格
striped: false, // 斑马纹
clickToRowSelect: false, // 点击行是否触发选择
actionColumn: { // 操作列
width: 200,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: 'right',
},
rowSelection: { // 行选择(也可通过 useBatchDelete 注入)
type: 'checkbox',
},
});
常用表格方法:
reload() — 重新加载数据setProps(props) — 动态设置表格属性getRowSelection() — 获取当前选中行getSelectRowKeys() — 获取选中行 keysetSelectedRowKeys(keys) — 设置选中行collapseAll() — 折叠所有(树表用)父组件:
const [registerDrawer, { openDrawer, closeDrawer }] = useDrawer();
// 打开抽屉
openDrawer(true, {
isUpdate: false, // 新增模式
// isUpdate: true, // 编辑模式
record: data, // 编辑时传入行数据
});
子组件(Drawer 内部):
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await resetFields();
setDrawerProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
await setFieldsValue({ ...data.record });
}
});
父组件:
const [registerModal, { openModal, closeModal }] = useModal();
// 打开弹窗
openModal(true, {
isUpdate: false,
record: data,
});
子组件(Modal 内部):
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
await setFieldsValue({ ...data.record });
}
});
const [registerForm, {
resetFields, // 重置表单
setFieldsValue, // 设置表单值
validate, // 校验表单
getFieldsValue, // 获取表单值
updateSchema, // 动态更新 Schema(如禁用、隐藏字段)
clearValidate, // 清除校验
}] = useForm({
labelWidth: 100,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { lg: 12, md: 24 }, // 默认栅格
});
<!-- 按钮级权限控制 -->
<Authority value="api:yt:{module}:post">
<a-button type="primary">新增</a-button>
</Authority>
<!-- TableAction 行内权限 -->
{
label: '编辑',
auth: 'api:yt:{module}:update',
icon: 'clarity:note-edit-line',
onClick: handleEdit.bind(null, record),
}
import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions, resetSelectedRowKeys } =
useBatchDelete(deleteFn, handleSuccess, setProps);
// 注入选择配置到表格
nextTick(() => {
setProps(selectionOptions);
});
// 自定义 checkbox 禁用
selectionOptions.rowSelection.getCheckboxProps = (record: Recordable) => {
if (record.status === 1) {
return { disabled: true };
}
return { disabled: false };
};
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
createMessage.success('操作成功');
createMessage.error('操作失败');
createMessage.warning('警告信息');
createMessage.info('提示信息');
import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm';
const { createSyncConfirm } = useSyncConfirm();
// 返回 Promise,支持 async/await
await createSyncConfirm({
iconType: 'warning',
content: '确认后数据将被删除',
});
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
// 在 .data.ts 中使用
title: t('routes.common.system.tableTitleSystemMenuName'),
// 在组件中使用
const getI18nCreateMenu = computed(() => t('routes.common.system.pageSystemTitleCreateMenu'));
<BasicTree
v-if="treeData.length"
checkable
toolbar
ref="treeRef"
:treeData="treeData"
:replace-fields="{ title: 'name', key: 'id' }"
:checkedKeys="checkedKeys"
@check="handleCheckClick"
title="菜单分配"
/>
// 静态树形选择
{
field: 'parentId',
label: '上级菜单',
component: 'TreeSelect',
componentProps: {
replaceFields: { title: 'menuName', key: 'id', value: 'id' },
getPopupContainer: () => document.body,
},
}
// 动态 API 树形选择
{
field: 'organizationId',
label: '所属组织',
required: true,
component: 'ApiTreeSelect',
componentProps: {
api: async () => {
const data = await getOrganizationList();
copyTransFun(data as any as any[]);
return data;
},
},
}
defineComponent + setup() 函数(项目统一风格)<script setup> 语法糖name 属性(PascalCase)组件必须显式注册 components
export default defineComponent({
name: 'FeatureDrawer',
components: { BasicDrawer, BasicForm },
emits: ['success', 'register'],
setup(_, { emit }) {
// ...
return { /* 模板需要用到的变量和方法 */ };
},
});
Reflect.get(obj, key) 和 Reflect.set(obj, key, value) 操作对象属性(项目惯例)Reflect.deleteProperty(obj, key) 删除属性使用 unref(isUpdate) 而非 isUpdate.value 读取 ref(在计算/判断中)
// 推荐
Reflect.set(values, 'id', id);
const name = Reflect.get(data.record, 'name');
// 不推荐
values.id = id; // 项目不使用此风格
async function handleSubmit() {
setDrawerProps({ confirmLoading: true }); // 或 setModalProps
try {
const values = await validate();
// 处理数据...
await saveApi(values);
closeDrawer(); // 或 closeModal
emit('success');
createMessage.success('操作成功');
} finally {
setTimeout(() => {
setDrawerProps({ confirmLoading: false }); // 延迟关闭loading,防止闪烁
}, 300);
}
}
required: truedynamicRules 返回 validator特殊格式(手机/邮箱):使用 rules: phoneRule / rules: emailRule(来自 /@/utils/rules)
dynamicRules: () => {
return [
{
required: false,
validator: (_, value) => {
if (String(value).length > 255) {
return Promise.reject('字数不超过255个字');
}
return Promise.resolve();
},
},
];
},
actionColumn: {
width: 200, // 一般200,3个操作时300
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: 'right',
}
操作按钮配置:
{
label: '编辑',
auth: 'api:yt:{module}:update', // 权限标识
icon: 'clarity:note-edit-line', // 编辑图标
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
auth: 'api:yt:{module}:delete',
icon: 'ant-design:delete-outlined', // 删除图标
color: 'error', // 红色
popConfirm: { // 二次确认
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
公共权限模式见公司统一配置 §6.5。以下为 Vben2 专属实现。
<Authority value="api:yt:{module}:{action}"> 包裹auth: 'api:yt:{module}:{action}' 属性api:yt:{模块}:{操作}:{细化}<style scoped lang="less">p-4, w-3/4):deep()Vben 组件样式覆盖示例:
:deep(.vben-basic-tree) {
width: 100% !important;
}
当需要树形表格(如菜单管理、组织管理)时,与标准 CRUD 的差异:
// useTable 配置差异
const [registerTable, { reload, collapseAll }] = useTable({
isTreeTable: true,
pagination: false, // 树表不分页
striped: false,
canResize: false,
rowKey: (record) => record.id,
// 不使用 useSearchForm + formConfig
// 而是使用外部搜索组件或 searchFormSchema
});
// 数据加载后默认展开
function onFetchSuccess() {
nextTick(collapseAll); // 或 expandAll
}
树形数据的上级选择:
// 在 useDrawerInner 中加载树形数据
let treeData = await getMenuList(1);
treeData = listToTree(treeData);
updateSchema({
field: 'parentId',
componentProps: { treeData },
});
当需要左侧树 + 右侧表格布局时:
<template>
<div>
<PageWrapper dense contentFullHeight contentClass="flex">
<OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
<BasicTable style="flex: auto" @register="registerTable" :searchInfo="searchInfo">
<!-- toolbar, action slots... -->
</BasicTable>
</PageWrapper>
</div>
</template>
| 操作 | 图标 |
|---|---|
| 编辑 | clarity:note-edit-line |
| 删除 | ant-design:delete-outlined |
| 新增 | ant-design:plus-outlined |
| 查看 | ant-design:eye-outlined |
| 详情 | ant-design:profile-outlined |
当用户请求生成一个新的 CRUD 页面时,按以下步骤执行:
src/api/{module}/model/{feature}Model.tssrc/api/{module}/{feature}.tssrc/views/{module}/{feature}/{feature}.data.tssrc/views/{module}/{feature}/{Feature}Drawer.vuesrc/views/{module}/{feature}/index.vue// 组件
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { BasicForm, useForm } from '/@/components/Form';
import { BasicDrawer, useDrawer, useDrawerInner } from '/@/components/Drawer';
import { BasicModal, useModal, useModalInner } from '/@/components/Modal';
import { BasicTree, TreeItem } from '/@/components/Tree';
import { Icon } from '/@/components/Icon';
import { Authority } from '/@/components/Authority';
import { PageWrapper } from '/@/components/Page';
// Hooks
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm';
// HTTP
import { defHttp } from '/@/utils/http/axios';
// 工具
import { listToTree } from '/@/utils/menuUtil';
import { copyTransFun, copyTransTreeFun } from '/@/utils/fnUtils';
import { emailRule, phoneRule } from '/@/utils/rules';
import { isArray } from '/@/utils/is';
import { getPageData, BaseQueryRequest } from '/@/api/base';
// 基础模型
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
// 枚举
import { RoleEnum } from '/@/enums/roleEnum';
// Ant Design Vue
import { Tag, Switch, Popconfirm, Spin, notification } from 'ant-design-vue';