Vue3 还原一个企业级后台-08-API注册管理

业务实现 1:API 注册管理模块

脚手架、布局、通用组件都就位了,现在该上战场了。第一个业务模块------API 注册管理,是整个项目的"列表页范式"样板间。把这一篇吃透,后面两个业务模块(模型汇聚、模型标准发布)都是它的变体。


一、业务理解:API 注册管理是什么

"管网模型工具"是 AI 模型生命周期管理后台。模型要被调用,得先有 API。API 注册管理,就是把后端已经写好的 API 登记到平台里,供模型统一调用。

打个比方:平台是一个"调度中心",API 是"可调用的能力清单"。注册管理就是维护这份清单------谁提供了什么能力、怎么调用、现在能不能用。

核心字段

字段 说明 示例
名称 API 的展示名 管网泄漏检测接口
协议 通信协议 HTTP / HTTPS / gRPC
URL 接口地址 https://api.pipe.com/leak-detect
请求方式 HTTP 方法 GET / POST / PUT
状态 启用 / 禁用 启用

操作

每个 API 有 5 个操作:新增、编辑、删除、详情、调试。其中"调试"是本项目最有特色的功能------直接在平台里发请求看响应,不用开 Postman。


二、列表页实现:用 AppTable 一路平推

列表页是 API 注册管理的核心。有了第 7 篇封装的 AppTable,整个列表页的 template 只有 20 行:

vue 复制代码
<!-- views/api-manage/ApiRegistry.vue -->
<template>
  <div class="page-card">
    <AppBreadcrumb :items="[{ title: 'API 注册管理' }]" />

    <!-- 筛选区 -->
    <div class="filter-bar">
      <el-select v-model="filters.protocol" placeholder="协议" clearable style="width: 140px">
        <el-option label="HTTP" value="HTTP" />
        <el-option label="HTTPS" value="HTTPS" />
        <el-option label="gRPC" value="gRPC" />
      </el-select>
      <el-select v-model="filters.status" placeholder="状态" clearable style="width: 140px">
        <el-option label="启用" value="enabled" />
        <el-option label="禁用" value="disabled" />
      </el-select>
      <AppSearchInput v-model="filters.keyword" placeholder="搜索 API 名称" @search="onSearch" />
      <el-button type="primary" :icon="Plus" @click="openCreate">新增 API</el-button>
    </div>

    <!-- 表格 -->
    <AppTable
      :data="tableData"
      :columns="columns"
      :loading="loading"
      :total="total"
      v-model:page="page"
      v-model:selection="selectedRows"
      @page-change="fetchData"
    >
      <template #status="{ row }">
        <StatusTag :status="row.status" />
      </template>
      <template #action="{ row }">
        <el-button type="primary" link @click="goDetail(row)">详情</el-button>
        <el-button type="primary" link @click="openEdit(row)">编辑</el-button>
        <el-button type="danger" link @click="del(row)">删除</el-button>
        <el-button type="primary" link @click="goDebug(row)">调试</el-button>
      </template>
    </AppTable>
  </div>
</template>

2.1 columns 配置

columns 数组告诉 AppTable 表格有哪些列、每列怎么渲染:

js 复制代码
const columns = [
  { prop: 'name', label: 'API 名称', width: 200, align: 'left' },
  { prop: 'protocol', label: '协议', width: 100, align: 'center' },
  { prop: 'url', label: '接口地址', minWidth: 280, align: 'left' },
  { prop: 'method', label: '请求方式', width: 100, align: 'center' },
  { prop: 'status', label: '状态', width: 100, align: 'center', slot: 'status' },
  { prop: 'createTime', label: '创建时间', width: 180, align: 'center' }
]

注意 status 列配了 slot: 'status'------意思是"这一列不用文本渲染,用名为 status 的 slot"。在 template 里,我们写了 <template #status="{ row }"> 来渲染 StatusTag 组件。

2.2 数据获取

