mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix: refine frontend admin and auth UI
This commit is contained in:
@@ -58,6 +58,11 @@ ADMIN_USERNAME=admin
|
||||
# AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||
|
||||
# PostgreSQL 连接池配置(默认适合单实例/小型部署;高并发可按需调大)
|
||||
# 推荐计算方式(单实例):
|
||||
# MAX = CPU 核数 × 10(AI 网关偏 IO 等待,可激进些;纯 OLTP 用 × 4)
|
||||
# MIN = MAX × 0.2(保留常驻连接应对突发流量,避免冷启动握手开销)
|
||||
# 多实例部署时请按 实例数 × MAX 控制总和,PG 端 max_connections 至少为该总和 + 20 余量
|
||||
# AETHER_GATEWAY_DATA_POSTGRES_MIN_CONNECTIONS=1
|
||||
# AETHER_GATEWAY_DATA_POSTGRES_MAX_CONNECTIONS=20
|
||||
# AETHER_GATEWAY_DATA_POSTGRES_IDLE_TIMEOUT_MS=30000
|
||||
# AETHER_GATEWAY_DATA_POSTGRES_STATEMENT_CACHE_CAPACITY=100
|
||||
# AETHER_GATEWAY_DATA_POSTGRES_ACQUIRE_TIMEOUT_MS=3000
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
import { PopoverTrigger } from 'radix-vue'
|
||||
import { useAttrs } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
withDefaults(defineProps<{
|
||||
asChild?: boolean
|
||||
as?: string
|
||||
@@ -14,6 +10,10 @@ withDefaults(defineProps<{
|
||||
as: 'button',
|
||||
})
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const attrs = useAttrs()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -211,7 +211,6 @@
|
||||
两次输入的密码不一致
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- 登录链接 -->
|
||||
|
||||
@@ -17,6 +17,22 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: string
|
||||
siteKey: string
|
||||
action?: string
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
modelValue: '',
|
||||
action: undefined,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
error: [message: string]
|
||||
}>()
|
||||
|
||||
const TURNSTILE_SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
|
||||
|
||||
type TurnstileWidgetId = string
|
||||
@@ -46,22 +62,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: string
|
||||
siteKey: string
|
||||
action?: string
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
modelValue: '',
|
||||
action: undefined,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
error: [message: string]
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const widgetId = ref<TurnstileWidgetId | null>(null)
|
||||
const errorMessage = ref('')
|
||||
|
||||
@@ -28,7 +28,10 @@
|
||||
{{ manualGlobalModelMode ? '选择已有模型' : '手动添加' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="!manualGlobalModelMode" class="space-y-2">
|
||||
<div
|
||||
v-if="!manualGlobalModelMode"
|
||||
class="space-y-2"
|
||||
>
|
||||
<Select
|
||||
:model-value="form.global_model_id"
|
||||
:disabled="loadingGlobalModels"
|
||||
@@ -48,10 +51,16 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-else class="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3">
|
||||
<div
|
||||
v-else
|
||||
class="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="manual-global-model-name" class="text-xs">模型ID *</Label>
|
||||
<Label
|
||||
for="manual-global-model-name"
|
||||
class="text-xs"
|
||||
>模型ID *</Label>
|
||||
<Input
|
||||
id="manual-global-model-name"
|
||||
v-model="form.manual_global_model_name"
|
||||
@@ -60,7 +69,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="manual-global-model-display-name" class="text-xs">显示名称</Label>
|
||||
<Label
|
||||
for="manual-global-model-display-name"
|
||||
class="text-xs"
|
||||
>显示名称</Label>
|
||||
<Input
|
||||
id="manual-global-model-display-name"
|
||||
v-model="form.manual_global_model_display_name"
|
||||
@@ -79,7 +91,10 @@
|
||||
没有可选择的本地全局模型。可以切换到“手动添加”继续保存。
|
||||
</p>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="provider-model-name" class="text-xs">Provider 模型名 *</Label>
|
||||
<Label
|
||||
for="provider-model-name"
|
||||
class="text-xs"
|
||||
>Provider 模型名 *</Label>
|
||||
<Input
|
||||
id="provider-model-name"
|
||||
v-model="form.provider_model_name"
|
||||
|
||||
@@ -293,7 +293,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 桌面端表格视图 -->
|
||||
<Table :class="['hidden md:table table-fixed w-full', isAdmin ? 'min-w-[1120px]' : 'min-w-[960px]']">
|
||||
<Table
|
||||
class="hidden md:table table-fixed w-full"
|
||||
:class="[isAdmin ? 'min-w-[1120px]' : 'min-w-[960px]']"
|
||||
>
|
||||
<colgroup v-if="isAdmin">
|
||||
<col class="w-[8%]">
|
||||
<col class="w-[12%]">
|
||||
@@ -346,7 +349,8 @@
|
||||
密钥
|
||||
</TableHead>
|
||||
<SortableTableHead
|
||||
:class="['h-12 font-semibold', isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
||||
class="h-12 font-semibold"
|
||||
:class="[isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
||||
column-key="model"
|
||||
:sortable="false"
|
||||
:filter-active="filterModel !== '__all__'"
|
||||
@@ -383,7 +387,8 @@
|
||||
</template>
|
||||
</SortableTableHead>
|
||||
<SortableTableHead
|
||||
:class="['h-12 font-semibold', isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
||||
class="h-12 font-semibold"
|
||||
:class="[isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
||||
column-key="api_format"
|
||||
:sortable="false"
|
||||
:filter-active="filterApiFormat !== '__all__'"
|
||||
@@ -495,7 +500,8 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
:class="['font-medium py-4', isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
||||
class="font-medium py-4"
|
||||
:class="[isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
||||
:title="getModelTooltip(record)"
|
||||
>
|
||||
<div
|
||||
@@ -582,7 +588,8 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
:class="['py-4', isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
||||
class="py-4"
|
||||
:class="[isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
||||
:title="getApiFormatTooltip(record)"
|
||||
>
|
||||
<!-- 有格式转换或同族格式差异:两行显示 -->
|
||||
@@ -682,8 +689,8 @@
|
||||
</div>
|
||||
<div class="mt-0.5 grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] gap-x-1 text-xs leading-tight tabular-nums text-muted-foreground">
|
||||
<span
|
||||
class="justify-self-end whitespace-nowrap text-right"
|
||||
:class="[
|
||||
'justify-self-end whitespace-nowrap text-right',
|
||||
hasPositiveTokens(getRecordCacheReadTokens(record)) ? 'text-foreground/70' : ''
|
||||
]"
|
||||
>
|
||||
@@ -693,8 +700,8 @@
|
||||
/
|
||||
</span>
|
||||
<span
|
||||
class="justify-self-start whitespace-nowrap text-left"
|
||||
:class="[
|
||||
'justify-self-start whitespace-nowrap text-left',
|
||||
hasPositiveTokens(getRecordCacheCreationTokens(record)) ? 'text-foreground/70' : ''
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -25,11 +25,11 @@ describe('parseDateLike', () => {
|
||||
describe('datetime-local conversion', () => {
|
||||
it('formats RFC3339 instants for datetime-local inputs using local clock fields', () => {
|
||||
const date = new Date('2026-04-12T15:30:00Z')
|
||||
const expected = [
|
||||
const expected = `${[
|
||||
date.getFullYear(),
|
||||
padDatePart(date.getMonth() + 1),
|
||||
padDatePart(date.getDate()),
|
||||
].join('-') + `T${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
||||
].join('-') }T${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
||||
|
||||
expect(formatDateTimeLocalInput('2026-04-12T15:30:00Z')).toBe(expected)
|
||||
})
|
||||
|
||||
@@ -511,7 +511,7 @@
|
||||
<span>用户: {{ selectedTask.username }}</span>
|
||||
</template>
|
||||
<span class="opacity-40">|</span>
|
||||
<span>{{ displayTaskSource(selectedTask) }}</span>
|
||||
<span>{{ displayTaskSource(selectedTask) }}</span>
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div
|
||||
|
||||
@@ -41,12 +41,19 @@
|
||||
:class="selectedType === '__new__' ? 'bg-primary/10 text-primary' : 'text-foreground hover:bg-muted'"
|
||||
@click="selectNewConfig()"
|
||||
>
|
||||
<div class="w-7 h-7 rounded-md flex items-center justify-center text-xs font-semibold shrink-0"
|
||||
<div
|
||||
class="w-7 h-7 rounded-md flex items-center justify-center text-xs font-semibold shrink-0"
|
||||
:class="selectedType === '__new__' ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'"
|
||||
>+</div>
|
||||
>
|
||||
+
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 text-left">
|
||||
<div class="truncate font-medium text-sm">新配置</div>
|
||||
<div class="text-[10px] text-muted-foreground">未保存</div>
|
||||
<div class="truncate font-medium text-sm">
|
||||
新配置
|
||||
</div>
|
||||
<div class="text-[10px] text-muted-foreground">
|
||||
未保存
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -70,10 +77,12 @@
|
||||
src="https://cdn.linux.do/uploads/default/optimized/3X/9/d/9dd49731091ce8656243f3c2b6e5d5e5a7e3e3e3_2_32x32.png"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
@error="($event.target as HTMLImageElement).remove()"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 text-left">
|
||||
<div class="truncate font-medium text-sm">{{ item.display_name }}</div>
|
||||
<div class="truncate font-medium text-sm">
|
||||
{{ item.display_name }}
|
||||
</div>
|
||||
<div class="text-[10px] text-muted-foreground">
|
||||
{{ item.configured ? (item.is_enabled ? '已启用' : '已禁用') : '未配置' }}
|
||||
</div>
|
||||
@@ -96,262 +105,261 @@
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 配置表单 -->
|
||||
<CardSection
|
||||
v-if="selectedType"
|
||||
:title="selectedType === '__new__' ? '新建配置' : (selectedTypeMeta?.display_name || selectedType)"
|
||||
:description="selectedType === '__new__' ? '填写后点击保存' : (configs[selectedType]?.is_enabled ? '已启用' : '已禁用')"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
:disabled="saving || testing"
|
||||
@click="handleTest"
|
||||
>
|
||||
{{ testing ? '测试中...' : '测试' }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
:disabled="saving"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<CardSection
|
||||
v-if="selectedType"
|
||||
:title="selectedType === '__new__' ? '新建配置' : (selectedTypeMeta?.display_name || selectedType)"
|
||||
:description="selectedType === '__new__' ? '填写后点击保存' : (configs[selectedType]?.is_enabled ? '已启用' : '已禁用')"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
:disabled="saving || testing"
|
||||
@click="handleTest"
|
||||
<div class="space-y-6">
|
||||
<!-- 新建时的 Display Name -->
|
||||
<div
|
||||
v-if="selectedType === '__new__'"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
{{ testing ? '测试中...' : '测试' }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
:disabled="saving"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- 新建时的 Display Name -->
|
||||
<div
|
||||
v-if="selectedType === '__new__'"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">显示名称</Label>
|
||||
<Input
|
||||
v-model="form.new_display_name"
|
||||
class="mt-1"
|
||||
placeholder="例如:My OIDC Provider"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">配置标识</Label>
|
||||
<Input
|
||||
v-model="form.new_provider_type"
|
||||
class="mt-1"
|
||||
placeholder="custom_oidc_work"
|
||||
autocomplete="off"
|
||||
@blur="normalizeNewProviderType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 凭证配置 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client ID</Label>
|
||||
<Input
|
||||
v-model="form.client_id"
|
||||
class="mt-1"
|
||||
placeholder="client_id"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client Secret</Label>
|
||||
<Input
|
||||
v-model="form.client_secret"
|
||||
masked
|
||||
class="mt-1"
|
||||
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回调地址 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
||||
<Input
|
||||
v-model="form.redirect_uri"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">前端回调页</Label>
|
||||
<Input
|
||||
v-model="form.frontend_callback_url"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:5173/auth/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- custom_oidc 必填端点 -->
|
||||
<div
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/authorize"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/token"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/api/user"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级选项(折叠) -->
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
||||
高级选项
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Scopes</Label>
|
||||
<Label class="block text-sm font-medium">显示名称</Label>
|
||||
<Input
|
||||
v-model="form.scopes_input"
|
||||
v-model="form.new_display_name"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
||||
placeholder="例如:My OIDC Provider"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
空格/逗号分隔;留空使用默认值
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- linuxdo 的可选端点覆盖 -->
|
||||
<div
|
||||
v-if="!isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">配置标识</Label>
|
||||
<Input
|
||||
v-model="form.new_provider_type"
|
||||
class="mt-1"
|
||||
placeholder="custom_oidc_work"
|
||||
autocomplete="off"
|
||||
@blur="normalizeNewProviderType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 凭证配置 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client ID</Label>
|
||||
<Input
|
||||
v-model="form.client_id"
|
||||
class="mt-1"
|
||||
placeholder="client_id"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client Secret</Label>
|
||||
<Input
|
||||
v-model="form.client_secret"
|
||||
masked
|
||||
class="mt-1"
|
||||
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回调地址 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
||||
<Input
|
||||
v-model="form.redirect_uri"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">前端回调页</Label>
|
||||
<Input
|
||||
v-model="form.frontend_callback_url"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:5173/auth/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- custom_oidc 必填端点 -->
|
||||
<div
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/authorize"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/token"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/api/user"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级选项(折叠) -->
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
||||
高级选项
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
||||
<Textarea
|
||||
v-model="form.attribute_mapping_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
placeholder="{"id": "user_id", "username": "login"}"
|
||||
<Label class="block text-sm font-medium">Scopes</Label>
|
||||
<Input
|
||||
v-model="form.scopes_input"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">
|
||||
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
||||
</Label>
|
||||
<Textarea
|
||||
v-model="form.extra_config_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
:placeholder="extraConfigPlaceholder"
|
||||
/>
|
||||
<p
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="mt-1 text-xs text-muted-foreground"
|
||||
>
|
||||
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
空格/逗号分隔;留空使用默认值
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<div
|
||||
v-if="lastTestResult"
|
||||
class="mt-6 rounded-lg border border-border p-4 text-sm"
|
||||
>
|
||||
<div class="font-medium mb-2">
|
||||
测试结果
|
||||
<!-- linuxdo 的可选端点覆盖 -->
|
||||
<div
|
||||
v-if="!isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
||||
<Textarea
|
||||
v-model="form.attribute_mapping_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
placeholder="{"id": "user_id", "username": "login"}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">
|
||||
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
||||
</Label>
|
||||
<Textarea
|
||||
v-model="form.extra_config_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
:placeholder="extraConfigPlaceholder"
|
||||
/>
|
||||
<p
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="mt-1 text-xs text-muted-foreground"
|
||||
>
|
||||
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="lastTestResult.authorization_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<span class="text-muted-foreground">Authorization URL</span>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<div
|
||||
v-if="lastTestResult"
|
||||
class="mt-6 rounded-lg border border-border p-4 text-sm"
|
||||
>
|
||||
<div class="font-medium mb-2">
|
||||
测试结果
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap gap-4 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="lastTestResult.authorization_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<span class="text-muted-foreground">Authorization URL</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="lastTestResult.token_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<span class="text-muted-foreground">Token URL</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="lastTestResult.secret_status === 'likely_valid' ? 'bg-green-500' : lastTestResult.secret_status === 'invalid' ? 'bg-red-500' : 'bg-yellow-500'"
|
||||
/>
|
||||
<span class="text-muted-foreground">Secret: {{ lastTestResult.secret_status }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="lastTestResult.token_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<span class="text-muted-foreground">Token URL</span>
|
||||
v-if="lastTestResult.details"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ lastTestResult.details }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="lastTestResult.secret_status === 'likely_valid' ? 'bg-green-500' : lastTestResult.secret_status === 'invalid' ? 'bg-red-500' : 'bg-yellow-500'"
|
||||
/>
|
||||
<span class="text-muted-foreground">Secret: {{ lastTestResult.secret_status }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="lastTestResult.details"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ lastTestResult.details }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
</CardSection>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
|
||||
@@ -822,7 +822,6 @@
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
<div>API 端点</div>
|
||||
<div>推理程度</div>
|
||||
<div>映射参数</div>
|
||||
<div class="md:text-right">状态</div>
|
||||
<div class="md:text-right">
|
||||
状态
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
<div
|
||||
|
||||
@@ -50,15 +50,15 @@
|
||||
>
|
||||
<UsageModelTable
|
||||
:data="enhancedModelStats"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
/>
|
||||
<UsageProviderTable
|
||||
:data="providerStats"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
/>
|
||||
<UsageApiFormatTable
|
||||
:data="apiFormatStats"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
/>
|
||||
</div>
|
||||
<!-- 用户:模型 + API格式(2列) -->
|
||||
@@ -68,7 +68,7 @@
|
||||
>
|
||||
<UsageModelTable
|
||||
:data="enhancedModelStats"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
:is-admin="authStore.canAccessAdmin"
|
||||
/>
|
||||
<UsageApiFormatTable
|
||||
:data="apiFormatStats"
|
||||
@@ -81,7 +81,7 @@
|
||||
<UsageRecordsTable
|
||||
:records="displayRecords"
|
||||
:is-admin="isAdminPage"
|
||||
:show-actual-cost="authStore.canAccessAdmin"
|
||||
:show-actual-cost="authStore.canAccessAdmin"
|
||||
:loading="isLoadingRecords"
|
||||
:time-range="timeRange"
|
||||
:filter-search="filterSearch"
|
||||
|
||||
Reference in New Issue
Block a user