Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/guide/frontend-integration.md
T
2026-10-11 19:06:33 +08:00

342 lines
9.5 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.
# 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)。