Skip to content

9.12 新增页面开发步骤 ​

概述

以 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}PageGET
全量列表get{Module}ListGET
详情get{Module}DetailGET
新增{module}AddPOST
修改{module}UpdatePUT
删除{module}DeleteDELETE
批量删除{module}BatchDeleteDELETE

步骤二:创建表格列配置 ​

typescript
// 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,
  },
];

列配置字段说明 ​

字段类型说明
typestring列类型:selection(多选框)/ index(序号)
labelstring表头标题
propstring数据字段名(camelCase)
fixedstring固定列:left / right
widthnumber固定宽度
minWidthnumber最小宽度(自适应)
renderfunction自定义渲染函数

步骤三:创建查询表单配置 ​

typescript
// 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日期字段日期选择器

步骤四:创建列表页 ​

vue
<!-- 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>

步骤五:创建编辑弹窗 ​

vue
<!-- 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>

步骤六:创建详情弹窗 ​

vue
<!-- 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

步骤八:测试 ​

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

代码生成器(快速创建) ​

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

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

详见代码生成器。

小蚂蚁云团队 · 提供技术支持