js 复制代码
import { ref, reactive, onMounted } from 'vue'
import { getApiList } from '@/api/apiManage'

const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const pageSize = 30

const filters = reactive({
  protocol: '',
  status: '',
  keyword: ''
})

async function fetchData() {
  loading.value = true
  try {
    const res = await getApiList({
      page: page.value,
      pageSize,
      ...filters
    })
    tableData.value = res.data.list
    total.value = res.data.total
  } finally {
    loading.value = false
  }
}

onMounted(fetchData)

fetchData 是列表页的"心脏"------筛选条件变了、翻页了、搜索了,都调它重新拉数据。它只关心"拿数据",不关心"数据怎么展示"------展示是 AppTable 的事。


三、筛选 + 搜索逻辑:多条件组合

3.1 多条件组合筛选

协议下拉 + 状态下拉 + 搜索框,三个条件可以任意组合。实现方式很简单------把它们都放进 filters 对象,调 fetchData 时一起传给后端(Mock):

js 复制代码
function onFilterChange() {
  page.value = 1  // 筛选后回到第一页
  fetchData()
}

注意 page.value = 1------筛选后必须重置页码,否则可能出现"筛选后第 3 页没数据"的尴尬。

3.2 防抖搜索

搜索框如果每敲一个字就发一次请求,用户快速输入"管网泄漏"四个字会发 4 次请求。用 500ms 防抖,等用户停下来再发:

js 复制代码
import { debounce } from '@/utils/debounce'

const onSearch = debounce(() => {
  page.value = 1
  fetchData()
}, 500)

debounce 是一个 10 行的小工具函数:

js 复制代码
// utils/debounce.js
export function debounce(fn, delay = 300) {
  let timer = null
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), delay)
  }
}

3.3 筛选状态同步到 URL query

这是一个"加分项"------把筛选条件写进 URL,刷新页面后筛选条件还在。实现用 useRoute + useRouter

js 复制代码
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 初始化时从 URL 读取筛选条件
onMounted(() => {
  const { protocol, status, keyword, page: p } = route.query
  if (protocol) filters.protocol = protocol
  if (status) filters.status = status
  if (keyword) filters.keyword = keyword
  if (p) page.value = Number(p)
  fetchData()
})

// 筛选变化时写回 URL
watch(filters, () => {
  router.replace({
    query: {
      ...filters,
      page: page.value
    }
  })
}, { deep: true })

原型阶段不做这个也完全 OK------但做了之后演示效果会好很多(客户刷新页面数据不丢)。


四、复选框 + 批量操作:跨页选择是难点

4.1 选中行的获取

AppTable 已经把 selection-change 事件封装好了,父组件通过 v-model:selection 拿到选中行:

js 复制代码
const selectedRows = ref([])

// 批量删除
function batchDelete() {
  if (selectedRows.value.length === 0) {
    ElMessage.warning('请先选择要删除的 API')
    return
  }
  ElMessageBox.confirm(
    `确认删除选中的 ${selectedRows.value.length} 个 API?`,
    '批量删除',
    { type: 'warning' }
  ).then(() => {
    // 调用删除接口
    ElMessage.success('删除成功')
    fetchData()
  })
}

4.2 跨页保持选择

第 7 篇提到,AppTable 的复选框列设了 :reserve-selection="true"。这保证了用户在第 1 页选了 3 条、翻到第 2 页选了 2 条,selectedRows 里是 5 条而不是 2 条。

但有个前提------el-table 必须知道"哪行是同一行",靠 row-key 属性。AppTable 默认 rowKey: 'id',所以每条数据必须有唯一的 id 字段。Mock 数据里 @id 生成的 id 正好满足。

4.3 全选/反选/单选

这些 Element Plus 表格自带,不需要额外写代码。AppTable 只是把 selection-change 事件透传出来。


五、行操作:四个按钮各司其职

行操作列的四个按钮,对应四种交互:

