diff --git a/messages/en/settings.json b/messages/en/settings.json index 6654ba5d1..5bab1013a 100644 --- a/messages/en/settings.json +++ b/messages/en/settings.json @@ -1150,6 +1150,7 @@ "create": "Add Provider", "edit": "Edit Provider" }, + "dialogDescription": "Configure provider details and advanced settings.", "url": { "label": "API Address *", "placeholder": "e.g. https://open.bigmodel.cn/api/anthropic" @@ -1470,6 +1471,7 @@ "errors": { "invalidUrl": "Please enter a valid API address", "invalidWebsiteUrl": "Please enter a valid provider website URL", + "groupTagTooLong": "Provider group tags are too long (max {max} chars total)", "addFailed": "Failed to add provider", "updateFailed": "Failed to update provider", "deleteFailed": "Failed to delete provider" diff --git a/messages/ja/settings.json b/messages/ja/settings.json index 23d7d5107..d3d1c5e60 100644 --- a/messages/ja/settings.json +++ b/messages/ja/settings.json @@ -1045,6 +1045,7 @@ "create": "プロバイダーを追加", "edit": "プロバイダーを編集" }, + "dialogDescription": "プロバイダーの詳細と高度な設定を構成します。", "url": { "label": "API アドレス *", "placeholder": "例: https://open.bigmodel.cn/api/anthropic" @@ -1340,6 +1341,7 @@ "errors": { "invalidUrl": "有効な API アドレスを入力してください", "invalidWebsiteUrl": "有効な公式サイト URL を入力してください", + "groupTagTooLong": "プロバイダーグループが長すぎます(合計{max}文字まで)", "addFailed": "プロバイダーの追加に失敗しました", "updateFailed": "プロバイダーの更新に失敗しました", "deleteFailed": "プロバイダーの削除に失敗しました" diff --git a/messages/ru/settings.json b/messages/ru/settings.json index dd248698d..ba5a032b8 100644 --- a/messages/ru/settings.json +++ b/messages/ru/settings.json @@ -1045,6 +1045,7 @@ "create": "Добавить провайдера", "edit": "Редактировать провайдера" }, + "dialogDescription": "Настройте детали провайдера и расширенные параметры.", "url": { "label": "Адрес API *", "placeholder": "например: https://open.bigmodel.cn/api/anthropic" @@ -1340,6 +1341,7 @@ "errors": { "invalidUrl": "Введите корректный адрес API", "invalidWebsiteUrl": "Введите корректный адрес сайта провайдера", + "groupTagTooLong": "Список групп провайдера слишком длинный (макс. {max} символов всего)", "addFailed": "Не удалось добавить провайдера", "updateFailed": "Не удалось обновить провайдера", "deleteFailed": "Не удалось удалить провайдера" diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json index 9f2521247..75fc54d52 100644 --- a/messages/zh-CN/settings.json +++ b/messages/zh-CN/settings.json @@ -659,6 +659,7 @@ "create": "新增服务商", "edit": "编辑服务商" }, + "dialogDescription": "配置供应商信息及高级设置。", "url": { "label": "API 地址 *", "placeholder": "例如: https://open.bigmodel.cn/api/anthropic" @@ -979,6 +980,7 @@ "errors": { "invalidUrl": "请输入有效的 API 地址", "invalidWebsiteUrl": "请输入有效的供应商官网地址", + "groupTagTooLong": "分组标签总长度不能超过 {max} 个字符", "addFailed": "添加服务商失败", "updateFailed": "更新服务商失败", "deleteFailed": "删除服务商失败" diff --git a/messages/zh-TW/settings.json b/messages/zh-TW/settings.json index 258f023a9..379d6374c 100644 --- a/messages/zh-TW/settings.json +++ b/messages/zh-TW/settings.json @@ -1045,6 +1045,7 @@ "create": "新增供應商", "edit": "編輯供應商" }, + "dialogDescription": "設定供應商資訊與進階設定。", "url": { "label": "API 位址 *", "placeholder": "例如:https://open.bigmodel.cn/api/anthropic" @@ -1346,6 +1347,7 @@ "errors": { "invalidUrl": "請輸入有效的 API 位址", "invalidWebsiteUrl": "請輸入有效的供應商官網", + "groupTagTooLong": "分組標籤總長度不能超過 {max} 個字元", "addFailed": "新增供應商失敗", "updateFailed": "更新供應商失敗", "deleteFailed": "刪除供應商失敗" diff --git a/src/app/[locale]/dashboard/_components/user/user-management-table.tsx b/src/app/[locale]/dashboard/_components/user/user-management-table.tsx index 50e1c932c..f184a0362 100644 --- a/src/app/[locale]/dashboard/_components/user/user-management-table.tsx +++ b/src/app/[locale]/dashboard/_components/user/user-management-table.tsx @@ -261,12 +261,7 @@ export function UserManagementTable({ useEffect(() => { if (!scrollResetKey) return; - parentRef.current?.scrollTo({ top: 0 }); - // Defer measurement to next frame to ensure DOM has updated - const rafId = requestAnimationFrame(() => { - rowVirtualizer.measure(); - }); - return () => cancelAnimationFrame(rafId); + rowVirtualizer.scrollToOffset(0); }, [scrollResetKey, rowVirtualizer]); const quickRenewTranslations = useMemo(() => { diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx index d241912cf..d93a90ed4 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx @@ -19,7 +19,7 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -41,6 +41,8 @@ import { ApiTestButton } from "./api-test-button"; import { ProxyTestButton } from "./proxy-test-button"; import { UrlPreview } from "./url-preview"; +const GROUP_TAG_MAX_TOTAL_LENGTH = 50; + type Mode = "create" | "edit"; interface ProviderFormProps { @@ -295,6 +297,14 @@ export function ProviderForm({ return; } + // group_tag 在 DB/schema 中限制为 varchar(50),并且后端按整串校验 max(50) + // 这里限制逗号拼接后的总长度,避免“UI 看似可选多标签,但保存必失败”的体验 + const serializedGroupTag = groupTag.join(","); + if (serializedGroupTag.length > GROUP_TAG_MAX_TOTAL_LENGTH) { + toast.error(t("errors.groupTagTooLong", { max: GROUP_TAG_MAX_TOTAL_LENGTH })); + return; + } + // 检查 failureThreshold 是否为特殊值(0 或大于 100) const threshold = failureThreshold ?? 5; if (threshold === 0 || threshold > 100) { @@ -306,6 +316,15 @@ export function ProviderForm({ performSubmit(); }; + const handleGroupTagChange = (nextTags: string[]) => { + const serialized = nextTags.join(","); + if (serialized.length > GROUP_TAG_MAX_TOTAL_LENGTH) { + toast.error(t("errors.groupTagTooLong", { max: GROUP_TAG_MAX_TOTAL_LENGTH })); + return; + } + setGroupTag(nextTags); + }; + // 实际提交逻辑 const performSubmit = () => { // 处理模型重定向(空对象转为 null) @@ -519,6 +538,7 @@ export function ProviderForm({ <> {isEdit ? t("title.edit") : t("title.create")} + {t("dialogDescription")}
@@ -692,16 +712,16 @@ export function ProviderForm({ { const messages: Record = { empty: tUI("emptyTag"), duplicate: tUI("duplicateTag"), - too_long: tUI("tooLong", { max: 50 }), + too_long: tUI("tooLong", { max: GROUP_TAG_MAX_TOTAL_LENGTH }), invalid_format: tUI("invalidFormat"), max_tags: tUI("maxTags"), }; diff --git a/src/app/[locale]/settings/providers/_components/forms/url-preview.tsx b/src/app/[locale]/settings/providers/_components/forms/url-preview.tsx index 405f2b2b5..8c4b240e8 100644 --- a/src/app/[locale]/settings/providers/_components/forms/url-preview.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/url-preview.tsx @@ -39,7 +39,8 @@ export function UrlPreview({ baseUrl, providerType }: UrlPreviewProps) { } try { - return previewProxyUrls(baseUrl, providerType); + const result = previewProxyUrls(baseUrl, providerType); + return Object.keys(result).length > 0 ? result : null; } catch { return null; } diff --git a/src/app/[locale]/settings/providers/_components/provider-manager.tsx b/src/app/[locale]/settings/providers/_components/provider-manager.tsx index 2648481d8..01307d567 100644 --- a/src/app/[locale]/settings/providers/_components/provider-manager.tsx +++ b/src/app/[locale]/settings/providers/_components/provider-manager.tsx @@ -1,7 +1,7 @@ "use client"; import { AlertTriangle, Loader2, Search, X } from "lucide-react"; import { useTranslations } from "next-intl"; -import { type ReactNode, useMemo, useState } from "react"; +import { type ReactNode, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -70,6 +70,13 @@ export function ProviderManager({ return providers.filter((p) => healthStatus[p.id]?.circuitState === "open").length; }, [providers, healthStatus]); + // Auto-reset circuit broken filter when no providers are broken + useEffect(() => { + if (circuitBrokenCount === 0 && circuitBrokenFilter) { + setCircuitBrokenFilter(false); + } + }, [circuitBrokenCount, circuitBrokenFilter]); + // Extract unique groups from all providers const allGroups = useMemo(() => { const groups = new Set(); diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index afdf4a1b2..ca82f9b5b 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -60,7 +60,11 @@ function ChartContainer({ {...props} > - + {children}