Skip to content

Commit 72de6ef

Browse files
author
linyuan.yang
committed
替换 stable
1 parent 60f0acd commit 72de6ef

12 files changed

Lines changed: 331 additions & 580 deletions

File tree

packages/admin/src/components/SkillHubModal.vue

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import axios from 'axios'
55
import { apiFetch } from '@/api'
66
import { useToast } from 'sbot-ui'
77
import { badgeClawhub, badgeSkillssh } from '@/utils/badges'
8-
import { SModal, SButton, SInput, STabBar, STab, SCheckCard } from 'sbot-ui'
8+
import { SModal, SButton, SInput, STabBar, STab, SCheckCard, STable, type STableColumn } from 'sbot-ui'
99
1010
interface HubSkillResult {
1111
id: string
@@ -58,6 +58,19 @@ const zipInstallUrl = computed(() => props.installApiUrl.replace(/\/install$/, '
5858
const showInstall = ref(false)
5959
const installing = ref(false)
6060
const selected = ref<HubSkillResult | null>(null)
61+
62+
const hubColumns = computed<STableColumn[]>(() => [
63+
{ key: 'name', label: t('common.name'), primary: true, ellipsis: true, width: '180px' },
64+
{ key: 'description', label: t('common.description'), ellipsis: true },
65+
{ key: 'popularity', label: t('skills.popularity_col'), width: '80px' },
66+
{ key: 'source', label: t('skills.source_col'), width: '90px' },
67+
{ key: 'link', label: t('skills.link_col'), width: '50px' },
68+
{ key: 'ops', label: t('common.ops'), ops: true, width: '90px' },
69+
])
70+
71+
function hubRowKey(row: HubSkillResult): string {
72+
return row.provider + ':' + row.id
73+
}
6174
const overwrite = ref(false)
6275
6376
function open() {
@@ -250,40 +263,35 @@ defineExpose({ open })
250263
<div style="flex:1;overflow-y:auto;min-height:0">
251264
<div v-if="hubSearching" class="hub-empty-text">{{ t('skills.searching') }}</div>
252265
<template v-else-if="hubSearched">
253-
<div v-if="hubResults.length === 0" class="hub-empty-text">{{ t('skills.no_search_result') }}</div>
254-
<table v-else style="width:100%;table-layout:fixed">
255-
<colgroup>
256-
<col style="width:180px" />
257-
<col />
258-
<col style="width:80px" />
259-
<col style="width:90px" />
260-
<col style="width:50px" />
261-
<col style="width:90px" />
262-
</colgroup>
263-
<thead>
264-
<tr><th>{{ t('common.name') }}</th><th>{{ t('common.description') }}</th><th>{{ t('skills.popularity_col') }}</th><th>{{ t('skills.source_col') }}</th><th>{{ t('skills.link_col') }}</th><th>{{ t('common.ops') }}</th></tr>
265-
</thead>
266-
<tbody>
267-
<tr v-for="s in hubResults" :key="s.provider + ':' + s.id">
268-
<td style="font-family:var(--sui-font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ s.name || s.id }}</td>
269-
<td style="color:var(--sui-fg-secondary);font-size:var(--sui-fs-md);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ s.description || '-' }}</td>
270-
<td style="font-size:var(--sui-fs-sm);color:var(--sui-fg-disabled);white-space:nowrap">
271-
<template v-if="s.installs != null">{{ s.installs >= 1000 ? (s.installs / 1000).toFixed(1) + 'K' : s.installs }} installs</template>
272-
<template v-else-if="s.score != null">{{ s.score.toFixed(1) }}</template>
273-
<template v-else>-</template>
274-
</td>
275-
<td>
276-
<span :style="s.provider === 'skills.sh' ? badgeSkillssh() : badgeClawhub()">{{ s.provider === 'skills.sh' ? 'Skills.sh' : 'ClawHub' }}</span>
277-
</td>
278-
<td>
279-
<a :href="s.sourceUrl" target="_blank" rel="noopener" class="hub-link" title="Open">&#x2197;</a>
280-
</td>
281-
<td style="white-space:nowrap">
282-
<SButton type="primary" size="sm" @click="openInstall(s)">{{ t('skills.install_title') }}</SButton>
283-
</td>
284-
</tr>
285-
</tbody>
286-
</table>
266+
<STable
267+
:columns="hubColumns"
268+
:rows="hubResults"
269+
:row-key="hubRowKey"
270+
:empty-text="t('skills.no_search_result')"
271+
>
272+
<template #name="{ row }">
273+
<span style="font-family:var(--sui-font-mono)">{{ row.name || row.id }}</span>
274+
</template>
275+
<template #description="{ row }">
276+
<span style="color:var(--sui-fg-secondary)">{{ row.description || '-' }}</span>
277+
</template>
278+
<template #popularity="{ row }">
279+
<span style="font-size:var(--sui-fs-sm);color:var(--sui-fg-disabled);white-space:nowrap">
280+
<template v-if="row.installs != null">{{ row.installs >= 1000 ? (row.installs / 1000).toFixed(1) + 'K' : row.installs }} installs</template>
281+
<template v-else-if="row.score != null">{{ row.score.toFixed(1) }}</template>
282+
<template v-else>-</template>
283+
</span>
284+
</template>
285+
<template #source="{ row }">
286+
<span :style="row.provider === 'skills.sh' ? badgeSkillssh() : badgeClawhub()">{{ row.provider === 'skills.sh' ? 'Skills.sh' : 'ClawHub' }}</span>
287+
</template>
288+
<template #link="{ row }">
289+
<a :href="row.sourceUrl" target="_blank" rel="noopener" class="hub-link" title="Open">&#x2197;</a>
290+
</template>
291+
<template #ops="{ row }">
292+
<SButton type="primary" size="sm" @click="openInstall(row)">{{ t('skills.install_title') }}</SButton>
293+
</template>
294+
</STable>
287295
</template>
288296
<div v-else class="hub-empty-text" style="padding:60px">
289297
输入关键词搜索 Skill Hub

packages/admin/src/views/McpView.vue

Lines changed: 31 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
<script setup lang="ts">
22
import { ref, computed, onMounted } from 'vue'
33
import { useI18n } from 'vue-i18n'
4-
import { useResponsive } from '../composables/useResponsive'
54
import { apiFetch } from '@/api'
65
import { store, applyMcpList } from '@/store'
7-
import { useToast, SButton, SInput, SSelect, SModal, SFormItem, SFormSection, STabBar, STab, SPageToolbar, SPageContent } from 'sbot-ui'
6+
import { useToast, SButton, SInput, SSelect, SModal, SFormItem, SFormSection, STabBar, STab, SPageToolbar, SPageContent, STable, type STableColumn } from 'sbot-ui'
87
import { McpTransport } from '@/types'
98
import type { McpEntry, McpTool, McpPrompt, McpResource, McpResourceTemplate } from '@/types'
109
import { serverAddr } from '@/utils/mcpSchema'
@@ -13,11 +12,17 @@ import { sourceBadgeStyle } from '@/utils/badges'
1312
1413
const { t } = useI18n()
1514
const { show } = useToast()
16-
const { isMobile } = useResponsive()
1715
1816
const searchQuery = ref('')
1917
const activeTab = ref('all')
2018
19+
const columns = computed<STableColumn[]>(() => [
20+
{ key: 'name', label: t('common.name'), primary: true, ellipsis: true, width: '220px' },
21+
{ key: 'description', label: t('common.description'), ellipsis: true },
22+
{ key: 'address', label: t('mcp.address_col'), ellipsis: true, width: '260px' },
23+
{ key: 'ops', label: t('common.ops'), ops: true, width: '190px' },
24+
])
25+
2126
const sources = computed(() => {
2227
const seen = new Set<string>()
2328
for (const m of store.allMcps) if (m.source) seen.add(m.source)
@@ -226,65 +231,30 @@ onMounted(load)
226231
</STabBar>
227232

228233
<SPageContent>
229-
<table v-if="!isMobile" class="mcp-table">
230-
<colgroup>
231-
<col style="width:220px" />
232-
<col />
233-
<col style="width:260px" />
234-
<col style="width:190px" />
235-
</colgroup>
236-
<thead>
237-
<tr>
238-
<th>{{ t('common.name') }}</th>
239-
<th>{{ t('common.description') }}</th>
240-
<th>{{ t('mcp.address_col') }}</th>
241-
<th>{{ t('common.ops') }}</th>
242-
</tr>
243-
</thead>
244-
<tbody>
245-
<tr v-if="filteredMcps.length === 0">
246-
<td colspan="4" class="mcp-empty">
247-
{{ searchQuery.trim() ? t('mcp.no_match') : t('mcp.empty') }}
248-
</td>
249-
</tr>
250-
<tr v-for="m in filteredMcps" :key="m.id">
251-
<td class="mcp-name">
252-
<span :style="`font-size:10px;padding:1px 6px;border-radius:8px;font-weight:600;margin-right:6px;${sourceBadgeStyle(m.source)}`">{{ m.source }}</span>{{ m.name }}
253-
</td>
254-
<td class="mcp-desc">{{ m.description || '—' }}</td>
255-
<td class="mcp-addr">{{ serverAddr(m as any) || '—' }}</td>
256-
<td class="mcp-ops">
257-
<div class="ops-cell">
258-
<SButton type="outline" size="sm" @click="viewTools(m.id)">{{ t('common.view') }}</SButton>
259-
<SButton v-if="m.source !== '内置'" type="outline" size="sm" @click="openEdit(m.id)">{{ t('common.edit') }}</SButton>
260-
<SButton v-if="m.source !== '内置'" type="danger" size="sm" @click="remove(m.id)">{{ t('common.delete') }}</SButton>
261-
</div>
262-
</td>
263-
</tr>
264-
</tbody>
265-
</table>
266-
<div v-else class="card-list">
267-
<div v-for="m in filteredMcps" :key="m.id" class="mobile-card">
268-
<div class="mobile-card-header">
269-
<span :style="`font-size:10px;padding:1px 6px;border-radius:8px;font-weight:600;margin-right:6px;${sourceBadgeStyle(m.source)}`">{{ m.source }}</span>
270-
{{ m.name }}
271-
</div>
272-
<div class="mobile-card-fields">
273-
<span class="mobile-card-label">{{ t('common.description') }}</span>
274-
<span class="mobile-card-value">{{ m.description || '—' }}</span>
275-
<span class="mobile-card-label">{{ t('mcp.address_col') }}</span>
276-
<span class="mobile-card-value mcp-addr-mobile">{{ serverAddr(m as any) || '—' }}</span>
234+
<STable
235+
:columns="columns"
236+
:rows="filteredMcps"
237+
row-key="id"
238+
:empty-text="searchQuery.trim() ? t('mcp.no_match') : t('mcp.empty')"
239+
>
240+
<template #name="{ row }">
241+
<span :style="`font-size:10px;padding:1px 6px;border-radius:8px;font-weight:600;margin-right:6px;${sourceBadgeStyle(row.source)}`">{{ row.source }}</span>
242+
<span style="font-family:var(--sui-font-mono)">{{ row.name }}</span>
243+
</template>
244+
<template #description="{ row }">
245+
<span class="mcp-desc">{{ row.description || '—' }}</span>
246+
</template>
247+
<template #address="{ row }">
248+
<span class="mcp-addr">{{ serverAddr(row as any) || '—' }}</span>
249+
</template>
250+
<template #ops="{ row }">
251+
<div class="ops-cell">
252+
<SButton type="outline" size="sm" @click="viewTools(row.id)">{{ t('common.view') }}</SButton>
253+
<SButton v-if="row.source !== '内置'" type="outline" size="sm" @click="openEdit(row.id)">{{ t('common.edit') }}</SButton>
254+
<SButton v-if="row.source !== '内置'" type="danger" size="sm" @click="remove(row.id)">{{ t('common.delete') }}</SButton>
277255
</div>
278-
<div class="mobile-card-ops">
279-
<SButton type="outline" size="sm" @click="viewTools(m.id)">{{ t('common.view') }}</SButton>
280-
<SButton v-if="m.source !== '内置'" type="outline" size="sm" @click="openEdit(m.id)">{{ t('common.edit') }}</SButton>
281-
<SButton v-if="m.source !== '内置'" type="danger" size="sm" @click="remove(m.id)">{{ t('common.delete') }}</SButton>
282-
</div>
283-
</div>
284-
<div v-if="filteredMcps.length === 0" class="mobile-card-empty">
285-
{{ searchQuery.trim() ? t('mcp.no_match') : t('mcp.empty') }}
286-
</div>
287-
</div>
256+
</template>
257+
</STable>
288258
</SPageContent>
289259

290260
<!-- Edit / Add MCP Modal -->
@@ -381,34 +351,14 @@ onMounted(load)
381351
<style scoped>
382352
.tab-bar-spacer { flex: 1; }
383353
.mcp-search { width: 220px; }
384-
.mcp-table { width: 100%; table-layout: fixed; }
385-
.mcp-empty {
386-
text-align: center;
387-
color: var(--sui-fg-disabled);
388-
padding: 40px;
389-
}
390-
.mcp-name {
391-
font-family: var(--sui-font-mono);
392-
overflow: hidden;
393-
text-overflow: ellipsis;
394-
white-space: nowrap;
395-
}
396354
.mcp-desc {
397355
color: var(--sui-fg-muted);
398356
font-size: var(--sui-fs-sm);
399-
overflow: hidden;
400-
text-overflow: ellipsis;
401-
white-space: nowrap;
402357
}
403358
.mcp-addr {
404-
overflow: hidden;
405-
text-overflow: ellipsis;
406-
white-space: nowrap;
407359
color: var(--sui-fg-disabled);
408360
font-size: var(--sui-fs-sm);
409361
}
410-
.mcp-addr-mobile { word-break: break-all; }
411-
.mcp-ops { white-space: nowrap; }
412362
413363
.kv-row {
414364
display: flex;

0 commit comments

Comments
 (0)