操作 行为 实现
详情 跳转到详情页 router.push('/api-manage/detail/' + row.id)
编辑 打开编辑弹窗 openEdit(row)
删除 带确认后删除 ElMessageBox.confirm + 删除接口
调试 跳转到运行调试 tab router.push('/api-manage/detail/' + row.id + '?tab=debug')
js 复制代码
import { useRouter } from 'vue-router'

const router = useRouter()

function goDetail(row) {
  router.push(`/api-manage/detail/${row.id}`)
}

function goDebug(row) {
  router.push(`/api-manage/detail/${row.id}?tab=debug`)
}

function del(row) {
  ElMessageBox.confirm(`确认删除「${row.name}」?`, '删除确认', {
    type: 'warning'
  }).then(async () => {
    await deleteApi(row.id)
    ElMessage.success('删除成功')
    fetchData()
  }).catch(() => {})
}

删除操作必须带 ElMessageBox.confirm 二次确认------这是后台系统的铁律。误删一条生产数据,后果可能很严重。


六、详情页:双 Tab 设计

详情页是 API 注册管理最有特色的部分------它有两个 Tab:

  • 基本信息 Tab:只读展示 API 的所有字段
  • 运行调试 Tab:JSON 编辑器 + Send 按钮 + 响应展示

6.1 路由与页面结构

js 复制代码
// router 配置
{
  path: 'api-manage/detail/:id',
  name: 'ApiDetail',
  component: () => import('@/views/api-manage/ApiDetail.vue'),
  meta: { title: 'API 详情', module: '/api-manage/registry', hidden: true }
}
vue 复制代码
<!-- views/api-manage/ApiDetail.vue -->
<template>
  <div class="page-card">
    <el-button :icon="ArrowLeft" @click="goBack">返回</el-button>
    <AppBreadcrumb :items="[{ title: 'API 注册管理', path: '/api-manage/registry' }, { title: 'API 详情' }]" />

    <el-tabs v-model="activeTab" @tab-change="onTabChange">
      <el-tab-pane label="基本信息" name="info" />
      <el-tab-pane label="运行调试" name="debug" />
    </el-tabs>

    <div v-show="activeTab === 'info'">
      <!-- 基本信息只读展示 -->
      <el-descriptions :column="2" border>
        <el-descriptions-item label="API 名称">{{ detail.name }}</el-descriptions-item>
        <el-descriptions-item label="协议">{{ detail.protocol }}</el-descriptions-item>
        <el-descriptions-item label="接口地址">{{ detail.url }}</el-descriptions-item>
        <el-descriptions-item label="请求方式">{{ detail.method }}</el-descriptions-item>
        <el-descriptions-item label="状态">
          <StatusTag :status="detail.status" />
        </el-descriptions-item>
        <el-descriptions-item label="描述" :span="2">{{ detail.description }}</el-descriptions-item>
      </el-descriptions>
    </div>

    <div v-show="activeTab === 'debug'">
      <!-- 运行调试 -->
      <div class="debug-area">
        <div class="debug-request">
          <div class="debug-toolbar">
            <el-select v-model="debugMethod" style="width: 120px">
              <el-option label="GET" value="GET" />
              <el-option label="POST" value="POST" />
            </el-select>
            <el-input v-model="debugUrl" readonly style="flex: 1" />
            <el-button type="primary" :loading="debugging" @click="sendRequest">Send</el-button>
          </div>
          <el-input
            v-model="debugBody"
            type="textarea"
            :rows="10"
            placeholder="请求体(JSON)"
          />
        </div>
        <div class="debug-response">
          <div class="debug-response-title">响应</div>
          <pre>{{ debugResult }}</pre>
        </div>
      </div>
    </div>
  </div>
</template>

6.2 运行调试的实现

调试 Tab 的核心是一个"假请求"------因为后端还没好,我们用 Mock 模拟响应:

js 复制代码
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getApiDetail } from '@/api/apiManage'

