签入版本
This commit is contained in:
@@ -1,275 +1,109 @@
|
||||
# 3.9 前端联调指引
|
||||
# 3.10 前端联调指引
|
||||
|
||||
:::tip 概述
|
||||
后端接口开发完成后,需要在前端创建对应的 API 模块和页面组件。本节以 example 模块为例,提供简要指引,详细内容参考[第 9 章](/zh/frontend/overview.html)。
|
||||
后端四件套就绪后,前端补两块:API 模块(`ui/src/api`)与页面(`ui/src/views`)。本页以 example 模块的真实前端文件为准,深入开发规范见 [第 9 章 前端开发](/zh/frontend/overview.html)。
|
||||
:::
|
||||
|
||||
## 联调流程
|
||||
## 涉及文件(example 模块实际结构)
|
||||
|
||||
```text
|
||||
1. 创建 API 模块 → ui/src/api/tool/example.ts
|
||||
2. 创建页面组件 → ui/src/views/tool/example/index.vue
|
||||
3. 配置菜单组件路径 → 后端菜单管理中设置 component
|
||||
4. 测试联调
|
||||
ui/src/api/tool/example.ts # API 模块(九个接口函数)
|
||||
ui/src/views/tool/example/
|
||||
├── index.vue # 列表页(BasicTable + 查询表单 + 工具栏按钮)
|
||||
├── edit.vue # 新增/编辑弹窗(主键为 0 时表示新增)
|
||||
├── detail.vue # 详情弹窗
|
||||
├── columns.ts # 表格列配置(render 函数自定义图片/标签渲染)
|
||||
└── querySchemas.ts # 查询表单配置(FormSchema 声明式)
|
||||
```
|
||||
|
||||
## 步骤一:创建 API 模块
|
||||
|
||||
在 `ui/src/api/tool/` 下创建 `example.ts`(来自项目实际代码):
|
||||
## API 模块
|
||||
|
||||
```typescript
|
||||
// ui/src/api/tool/example.ts
|
||||
// ui/src/api/tool/example.ts(节选,完整内容见源文件)
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 案例演示模块接口
|
||||
*
|
||||
* 提供案例演示的增删改查及导入导出接口调用。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取案例演示分页列表
|
||||
*
|
||||
* @param params 查询参数,包含 pageNo、pageSize 及业务筛选条件
|
||||
* @returns 分页结果,含 records、total、size、current、pages
|
||||
*/
|
||||
export function getExamplePage(params?: Record<string, any>) {
|
||||
return http.request({
|
||||
url: '/example/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
return http.request({ url: '/example/page', method: 'GET', params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部案例演示列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns 案例演示列表
|
||||
*/
|
||||
export function getExampleList(params?: Record<string, any>) {
|
||||
return http.request({
|
||||
url: '/example/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取案例演示详情
|
||||
*
|
||||
* @param id 案例演示ID
|
||||
* @returns 案例演示详情数据
|
||||
*/
|
||||
export function getExampleDetail(id) {
|
||||
return http.request({
|
||||
url: '/example/detail/' + id,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加案例演示
|
||||
*
|
||||
* @param data 案例演示数据
|
||||
* @returns 新记录的 ID
|
||||
*/
|
||||
export function exampleAdd(data: any) {
|
||||
return http.request({
|
||||
url: '/example/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
return http.request({ url: '/example/add', method: 'POST', data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新案例演示
|
||||
*
|
||||
* @param data 案例演示数据,需包含主键 id
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function exampleUpdate(data: any) {
|
||||
return http.request({
|
||||
url: '/example/update',
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除案例演示
|
||||
*
|
||||
* @param id 案例演示ID
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function exampleDelete(id) {
|
||||
return http.request({
|
||||
url: '/example/delete/' + id,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除案例演示
|
||||
*
|
||||
* @param data 含 ids 数组的对象,形如 { ids: [1, 2, 3] }
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function exampleBatchDelete(data: any) {
|
||||
return http.request({
|
||||
url: '/example/batchDelete',
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入案例演示
|
||||
*
|
||||
* @param data 文件数据(FormData)
|
||||
* @returns 导入结果,含成功数量与失败明细
|
||||
*/
|
||||
export function exampleImport(data) {
|
||||
return http.request({
|
||||
url: '/example/import',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出案例演示
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns 导出文件的 URL
|
||||
*/
|
||||
export function exampleExport(params) {
|
||||
return http.request({
|
||||
url: '/example/export',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
return http.request({ url: '/example/delete/' + id, method: 'DELETE' });
|
||||
}
|
||||
```
|
||||
|
||||
## 步骤二:创建页面组件
|
||||
约定:URL 不带 `/api` 前缀(`VITE_GLOB_API_URL_PREFIX` 统一拼接);Token 由请求拦截器自动携带;响应信封由拦截器解包,业务层直接拿 data。
|
||||
|
||||
在 `ui/src/views/tool/example/` 下创建以下 5 个文件:
|
||||
## 页面结构
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `index.vue` | 列表页(表格 + 搜索 + 分页) |
|
||||
| `edit.vue` | 新增/编辑弹窗表单 |
|
||||
| `detail.vue` | 详情查看页 |
|
||||
| `columns.ts` | 表格列定义 |
|
||||
| `querySchemas.ts` | 搜索表单字段定义 |
|
||||
列表页(index.vue)骨架摘自 `ui/src/views/tool/example/index.vue`:
|
||||
|
||||
:::tip 关于以下示例
|
||||
以下代码使用原生 `el-table` / `el-form` 简化展示核心逻辑。实际项目中应使用封装好的 `BasicTable` + `useForm` 等组合式组件,详见[第 9 章组件文档](/zh/frontend/overview.html)。
|
||||
:::
|
||||
```html
|
||||
<PageWrapper>
|
||||
<!-- 查询表单卡片(querySchemas.ts 声明驱动) -->
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
|
||||
`index.vue`(列表页)示例:
|
||||
|
||||
```vue
|
||||
<!-- ui/src/views/tool/example/index.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-button type="primary" @click="handleAdd">新增</el-button>
|
||||
<el-button type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="typeText" label="类型" width="100" />
|
||||
<el-table-column prop="statusText" label="状态" width="100" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @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"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
|
||||
const searchForm = ref({ name: '', status: '' });
|
||||
const tableData = ref([]);
|
||||
const pageNo = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
const selectedIds = ref([]);
|
||||
|
||||
const fetchData = async () => {
|
||||
const res = await getExamplePage({
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchForm.value,
|
||||
});
|
||||
tableData.value = res.records;
|
||||
total.value = res.total;
|
||||
};
|
||||
|
||||
const handleSearch = () => { pageNo.value = 1; fetchData(); };
|
||||
const handleReset = () => { searchForm.value = { name: '', status: '' }; handleSearch(); };
|
||||
const handleDelete = async (id: number) => { await exampleDelete(id); fetchData(); };
|
||||
const handleBatchDelete = async () => { await exampleBatchDelete({ ids: selectedIds.value }); fetchData(); };
|
||||
const handleSelectionChange = (rows: any[]) => { selectedIds.value = rows.map((r) => r.id); };
|
||||
|
||||
onMounted(() => fetchData());
|
||||
</script>
|
||||
<!-- 数据表格卡片(columns.ts 列配置 + 分页请求) -->
|
||||
<el-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
:actionColumn="actionColumn"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">添加案例演示</el-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
</PageWrapper>
|
||||
```
|
||||
|
||||
## 步骤三:配置菜单
|
||||
模式要点:`BasicForm`/`BasicTable` 经 `@register` 注册使用;表格数据由 `request` 回调返回(页面里即 `getExamplePage`);工具栏按钮挂 `v-perm` 数组入参。
|
||||
|
||||
在后端菜单管理中,将组件路径设置为:
|
||||
## 列配置与查询配置
|
||||
|
||||
```text
|
||||
tool/example/index
|
||||
```typescript
|
||||
// ui/src/views/tool/example/columns.ts(节选)
|
||||
export const columns = [
|
||||
{ type: 'selection' },
|
||||
{ label: '主键ID', prop: 'id', minWidth: 100 },
|
||||
{ label: '案例名称', prop: 'name', minWidth: 120 },
|
||||
{ label: '案例图片', prop: 'avatar', minWidth: 120,
|
||||
render(record) { return h(ElAvatar, { size: 48 /* ... */ }); } },
|
||||
// ...
|
||||
];
|
||||
|
||||
// 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 } },
|
||||
// 类型/状态用 Select(后端 EqFields 过滤)
|
||||
];
|
||||
```
|
||||
|
||||
前端路由系统会自动将此路径映射到 `ui/src/views/tool/example/index.vue`。
|
||||
字典列(statusText)直接配置为普通列显示——显示名已由后端 SerializeMaps 回填。
|
||||
|
||||
## 步骤四:测试联调
|
||||
## 联调检查清单
|
||||
|
||||
1. 确保后端服务运行:`php think run`
|
||||
2. 确保前端服务运行:`pnpm dev`
|
||||
3. 刷新前端页面,侧边栏应出现"案例演示"菜单
|
||||
4. 点击菜单,测试增删改查功能
|
||||
- [ ] 菜单已配置且 `component` 路径指向新页面(否则路由 404)
|
||||
- [ ] 角色已授权页面与按钮权限节点
|
||||
- [ ] API 模块函数与 RouteSpec 动作一一对应
|
||||
- [ ] 表单字段与 dto json 名一致(全量提交语义)
|
||||
- [ ] 导入走 FormData(`UnmarshalBody` 契约,页面经 importFile 组件),导出拿 URL 下载
|
||||
- [ ] 字典下拉走字典接口取值,不硬编码枚举
|
||||
|
||||
详细前端开发内容请参考[第 9 章](/zh/frontend/overview.html)。
|
||||
## 用生成器起步
|
||||
|
||||
`make gen` 会同时产出 API 模块与上述五个页面文件(树形表走 ui2 模板),生成后按业务微调即可。生成细节见 [第 10 章 代码生成器](/zh/generator/overview.html)。
|
||||
|
||||
## 下一步
|
||||
|
||||
→ [3.11 完整代码清单](/zh/guide/code-list.html)
|
||||
|
||||
Reference in New Issue
Block a user