后端 - 新增 proxy/convert 三协议(chat/messages/responses)请求、响应与 SSE 流式互转, 以 Chat 为中间模型;usage.go 统一提取三协议 token 用量(含单测) - gateway: 跨协议调度(渠道未声明客户端协议时转为渠道首选格式), streamResponse 按 \n\n 分块逐行转换直通,bufferResponse 转换失败时剥非 JSON 前缀 - gateway: 新增 SetUsageRecorder 注入异步用量记录器 - auth_llm: 修复 key_prefix 查询长度错配([:8] vs 存储的 [:12])导致全部 401; 修复长度 8-11 的 key 切片越界 panic;统一 unauthorized 响应 - usage: 日报表改为增量累加 upsert,避免多次 flush 互相清零;记录协议/错误码/时延等字段 - channel: 新增渠道并发槽 TryAcquire;健康检查支持可配置参数 - api: 新增 admin 渠道/模型/系统配置管理端点(旧端点保留兼容) 前端 - 新增渠道管理、模型管理、系统配置视图与 ChannelModelsDrawer - 新增 ui 基础组件(Button/Badge/Input/Modal)与 protocol.ts - 调整 Toast 样式、密钥页、路由菜单;dev 代理默认指向 3000 端口
85 lines
2.5 KiB
Vue
85 lines
2.5 KiB
Vue
<script setup lang="ts">
|
|
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
import { X } from '@lucide/vue'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
open: boolean
|
|
title?: string
|
|
width?: string
|
|
}>(),
|
|
{ width: 'max-w-md' },
|
|
)
|
|
const emit = defineEmits<{ close: [] }>()
|
|
|
|
const panel = ref<HTMLElement | null>(null)
|
|
|
|
function onKey(e: KeyboardEvent) {
|
|
if (e.key === 'Escape' && props.open) emit('close')
|
|
}
|
|
onMounted(() => window.addEventListener('keydown', onKey))
|
|
onUnmounted(() => window.removeEventListener('keydown', onKey))
|
|
|
|
watch(
|
|
() => props.open,
|
|
async (open) => {
|
|
document.body.style.overflow = open ? 'hidden' : ''
|
|
if (open) {
|
|
await nextTick()
|
|
panel.value?.focus()
|
|
}
|
|
},
|
|
)
|
|
onUnmounted(() => {
|
|
document.body.style.overflow = ''
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<Transition
|
|
enter-active-class="transition-opacity duration-150"
|
|
enter-from-class="opacity-0"
|
|
leave-active-class="transition-opacity duration-150"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="open"
|
|
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/60 p-4 pt-[12vh] backdrop-blur-sm"
|
|
@mousedown.self="emit('close')"
|
|
>
|
|
<Transition
|
|
enter-active-class="transition-transform duration-150"
|
|
enter-from-class="scale-[0.97] opacity-0"
|
|
leave-active-class="transition-transform duration-150"
|
|
leave-to-class="scale-[0.97] opacity-0"
|
|
>
|
|
<div
|
|
v-if="open"
|
|
ref="panel"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
:aria-label="title || '对话框'"
|
|
tabindex="-1"
|
|
class="card w-full bg-base-100 shadow-xl outline-none"
|
|
:class="width"
|
|
>
|
|
<div class="flex items-center justify-between border-b border-base-300/60 px-5 py-3.5">
|
|
<h3 class="text-sm font-semibold text-base-content">{{ title }}</h3>
|
|
<button class="rounded-md p-1 text-base-content/40 hover:bg-base-200 hover:text-base-content" aria-label="关闭" @click="emit('close')">
|
|
<X :size="16" />
|
|
</button>
|
|
</div>
|
|
<div class="px-5 py-4">
|
|
<slot />
|
|
</div>
|
|
<div v-if="$slots.footer" class="flex justify-end gap-2 border-t border-base-300/60 px-5 py-3.5">
|
|
<slot name="footer" />
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|