mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: update MCP servers management UI and add descriptions for better clarity
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 256 icons */
|
||||
/* Auto-generated MDI subset – 258 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -364,6 +364,10 @@
|
||||
content: "\F01DA";
|
||||
}
|
||||
|
||||
.mdi-download-outline::before {
|
||||
content: "\F0B8F";
|
||||
}
|
||||
|
||||
.mdi-emoticon::before {
|
||||
content: "\F0C68";
|
||||
}
|
||||
@@ -404,8 +408,8 @@
|
||||
content: "\F0215";
|
||||
}
|
||||
|
||||
.mdi-file-code::before {
|
||||
content: "\F022E";
|
||||
.mdi-file-code-outline::before {
|
||||
content: "\F102B";
|
||||
}
|
||||
|
||||
.mdi-file-delimited-outline::before {
|
||||
@@ -940,6 +944,10 @@
|
||||
content: "\F060D";
|
||||
}
|
||||
|
||||
.mdi-sync::before {
|
||||
content: "\F04E6";
|
||||
}
|
||||
|
||||
.mdi-text::before {
|
||||
content: "\F09A8";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,85 +1,175 @@
|
||||
<template>
|
||||
<div class="tools-page">
|
||||
<v-container fluid class="pa-0" elevation="0">
|
||||
<!-- 页面标题 -->
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<v-btn color="success" prepend-icon="mdi-plus" class="me-2" variant="tonal"
|
||||
@click="showMcpServerDialog = true" >
|
||||
{{ tm('mcpServers.buttons.add') }}
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-refresh" variant="tonal" @click="showSyncMcpServerDialog = true"
|
||||
>
|
||||
{{ tm('mcpServers.buttons.sync') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<!-- MCP 服务器部分 -->
|
||||
<div v-if="mcpServers.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-server-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('mcpServers.empty') }}</p>
|
||||
</div>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col v-for="(server, index) in mcpServers || []" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card style="background-color: rgb(var(--v-theme-mcpCardBg));" :item="server" title-field="name"
|
||||
enabled-field="active" @toggle-enabled="updateServerStatus" @delete="deleteServer" @edit="editServer">
|
||||
<template v-slot:item-details="{ item }">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-file-code</v-icon>
|
||||
<span class="text-caption text-medium-emphasis text-truncate" :title="getServerConfigSummary(item)">
|
||||
{{ getServerConfigSummary(item) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="mcp-server-list">
|
||||
<OutlinedActionListItem
|
||||
v-for="server in mcpServers || []"
|
||||
:key="server.name"
|
||||
:title="server.name"
|
||||
>
|
||||
<div
|
||||
class="mcp-server-config text-body-2 text-medium-emphasis"
|
||||
:title="getServerConfigSummary(server)"
|
||||
>
|
||||
<v-icon
|
||||
:icon="getServerConfigIcon(server)"
|
||||
size="small"
|
||||
class="me-1"
|
||||
/>
|
||||
<span>{{ getServerConfigSummary(server) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<div>
|
||||
<div v-if="item.tools && item.tools.length > 0">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-tools</v-icon>
|
||||
<v-dialog max-width="600px">
|
||||
<template v-slot:activator="{ props: listToolsProps }">
|
||||
<span class="text-caption text-medium-emphasis cursor-pointer" v-bind="listToolsProps"
|
||||
style="text-decoration: underline;">
|
||||
{{ tm('mcpServers.status.availableTools', { count: item.tools.length }) }} ({{ item.tools.length }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card style="padding: 16px;">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span>{{ tm('mcpServers.status.availableTools') }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li v-for="(tool, idx) in item.tools" :key="idx" style="margin: 8px 0px;">{{ tool }}</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
<v-btn variant="text" color="primary" @click="isActive.value = false">
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-caption text-medium-emphasis">
|
||||
<v-icon size="small" color="warning" class="me-1">mdi-alert-circle</v-icon>
|
||||
{{ tm('mcpServers.status.noTools') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mcpServerUpdateLoaders[item.name]" class="text-caption text-medium-emphasis">
|
||||
<v-progress-circular indeterminate color="primary" size="16"></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mcp-server-tools text-caption text-medium-emphasis">
|
||||
<template v-if="server.tools && server.tools.length > 0">
|
||||
<v-dialog max-width="600px">
|
||||
<template v-slot:activator="{ props: listToolsProps }">
|
||||
<button
|
||||
v-bind="listToolsProps"
|
||||
class="mcp-server-tools__button"
|
||||
type="button"
|
||||
>
|
||||
<v-icon size="small" class="me-1">mdi-tools</v-icon>
|
||||
{{
|
||||
tm('mcpServers.status.availableTools', {
|
||||
count: server.tools.length,
|
||||
})
|
||||
}}
|
||||
({{ server.tools.length }})
|
||||
</button>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card style="padding: 16px;">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span>{{ tm('mcpServers.status.availableTools') }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(tool, idx) in server.tools"
|
||||
:key="idx"
|
||||
style="margin: 8px 0px;"
|
||||
>
|
||||
{{ tool }}
|
||||
</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="isActive.value = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-else>
|
||||
<v-icon size="small" color="warning" class="me-1">
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
{{ tm('mcpServers.status.noTools') }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<v-tooltip :text="t('core.common.itemCard.edit')" location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon="mdi-pencil-outline"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="list-action-icon-btn"
|
||||
@click="editServer(server)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip :text="t('core.common.itemCard.delete')" location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon="mdi-delete-outline"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="list-action-icon-btn"
|
||||
@click="deleteServer(server)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<template #control>
|
||||
<v-progress-circular
|
||||
v-if="mcpServerUpdateLoaders[server.name]"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="18"
|
||||
/>
|
||||
|
||||
<v-tooltip location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-switch
|
||||
v-bind="props"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
inset
|
||||
:model-value="server.active"
|
||||
:loading="mcpServerUpdateLoaders[server.name] || false"
|
||||
:disabled="mcpServerUpdateLoaders[server.name] || false"
|
||||
@update:model-value="updateServerStatus(server)"
|
||||
/>
|
||||
</template>
|
||||
<span>{{
|
||||
server.active
|
||||
? t('core.common.itemCard.enabled')
|
||||
: t('core.common.itemCard.disabled')
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</OutlinedActionListItem>
|
||||
</div>
|
||||
</v-container>
|
||||
|
||||
<div class="mcp-fab-stack">
|
||||
<v-tooltip :text="tm('mcpServers.buttons.sync')" location="left">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
color="darkprimary"
|
||||
icon="mdi-sync"
|
||||
size="x-large"
|
||||
variant="elevated"
|
||||
class="mcp-fab"
|
||||
@click="showSyncMcpServerDialog = true"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip :text="tm('mcpServers.buttons.add')" location="left">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
color="darkprimary"
|
||||
icon="mdi-plus"
|
||||
size="x-large"
|
||||
variant="elevated"
|
||||
class="mcp-fab"
|
||||
@click="showMcpServerDialog = true"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑 MCP 服务器对话框 -->
|
||||
<v-dialog v-model="showMcpServerDialog" max-width="750px">
|
||||
<v-card>
|
||||
@@ -220,8 +310,8 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import OutlinedActionListItem from '@/components/shared/OutlinedActionListItem.vue';
|
||||
import {
|
||||
askForConfirmation as askForConfirmationDialog,
|
||||
useConfirmDialog
|
||||
@@ -231,7 +321,7 @@ export default {
|
||||
name: 'McpServersSection',
|
||||
components: {
|
||||
VueMonacoEditor,
|
||||
ItemCard
|
||||
OutlinedActionListItem
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
@@ -272,6 +362,9 @@ export default {
|
||||
},
|
||||
getServerConfigSummary() {
|
||||
return (server) => {
|
||||
if (server.transport) {
|
||||
return String(server.transport).trim();
|
||||
}
|
||||
if (server.command) {
|
||||
return `${server.command} ${(server.args || []).join(' ')}`;
|
||||
}
|
||||
@@ -283,6 +376,21 @@ export default {
|
||||
}
|
||||
return this.tm('mcpServers.status.noConfig');
|
||||
};
|
||||
},
|
||||
getServerConfigIcon() {
|
||||
return (server) => {
|
||||
const transport = String(server.transport || '').toLowerCase();
|
||||
if (transport === 'streamable_http') {
|
||||
return 'mdi-web';
|
||||
}
|
||||
if (transport === 'sse') {
|
||||
return 'mdi-broadcast';
|
||||
}
|
||||
if (server.command) {
|
||||
return 'mdi-console-line';
|
||||
}
|
||||
return 'mdi-file-code-outline';
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -544,6 +652,73 @@ export default {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.mcp-server-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mcp-server-config {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mcp-server-config span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mcp-server-tools {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mcp-server-tools__button {
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||
display: inline-flex;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mcp-server-tools__button:hover {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.list-action-icon-btn {
|
||||
color: rgba(var(--v-theme-on-surface), 0.78);
|
||||
}
|
||||
|
||||
.list-action-icon-btn:hover {
|
||||
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.mcp-fab-stack {
|
||||
align-items: center;
|
||||
bottom: 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: fixed;
|
||||
right: 52px;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.mcp-fab {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.mcp-fab:hover {
|
||||
box-shadow: 0 12px 20px rgba(var(--v-theme-primary), 0.4);
|
||||
transform: translateY(-4px) scale(1.05);
|
||||
}
|
||||
|
||||
.monaco-container {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
@@ -551,4 +726,5 @@ export default {
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
<template>
|
||||
<div class="skills-page">
|
||||
<v-container fluid class="pa-0" elevation="0">
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-4">
|
||||
<div>
|
||||
<v-btn
|
||||
v-if="mode === 'local'"
|
||||
color="primary"
|
||||
prepend-icon="mdi-upload"
|
||||
class="me-2"
|
||||
variant="tonal"
|
||||
@click="openUploadDialog"
|
||||
>
|
||||
{{ tm("skills.upload") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
variant="tonal"
|
||||
@click="refreshCurrentMode"
|
||||
>
|
||||
{{ tm("skills.refresh") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn-toggle v-model="mode" mandatory divided density="comfortable">
|
||||
<v-row
|
||||
v-if="neoEnabled"
|
||||
class="d-flex justify-end align-center px-4 py-3 pb-4"
|
||||
>
|
||||
<v-btn-toggle
|
||||
v-model="mode"
|
||||
mandatory
|
||||
divided
|
||||
density="comfortable"
|
||||
>
|
||||
<v-btn value="local">{{ tm("skills.modeLocal") }}</v-btn>
|
||||
<v-btn value="neo" :disabled="!neoEnabled">{{
|
||||
tm("skills.modeNeo")
|
||||
}}</v-btn>
|
||||
<v-btn value="neo">{{ tm("skills.modeNeo") }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-row>
|
||||
|
||||
<div v-if="mode === 'local'" class="px-2 pb-2 d-flex flex-column ga-2">
|
||||
<small style="color: grey">{{ tm("skills.runtimeHint") }}</small>
|
||||
<v-alert
|
||||
v-if="runtime === 'sandbox' && !sandboxCache.ready"
|
||||
type="info"
|
||||
@@ -67,67 +52,89 @@
|
||||
<small class="text-grey">{{ tm("skills.emptyHint") }}</small>
|
||||
</div>
|
||||
|
||||
<v-row v-else align="stretch">
|
||||
<v-col
|
||||
<div v-else class="skills-list pb-3">
|
||||
<OutlinedActionListItem
|
||||
v-for="skill in skills"
|
||||
:key="skill.name"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
class="d-flex"
|
||||
:title="skill.name"
|
||||
>
|
||||
<item-card
|
||||
:item="skill"
|
||||
title-field="name"
|
||||
enabled-field="active"
|
||||
:loading="itemLoading[skill.name] || false"
|
||||
:show-edit-button="false"
|
||||
:disable-toggle="isSandboxPresetSkill(skill)"
|
||||
:disable-delete="isSandboxPresetSkill(skill)"
|
||||
@toggle-enabled="toggleSkill"
|
||||
@delete="confirmDelete"
|
||||
>
|
||||
<template #item-details="{ item }">
|
||||
<div class="d-flex align-center mb-2 ga-2 flex-wrap">
|
||||
<v-chip
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
:color="sourceTypeColor(item.source_type)"
|
||||
>
|
||||
{{ sourceTypeLabel(item.source_type) }}
|
||||
</v-chip>
|
||||
<div
|
||||
class="text-caption text-medium-emphasis skill-description"
|
||||
>
|
||||
<v-icon size="small" class="me-1">mdi-text</v-icon>
|
||||
{{ item.description || tm("skills.noDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis skill-path">
|
||||
<v-icon size="small" class="me-1">mdi-file-document</v-icon>
|
||||
{{ tm("skills.path") }}: {{ item.path }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="small"
|
||||
rounded="xl"
|
||||
:disabled="
|
||||
itemLoading[item.name] ||
|
||||
false ||
|
||||
isSandboxPresetSkill(item)
|
||||
"
|
||||
@click="downloadSkill(item)"
|
||||
>
|
||||
{{ tm("skills.download") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template #title-extra>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
:color="sourceTypeColor(skill.source_type)"
|
||||
>
|
||||
{{ sourceTypeLabel(skill.source_type) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<div class="skill-description text-body-2 text-medium-emphasis">
|
||||
{{ skill.description || tm("skills.noDescription") }}
|
||||
</div>
|
||||
|
||||
<div class="skill-path text-caption text-medium-emphasis">
|
||||
<v-icon size="small" class="me-1">mdi-file-document</v-icon>
|
||||
{{ tm("skills.path") }}: {{ skill.path }}
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<v-tooltip :text="tm('skills.download')" location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon="mdi-download-outline"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="list-action-icon-btn"
|
||||
:disabled="
|
||||
itemLoading[skill.name] ||
|
||||
false ||
|
||||
isSandboxPresetSkill(skill)
|
||||
"
|
||||
@click="downloadSkill(skill)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip :text="t('core.common.itemCard.delete')" location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon="mdi-delete-outline"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="list-action-icon-btn"
|
||||
:disabled="itemLoading[skill.name] || isSandboxPresetSkill(skill)"
|
||||
@click="confirmDelete(skill)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<template #control>
|
||||
<v-tooltip location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-switch
|
||||
v-bind="props"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
inset
|
||||
:model-value="skill.active"
|
||||
:loading="itemLoading[skill.name] || false"
|
||||
:disabled="itemLoading[skill.name] || isSandboxPresetSkill(skill)"
|
||||
@update:model-value="toggleSkill(skill)"
|
||||
/>
|
||||
</template>
|
||||
<span>{{
|
||||
skill.active
|
||||
? t("core.common.itemCard.enabled")
|
||||
: t("core.common.itemCard.disabled")
|
||||
}}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</OutlinedActionListItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="mode === 'neo' && neoEnabled">
|
||||
@@ -141,14 +148,6 @@
|
||||
{{ tm("skills.neoFilterHint") }}
|
||||
</div>
|
||||
</div>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
variant="flat"
|
||||
@click="fetchNeoData"
|
||||
>
|
||||
{{ tm("skills.refresh") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-row class="ga-md-0 ga-2">
|
||||
@@ -339,6 +338,39 @@
|
||||
</template>
|
||||
</v-container>
|
||||
|
||||
<div class="skills-fab-stack">
|
||||
<v-tooltip :text="tm('skills.refresh')" location="left">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
color="darkprimary"
|
||||
icon="mdi-refresh"
|
||||
size="x-large"
|
||||
variant="elevated"
|
||||
class="skills-fab"
|
||||
@click="refreshCurrentMode"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="mode === 'local'"
|
||||
:text="tm('skills.upload')"
|
||||
location="left"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
color="darkprimary"
|
||||
icon="mdi-upload"
|
||||
size="x-large"
|
||||
variant="elevated"
|
||||
class="skills-fab"
|
||||
@click="openUploadDialog"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="uploadDialog" max-width="880px" :persistent="uploading">
|
||||
<v-card class="skills-upload-dialog">
|
||||
<v-card-title class="skills-upload-dialog__header px-6 pt-6 pb-2">
|
||||
@@ -589,8 +621,8 @@
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import ItemCard from "@/components/shared/ItemCard.vue";
|
||||
import { useI18n, useModuleI18n } from "@/i18n/composables";
|
||||
import OutlinedActionListItem from "@/components/shared/OutlinedActionListItem.vue";
|
||||
|
||||
const STATUS_WAITING = "waiting";
|
||||
const STATUS_UPLOADING = "uploading";
|
||||
@@ -600,7 +632,7 @@ const STATUS_SKIPPED = "skipped";
|
||||
|
||||
export default {
|
||||
name: "SkillsSection",
|
||||
components: { ItemCard },
|
||||
components: { OutlinedActionListItem },
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
@@ -1448,20 +1480,56 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.skills-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-action-icon-btn {
|
||||
color: rgba(var(--v-theme-on-surface), 0.78);
|
||||
}
|
||||
|
||||
.list-action-icon-btn:hover {
|
||||
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.skills-fab-stack {
|
||||
align-items: center;
|
||||
bottom: 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: fixed;
|
||||
right: 52px;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.skills-fab {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.skills-fab:hover {
|
||||
box-shadow: 0 12px 20px rgba(var(--v-theme-primary), 0.4);
|
||||
transform: translateY(-4px) scale(1.05);
|
||||
}
|
||||
|
||||
.skill-description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.skill-path {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
min-height: 40px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
|
||||
116
dashboard/src/components/shared/OutlinedActionListItem.vue
Normal file
116
dashboard/src/components/shared/OutlinedActionListItem.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<v-card class="outlined-action-list-item rounded-lg" variant="outlined">
|
||||
<div class="outlined-action-list-item__main">
|
||||
<div class="outlined-action-list-item__content">
|
||||
<div class="outlined-action-list-item__header">
|
||||
<div class="outlined-action-list-item__title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<slot name="title-extra"></slot>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="$slots.actions || $slots.control"
|
||||
class="outlined-action-list-item__actions"
|
||||
>
|
||||
<div
|
||||
v-if="$slots.actions"
|
||||
class="outlined-action-list-item__hover-actions"
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
|
||||
<slot name="control"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.outlined-action-list-item {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
.outlined-action-list-item__main {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
min-height: 104px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.outlined-action-list-item__content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.outlined-action-list-item__header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.outlined-action-list-item__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.outlined-action-list-item__actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.outlined-action-list-item__hover-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.outlined-action-list-item:hover .outlined-action-list-item__hover-actions,
|
||||
.outlined-action-list-item:focus-within .outlined-action-list-item__hover-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.outlined-action-list-item__main {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.outlined-action-list-item__actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.outlined-action-list-item__hover-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"mcpServers": {
|
||||
"title": "MCP Servers",
|
||||
"description": "Manage MCP servers",
|
||||
"buttons": {
|
||||
"refresh": "Refresh",
|
||||
"add": "Add Server",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"mcpServers": {
|
||||
"title": "MCP Сервера",
|
||||
"description": "Управление MCP-серверами",
|
||||
"buttons": {
|
||||
"refresh": "Обновить",
|
||||
"add": "Добавить сервер",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"mcpServers": {
|
||||
"title": "MCP 服务器",
|
||||
"description": "管理 MCP 服务器",
|
||||
"buttons": {
|
||||
"refresh": "刷新",
|
||||
"add": "新增服务器",
|
||||
|
||||
@@ -237,8 +237,11 @@ const selectedMarketPlugin = computed(() => {
|
||||
<!-- 已安装的 MCP 服务器标签页内容 -->
|
||||
<v-tab-item v-if="activeTab === 'mcp'">
|
||||
<div class="mb-4 pt-4 pb-4">
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 12px">
|
||||
<div class="d-flex flex-column" style="gap: 6px">
|
||||
<h2 class="text-h2 mb-0">{{ tm("tabs.installedMcpServers") }}</h2>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ t("features.tooluse.mcpServers.description") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-card
|
||||
@@ -255,8 +258,11 @@ const selectedMarketPlugin = computed(() => {
|
||||
<!-- Skills 标签页内容 -->
|
||||
<v-tab-item v-if="activeTab === 'skills'">
|
||||
<div class="mb-4 pt-4 pb-4">
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 12px">
|
||||
<div class="d-flex flex-column" style="gap: 6px">
|
||||
<h2 class="text-h2 mb-0">{{ tm("tabs.skills") }}</h2>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ tm("skills.runtimeHint") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-card
|
||||
|
||||
Reference in New Issue
Block a user