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