zhou zhou
3 天以前 50e95b985a72fcec4a93a2470e9efdfb2620148a
rsf-design/src/views/system/ai-mcp-mount/index.vue
@@ -11,20 +11,18 @@
    <ElCard class="art-table-card">
      <div class="mb-5 flex flex-wrap items-center justify-between gap-4">
        <div>
          <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">MCP 挂载</h3>
          <p class="mt-1 text-sm text-[var(--art-gray-500)]"
            >按传输类型管理 MCP 挂载、连通性和工具预览。</p
          >
          <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">{{ t('pages.system.aiMcpMount.title') }}</h3>
          <p class="mt-1 text-sm text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.subtitle') }}</p>
        </div>
        <ElSpace wrap>
          <ElButton v-auth="'save'" @click="openCreateDialog" v-ripple>新建挂载</ElButton>
          <ElButton :loading="loading" @click="refreshData" v-ripple>刷新</ElButton>
          <ElButton v-auth="'save'" @click="openCreateDialog" v-ripple>{{ t('pages.system.aiMcpMount.buttons.add') }}</ElButton>
          <ElButton :loading="loading" @click="refreshData" v-ripple>{{ t('common.actions.refresh') }}</ElButton>
        </ElSpace>
      </div>
      <div v-loading="loading" class="space-y-6">
        <ElEmpty v-if="!groupedRecords.length" description="暂无 MCP 挂载数据" :image-size="110" />
        <ElEmpty v-if="!groupedRecords.length" :description="t('pages.system.aiMcpMount.empty')" :image-size="110" />
        <section v-for="group in groupedRecords" :key="group.key" class="space-y-4">
          <div>
@@ -67,7 +65,7 @@
                <div
                  class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]"
                >
                  <p class="text-xs text-[var(--art-gray-500)]">目标地址</p>
                  <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.target') }}</p>
                  <p class="mt-2 break-all text-[var(--art-gray-900)]">{{
                    item.targetLabel || '--'
                  }}</p>
@@ -75,26 +73,26 @@
                <div
                  class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]"
                >
                  <p class="text-xs text-[var(--art-gray-500)]">最近测试</p>
                  <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastTestTime') }}</p>
                  <p class="mt-2 text-[var(--art-gray-900)]">{{
                    item['lastTestTime$'] || '未测试'
                    item['lastTestTime$'] || t('pages.system.aiMcpMount.health.notTested')
                  }}</p>
                </div>
              </div>
              <div class="mt-4 grid gap-3 text-sm sm:grid-cols-3">
                <div class="rounded-2xl bg-slate-50 px-3 py-2">
                  <p class="text-xs text-[var(--art-gray-500)]">超时</p>
                  <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.timeoutMs') }}</p>
                  <p class="mt-1 font-medium text-[var(--art-gray-900)]"
                    >{{ item.requestTimeoutMs ?? '--' }} ms</p
                  >
                </div>
                <div class="rounded-2xl bg-slate-50 px-3 py-2">
                  <p class="text-xs text-[var(--art-gray-500)]">排序</p>
                  <p class="text-xs text-[var(--art-gray-500)]">{{ t('table.sort') }}</p>
                  <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.sort ?? '--' }}</p>
                </div>
                <div class="rounded-2xl bg-slate-50 px-3 py-2">
                  <p class="text-xs text-[var(--art-gray-500)]">初始化耗时</p>
                  <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastInitElapsedMs') }}</p>
                  <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{
                    item.lastInitElapsedMs ?? '--'
                  }}</p>
@@ -102,7 +100,7 @@
              </div>
              <div class="mt-4 rounded-2xl bg-amber-50/80 px-4 py-3">
                <p class="text-xs text-[var(--art-gray-500)]">备注</p>
                <p class="text-xs text-[var(--art-gray-500)]">{{ t('table.remark') }}</p>
                <p class="mt-2 line-clamp-3 text-sm leading-6 text-[var(--art-gray-900)]">{{
                  item.memo || '--'
                }}</p>
@@ -116,20 +114,18 @@
                }}</div>
                <ElSpace wrap>
                  <ElButton text @click="openDetailDialog(item)">详情</ElButton>
                  <ElButton v-auth="'update'" text @click="openEditDialog(item)">编辑</ElButton>
                  <ElButton text @click="openDetailDialog(item)">{{ t('common.actions.detail') }}</ElButton>
                  <ElButton v-auth="'update'" text @click="openEditDialog(item)">{{ t('common.actions.edit') }}</ElButton>
                  <ElButton
                    v-auth="'update'"
                    text
                    :loading="connectivityTestingId === item.id"
                    @click="handleConnectivityTest(item)"
                  >
                    连通性测试
                    {{ t('pages.system.aiMcpMount.actions.connectivityTest') }}
                  </ElButton>
                  <ElButton v-auth="'list'" text @click="openToolsDrawer(item)">工具预览</ElButton>
                  <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)"
                    >删除</ElButton
                  >
                  <ElButton v-auth="'list'" text @click="openToolsDrawer(item)">{{ t('pages.system.aiMcpMount.actions.toolsPreview') }}</ElButton>
                  <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)">{{ t('common.actions.delete') }}</ElButton>
                </ElSpace>
              </div>
            </article>
