342 lines
9.5 KiB
Markdown
342 lines
9.5 KiB
Markdown
# 3.9 前端联调指引
|
||
|
||
:::tip 概述
|
||
后端接口开发完成后,需要在前端创建对应的 API 模块和页面组件。本节以 example 模块为例,提供简要指引,详细内容参考 [第 9 章](/zh/frontend/overview.html)。
|
||
:::
|
||
|
||
## 联调流程
|
||
|
||
```text
|
||
1. 创建 API 模块 → ui/src/api/tool/example.ts
|
||
2. 创建页面组件 → ui/src/views/tool/example/index.vue
|
||
3. 配置菜单组件路径 → 后端菜单管理中设置 component
|
||
4. 测试联调
|
||
```
|
||
|
||
## 步骤一:创建 API 模块
|
||
|
||
在 `ui/src/api/tool/` 下创建 `example.ts`:
|
||
|
||
```typescript
|
||
// ui/src/api/tool/example.ts
|
||
import { http } from '@/utils/http/axios';
|
||
|
||
/**
|
||
* 案例演示模块接口
|
||
*
|
||
* 提供案例演示的增删改查及导入导出接口调用。
|
||
*/
|
||
|
||
/**
|
||
* 获取案例演示分页列表
|
||
*
|
||
* @param params 查询参数,包含 pageNo、pageSize 及业务筛选条件
|
||
* @returns 分页结果,含 records、total、current、size
|
||
*/
|
||
export function getExamplePage(params?: Record<string, any>) {
|
||
return http.request({
|
||
url: '/example/page',
|
||
method: 'GET',
|
||
params,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取全部案例演示列表
|
||
*/
|
||
export function getExampleList(params?: Record<string, any>) {
|
||
return http.request({
|
||
url: '/example/list',
|
||
method: 'GET',
|
||
params,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 根据 ID 获取案例演示详情
|
||
*/
|
||
export function getExampleDetail(id: number) {
|
||
return http.request({
|
||
url: '/example/detail/' + id,
|
||
method: 'get',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 添加案例演示(入参 camelCase,后端 FormRequest 自动转换)
|
||
*/
|
||
export function exampleAdd(data: any) {
|
||
return http.request({
|
||
url: '/example/add',
|
||
method: 'POST',
|
||
data,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新案例演示(需包含主键 id)
|
||
*/
|
||
export function exampleUpdate(data: any) {
|
||
return http.request({
|
||
url: '/example/update',
|
||
method: 'PUT',
|
||
data,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 删除案例演示
|
||
*/
|
||
export function exampleDelete(id: number) {
|
||
return http.request({
|
||
url: '/example/delete/' + id,
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 批量删除案例演示(形如 { ids: [1, 2, 3] })
|
||
*
|
||
* 注意:批量删除后端路由为 DELETE(请求体仍传 ids 数组)。
|
||
*/
|
||
export function exampleBatchDelete(data: any) {
|
||
return http.request({
|
||
url: '/example/batchDelete',
|
||
method: 'DELETE',
|
||
data,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 导入案例演示(FormData 文件)
|
||
*/
|
||
export function exampleImport(data: any) {
|
||
return http.request({
|
||
url: '/example/import',
|
||
method: 'POST',
|
||
data,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 导出案例演示(返回文件 URL)
|
||
*/
|
||
export function exampleExport(params?: Record<string, any>) {
|
||
return http.request({
|
||
url: '/example/export',
|
||
method: 'GET',
|
||
params,
|
||
});
|
||
}
|
||
```
|
||
|
||
:::tip
|
||
HTTP 封装(Token 携带、401 处理、统一错误提示、camel/snake 转换)由 `http` 实例统一完成,API 模块只描述"哪个地址、什么方法"。见 [9.4 HTTP 请求封装](/zh/frontend/http.html)。
|
||
:::
|
||
|
||
## 步骤二:创建页面组件
|
||
|
||
在 `ui/src/views/tool/example/` 下创建以下 5 个文件(与代码生成器输出结构一致):
|
||
|
||
| 文件 | 说明 |
|
||
|------|------|
|
||
| `index.vue` | 列表页(表格 + 搜索 + 分页) |
|
||
| `edit.vue` | 新增/编辑弹窗表单 |
|
||
| `detail.vue` | 详情查看页 |
|
||
| `columns.ts` | 表格列定义 |
|
||
| `querySchemas.ts` | 搜索表单字段定义 |
|
||
|
||
:::tip 关于以下示例
|
||
以下代码来自项目实际的 `example` 演示模块,使用封装好的 `BasicTable` + `BasicForm` 组合式组件(n-card 布局、`v-perm` 按钮权限、`$message`/`$dialog` 反馈),详见 [第 9 章组件文档](/zh/frontend/overview.html)。
|
||
:::
|
||
|
||
`index.vue`(列表页)示例:
|
||
|
||
```vue
|
||
<!-- ui/src/views/tool/example/index.vue(节选,来自项目实际代码) -->
|
||
<template>
|
||
<div>
|
||
<!-- 搜索表单卡片 -->
|
||
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||
</n-card>
|
||
|
||
<!-- 数据表格卡片 -->
|
||
<n-card :bordered="false" class="proCard">
|
||
<BasicTable
|
||
:columns="columns"
|
||
:request="loadDataTable"
|
||
:row-key="(row) => row.id"
|
||
ref="basicTableRef"
|
||
:actionColumn="actionColumn"
|
||
@update:checked-row-keys="onCheckedRow"
|
||
>
|
||
<!-- 表格顶部工具栏(v-perm 控制显示,与后端权限码一致) -->
|
||
<template #tableTitle>
|
||
<n-space>
|
||
<n-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
|
||
新增
|
||
</n-button>
|
||
<n-button
|
||
type="error"
|
||
@click="handleDelete()"
|
||
:disabled="!rowKeys.length"
|
||
v-perm="['sys:example:batchDelete']"
|
||
>
|
||
批量删除
|
||
</n-button>
|
||
</n-space>
|
||
</template>
|
||
</BasicTable>
|
||
</n-card>
|
||
|
||
<!-- 新增/编辑弹窗,主键为 0 时表示新增 -->
|
||
<editDialog
|
||
ref="createModalRef"
|
||
v-if="editVisible"
|
||
:id="currentId"
|
||
v-model:visible="editVisible"
|
||
@success="reloadTable('noRefresh')"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { h, nextTick, reactive, ref } from 'vue';
|
||
import { useMessage } from '@/hooks/web/useMessage';
|
||
import { useDialog } from '@/hooks/web/useDialog';
|
||
import { BasicTable, TableAction } from '@/components/Table';
|
||
import { BasicForm, useForm } from '@/components/Form';
|
||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||
import { columns } from './columns';
|
||
import { schemas } from './querySchemas';
|
||
import editDialog from './edit.vue';
|
||
|
||
const $message = useMessage();
|
||
const $dialog = useDialog();
|
||
const basicTableRef = ref();
|
||
const createModalRef = ref();
|
||
const editVisible = ref(false);
|
||
const currentId = ref(0);
|
||
const rowKeys = ref([]);
|
||
|
||
// 查询参数(与搜索表单字段一一对应,提交后合并到分页请求中)
|
||
const formParams = reactive({ name: '', type: '', status: '' });
|
||
|
||
// 操作列(固定右侧,auth 声明权限码,配合 v-perm 控制显隐)
|
||
const actionColumn = reactive({
|
||
width: 280,
|
||
title: '操作',
|
||
key: 'action',
|
||
fixed: 'right',
|
||
render(record) {
|
||
return h(TableAction as any, {
|
||
style: 'button',
|
||
actions: [
|
||
{ label: '编辑', type: 'info', onClick: handleEdit.bind(null, record), auth: ['sys:example:update'] },
|
||
{ label: '删除', type: 'error', onClick: handleDelete.bind(null, record), auth: ['sys:example:delete'] },
|
||
],
|
||
});
|
||
},
|
||
});
|
||
|
||
// 分页数据请求(BasicTable 内置分页,后端固定返回 records / total / current / size / pages)
|
||
const loadDataTable = async (params) => {
|
||
rowKeys.value = [];
|
||
return await getExamplePage({ ...formParams, ...params });
|
||
};
|
||
|
||
// 数据行选中(记录勾选行的 ID,供批量删除使用)
|
||
function onCheckedRow(keys) {
|
||
rowKeys.value = keys;
|
||
}
|
||
|
||
// 刷新表格(不传参回第一页,传 'noRefresh' 保持当前页)
|
||
function reloadTable(noRefresh = '') {
|
||
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||
}
|
||
|
||
// 注册搜索表单(字段来自 querySchemas.ts)
|
||
const [register] = useForm({
|
||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||
labelWidth: 80,
|
||
schemas,
|
||
});
|
||
|
||
// 提交搜索(条件写回 formParams 后回第一页)
|
||
function handleSubmit(values) {
|
||
for (const key in formParams) formParams[key] = '';
|
||
Object.assign(formParams, values);
|
||
reloadTable();
|
||
}
|
||
|
||
// 重置搜索(清空全部筛选项)
|
||
function handleReset() {
|
||
for (const key in formParams) formParams[key] = '';
|
||
reloadTable();
|
||
}
|
||
|
||
// 新增(主键置 0 表示新增)
|
||
const handleAdd = async () => {
|
||
currentId.value = 0;
|
||
editVisible.value = true;
|
||
await nextTick();
|
||
createModalRef.value.openModal();
|
||
};
|
||
|
||
// 编辑
|
||
async function handleEdit(record) {
|
||
currentId.value = record.id;
|
||
editVisible.value = true;
|
||
await nextTick();
|
||
createModalRef.value.openModal();
|
||
}
|
||
|
||
// 删除(单条 / 批量,先弹确认框,成功后 $message 提示并刷新)
|
||
async function handleDelete(record?) {
|
||
$dialog.warning({
|
||
title: '提示',
|
||
content: '确定要删除选中的案例演示吗?',
|
||
positiveText: '确定',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
record?.id ? await exampleDelete(record.id) : await exampleBatchDelete({ ids: rowKeys.value });
|
||
$message.success('删除成功');
|
||
reloadTable();
|
||
},
|
||
});
|
||
}
|
||
</script>
|
||
```
|
||
|
||
## 步骤三:配置菜单
|
||
|
||
在后端菜单管理中,将组件路径设置为:
|
||
|
||
```text
|
||
tool/example/index
|
||
```
|
||
|
||
前端路由系统会自动将此路径映射到 `ui/src/views/tool/example/index.vue`(动态路由由后端菜单数据驱动)。
|
||
|
||
## 步骤四:测试联调
|
||
|
||
1. 确保后端服务运行:`php artisan serve`(或 `composer dev`)
|
||
2. 确保前端服务运行:`cd ui && pnpm dev`(开发端口 8001)
|
||
3. 为测试角色分配新菜单权限(见 [3.8 权限节点配置](/zh/guide/permission.html))
|
||
4. 刷新前端页面,侧边栏应出现"案例演示"菜单
|
||
5. 点击菜单,测试增删改查功能
|
||
|
||
### 联调自查顺序
|
||
|
||
```text
|
||
接口 404 → 路由未注册 / URL、HTTP 方法与 routes/api.php 不一致
|
||
接口 401 → Token 未携带或过期(看 http 封装是否注入 Authorization)
|
||
接口 403 → 权限码未配置 / 角色未分配(见 3.8)
|
||
接口 code=1 → 看 msg 与 data(FormRequest 字段错误明细)
|
||
表格不显示 → 响应结构应为 data.records / data.total,检查绑定字段
|
||
菜单不显示 → 组件路径错误 / 角色未分配菜单 / 前端 views 下无对应 .vue
|
||
```
|
||
|
||
详细前端开发内容请参考 [第 9 章](/zh/frontend/overview.html)。
|