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

18 KiB
Raw Blame History

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

步骤八:测试

  1. 刷新前端页面
  2. 点击菜单"案例管理"
  3. 测试搜索、新增、编辑、详情、删除、批量删除功能

代码生成器(快速创建)

如果模块结构简单,可使用代码生成器一键生成前后端代码:

  1. 建好数据库表
  2. 访问 /api/generator/page 查看可生成的表
  3. POST /api/generator/preview 预览代码
  4. POST /api/generator/generate 生成代码
  5. 在菜单管理中配置菜单

详见代码生成器。