const route = useRoute()
const detail = ref({})
const activeTab = ref('info')
const debugMethod = ref('GET')
const debugUrl = ref('')
const debugBody = ref('{\n  "param": "value"\n}')
const debugResult = ref('')
const debugging = ref(false)

onMounted(async () => {
  const res = await getApiDetail(route.params.id)
  detail.value = res.data
  debugUrl.value = res.data.url
  // 从 URL query 读取初始 tab(?tab=debug)
  if (route.query.tab) activeTab.value = route.query.tab
})

async function sendRequest() {
  debugging.value = true
  try {
    // 模拟请求延迟
    await new Promise(r => setTimeout(r, 800))
    debugResult.value = JSON.stringify({
      code: 200,
      message: 'success',
      {
        leakProbability: 0.87,
        location: [120.15, 30.28],
        timestamp: new Date().toISOString()
      }
    }, null, 2)
  } finally {
    debugging.value = false
  }
}

调试 Tab 让原型"活"了起来------客户在演示现场点一下 Send,800ms 后看到一段真实的 JSON 响应,比"这里应该会返回数据"有说服力得多。

6.3 返回按钮 + 面包屑

返回按钮用 router.back()router.push('/api-manage/registry')。面包屑的第二项带了 path,点击能跳回列表页。


七、注册弹窗:新增/编辑复用同一个

新增和编辑的表单字段完全一样------区别只是"编辑时表单预填了数据"。所以用一个弹窗组件,通过 mode 区分:

vue 复制代码
<!-- 在 ApiRegistry.vue 中 -->
<AppDialog
  v-model="dialogVisible"
  :title="dialogMode === 'create' ? '新增 API' : '编辑 API'"
  :loading="submitting"
  @confirm="handleSubmit"
>
  <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
    <el-form-item label="API 名称" prop="name">
      <el-input v-model="form.name" placeholder="请输入 API 名称" />
    </el-form-item>
    <el-form-item label="协议" prop="protocol">
      <el-select v-model="form.protocol" style="width: 100%">
        <el-option label="HTTP" value="HTTP" />
        <el-option label="HTTPS" value="HTTPS" />
        <el-option label="gRPC" value="gRPC" />
      </el-select>
    </el-form-item>
    <el-form-item label="接口地址" prop="url">
      <el-input v-model="form.url" placeholder="https://..." />
    </el-form-item>
    <el-form-item label="请求方式" prop="method">
      <el-select v-model="form.method" style="width: 100%">
        <el-option label="GET" value="GET" />
        <el-option label="POST" value="POST" />
        <el-option label="PUT" value="PUT" />
      </el-select>
    </el-form-item>
    <el-form-item label="描述">
      <el-input v-model="form.description" type="textarea" :rows="3" />
    </el-form-item>
  </el-form>
</AppDialog>
js 复制代码
const dialogVisible = ref(false)
const dialogMode = ref('create')
const formRef = ref()
const submitting = ref(false)
const form = reactive({
  id: null,
  name: '',
  protocol: 'HTTP',
  url: '',
  method: 'GET',
  description: ''
})

const rules = {
  name: [{ required: true, message: '请输入 API 名称', trigger: 'blur' }],
  protocol: [{ required: true, message: '请选择协议', trigger: 'change' }],
  url: [{ required: true, message: '请输入接口地址', trigger: 'blur' }],
  method: [{ required: true, message: '请选择请求方式', trigger: 'change' }]
}

function openCreate() {
  dialogMode.value = 'create'
  resetForm()
  dialogVisible.value = true
}

function openEdit(row) {
  dialogMode.value = 'edit'
  Object.assign(form, row)
  dialogVisible.value = true
}

function resetForm() {
  form.id = null
  form.name = ''
  form.protocol = 'HTTP'
  form.url = ''
  form.method = 'GET'
  form.description = ''
}

