You've already forked iFlow-Settings-Editor-GUI
优化 MCP服务器添加交互为侧边面板一步操作,优化全局滚动条样式
This commit is contained in:
382
src/App.vue
382
src/App.vue
@@ -221,42 +221,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentServer" class="card">
|
|
||||||
<div class="card-title">
|
|
||||||
<Edit size="16" />
|
|
||||||
编辑服务器
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">名称</label>
|
|
||||||
<input type="text" class="form-input" id="serverName" :value="currentServerName" readonly />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">描述</label>
|
|
||||||
<input type="text" class="form-input" id="serverDescription" :value="currentServer.description || ''" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">命令</label>
|
|
||||||
<input type="text" class="form-input" id="serverCommand" :value="currentServer.command || ''" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">工作目录</label>
|
|
||||||
<input type="text" class="form-input" id="serverCwd" :value="currentServer.cwd || '.'" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">参数 (每行一个)</label>
|
|
||||||
<textarea class="form-textarea" id="serverArgs" rows="4">{{ serverArgsText }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">环境变量 (JSON 格式)</label>
|
|
||||||
<textarea class="form-textarea" id="serverEnv" rows="3">{{ serverEnvText }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 16px">
|
|
||||||
<button class="btn btn-danger" @click="deleteServer">
|
|
||||||
<Delete size="12" />
|
|
||||||
删除服务器
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -270,7 +234,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Input Dialog -->
|
<!-- Input Dialog -->
|
||||||
<div v-if="showInputDialog.show" class="dialog-overlay" @click.self="closeInputDialog(false)">
|
<div v-if="showInputDialog.show" class="dialog-overlay dialog-overlay-top" @click.self="closeInputDialog(false)">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<div class="dialog-title">{{ showInputDialog.title }}</div>
|
<div class="dialog-title">{{ showInputDialog.title }}</div>
|
||||||
<div v-if="showInputDialog.isConfirm" class="dialog-confirm-text">{{ showInputDialog.placeholder }}</div>
|
<div v-if="showInputDialog.isConfirm" class="dialog-confirm-text">{{ showInputDialog.placeholder }}</div>
|
||||||
@@ -281,11 +245,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Side Panel -->
|
||||||
|
<div v-if="showServerPanel" class="side-panel-overlay" @click.self="closeServerPanel" @keyup.esc="closeServerPanel" tabindex="-1" ref="serverPanelOverlay">
|
||||||
|
<div class="side-panel" @click.stop>
|
||||||
|
<div class="side-panel-header">
|
||||||
|
<div class="side-panel-title">
|
||||||
|
<Server size="18" />
|
||||||
|
{{ isEditingServer ? '编辑服务器' : '添加服务器' }}
|
||||||
|
</div>
|
||||||
|
<button class="side-panel-close" @click="closeServerPanel">
|
||||||
|
<svg viewBox="0 0 10 10">
|
||||||
|
<line x1="0" y1="0" x2="10" y2="10" />
|
||||||
|
<line x1="10" y1="0" x2="0" y2="10" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="side-panel-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">服务器名称 <span class="form-required">*</span></label>
|
||||||
|
<input type="text" class="form-input" v-model="editingServerData.name" placeholder="my-mcp-server" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">描述</label>
|
||||||
|
<input type="text" class="form-input" v-model="editingServerData.description" placeholder="服务器描述信息" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">命令 <span class="form-required">*</span></label>
|
||||||
|
<input type="text" class="form-input" v-model="editingServerData.command" placeholder="npx" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">工作目录</label>
|
||||||
|
<input type="text" class="form-input" v-model="editingServerData.cwd" placeholder="." />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">参数 (每行一个)</label>
|
||||||
|
<textarea class="form-textarea" v-model="editingServerData.args" rows="4" placeholder="-y package-name"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">环境变量 (JSON 格式)</label>
|
||||||
|
<textarea class="form-textarea" v-model="editingServerData.env" rows="3" placeholder='{"API_KEY": "xxx"}'></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-panel-footer">
|
||||||
|
<button v-if="isEditingServer" class="btn btn-danger" @click="deleteServer">
|
||||||
|
<Delete size="14" />
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
<div class="side-panel-footer-right">
|
||||||
|
<button class="btn btn-secondary" @click="closeServerPanel">取消</button>
|
||||||
|
<button class="btn btn-primary" @click="saveServerFromPanel">
|
||||||
|
<Save size="14" />
|
||||||
|
{{ isEditingServer ? '保存更改' : '添加服务器' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { Refresh, Save, Config, Key, Server, Globe, Setting, Robot, Search, Add, Edit, Delete, Exchange } from '@icon-park/vue-next'
|
import { Refresh, Save, Config, Key, Server, Globe, Setting, Robot, Search, Add, Edit, Delete, Exchange } from '@icon-park/vue-next'
|
||||||
|
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
@@ -313,6 +334,16 @@ const apiProfiles = ref([])
|
|||||||
const currentApiProfile = ref('default')
|
const currentApiProfile = ref('default')
|
||||||
const showInputDialog = ref({ show: false, title: '', placeholder: '', callback: null })
|
const showInputDialog = ref({ show: false, title: '', placeholder: '', callback: null })
|
||||||
const inputDialogValue = ref('')
|
const inputDialogValue = ref('')
|
||||||
|
const showServerPanel = ref(false)
|
||||||
|
const isEditingServer = ref(false)
|
||||||
|
const editingServerData = ref({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
command: 'npx',
|
||||||
|
cwd: '.',
|
||||||
|
args: '',
|
||||||
|
env: ''
|
||||||
|
})
|
||||||
|
|
||||||
// Load API profiles list
|
// Load API profiles list
|
||||||
const loadApiProfiles = async () => {
|
const loadApiProfiles = async () => {
|
||||||
@@ -419,7 +450,6 @@ const loadSettings = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
collectServerData()
|
|
||||||
const dataToSave = JSON.parse(JSON.stringify(settings.value))
|
const dataToSave = JSON.parse(JSON.stringify(settings.value))
|
||||||
const result = await window.electronAPI.saveSettings(dataToSave)
|
const result = await window.electronAPI.saveSettings(dataToSave)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -458,59 +488,101 @@ const serverCount = computed(() => (settings.value.mcpServers ? Object.keys(sett
|
|||||||
|
|
||||||
const selectServer = name => {
|
const selectServer = name => {
|
||||||
currentServerName.value = name
|
currentServerName.value = name
|
||||||
|
openEditServerPanel(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverPanelOverlay = ref(null)
|
||||||
|
|
||||||
|
const openAddServerPanel = () => {
|
||||||
|
isEditingServer.value = false
|
||||||
|
editingServerData.value = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
command: 'npx',
|
||||||
|
cwd: '.',
|
||||||
|
args: '-y\npackage-name',
|
||||||
|
env: ''
|
||||||
|
}
|
||||||
|
showServerPanel.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
serverPanelOverlay.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditServerPanel = (name) => {
|
||||||
|
const server = settings.value.mcpServers[name]
|
||||||
|
if (!server) return
|
||||||
|
isEditingServer.value = true
|
||||||
|
editingServerData.value = {
|
||||||
|
name: name,
|
||||||
|
description: server.description || '',
|
||||||
|
command: server.command || '',
|
||||||
|
cwd: server.cwd || '.',
|
||||||
|
args: (server.args || []).join('\n'),
|
||||||
|
env: server.env ? JSON.stringify(server.env, null, 2) : ''
|
||||||
|
}
|
||||||
|
showServerPanel.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
serverPanelOverlay.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeServerPanel = () => {
|
||||||
|
showServerPanel.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveServerFromPanel = async () => {
|
||||||
|
const name = editingServerData.value.name.trim()
|
||||||
|
if (!name) {
|
||||||
|
await window.electronAPI.showMessage({ type: 'warning', title: '错误', message: '请输入服务器名称' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isEditingServer.value && settings.value.mcpServers[name]) {
|
||||||
|
await window.electronAPI.showMessage({ type: 'warning', title: '错误', message: '服务器名称已存在' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是编辑模式且名称改变了,需要删除旧的服务器
|
||||||
|
if (isEditingServer.value && currentServerName.value && currentServerName.value !== name) {
|
||||||
|
delete settings.value.mcpServers[currentServerName.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverConfig = {
|
||||||
|
command: editingServerData.value.command.trim(),
|
||||||
|
description: editingServerData.value.description.trim(),
|
||||||
|
cwd: editingServerData.value.cwd.trim() || '.',
|
||||||
|
args: editingServerData.value.args.split('\n').map(s => s.trim()).filter(s => s)
|
||||||
|
}
|
||||||
|
|
||||||
|
const envText = editingServerData.value.env.trim()
|
||||||
|
if (envText) {
|
||||||
|
try {
|
||||||
|
serverConfig.env = JSON.parse(envText)
|
||||||
|
} catch (e) {
|
||||||
|
await window.electronAPI.showMessage({ type: 'error', title: '错误', message: '环境变量 JSON 格式错误' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.value.mcpServers[name] = serverConfig
|
||||||
|
currentServerName.value = name
|
||||||
|
showServerPanel.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const addServer = async () => {
|
const addServer = async () => {
|
||||||
const name = await new Promise(resolve => {
|
openAddServerPanel()
|
||||||
showInputDialog.value = { show: true, title: '添加服务器', placeholder: '请输入服务器名称', callback: resolve }
|
|
||||||
})
|
|
||||||
if (!name) return
|
|
||||||
if (!settings.value.mcpServers) settings.value.mcpServers = {}
|
|
||||||
if (settings.value.mcpServers[name]) {
|
|
||||||
await window.electronAPI.showMessage({ type: 'warning', title: '错误', message: '服务器已存在' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
settings.value.mcpServers[name] = { command: 'npx', args: ['-y', 'package-name'] }
|
|
||||||
currentServerName.value = name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteServer = async () => {
|
const deleteServer = async () => {
|
||||||
if (!currentServerName.value) return
|
const serverName = isEditingServer.value ? editingServerData.value.name : currentServerName.value
|
||||||
|
if (!serverName) return
|
||||||
const confirmed = await new Promise(resolve => {
|
const confirmed = await new Promise(resolve => {
|
||||||
showInputDialog.value = { show: true, title: '删除服务器', placeholder: `确定要删除服务器 "${currentServerName.value}" 吗?`, callback: resolve, isConfirm: true }
|
showInputDialog.value = { show: true, title: '删除服务器', placeholder: `确定要删除服务器 "${serverName}" 吗?`, callback: resolve, isConfirm: true }
|
||||||
})
|
})
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
delete settings.value.mcpServers[currentServerName.value]
|
delete settings.value.mcpServers[serverName]
|
||||||
currentServerName.value = null
|
currentServerName.value = null
|
||||||
}
|
showServerPanel.value = false
|
||||||
|
|
||||||
const collectServerData = () => {
|
|
||||||
if (!currentServerName.value) return
|
|
||||||
const el = document.getElementById('serverName')
|
|
||||||
if (el) {
|
|
||||||
const name = el.value.trim()
|
|
||||||
if (name !== currentServerName.value) {
|
|
||||||
delete settings.value.mcpServers[currentServerName.value]
|
|
||||||
currentServerName.value = name
|
|
||||||
}
|
|
||||||
settings.value.mcpServers[name] = {
|
|
||||||
command: document.getElementById('serverCommand')?.value || '',
|
|
||||||
description: document.getElementById('serverDescription')?.value || '',
|
|
||||||
cwd: document.getElementById('serverCwd')?.value || '.',
|
|
||||||
args: (document.getElementById('serverArgs')?.value || '')
|
|
||||||
.split('\n')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(s => s),
|
|
||||||
}
|
|
||||||
const envText = document.getElementById('serverEnv')?.value || ''
|
|
||||||
if (envText) {
|
|
||||||
try {
|
|
||||||
settings.value.mcpServers[name].env = JSON.parse(envText)
|
|
||||||
} catch (e) {
|
|
||||||
alert('环境变量 JSON 格式错误')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentServer = computed(() => {
|
const currentServer = computed(() => {
|
||||||
@@ -518,16 +590,6 @@ const currentServer = computed(() => {
|
|||||||
return settings.value.mcpServers[currentServerName.value]
|
return settings.value.mcpServers[currentServerName.value]
|
||||||
})
|
})
|
||||||
|
|
||||||
const serverArgsText = computed(() => {
|
|
||||||
if (!currentServer.value) return ''
|
|
||||||
return (currentServer.value.args || []).join('\n')
|
|
||||||
})
|
|
||||||
|
|
||||||
const serverEnvText = computed(() => {
|
|
||||||
if (!currentServer.value || !currentServer.value.env) return ''
|
|
||||||
return JSON.stringify(currentServer.value.env, null, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
const minimize = () => window.electronAPI.minimize()
|
const minimize = () => window.electronAPI.minimize()
|
||||||
const maximize = () => window.electronAPI.maximize()
|
const maximize = () => window.electronAPI.maximize()
|
||||||
const close = () => window.electronAPI.close()
|
const close = () => window.electronAPI.close()
|
||||||
@@ -629,6 +691,32 @@ body {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styles */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1256,6 +1344,9 @@ body {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
|
.dialog-overlay-top {
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
.iconpark-icon {
|
.iconpark-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1267,4 +1358,111 @@ body {
|
|||||||
.iconpark-icon svg {
|
.iconpark-icon svg {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Side Panel */
|
||||||
|
.side-panel-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
.side-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 420px;
|
||||||
|
max-width: 100%;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: slideInRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.side-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
.side-panel-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.side-panel-title .iconpark-icon {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.side-panel-close {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.side-panel-close:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.side-panel-close svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
.side-panel-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.side-panel-body .form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.side-panel-body .form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.form-required {
|
||||||
|
color: var(--danger);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.side-panel-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
.side-panel-footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user