Become a sponsor

概述
以 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 # 详情弹窗// 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 });
}| 操作 | 函数名 | 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 { ElAvatar, ElTag } from 'element-plus';
export const columns = [
{
type: 'selection', // 多选框列
},
{
label: 'ID',
prop: 'id',
fixed: 'left',
width: 50,
},
{
label: '名称',
prop: 'name',
minWidth: 120,
},
{
label: '状态',
prop: 'statusText',
minWidth: 100,
// 选项标签渲染:按值映射文字与颜色
render(record) {
const statusMap = {
0: { text: '禁用', type: 'danger' },
1: { text: '启用', type: 'success' },
};
const status = statusMap[record.row.status];
return h(ElTag, { type: status?.type || 'info' }, {
default: () => status?.text || record.row.statusText,
});
},
},
{
label: '头像',
prop: 'avatar',
minWidth: 80,
// 图片渲染:以方形头像形式展示
render(record) {
return h(ElAvatar, {
size: 48,
src: record.row.avatar,
shape: 'square',
fit: 'fill',
});
},
},
{
label: '创建人',
prop: 'createUser',
minWidth: 100,
},
{
label: '创建时间',
prop: 'createTime',
width: 180,
},
];| 字段 | 类型 | 说明 |
|---|---|---|
type | string | 列类型:selection(多选框)/ index(序号) |
label | string | 表头标题 |
prop | string | 数据字段名(camelCase) |
fixed | string | 固定列:left / right |
width | number | 固定宽度 |
minWidth | number | 最小宽度(自适应) |
render | function | 自定义渲染函数 |
// ui/src/views/tool/example/querySchemas.ts
import { FormSchema } from '@/components/Form/index';
export const schemas: FormSchema[] = [
{
field: 'name',
component: 'Input',
label: '名称',
componentProps: {
placeholder: '请输入名称',
clearable: true,
},
},
{
field: 'status',
component: 'Select',
label: '状态',
componentProps: {
placeholder: '请选择状态',
clearable: true,
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
},
];| component | 适用场景 | 说明 |
|---|---|---|
Input | 字符串字段 | 文本输入框 |
Select | 枚举/选项字段 | 下拉选择框 |
InputNumber | 数字字段 | 数字输入框 |
DatePicker | 日期字段 | 日期选择器 |
<!-- ui/src/views/tool/example/index.vue -->
<template>
<div>
<!-- 查询表单 -->
<SearchForm :schemas="querySchemas" @search="handleSearch" @reset="handleReset" />
<!-- 操作栏 -->
<el-row :gutter="10" style="margin-bottom: 10px;">
<el-col :span="1.5">
<el-button type="primary" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">
批量删除
</el-button>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table :data="tableData" @selection-change="handleSelectionChange" v-loading="loading">
<el-table-column v-for="col in columns" v-bind="col" :key="col.prop || col.type">
<template v-if="col.render" #default="scope">
<component :is="() => col.render(scope)" />
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
<el-button type="danger" link @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@change="fetchData"
style="margin-top: 10px; justify-content: flex-end;"
/>
<!-- 编辑弹窗 -->
<EditDialog ref="editRef" @success="fetchData" />
<!-- 详情弹窗 -->
<DetailDialog v-model:visible="detailVisible" :id="detailId" />
</div>
</template>
<script setup lang="ts">
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
import { columns } from './columns';
import { schemas as querySchemas } from './querySchemas';
import { ElMessage, ElMessageBox } from 'element-plus';
import EditDialog from './edit.vue';
import DetailDialog from './detail.vue';
const editRef = ref();
const loading = ref(false);
const tableData = ref([]);
const pageNo = ref(1);
const pageSize = ref(20);
const total = ref(0);
const selectedIds = ref<number[]>([]);
const detailVisible = ref(false);
const detailId = ref(0);
// 加载数据
const fetchData = async () => {
loading.value = true;
try {
const res = await getExamplePage({
pageNo: pageNo.value,
pageSize: pageSize.value,
});
tableData.value = res.records;
total.value = res.total;
} finally {
loading.value = false;
}
};
// 搜索
const handleSearch = (params) => {
pageNo.value = 1;
fetchData();
};
// 重置
const handleReset = () => {
pageNo.value = 1;
fetchData();
};
// 新增
const handleAdd = () => {
editRef.value.open();
};
// 编辑
const handleEdit = (row) => {
editRef.value.open(row);
};
// 详情
const handleDetail = (row) => {
detailId.value = row.id;
detailVisible.value = true;
};
// 删除
const handleDelete = async (id) => {
await ElMessageBox.confirm('确认删除该记录?', '提示', { type: 'warning' });
await exampleDelete(id);
ElMessage.success('删除成功');
fetchData();
};
// 批量删除
const handleBatchDelete = async () => {
await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条记录?`, '提示', { type: 'warning' });
await exampleBatchDelete({ ids: selectedIds.value });
ElMessage.success('删除成功');
fetchData();
};
// 多选变化
const handleSelectionChange = (rows) => {
selectedIds.value = rows.map(row => row.id);
};
onMounted(() => fetchData());
</script><!-- ui/src/views/tool/example/edit.vue -->
<template>
<el-dialog v-model="visible" :title="formData.id ? '编辑' : '新增'" width="500px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型">
<el-option v-for="item in typeOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { exampleAdd, exampleUpdate, getExampleDetail } from '@/api/tool/example';
import { getDictItemList } from '@/api/common/index';
import { ElMessage } from 'element-plus';
const emit = defineEmits(['success']);
const visible = ref(false);
const submitting = ref(false);
const formRef = ref();
const typeOptions = ref([]);
const formData = ref({
id: 0,
name: '',
type: '',
status: 1,
sort: 0,
});
const rules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
};
// 打开弹窗
const open = async (row?) => {
visible.value = true;
formData.value = { id: 0, name: '', type: '', status: 1, sort: 0 };
// 加载字典选项
typeOptions.value = await getDictItemList('example_type');
// 编辑时加载详情
if (row?.id) {
const detail = await getExampleDetail(row.id);
Object.assign(formData.value, detail);
}
};
// 提交
const handleSubmit = async () => {
await formRef.value.validate();
submitting.value = true;
try {
if (formData.value.id) {
await exampleUpdate(formData.value);
ElMessage.success('修改成功');
} else {
await exampleAdd(formData.value);
ElMessage.success('新增成功');
}
visible.value = false;
emit('success');
} finally {
submitting.value = false;
}
};
defineExpose({ open });
</script><!-- ui/src/views/tool/example/detail.vue -->
<template>
<el-dialog v-model="props.visible" title="案例详情" width="750" :close-on-click-modal="false">
<el-descriptions column="2" border>
<el-descriptions-item label="名称:">
{{ formData.name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="类型:">
{{ formData.typeText || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态:">
<el-tag :type="formData.status === 1 ? 'success' : 'danger'">
{{ formData.statusText || '-' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="排序:">
{{ formData.sort ?? '-' }}
</el-descriptions-item>
<el-descriptions-item label="头像:">
<el-image
v-if="formData.avatar"
:src="formData.avatar"
:preview-src-list="[formData.avatar]"
fit="cover"
style="width: 80px; height: 80px"
/>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="创建人:">
{{ formData.createUser || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间:">
{{ formData.createTime || '-' }}
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="emit('update:visible', false)">关闭</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { getExampleDetail } from '@/api/tool/example';
const emit = defineEmits(['update:visible']);
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);
}
};
// 监听弹窗显示
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 生成代码详见代码生成器。