18 KiB
18 KiB
9.12 新增页面开发步骤
:::tip 概述 以 example 模块为例,演示从 API 到视图的完整开发步骤。一个完整模块涉及 6 个文件:API 接口、表格列配置、查询表单配置、列表页、编辑弹窗、详情弹窗。 :::
开发流程
1. 创建 API 接口 src/api/{module}/{name}.ts
2. 创建表格列配置 src/views/{module}/{name}/columns.ts
3. 创建查询表单配置 src/views/{module}/{name}/querySchemas.ts
4. 创建列表页 src/views/{module}/{name}/index.vue
5. 创建编辑弹窗 src/views/{module}/{name}/edit.vue
6. 创建详情弹窗 src/views/{module}/{name}/detail.vue
7. 配置菜单 后端菜单管理中设置组件路径
8. 测试 刷新前端页面,点击菜单测试
完整文件结构
ui/src/
├── api/
│ └── tool/
│ └── example.ts # API 接口
└── views/
└── tool/
└── example/
├── index.vue # 列表页(主页面)
├── columns.ts # 表格列配置
├── querySchemas.ts # 查询表单配置
├── edit.vue # 编辑弹窗(新增/修改)
└── detail.vue # 详情弹窗
步骤一:创建 API 接口
// ui/src/api/tool/example.ts
import { http } from '@/utils/http/axios';
// 分页查询
export function getExamplePage(params?) {
return http.request({ url: '/example/page', method: 'GET', params });
}
// 全量列表(下拉框数据源)
export function getExampleList(params?) {
return http.request({ url: '/example/list', method: 'GET', params });
}
// 详情
export function getExampleDetail(id) {
return http.request({ url: '/example/detail/' + id, method: 'GET' });
}
// 新增
export function exampleAdd(data: any) {
return http.request({ url: '/example/add', method: 'POST', data });
}
// 修改
export function exampleUpdate(data: any) {
return http.request({ url: '/example/update', method: 'PUT', data });
}
// 删除
export function exampleDelete(id) {
return http.request({ url: '/example/delete/' + id, method: 'DELETE' });
}
// 批量删除
export function exampleBatchDelete(data: any) {
return http.request({ url: '/example/batchDelete', method: 'DELETE', data });
}
API 命名规范
| 操作 | 函数名 | HTTP 方法 |
|---|---|---|
| 分页查询 | get{Module}Page |
GET |
| 全量列表 | get{Module}List |
GET |
| 详情 | get{Module}Detail |
GET |
| 新增 | {module}Add |
POST |
| 修改 | {module}Update |
PUT |
| 删除 | {module}Delete |
DELETE |
| 批量删除 | {module}BatchDelete |
DELETE |
步骤二:创建表格列配置
// ui/src/views/tool/example/columns.ts
import { h } from 'vue';
import { Tag, Avatar } from 'ant-design-vue';
export const columns = [
{
title: 'ID',
dataIndex: 'id',
fixed: 'left',
width: 50,
},
{
title: '名称',
dataIndex: 'name',
minWidth: 120,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
minWidth: 100,
// 选项标签渲染:按值映射文字与颜色
customRender({ record }) {
const statusMap = {
0: { text: '禁用', color: 'error' },
1: { text: '启用', color: 'success' },
};
const status = statusMap[record.status];
return h(
Tag,
{ color: status?.color || 'default' },
{ default: () => status?.text || record.statusText },
);
},
},
{
title: '头像',
dataIndex: 'avatar',
key: 'avatar',
minWidth: 80,
// 图片渲染:以方形头像形式展示
customRender({ record }) {
return h(Avatar, {
size: 48,
src: record.avatar,
shape: 'square',
});
},
},
{
title: '创建人',
dataIndex: 'createUser',
minWidth: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
},
];
列配置字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
title |
string | 表头标题 |
dataIndex |
string | 数据字段名(camelCase) |
key |
string | 列唯一标识(自定义渲染时必填,供 #bodyCell 插槽匹配) |
fixed |
string | 固定列:left / right |
width |
number | 固定宽度 |
minWidth |
number | 最小宽度(自适应) |
customRender |
function | 自定义渲染函数,入参 { record, text, index } |
:::tip 说明
多选框列由表格的 row-selection 属性开启,无需在 columns 中配置。表格行渲染通过 #bodyCell 插槽配合 column.key 分发,详见步骤四。
:::
步骤三:创建查询表单配置
// ui/src/views/tool/example/querySchemas.ts
import { FormSchema } from '@/components/Form/index';
export const schemas: FormSchema[] = [
{
name: 'name',
component: 'Input',
label: '名称',
componentProps: {
placeholder: '请输入名称',
allowClear: true,
},
},
{
name: 'status',
component: 'Select',
label: '状态',
componentProps: {
placeholder: '请选择状态',
allowClear: true,
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
},
];
表单组件类型
| component | 适用场景 | 说明 |
|---|---|---|
Input |
字符串字段 | 文本输入框 |
Select |
枚举/选项字段 | 下拉选择框 |
InputNumber |
数字字段 | 数字输入框 |
DatePicker |
日期字段 | 日期选择器 |
步骤四:创建列表页
列表页基于 PageWrapper + BasicForm + BasicTable 组合式组件构建:
<!-- ui/src/views/tool/example/index.vue -->
<template>
<PageWrapper>
<!-- 查询表单卡片 -->
<a-card :bordered="false" class="pt-3 mb-3 proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
</a-card>
<!-- 数据表格卡片 -->
<a-card :bordered="false" class="proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="tableRef"
scroll-x="1200"
:row-selection="{ onChange: onSelectionChange }"
virtual-scroll
>
<!-- 表格顶部工具栏 -->
<template #tableTitle>
<a-space>
<a-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
<template #icon><PlusOutlined /></template>
新增
</a-button>
<a-button
type="danger"
@click="handleDelete()"
:disabled="!selectionData.length"
v-perm="['sys:example:batchDelete']"
>
<template #icon><DeleteOutlined /></template>
批量删除
</a-button>
</a-space>
</template>
<!-- 操作列渲染 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="handleEdit(record.id)" v-perm="['sys:example:update']">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-button type="primary" @click="handleDetail(record.id)" v-perm="['sys:example:detail']">
<template #icon><EyeOutlined /></template>
详情
</a-button>
<a-button type="primary" danger @click="handleDelete(record.id)" v-perm="['sys:example:delete']">
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-space>
</template>
</template>
</BasicTable>
</a-card>
<!-- 编辑弹窗(主键为 0 时表示新增) -->
<editDialog v-if="editVisible" :id="currentId" v-model:visible="editVisible" @success="reloadTable('noRefresh')" />
<!-- 详情弹窗 -->
<detailDialog v-if="detailVisible" :id="currentId" v-model:visible="detailVisible" @success="reloadTable('noRefresh')" />
</PageWrapper>
</template>
<script lang="ts" setup>
import { reactive, ref, nextTick, defineAsyncComponent } from 'vue';
import { useForm } from '@/components/Form/index';
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
import { columns } from './columns';
import { schemas } from './querySchemas';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
// 动态导入弹窗组件
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
const detailDialog = defineAsyncComponent(() => import('./detail.vue'));
const currentId = ref(0);
const editVisible = ref(false);
const detailVisible = ref(false);
const selectionData = ref([]);
const tableRef = ref();
// 查询参数(与搜索表单字段一一对应)
const formParams = reactive({
name: '',
status: '',
});
// 加载表格数据:合并搜索条件与分页参数
const loadDataTable = async (params: any) => {
return await getExamplePage({ ...formParams, ...params });
};
// 刷新表格:传入非空值保持当前页码,否则回到第一页
function reloadTable(noRefresh = '') {
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
}
// 注册搜索表单
const [register, { getFieldsValue }] = useForm({
rowProps: { gutter: [16, 0] },
colProps: { xs: 24, sm: 24, md: 12, lg: 8, xl: 6 },
labelCol: { span: 6, offset: 0 },
schemas,
});
// 提交搜索:把条件写回 formParams,从第一页重新加载
function handleSubmit(values: Recordable) {
handleReset();
for (const key in values) {
formParams[key] = values[key];
}
reloadTable();
}
// 重置搜索条件
function handleReset() {
for (const key in formParams) {
formParams[key] = '';
}
}
// 打开新增弹窗
const handleAdd = async () => {
currentId.value = 0;
await nextTick();
editVisible.value = true;
};
// 打开编辑弹窗
const handleEdit = async (id: number) => {
currentId.value = id;
await nextTick();
editVisible.value = true;
};
// 打开详情弹窗
const handleDetail = async (id: number) => {
currentId.value = id;
await nextTick();
detailVisible.value = true;
};
// 删除数据:不传 id 时为批量删除
async function handleDelete(id?: number) {
if (!id) {
if (!selectionData.value.length) {
message.warning('请选择要删除的数据');
return;
}
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的案例吗?',
onOk: async () => {
id ? await exampleDelete(id) : await exampleBatchDelete(selectionData.value);
message.success('删除成功');
reloadTable();
},
});
}
// 表格选中项变化
function onSelectionChange(keys: any[]) {
selectionData.value = keys;
}
</script>
步骤五:创建编辑弹窗
<!-- ui/src/views/tool/example/edit.vue -->
<template>
<a-modal
v-model:visible="props.visible"
:title="props.id ? '编辑案例' : '新增案例'"
width="500px"
:maskClosable="false"
@cancel="dialogClose"
>
<a-form ref="formRef" :model="formData" :label-col="{ style: { width: '85px' } }">
<a-form-item
label="名称"
name="name"
:rules="{ required: true, message: '请输入名称', trigger: 'blur' }"
>
<a-input v-model:value="formData.name" placeholder="请输入名称" allow-clear />
</a-form-item>
<a-form-item
label="类型"
name="type"
:rules="{ required: true, message: '请选择类型', trigger: 'change' }"
>
<a-select
v-model:value="formData.type"
placeholder="请选择类型"
allow-clear
:options="typeOptions"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="formData.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="formData.sort" :min="0" :max="9999" />
</a-form-item>
</a-form>
<template #footer>
<span class="dialog-footer">
<a-button @click="dialogClose">取消</a-button>
<a-button :loading="subLoading" type="primary" @click="submit">确定</a-button>
</span>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import { shallowRef } from 'vue';
import { exampleAdd, exampleUpdate, getExampleDetail } from '@/api/tool/example';
import { getDictItemList } from '@/api/common/index';
import { message } from 'ant-design-vue';
import { useLockFn } from '@/utils/useLockFn';
const emit = defineEmits(['success', 'update:visible']);
const formRef = shallowRef<FormInstance>();
const typeOptions = ref([]);
const props = defineProps({
visible: { type: Boolean, required: true, default: false },
id: { type: Number, required: true, default: 0 },
});
const formData = reactive({
id: 0,
name: '',
type: '',
status: 1,
sort: 0,
});
// 提交表单
const handleSubmit = async () => {
// 表单验证
await formRef.value?.validate();
// 按主键是否存在决定调用新增或更新
props.id ? await exampleUpdate(formData) : await exampleAdd(formData);
message.success('操作成功');
emit('update:visible', false);
emit('success');
};
// 防抖处理,防止重复提交
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
// 关闭弹窗
const dialogClose = () => {
emit('update:visible', false);
};
// 加载详情 / 初始化
watch(
() => props.visible,
async (visible) => {
if (!visible) return;
// 加载字典选项
typeOptions.value = await getDictItemList('example_type');
// 编辑时回填数据
if (props.id) {
const data = await getExampleDetail(props.id);
for (const key in formData) {
if (data[key] != null) {
formData[key] = data[key];
}
}
}
},
{ immediate: true },
);
</script>
步骤六:创建详情弹窗
<!-- ui/src/views/tool/example/detail.vue -->
<template>
<a-modal v-model:visible="props.visible" title="案例详情" width="800px" @cancel="dialogClose">
<a-descriptions class="margin-top" :column="2" bordered :labelStyle="{ width: '120px' }">
<a-descriptions-item label="名称">
{{ formData.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="类型">
{{ formData.typeText || '-' }}
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="formData.status === 1 ? 'success' : 'error'">
{{ formData.statusText || '-' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="排序">
{{ formData.sort ?? '-' }}
</a-descriptions-item>
<a-descriptions-item label="头像">
<Image
v-if="formData.avatar"
:src="formData.avatar"
:width="80"
:height="80"
/>
<span v-else>-</span>
</a-descriptions-item>
<a-descriptions-item label="创建人">
{{ formData.createUser || '-' }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ formData.createTime || '-' }}
</a-descriptions-item>
</a-descriptions>
<template #footer>
<span class="dialog-footer">
<a-button @click="dialogClose">关闭</a-button>
</span>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import { Image } from 'ant-design-vue';
import { getExampleDetail } from '@/api/tool/example';
const emit = defineEmits(['update:visible', 'success']);
const props = defineProps({
visible: { type: Boolean, required: true, default: false },
id: { type: Number, required: true, default: 0 },
});
const formData = reactive({
name: '',
type: '',
typeText: '',
status: 0,
statusText: '',
sort: 0,
avatar: '',
createUser: '',
createTime: '',
});
// 加载详情
const loadDetail = async () => {
try {
const data = await getExampleDetail(props.id);
for (const key in formData) {
if (data[key] != null) {
formData[key] = data[key];
}
}
} catch (error) {
console.error('获取详情失败', error);
}
};
// 关闭弹窗
const dialogClose = () => {
emit('update:visible', false);
};
// 监听弹窗显示
watch(
() => props.visible,
(newVal) => {
if (newVal && props.id) {
loadDetail();
}
},
{ immediate: true },
);
</script>
步骤七:配置菜单
在后端菜单管理中添加菜单项:
| 字段 | 值 |
|---|---|
| 菜单名称 | 案例管理 |
| 组件路径 | tool/example/index |
| 路由路径 | /tool/example |
| 权限标识 | sys:example:page |
添加按钮权限:
| 按钮名称 | 权限标识 |
|---|---|
| 新增 | sys:example:add |
| 修改 | sys:example:update |
| 删除 | sys:example:delete |
| 批量删除 | sys:example:batchDelete |
| 详情 | sys:example:detail |
步骤八:测试
- 刷新前端页面
- 点击菜单"案例管理"
- 测试搜索、新增、编辑、详情、删除、批量删除功能
代码生成器(快速创建)
如果模块结构简单,可使用代码生成器一键生成前后端代码:
- 建好数据库表
- 访问
/api/generator/page查看可生成的表 POST /api/generator/preview预览代码POST /api/generator/generate生成代码- 在菜单管理中配置菜单
详见代码生成器。