Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp6_AntdVue/zh/frontend/new-page.md
T
2026-10-10 12:20:37 +08:00

650 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 9.12 新增页面开发步骤
:::tip 概述
以 example 模块为例,演示从 API 到视图的完整开发步骤。一个完整模块涉及 6 个文件:API 接口、表格列配置、查询表单配置、列表页、编辑弹窗、详情弹窗。
:::
## 开发流程
```text
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. 测试 刷新前端页面,点击菜单测试
```
## 完整文件结构
```text
ui/src/
├── api/
│ └── tool/
│ └── example.ts # API 接口
└── views/
└── tool/
└── example/
├── index.vue # 列表页(主页面)
├── columns.ts # 表格列配置
├── querySchemas.ts # 查询表单配置
├── edit.vue # 编辑弹窗(新增/修改)
└── detail.vue # 详情弹窗
```
## 步骤一:创建 API 接口
```typescript
// 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 |
## 步骤二:创建表格列配置
```typescript
// 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` 分发,详见步骤四。
:::
## 步骤三:创建查询表单配置
```typescript
// 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` 组合式组件构建:
```vue
<!-- 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>
```
## 步骤五:创建编辑弹窗
```vue
<!-- 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>
```
## 步骤六:创建详情弹窗
```vue
<!-- 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` |
## 步骤八:测试
1. 刷新前端页面
2. 点击菜单"案例管理"
3. 测试搜索、新增、编辑、详情、删除、批量删除功能
## 代码生成器(快速创建)
如果模块结构简单,可使用代码生成器一键生成前后端代码:
1. 建好数据库表
2. 访问 `/api/generator/page` 查看可生成的表
3. `POST /api/generator/preview` 预览代码
4. `POST /api/generator/generate` 生成代码
5. 在菜单管理中配置菜单
详见[代码生成器](/zh/generator/overview.html)。