Files
openteam/web/src/views/admin/ChannelModelsDrawer.vue
T
SakurasanandClaude 4137b6fe20 渠道: 远程候选按渠道过滤 + key 掩码 + 移除批量导入端点
- 远程模型候选只排除本渠道已允许的模型,同名模型可被多个渠道各自允许
- 渠道 key 掩码统一 xxxxxxx******Mq4Y(保留前 7 位与后 4 位)
- 移除已弃用的批量导入端点 POST /channels/:id/models/import

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-16 18:55:05 +08:00

179 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { PhArrowsClockwise, PhPlus, PhX } from '@phosphor-icons/vue'
import { http, errMsg } from '@/api/client'
import { useToastStore } from '@/stores/toast'
import Button from '@/components/ui/Button.vue'
import type { Channel, ChannelModelMapping } from '@/types'
const props = defineProps<{ channel: Channel }>()
const toast = useToastStore()
const mappings = ref<ChannelModelMapping[]>([])
const remote = ref<string[]>([])
const selected = ref<string[]>([])
const loading = ref(false)
const fetched = ref(false)
const addForm = reactive({ custom_name: '', upstream_model: '' })
async function load() {
try {
const { data } = await http.get(`/admin/channels/${props.channel.id}/models`)
mappings.value = data.data.items
} catch (e) {
toast.err(errMsg(e))
}
}
async function fetchRemote() {
loading.value = true
try {
const { data } = await http.get(`/admin/channels/${props.channel.id}/models/remote`)
remote.value = data.data.items
selected.value = []
fetched.value = true
} catch (e) {
toast.err(errMsg(e))
} finally {
loading.value = false
}
}
async function addSelected() {
let added = 0
for (const name of selected.value) {
try {
await http.post(`/admin/channels/${props.channel.id}/models`, { upstream_model: name })
added++
} catch {
/* 单个失败不中断 */
}
}
selected.value = []
toast.ok(added ? `已添加 ${added} 个模型` : '所选均已添加')
await load()
// 已添加的模型已被渠道允许,从拉取候选中移除
await fetchRemote()
}
async function addManual() {
if (!addForm.upstream_model.trim()) return
try {
await http.post(`/admin/channels/${props.channel.id}/models`, {
upstream_model: addForm.upstream_model.trim(),
custom_name: addForm.custom_name.trim(),
})
toast.ok('已添加')
addForm.custom_name = ''
addForm.upstream_model = ''
await load()
} catch (e) {
toast.err(errMsg(e))
}
}
async function saveUpstream(b: ChannelModelMapping) {
try {
await http.patch(`/admin/channels/${props.channel.id}/models/${b.id}`, {
upstream_model: b.upstream_model,
})
toast.ok('已更新')
await load()
} catch (e) {
toast.err(errMsg(e))
}
}
async function remove(b: ChannelModelMapping) {
if (!confirm(`解除模型 ${b.model_name} 的绑定?`)) return
try {
await http.delete(`/admin/channels/${props.channel.id}/models/${b.id}`)
toast.ok('已解除')
await load()
} catch (e) {
toast.err(errMsg(e))
}
}
onMounted(load)
</script>
<template>
<div class="space-y-3">
<!-- 已允许的模型 -->
<div>
<p class="mb-1.5 text-xs font-medium text-muted">已允许的模型({{ mappings.length }})</p>
<div v-if="mappings.length" class="flex flex-wrap gap-2">
<div
v-for="b in mappings"
:key="b.id"
class="inline-flex items-center gap-1.5 rounded-md border border-edge bg-surface px-2 py-1 font-mono text-[11px] text-muted"
>
<span class="text-ink">{{ b.model_name }}</span>
<span class="opacity-60">→</span>
<input
v-model="b.upstream_model"
class="w-28 rounded border border-transparent bg-transparent px-1 text-[11px] text-accent outline-none transition focus:border-accent/50 focus:bg-surface2"
@change="saveUpstream(b)"
/>
<button class="text-muted hover:text-err" aria-label="解除" @click="remove(b)">
<PhX :size="12" />
</button>
</div>
</div>
<p v-else class="text-xs text-muted">尚未允许任何模型</p>
</div>
<!-- 从接口拉取 + 勾选 -->
<div class="border-t border-edge pt-3">
<div class="mb-1.5 flex items-center justify-between">
<p class="text-xs font-medium text-muted">从接口拉取模型</p>
<Button size="sm" variant="ghost" :loading="loading" @click="fetchRemote">
<PhArrowsClockwise :size="13" />
拉取
</Button>
</div>
<div v-if="remote.length" class="flex max-h-36 flex-wrap gap-2 overflow-y-auto">
<label
v-for="m in remote"
:key="m"
class="flex cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1 font-mono text-[11px] text-muted transition select-none"
:class="selected.includes(m) ? 'border-accent bg-accent-soft text-ink' : 'border-edge2 hover:border-edge'"
>
<input v-model="selected" type="checkbox" :value="m" class="size-3.5 accent-[var(--color-accent)]" />
{{ m }}
</label>
</div>
<div v-if="remote.length" class="mt-2">
<Button size="sm" @click="addSelected">
<PhPlus :size="13" />
添加所选({{ selected.length }})
</Button>
</div>
<p v-else-if="!loading" class="text-xs text-muted">
{{ remote.length === 0 && fetched ? '接口返回的模型均已允许,无新增候选' : '点「拉取」获取渠道接口返回的新模型,勾选需要的加入' }}
</p>
</div>
<!-- 手动添加 -->
<div class="flex items-center gap-2 border-t border-edge pt-3">
<input
v-model="addForm.custom_name"
placeholder="自定义名称(可选)"
class="h-8 min-w-0 flex-1 rounded-md border border-edge2 bg-surface px-2 font-mono text-xs outline-none focus:border-accent"
@keyup.enter="addManual"
/>
<input
v-model="addForm.upstream_model"
placeholder="上游模型名"
class="h-8 min-w-0 flex-1 rounded-md border border-edge2 bg-surface px-2 font-mono text-xs outline-none focus:border-accent"
@keyup.enter="addManual"
/>
<Button size="sm" class="shrink-0" @click="addManual">
<PhPlus :size="13" />
添加
</Button>
</div>
</div>
</template>