This commit is contained in:
2026-06-15 11:13:25 +08:00
parent c6007c710d
commit cfb75c30cf
+42 -106
View File
@@ -33,14 +33,28 @@ const emit = defineEmits<{
(e: 'cancel'): void (e: 'cancel'): void
}>() }>()
// v-model 双向同步,但必须打破「W1 回填 → local 变 → W2 emit → 父 modelValue 变 → W1 再回填」
// 的无限循环(曾导致表单任意输入/NSelect 选择即卡死浏览器)。
// 关键:W2 emit 出去的就是 local 本身(同一引用),父组件直接持有该引用。当 W1 看到的
// modelValue 与我们最近一次 emit 出去的值是同一个对象时,说明是回环而非真正的外部变更,
// 必须忽略;否则(如编辑模式下父组件 onMounted 用全新对象回填)才同步进 local。
const local = ref({ ...props.modelValue }) const local = ref({ ...props.modelValue })
let lastEmitted: typeof props.modelValue | undefined
watch( watch(
() => props.modelValue, () => props.modelValue,
(v) => { (v) => {
if (v === lastEmitted) return
local.value = { ...v } local.value = { ...v }
}, },
) )
watch(local, (v) => emit('update:modelValue', v), { deep: true }) watch(
local,
(v) => {
lastEmitted = v
emit('update:modelValue', v)
},
{ deep: true },
)
const argsText = computed({ const argsText = computed({
get: () => local.value.args.join('\n'), get: () => local.value.args.join('\n'),
@@ -52,7 +66,10 @@ const argsText = computed({
const allowlistText = computed({ const allowlistText = computed({
get: () => (local.value.tool_allowlist ?? []).join('\n'), get: () => (local.value.tool_allowlist ?? []).join('\n'),
set: (v: string) => { set: (v: string) => {
local.value.tool_allowlist = v.split('\n').map((x) => x.trim()).filter((x) => x !== '') local.value.tool_allowlist = v
.split('\n')
.map((x) => x.trim())
.filter((x) => x !== '')
}, },
}) })
@@ -192,30 +209,15 @@ function removeMcpServer(idx: number) {
</script> </script>
<template> <template>
<form <form class="space-y-4" @submit.prevent="emit('submit')">
class="space-y-4"
@submit.prevent="emit('submit')"
>
<div> <div>
<label class="block text-sm font-medium mb-1">Name</label> <label class="block text-sm font-medium mb-1">Name</label>
<NInput <NInput v-model:value="local.name" :readonly="isEdit" placeholder="claude_code" />
v-model:value="local.name" <p v-if="isEdit" class="text-xs text-gray-500 mt-1">Name cannot be changed after creation</p>
:readonly="isEdit"
placeholder="claude_code"
/>
<p
v-if="isEdit"
class="text-xs text-gray-500 mt-1"
>
Name cannot be changed after creation
</p>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1">Display Name</label> <label class="block text-sm font-medium mb-1">Display Name</label>
<NInput <NInput v-model:value="local.display_name" placeholder="Claude Code" />
v-model:value="local.display_name"
placeholder="Claude Code"
/>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1">Description</label> <label class="block text-sm font-medium mb-1">Description</label>
@@ -227,10 +229,7 @@ function removeMcpServer(idx: number) {
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1">客户端类型</label> <label class="block text-sm font-medium mb-1">客户端类型</label>
<NSelect <NSelect v-model:value="local.client_type" :options="clientTypeOptions" />
v-model:value="local.client_type"
:options="clientTypeOptions"
/>
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
决定 session 启动时注入的配置目录变量与下方配置文件表单 决定 session 启动时注入的配置目录变量与下方配置文件表单
</p> </p>
@@ -253,23 +252,12 @@ function removeMcpServer(idx: number) {
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1">Env</label> <label class="block text-sm font-medium mb-1">Env</label>
<p <p v-if="isEdit && existingEnvKeys?.length" class="text-xs text-gray-500 mb-2">
v-if="isEdit && existingEnvKeys?.length" Existing keys (values hidden): {{ existingEnvKeys.join(', ') }}. Edit below to replace
class="text-xs text-gray-500 mb-2" entirely.
>
Existing keys (values hidden): {{ existingEnvKeys.join(', ') }}.
Edit below to replace entirely.
</p> </p>
<div <div v-for="(e, idx) in envEntries" :key="e.id" class="flex gap-2 mb-2">
v-for="(e, idx) in envEntries" <NInput v-model:value="e.k" placeholder="KEY" class="flex-1" />
:key="e.id"
class="flex gap-2 mb-2"
>
<NInput
v-model:value="e.k"
placeholder="KEY"
class="flex-1"
/>
<NInput <NInput
v-model:value="e.v" v-model:value="e.v"
type="password" type="password"
@@ -277,20 +265,9 @@ function removeMcpServer(idx: number) {
placeholder="value" placeholder="value"
class="flex-1" class="flex-1"
/> />
<NButton <NButton quaternary @click="removeEnv(idx)"> × </NButton>
quaternary
@click="removeEnv(idx)"
>
×
</NButton>
</div> </div>
<NButton <NButton size="small" dashed @click="addEnv"> + Add </NButton>
size="small"
dashed
@click="addEnv"
>
+ Add
</NButton>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1"> <label class="block text-sm font-medium mb-1">
@@ -307,34 +284,14 @@ function removeMcpServer(idx: number) {
<label class="block text-sm font-medium mb-1"> <label class="block text-sm font-medium mb-1">
第三方 MCP Serverssession 启动时随平台 MCP 一并下发名称 acw 为平台保留 第三方 MCP Serverssession 启动时随平台 MCP 一并下发名称 acw 为平台保留
</label> </label>
<div <div v-for="(m, idx) in mcpRows" :key="m.id" class="border rounded p-3 mb-2 space-y-2">
v-for="(m, idx) in mcpRows"
:key="m.id"
class="border rounded p-3 mb-2 space-y-2"
>
<div class="flex gap-2"> <div class="flex gap-2">
<NInput <NInput v-model:value="m.name" placeholder="名称(如 context7)" class="flex-1" />
v-model:value="m.name" <NSelect v-model:value="m.type" :options="mcpTypeOptions" class="w-28" />
placeholder="名称(如 context7)" <NButton quaternary @click="removeMcpServer(idx)"> × </NButton>
class="flex-1"
/>
<NSelect
v-model:value="m.type"
:options="mcpTypeOptions"
class="w-28"
/>
<NButton
quaternary
@click="removeMcpServer(idx)"
>
×
</NButton>
</div> </div>
<template v-if="m.type === 'stdio'"> <template v-if="m.type === 'stdio'">
<NInput <NInput v-model:value="m.command" placeholder="command(如 npx)" />
v-model:value="m.command"
placeholder="command(如 npx)"
/>
<NInput <NInput
v-model:value="m.argsText" v-model:value="m.argsText"
type="textarea" type="textarea"
@@ -349,47 +306,26 @@ function removeMcpServer(idx: number) {
/> />
</template> </template>
<template v-else> <template v-else>
<NInput <NInput v-model:value="m.url" placeholder="URL(如 https://mcp.example.com/mcp)" />
v-model:value="m.url"
placeholder="URL(如 https://mcp.example.com/mcp)"
/>
<NInput <NInput
v-model:value="m.headersText" v-model:value="m.headersText"
type="textarea" type="textarea"
:autosize="{ minRows: 1, maxRows: 5 }" :autosize="{ minRows: 1, maxRows: 5 }"
placeholder="headers(每行 KEY=VALUE,如 Authorization=Bearer xxx)" placeholder="headers(每行 KEY=VALUE,如 Authorization=Bearer xxx)"
/> />
<p <p v-if="m.type === 'sse'" class="text-xs text-gray-500">
v-if="m.type === 'sse'"
class="text-xs text-gray-500"
>
注意codex 不支持 sse 类型会在启动时被跳过 注意codex 不支持 sse 类型会在启动时被跳过
</p> </p>
</template> </template>
</div> </div>
<NButton <NButton size="small" dashed @click="addMcpServer"> + 添加 MCP Server </NButton>
size="small"
dashed
@click="addMcpServer"
>
+ 添加 MCP Server
</NButton>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<NCheckbox v-model:checked="local.enabled"> <NCheckbox v-model:checked="local.enabled"> Enabled </NCheckbox>
Enabled
</NCheckbox>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<NButton <NButton type="primary" attr-type="submit"> Save </NButton>
type="primary" <NButton @click="emit('cancel')"> Cancel </NButton>
attr-type="submit"
>
Save
</NButton>
<NButton @click="emit('cancel')">
Cancel
</NButton>
</div> </div>
</form> </form>
</template> </template>