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

9.5 KiB
Raw Blame History

3.9 前端联调指引

:::tip 概述 后端接口开发完成后,需要在前端创建对应的 API 模块和页面组件。本节以 example 模块为例,提供简要指引,详细内容参考 第 9 章。 :::

联调流程

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:

// 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 请求封装。 :::

步骤二:创建页面组件

在 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 章组件文档。 :::

index.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>

步骤三:配置菜单

在后端菜单管理中,将组件路径设置为:

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 权限节点配置)
  4. 刷新前端页面,侧边栏应出现"案例演示"菜单
  5. 点击菜单,测试增删改查功能

联调自查顺序

接口 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 章。