@@ -168,6 +164,7 @@
<script setup>
  import { ElMessage, ElMessageBox } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import { useTable } from '@/hooks/core/useTable'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import {
@@ -191,6 +188,7 @@
  defineOptions({ name: 'AiMcpMount' })
  const { t } = useI18n()
  const searchForm = ref(createAiMcpMountSearchState())
  const dialogVisible = ref(false)
  const dialogMode = ref('create')
@@ -201,16 +199,16 @@
  const searchItems = computed(() => [
    {
      label: '关键字',
      label: t('pages.system.aiMcpMount.search.condition'),
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入名称'
        placeholder: t('pages.system.aiMcpMount.search.conditionPlaceholder')
      }
    },
    {
      label: '传输类型',
      label: t('pages.system.aiMcpMount.search.transportType'),
      key: 'transportType',
      type: 'select',
      props: {
@@ -223,14 +221,14 @@
      }
    },
    {
      label: '状态',
      label: t('pages.system.aiMcpMount.search.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: [
          { label: '启用', value: 1 },
          { label: '停用', value: 0 }
          { label: t('common.status.enabled'), value: 1 },
          { label: t('common.status.disabled'), value: 0 }
        ]
      }
    }
@@ -267,9 +265,9 @@
  const groupedRecords = computed(() => {
    const groups = [
      { key: 'BUILTIN', title: '内置 MCP', description: '系统内置的标准 MCP 挂载。' },
      { key: 'SSE_HTTP', title: 'SSE / HTTP', description: '通过服务地址和 SSE 端点接入的 MCP。' },
      { key: 'STDIO', title: 'STDIO', description: '通过本地命令启动的 MCP。' }
      { key: 'BUILTIN', title: t('pages.system.aiMcpMount.groups.builtin.title'), description: t('pages.system.aiMcpMount.groups.builtin.description') },
      { key: 'SSE_HTTP', title: t('pages.system.aiMcpMount.groups.sse.title'), description: t('pages.system.aiMcpMount.groups.sse.description') },
      { key: 'STDIO', title: t('pages.system.aiMcpMount.groups.stdio.title'), description: t('pages.system.aiMcpMount.groups.stdio.description') }
    ]
    return groups
      .map((group) => ({
@@ -321,13 +319,13 @@
    try {
      if (dialogMode.value === 'edit') {
        await fetchUpdateAiMcpMount(payload)
        ElMessage.success('修改成功')
        ElMessage.success(t('crud.messages.updateSuccess'))
        dialogVisible.value = false
        await refreshUpdate()
        return
      }
      await fetchSaveAiMcpMount(payload)
      ElMessage.success('新增成功')
      ElMessage.success(t('crud.messages.createSuccess'))
      dialogVisible.value = false
      await refreshCreate()
    } catch {
@@ -337,17 +335,23 @@
  async function handleDelete(record) {
    try {
      await ElMessageBox.confirm(`确定要删除挂载「${record.name || record.id}」吗?`, '删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
      await ElMessageBox.confirm(
        t('crud.confirm.deleteMessage', {
          entity: t('pages.system.aiMcpMount.entity'),
          label: record.name || record.id
        }),
        t('crud.confirm.deleteTitle'),
        {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      })
      await fetchDeleteAiMcpMount(record.id)
      ElMessage.success('删除成功')
      ElMessage.success(t('crud.messages.deleteSuccess'))
      await refreshRemove()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error(error?.message || '删除失败')
        ElMessage.error(error?.message || t('crud.messages.deleteFailed'))
      }
    }
  }
@@ -356,12 +360,12 @@
    connectivityTestingId.value = record.id
    try {
      const result = await guardRequestWithMessage(fetchTestAiMcpConnectivity(record.id), null, {
        timeoutMessage: '连通性测试超时,已停止等待'
        timeoutMessage: t('pages.system.aiMcpMount.messages.connectivityTimeout')
      })
      ElMessage.success(result?.message || '连通性测试成功')
      ElMessage.success(result?.message || t('pages.system.aiMcpMount.messages.connectivitySuccess'))
      await refreshUpdate()
    } catch (error) {
      ElMessage.error(error?.message || '连通性测试失败')
      ElMessage.error(error?.message || t('pages.system.aiMcpMount.messages.connectivityFailed'))
    } finally {
      connectivityTestingId.value = null
    }