async function handleSubmit() {
  await formRef.value.validate()
  submitting.value = true
  try {
    if (dialogMode.value === 'create') {
      await createApi(form)
      ElMessage.success('新增成功')
    } else {
      await updateApi(form)
      ElMessage.success('编辑成功')
    }
    dialogVisible.value = false
    fetchData()
  } finally {
    submitting.value = false
  }
}

7.1 必填校验

rules 对象定义了每个字段的校验规则。formRef.value.validate() 在提交前触发校验,不通过则不会发请求。这是 Element Plus 表单的标准用法,但值得强调------所有用户输入都必须校验,原型也不例外。客户演示时填了个空值点提交,如果没校验直接报错,比"提示请填写"尴尬得多。

7.2 提交 loading + 成功提示

提交时 submitting = true,AppDialog 的确认按钮自动进入 loading 态(第 7 篇封装的 :loading prop)。成功后 ElMessage.success 弹提示,关闭弹窗,重新拉列表。


八、Mock 数据:30 条真实感数据

API 注册管理需要 30 条 Mock 数据。第 11 篇会详细讲 Mock 策略,这里先给一个预览------数据要"有血有肉":

名称 协议 URL 状态
管网泄漏检测接口 HTTPS https://api.pipe.com/leak-detect 启用
压力监测服务 HTTP http://10.0.1.23:8080/pressure 启用
流量分析 API gRPC grpc://pipe-grpc/flow-analysis 禁用
水质预测模型 HTTPS https://api.pipe.com/water-quality 启用
... ... ... ...

每条数据的名称、URL、状态都有差异,时间字段分布在最近 3 个月------这样列表页看起来像真实生产数据,而不是"测试数据 1/2/3"。


九、小结:列表页范式

API 注册管理模块做完,我们沉淀出一个"列表页范式":

复制代码
筛选区(下拉 + 搜索)
  ↓
表格(复选框 + 序号 + 字段列 + 操作列)
  ↓
分页器(30 条/页)
  ↓
行操作(详情/编辑/删除/调试)
  ↓
弹窗(新增/编辑复用)
  ↓
详情页(双 Tab:基本信息 + 运行调试)

这个范式不是 API 注册管理独有的------模型汇聚、模型标准发布都是它的变体。区别只在于"表格有哪些列""弹窗有哪些字段""详情页有几个 Tab"。

范式 = 不变的结构 + 可变的内容。 把结构固化成通用组件(AppTable、AppDialog),把内容留给每个业务页面填------这就是企业级后台开发效率的秘诀。


上一篇:07 - 通用组件:分页器、表格、对话框的封装

下一篇预告:列表页范式跑通了,下一个模块"模型汇聚"是项目最复杂的------详情页有 3 个 Tab,Tab 切换还要同步 URL。怎么让多 Tab 详情页不丢数据?

相关推荐
笨鸟先飞,勤能补拙2 小时前
从预测到决策:人工智能与机器学习的系统方法、工程闭环与现实边界
大数据·人工智能·windows·python·机器学习·密码学
(╹◡╹)2 小时前
18.剪枝
算法·机器学习·剪枝
W_326004 小时前
Python-OpenCV图像像素与通道:通道拆分合并、深浅拷贝
图像处理·人工智能·python·opencv·机器学习
我想试一下名字可以取多长一点点再长一些4 小时前
从0开始学习机器学习-泛化与拟合
机器学习
空堂与归4 小时前
如何解决数据预测问题?用线性回归建模实现精准预测
机器学习
watersink5 小时前
机器学习XGBoost
人工智能·机器学习
watersink6 小时前
机器学习极大似然估计与EM算法
人工智能·算法·机器学习
叠层归一研究院6 小时前
如何用程序搭建一个 AGI 种子系统(一):从向量种子到无限生长引擎
人工智能·python·算法·机器学习·agi
l1258656 小时前
# RAG重排序实战:硅基流动bge-reranker-v2-m3在线API vs 本地CrossEncoder,一篇讲透两种方案
数据库·人工智能·python·深度学习·算法·机器学习·langchain