mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37:46 +08:00
refactor(frontend): modularize i18n architecture
This commit is contained in:
@@ -1002,7 +1002,7 @@
|
||||
variant="outline"
|
||||
@click="handleClose"
|
||||
>
|
||||
关闭
|
||||
{{ legacyT('关闭') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -1010,10 +1010,10 @@
|
||||
<!-- 删除端点确认弹窗 -->
|
||||
<AlertDialog
|
||||
:model-value="deleteConfirmOpen"
|
||||
title="删除端点"
|
||||
:title="legacyT('删除端点')"
|
||||
:description="deleteConfirmDescription"
|
||||
confirm-text="删除"
|
||||
cancel-text="取消"
|
||||
:confirm-text="legacyT('删除')"
|
||||
:cancel-text="legacyT('取消')"
|
||||
type="danger"
|
||||
@update:model-value="deleteConfirmOpen = $event"
|
||||
@confirm="confirmDeleteEndpoint"
|
||||
@@ -1047,6 +1047,7 @@ import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateC
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
import { useI18n } from '@/i18n'
|
||||
import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import EndpointConditionEditor from './EndpointConditionEditor.vue'
|
||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||
@@ -1142,16 +1143,21 @@ const isEndpointFormatConversionDisabled = computed(() => {
|
||||
// 获取禁用提示
|
||||
const formatConversionDisabledTooltip = computed(() => {
|
||||
if (props.systemFormatConversionEnabled) {
|
||||
return '请先关闭系统级开关'
|
||||
return legacyT('请先关闭系统级开关')
|
||||
}
|
||||
if (props.providerFormatConversionEnabled) {
|
||||
return '请先关闭提供商级开关'
|
||||
return legacyT('请先关闭提供商级开关')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const proxyNodesStore = useProxyNodesStore()
|
||||
const { legacyT, locale } = useI18n()
|
||||
|
||||
function localizedApiError(error: unknown, fallback: string): string {
|
||||
return legacyT(parseApiError(error, fallback))
|
||||
}
|
||||
|
||||
// 规则 Select 的展开状态(与 Collapsible 分开管理)
|
||||
const ruleSelectOpen = ref<Record<string, boolean>>({})
|
||||
@@ -1488,58 +1494,70 @@ function isJsonObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function formatJsonRuleFieldLabel(label: string, index: number, field: string): string {
|
||||
return locale.value === 'en-US'
|
||||
? `${label} entry ${index + 1}: ${field}`
|
||||
: `${label}第 ${index + 1} 条:${field}`
|
||||
}
|
||||
|
||||
function formatJsonRuleError(label: string, index: number, message: string): string {
|
||||
return locale.value === 'en-US'
|
||||
? `${label} entry ${index + 1}: ${legacyT(message)}`
|
||||
: `${label}第 ${index + 1} 条:${legacyT(message)}`
|
||||
}
|
||||
|
||||
function readJsonRulesArray(root: Record<string, unknown>, key: keyof EndpointRulesJsonPayload, label: string): { value: unknown[]; error: string | null } {
|
||||
const raw = root[key]
|
||||
if (raw === undefined || raw === null) return { value: [], error: null }
|
||||
if (!Array.isArray(raw)) return { value: [], error: `${label} 必须是数组或 null` }
|
||||
if (!Array.isArray(raw)) return { value: [], error: `${label} ${legacyT('必须是数组或 null')}` }
|
||||
return { value: raw, error: null }
|
||||
}
|
||||
|
||||
function validateJsonCondition(rule: Record<string, unknown>, label: string, index: number): string | null {
|
||||
const raw = rule.condition
|
||||
if (raw === undefined || raw === null) return null
|
||||
if (!isJsonObject(raw)) return `${label}第 ${index + 1} 条:condition 必须是对象`
|
||||
const shapeError = validateJsonConditionShape(raw, `${label}第 ${index + 1} 条:condition`)
|
||||
if (!isJsonObject(raw)) return formatJsonRuleError(label, index, 'condition 必须是对象')
|
||||
const shapeError = validateJsonConditionShape(raw, formatJsonRuleFieldLabel(label, index, 'condition'))
|
||||
if (shapeError) return shapeError
|
||||
const editable = conditionToEditable(raw as BodyRule['condition'])
|
||||
const err = validateEditableCondition(editable)
|
||||
return err ? `${label}第 ${index + 1} 条:${err}` : null
|
||||
return err ? formatJsonRuleError(label, index, err) : null
|
||||
}
|
||||
|
||||
function validateJsonConditionShape(condition: Record<string, unknown>, label: string): string | null {
|
||||
if (Object.prototype.hasOwnProperty.call(condition, 'all')) {
|
||||
if (!Array.isArray(condition.all)) return `${label}.all 必须是数组`
|
||||
if (!Array.isArray(condition.all)) return `${label}.all ${legacyT('必须是数组')}`
|
||||
for (let i = 0; i < condition.all.length; i++) {
|
||||
const child = condition.all[i]
|
||||
if (!isJsonObject(child)) return `${label}.all[${i}] 必须是对象`
|
||||
if (!isJsonObject(child)) return `${label}.all[${i}] ${legacyT('必须是对象')}`
|
||||
const err = validateJsonConditionShape(child, `${label}.all[${i}]`)
|
||||
if (err) return err
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(condition, 'any')) {
|
||||
if (!Array.isArray(condition.any)) return `${label}.any 必须是数组`
|
||||
if (!Array.isArray(condition.any)) return `${label}.any ${legacyT('必须是数组')}`
|
||||
for (let i = 0; i < condition.any.length; i++) {
|
||||
const child = condition.any[i]
|
||||
if (!isJsonObject(child)) return `${label}.any[${i}] 必须是对象`
|
||||
if (!isJsonObject(child)) return `${label}.any[${i}] ${legacyT('必须是对象')}`
|
||||
const err = validateJsonConditionShape(child, `${label}.any[${i}]`)
|
||||
if (err) return err
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof condition.path !== 'string') return `${label}.path 必须是字符串`
|
||||
if (typeof condition.path !== 'string') return `${label}.path ${legacyT('必须是字符串')}`
|
||||
if (typeof condition.op !== 'string' || !CONDITION_JSON_OPS.has(condition.op)) {
|
||||
return `${label}.op 无效`
|
||||
return `${label}.op ${legacyT('无效')}`
|
||||
}
|
||||
if (condition.source !== undefined && (typeof condition.source !== 'string' || !CONDITION_JSON_SOURCES.has(condition.source))) {
|
||||
return `${label}.source 无效`
|
||||
return `${label}.source ${legacyT('无效')}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function requireJsonString(rule: Record<string, unknown>, key: string, label: string, index: number): string | null {
|
||||
return typeof rule[key] === 'string' ? null : `${label}第 ${index + 1} 条:${key} 必须是字符串`
|
||||
return typeof rule[key] === 'string' ? null : formatJsonRuleError(label, index, `${key} ${legacyT('必须是字符串')}`)
|
||||
}
|
||||
|
||||
function normalizeHeaderRuleName(raw: string): string {
|
||||
@@ -1548,12 +1566,12 @@ function normalizeHeaderRuleName(raw: string): string {
|
||||
|
||||
function reservedHeaderRuleError(raw: string): string | null {
|
||||
const name = normalizeHeaderRuleName(raw)
|
||||
return name && RESERVED_HEADERS.has(name) ? `"${raw}" 是系统保留的请求头` : null
|
||||
return name && RESERVED_HEADERS.has(name) ? `"${raw}" ${legacyT('是系统保留的请求头')}` : null
|
||||
}
|
||||
|
||||
function reservedResponseHeaderRuleError(raw: string): string | null {
|
||||
const name = normalizeHeaderRuleName(raw)
|
||||
return name && RESERVED_RESPONSE_HEADERS.has(name) ? `"${raw}" 是系统保留的响应头` : null
|
||||
return name && RESERVED_RESPONSE_HEADERS.has(name) ? `"${raw}" ${legacyT('是系统保留的响应头')}` : null
|
||||
}
|
||||
|
||||
function bodyRuleTopLevelField(rawPath: string): string | null {
|
||||
@@ -1567,7 +1585,7 @@ function bodyRuleTopLevelField(rawPath: string): string | null {
|
||||
function reservedBodyRuleFieldError(rawPath: string): string | null {
|
||||
const topField = bodyRuleTopLevelField(rawPath)
|
||||
return topField && RESERVED_BODY_FIELDS.has(topField)
|
||||
? `"${topField}" 是系统保留的顶层字段`
|
||||
? `"${topField}" ${legacyT('是系统保留的顶层字段')}`
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -1577,13 +1595,13 @@ function validateHeaderRuleJson(
|
||||
index: number,
|
||||
reservedNameError: (raw: string) => string | null = reservedHeaderRuleError
|
||||
): string | null {
|
||||
if (!isJsonObject(rule)) return `${label}第 ${index + 1} 条必须是对象`
|
||||
if (!isJsonObject(rule)) return formatJsonRuleError(label, index, '必须是对象')
|
||||
if (rule.enabled !== undefined && typeof rule.enabled !== 'boolean') {
|
||||
return `${label}第 ${index + 1} 条:enabled 必须是布尔值`
|
||||
return formatJsonRuleError(label, index, 'enabled 必须是布尔值')
|
||||
}
|
||||
const action = rule.action
|
||||
if (action !== 'set' && action !== 'drop' && action !== 'rename') {
|
||||
return `${label}第 ${index + 1} 条:action 必须是 set/drop/rename`
|
||||
return formatJsonRuleError(label, index, 'action 必须是 set/drop/rename')
|
||||
}
|
||||
if (action === 'set') {
|
||||
return requireJsonString(rule, 'key', label, index)
|
||||
@@ -1604,19 +1622,19 @@ function validateHeaderRuleJson(
|
||||
}
|
||||
|
||||
function validateBodyRuleJson(rule: unknown, label: string, index: number): string | null {
|
||||
if (!isJsonObject(rule)) return `${label}第 ${index + 1} 条必须是对象`
|
||||
if (!isJsonObject(rule)) return formatJsonRuleError(label, index, '必须是对象')
|
||||
if (rule.enabled !== undefined && typeof rule.enabled !== 'boolean') {
|
||||
return `${label}第 ${index + 1} 条:enabled 必须是布尔值`
|
||||
return formatJsonRuleError(label, index, 'enabled 必须是布尔值')
|
||||
}
|
||||
const action = typeof rule.action === 'string' ? rule.action : ''
|
||||
if (!BODY_RULE_JSON_ACTIONS.has(action)) {
|
||||
return `${label}第 ${index + 1} 条:action 无效`
|
||||
return formatJsonRuleError(label, index, 'action 无效')
|
||||
}
|
||||
|
||||
if (action === 'set' || action === 'append') {
|
||||
return requireJsonString(rule, 'path', label, index)
|
||||
|| reservedBodyRuleFieldError(rule.path as string)
|
||||
|| (Object.prototype.hasOwnProperty.call(rule, 'value') ? null : `${label}第 ${index + 1} 条:value 不能为空`)
|
||||
|| (Object.prototype.hasOwnProperty.call(rule, 'value') ? null : formatJsonRuleError(label, index, 'value 不能为空'))
|
||||
|| validateJsonCondition(rule, label, index)
|
||||
}
|
||||
if (action === 'drop') {
|
||||
@@ -1634,20 +1652,20 @@ function validateBodyRuleJson(rule: unknown, label: string, index: number): stri
|
||||
if (action === 'insert') {
|
||||
if (requireJsonString(rule, 'path', label, index)) return requireJsonString(rule, 'path', label, index)
|
||||
if (reservedBodyRuleFieldError(rule.path as string)) return reservedBodyRuleFieldError(rule.path as string)
|
||||
if (!Number.isInteger(rule.index)) return `${label}第 ${index + 1} 条:index 必须是整数`
|
||||
if (!Object.prototype.hasOwnProperty.call(rule, 'value')) return `${label}第 ${index + 1} 条:value 不能为空`
|
||||
if (!Number.isInteger(rule.index)) return formatJsonRuleError(label, index, 'index 必须是整数')
|
||||
if (!Object.prototype.hasOwnProperty.call(rule, 'value')) return formatJsonRuleError(label, index, 'value 不能为空')
|
||||
return validateJsonCondition(rule, label, index)
|
||||
}
|
||||
if (action === 'regex_replace') {
|
||||
if (requireJsonString(rule, 'path', label, index)) return requireJsonString(rule, 'path', label, index)
|
||||
if (reservedBodyRuleFieldError(rule.path as string)) return reservedBodyRuleFieldError(rule.path as string)
|
||||
if (requireJsonString(rule, 'pattern', label, index)) return requireJsonString(rule, 'pattern', label, index)
|
||||
if (typeof rule.replacement !== 'string') return `${label}第 ${index + 1} 条:replacement 必须是字符串`
|
||||
if (rule.flags !== undefined && typeof rule.flags !== 'string') return `${label}第 ${index + 1} 条:flags 必须是字符串`
|
||||
if (rule.count !== undefined && !Number.isInteger(rule.count)) return `${label}第 ${index + 1} 条:count 必须是整数`
|
||||
if (typeof rule.replacement !== 'string') return formatJsonRuleError(label, index, 'replacement 必须是字符串')
|
||||
if (rule.flags !== undefined && typeof rule.flags !== 'string') return formatJsonRuleError(label, index, 'flags 必须是字符串')
|
||||
if (rule.count !== undefined && !Number.isInteger(rule.count)) return formatJsonRuleError(label, index, 'count 必须是整数')
|
||||
return validateJsonCondition(rule, label, index)
|
||||
}
|
||||
return `${label}第 ${index + 1} 条:action 无效`
|
||||
return formatJsonRuleError(label, index, 'action 无效')
|
||||
}
|
||||
|
||||
function parseEndpointRulesJsonDraft(draft: string): { value: EndpointRulesJsonPayload | null; error: string | null } {
|
||||
@@ -1660,9 +1678,9 @@ function parseEndpointRulesJsonDraft(draft: string): { value: EndpointRulesJsonP
|
||||
try {
|
||||
parsed = JSON.parse(raw)
|
||||
} catch (error: unknown) {
|
||||
return { value: null, error: error instanceof Error ? error.message : 'JSON 格式无效' }
|
||||
return { value: null, error: error instanceof Error ? error.message : legacyT('JSON 格式无效') }
|
||||
}
|
||||
if (!isJsonObject(parsed)) return { value: null, error: '规则 JSON 必须是对象' }
|
||||
if (!isJsonObject(parsed)) return { value: null, error: legacyT('规则 JSON 必须是对象') }
|
||||
|
||||
const header = readJsonRulesArray(parsed, 'header_rules', 'header_rules')
|
||||
if (header.error) return { value: null, error: header.error }
|
||||
@@ -1707,14 +1725,14 @@ function applyEndpointRulesJsonDraft(
|
||||
const parsed = parseEndpointRulesJsonDraft(endpointRulesJsonDraft.value[endpointId] ?? '')
|
||||
if (!parsed.value) {
|
||||
endpointRulesJsonError.value[endpointId] = parsed.error
|
||||
if (notifyError) showError(parsed.error || '规则 JSON 无效')
|
||||
if (notifyError) showError(legacyT(parsed.error || '规则 JSON 无效'))
|
||||
return false
|
||||
}
|
||||
|
||||
const state = ensureEndpointEditState(endpointId)
|
||||
if (!state) {
|
||||
endpointRulesJsonError.value[endpointId] = '端点编辑状态不可用'
|
||||
if (notifyError) showError('端点编辑状态不可用')
|
||||
endpointRulesJsonError.value[endpointId] = legacyT('端点编辑状态不可用')
|
||||
if (notifyError) showError(legacyT('端点编辑状态不可用'))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1734,14 +1752,14 @@ function applyEndpointRulesJsonDraft(
|
||||
|| getBodyValidationErrorForEndpoint(endpointId)
|
||||
if (validationError) {
|
||||
endpointRulesJsonError.value[endpointId] = validationError
|
||||
if (notifyError) showError(validationError)
|
||||
if (notifyError) showError(legacyT(validationError))
|
||||
return false
|
||||
}
|
||||
|
||||
endpointRulesJsonDraft.value[endpointId] = stringifyEndpointRulesJsonPayload(parsed.value)
|
||||
endpointRulesJsonError.value[endpointId] = null
|
||||
endpointRulesJsonDirty.value[endpointId] = false
|
||||
if (options.notify !== false) success('JSON 规则已应用')
|
||||
if (options.notify !== false) success(legacyT('JSON 规则已应用'))
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1878,7 +1896,9 @@ const availableFormats = computed(() => {
|
||||
const deleteConfirmDescription = computed(() => {
|
||||
if (!endpointToDelete.value) return ''
|
||||
const formatLabel = formatApiFormat(endpointToDelete.value.api_format)
|
||||
return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
|
||||
return locale.value === 'en-US'
|
||||
? `Delete the ${formatLabel} endpoint? Linked keys will no longer support this API format.`
|
||||
: `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
|
||||
})
|
||||
|
||||
function defaultBodyRulesCacheKey(apiFormat: string): string {
|
||||
@@ -1985,14 +2005,14 @@ function endpointProxyNodeId(endpoint: ProviderEndpoint): string {
|
||||
|
||||
function getEndpointProxyNodeName(endpoint: ProviderEndpoint): string {
|
||||
const nodeId = endpointProxyNodeId(endpoint)
|
||||
if (!nodeId) return '未知节点'
|
||||
if (!nodeId) return legacyT('未知节点')
|
||||
const node = proxyNodesStore.nodes.find(n => n.id === nodeId)
|
||||
return node ? node.name : `${nodeId.slice(0, 8)}...`
|
||||
}
|
||||
|
||||
function getEndpointProxyTitle(endpoint: ProviderEndpoint): string {
|
||||
const nodeId = endpointProxyNodeId(endpoint)
|
||||
return nodeId ? `端点代理: ${getEndpointProxyNodeName(endpoint)}` : '设置端点代理节点'
|
||||
return nodeId ? `${legacyT('端点代理')}: ${getEndpointProxyNodeName(endpoint)}` : legacyT('设置端点代理节点')
|
||||
}
|
||||
|
||||
function handleEndpointProxyPopoverToggle(endpointId: string, open: boolean) {
|
||||
@@ -2019,10 +2039,10 @@ async function setEndpointProxy(endpoint: ProviderEndpoint, nodeId: string) {
|
||||
})
|
||||
replaceLocalEndpoint(updated)
|
||||
endpointProxyPopoverOpen.value[endpoint.id] = false
|
||||
success('端点代理已更新')
|
||||
success(legacyT('端点代理已更新'))
|
||||
emit('endpointUpdated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '更新代理失败'), '错误')
|
||||
showError(localizedApiError(error, '更新代理失败'), legacyT('错误'))
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -2034,10 +2054,10 @@ async function clearEndpointProxy(endpoint: ProviderEndpoint) {
|
||||
const updated = await updateEndpoint(endpoint.id, { proxy: null })
|
||||
replaceLocalEndpoint(updated)
|
||||
endpointProxyPopoverOpen.value[endpoint.id] = false
|
||||
success('端点代理已清除')
|
||||
success(legacyT('端点代理已清除'))
|
||||
emit('endpointUpdated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '清除代理失败'), '错误')
|
||||
showError(localizedApiError(error, '清除代理失败'), legacyT('错误'))
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -2399,7 +2419,7 @@ function validateRuleKeyForEndpoint(endpointId: string, key: string, index: numb
|
||||
)
|
||||
)
|
||||
if (duplicate >= 0) {
|
||||
return '请求头名称重复'
|
||||
return legacyT('请求头名称重复')
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -2423,7 +2443,7 @@ function validateRenameFromForEndpoint(endpointId: string, from: string, index:
|
||||
(r.action === 'rename' && r.from.trim().toLowerCase() === trimmedFrom))
|
||||
)
|
||||
if (duplicate >= 0) {
|
||||
return '该请求头已被其他规则处理'
|
||||
return legacyT('该请求头已被其他规则处理')
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -2446,7 +2466,7 @@ function validateRenameToForEndpoint(endpointId: string, to: string, index: numb
|
||||
(r.action === 'rename' && r.to.trim().toLowerCase() === trimmedTo))
|
||||
)
|
||||
if (duplicate >= 0) {
|
||||
return '请求头名称重复'
|
||||
return legacyT('请求头名称重复')
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -2575,7 +2595,7 @@ function validateBodyRulePathForEndpoint(endpointId: string, path: string, index
|
||||
const dotPart = raw.includes('[') ? raw.slice(0, raw.indexOf('[')) : raw
|
||||
const parts = dotPart ? parseBodyRulePathParts(dotPart) : [raw.split('[')[0] || raw]
|
||||
if (!parts) {
|
||||
return '路径格式无效'
|
||||
return legacyT('路径格式无效')
|
||||
}
|
||||
|
||||
const reservedErr = reservedBodyRuleFieldError(raw)
|
||||
@@ -2594,7 +2614,7 @@ function validateBodyRulePathForEndpoint(endpointId: string, path: string, index
|
||||
)
|
||||
)
|
||||
if (duplicate >= 0) {
|
||||
return '字段路径重复'
|
||||
return legacyT('字段路径重复')
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -2607,7 +2627,7 @@ function validateBodyRenameFromForEndpoint(endpointId: string, from: string, ind
|
||||
|
||||
const parts = parseBodyRulePathParts(raw)
|
||||
if (!parts) {
|
||||
return '路径格式无效(不允许 .a / a. / a..b)'
|
||||
return legacyT('路径格式无效(不允许 .a / a. / a..b)')
|
||||
}
|
||||
|
||||
const reservedErr = reservedBodyRuleFieldError(raw)
|
||||
@@ -2625,7 +2645,7 @@ function validateBodyRenameFromForEndpoint(endpointId: string, from: string, ind
|
||||
(r.action === 'rename' && r.from.trim().toLowerCase() === normalizedFrom))
|
||||
)
|
||||
if (duplicate >= 0) {
|
||||
return '该路径已被其他规则处理'
|
||||
return legacyT('该路径已被其他规则处理')
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -2638,7 +2658,7 @@ function validateBodyRenameToForEndpoint(endpointId: string, to: string, index:
|
||||
|
||||
const parts = parseBodyRulePathParts(raw)
|
||||
if (!parts) {
|
||||
return '路径格式无效(不允许 .a / a. / a..b)'
|
||||
return legacyT('路径格式无效(不允许 .a / a. / a..b)')
|
||||
}
|
||||
|
||||
const reservedErr = reservedBodyRuleFieldError(raw)
|
||||
@@ -2655,7 +2675,7 @@ function validateBodyRenameToForEndpoint(endpointId: string, to: string, index:
|
||||
(r.action === 'rename' && r.to.trim().toLowerCase() === normalizedTo))
|
||||
)
|
||||
if (duplicate >= 0) {
|
||||
return '字段路径重复'
|
||||
return legacyT('字段路径重复')
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -2665,12 +2685,12 @@ function validateBodySetValue(rule: EditableBodyRule): string | null {
|
||||
if (rule.action !== 'set' && rule.action !== 'append' && rule.action !== 'insert') return null
|
||||
|
||||
const raw = rule.value.trim()
|
||||
if (!raw) return '值不能为空'
|
||||
if (!raw) return legacyT('值不能为空')
|
||||
try {
|
||||
JSON.parse(prepareValueForJsonParse(raw))
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
return `JSON 格式错误:${msg}`
|
||||
return locale.value === 'en-US' ? `JSON format error: ${msg}` : `JSON 格式错误:${msg}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -2712,12 +2732,12 @@ function getRegexPatternValidation(rule: EditableBodyRule): boolean | null {
|
||||
// 获取正则验证提示
|
||||
function getRegexPatternValidationTip(rule: EditableBodyRule): string {
|
||||
const validation = getRegexPatternValidation(rule)
|
||||
if (validation === null) return '输入正则表达式'
|
||||
if (validation === true) return '有效的正则表达式'
|
||||
if (validation === null) return legacyT('输入正则表达式')
|
||||
if (validation === true) return legacyT('有效的正则表达式')
|
||||
try {
|
||||
new RegExp(rule.pattern.trim())
|
||||
// 正则有效但 flags 无效
|
||||
return '无效的 flags(仅允许 i/m/s)'
|
||||
return legacyT('无效的 flags(仅允许 i/m/s)')
|
||||
} catch (err: unknown) {
|
||||
return err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
@@ -2726,11 +2746,21 @@ function getRegexPatternValidationTip(rule: EditableBodyRule): string {
|
||||
// 获取验证提示
|
||||
function getBodySetValueValidationTip(rule: EditableBodyRule): string {
|
||||
const validation = getBodySetValueValidation(rule)
|
||||
if (validation === null) return '点击验证 JSON'
|
||||
if (validation === null) return legacyT('点击验证 JSON')
|
||||
if (validation === true) {
|
||||
const parsed = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim())))
|
||||
const type = Array.isArray(parsed) ? '数组' : typeof parsed === 'object' && parsed !== null ? '对象' : typeof parsed === 'string' ? '字符串' : typeof parsed === 'number' ? '数字' : typeof parsed === 'boolean' ? '布尔' : 'null'
|
||||
return `有效的 JSON (${type})`
|
||||
const type = Array.isArray(parsed)
|
||||
? legacyT('数组')
|
||||
: typeof parsed === 'object' && parsed !== null
|
||||
? legacyT('对象')
|
||||
: typeof parsed === 'string'
|
||||
? legacyT('字符串')
|
||||
: typeof parsed === 'number'
|
||||
? legacyT('数字')
|
||||
: typeof parsed === 'boolean'
|
||||
? legacyT('布尔')
|
||||
: 'null'
|
||||
return `${legacyT('有效的 JSON')} (${type})`
|
||||
}
|
||||
try {
|
||||
JSON.parse(prepareValueForJsonParse(rule.value.trim()))
|
||||
@@ -2793,43 +2823,43 @@ function getTotalRulesCount(endpoint: ProviderEndpoint): number {
|
||||
// 格式化请求头规则的显示标签
|
||||
function _formatHeaderRuleLabel(rule: EditableRule): string {
|
||||
if (rule.action === 'set') {
|
||||
if (!rule.key) return '(未设置)'
|
||||
if (!rule.key) return legacyT('(未设置)')
|
||||
return `${rule.key}=${rule.value || '...'}`
|
||||
} else if (rule.action === 'drop') {
|
||||
if (!rule.key) return '(未设置)'
|
||||
if (!rule.key) return legacyT('(未设置)')
|
||||
return `-${rule.key}`
|
||||
} else if (rule.action === 'rename') {
|
||||
if (!rule.from || !rule.to) return '(未设置)'
|
||||
if (!rule.from || !rule.to) return legacyT('(未设置)')
|
||||
return `${rule.from}→${rule.to}`
|
||||
}
|
||||
return '(未知)'
|
||||
return legacyT('(未知)')
|
||||
}
|
||||
|
||||
// 格式化请求体规则的显示标签
|
||||
function _formatBodyRuleLabel(rule: EditableBodyRule): string {
|
||||
if (rule.action === 'set') {
|
||||
if (!rule.path) return '(未设置)'
|
||||
if (!rule.path) return legacyT('(未设置)')
|
||||
return `${rule.path}=${rule.value || '...'}`
|
||||
} else if (rule.action === 'drop') {
|
||||
if (!rule.path) return '(未设置)'
|
||||
if (!rule.path) return legacyT('(未设置)')
|
||||
return `-${rule.path}`
|
||||
} else if (rule.action === 'rename') {
|
||||
if (!rule.from || !rule.to) return '(未设置)'
|
||||
if (!rule.from || !rule.to) return legacyT('(未设置)')
|
||||
return `${rule.from}→${rule.to}`
|
||||
} else if (rule.action === 'append') {
|
||||
if (!rule.path) return '(未设置)'
|
||||
if (!rule.path) return legacyT('(未设置)')
|
||||
return `${rule.path}[]+=${rule.value || '...'}`
|
||||
} else if (rule.action === 'insert') {
|
||||
if (!rule.path) return '(未设置)'
|
||||
const idx = rule.index?.trim() || '末尾'
|
||||
if (!rule.path) return legacyT('(未设置)')
|
||||
const idx = rule.index?.trim() || legacyT('末尾')
|
||||
return `${rule.path}[${idx}]+=${rule.value || '...'}`
|
||||
} else if (rule.action === 'regex_replace') {
|
||||
if (!rule.path || !rule.pattern) return '(未设置)'
|
||||
if (!rule.path || !rule.pattern) return legacyT('(未设置)')
|
||||
const flags = rule.flags.trim()
|
||||
const count = rule.count.trim()
|
||||
return `${rule.path}: s/${rule.pattern}/${rule.replacement || ''}/${flags}${count ? ` ×${count}` : ''}`
|
||||
}
|
||||
return '(未知)'
|
||||
return legacyT('(未知)')
|
||||
}
|
||||
|
||||
// 检查端点请求体规则是否有修改
|
||||
@@ -2921,7 +2951,7 @@ function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (!rule.enabled) continue
|
||||
const prefix = `第 ${i + 1} 条请求体规则:`
|
||||
const prefix = locale.value === 'en-US' ? `Body rule ${i + 1}: ` : `第 ${i + 1} 条请求体规则:`
|
||||
|
||||
if (rule.action === 'set' || rule.action === 'drop') {
|
||||
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
|
||||
@@ -2944,29 +2974,32 @@ function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
|
||||
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
|
||||
if (pathErr) return `${prefix}${pathErr}`
|
||||
const indexStr = rule.index.trim()
|
||||
if (!indexStr) return `${prefix}插入位置不能为空`
|
||||
if (!isStrictIntegerString(indexStr)) return `${prefix}位置必须为整数`
|
||||
if (!indexStr) return `${prefix}${legacyT('插入位置不能为空')}`
|
||||
if (!isStrictIntegerString(indexStr)) return `${prefix}${legacyT('位置必须为整数')}`
|
||||
const valueErr = validateBodySetValue(rule)
|
||||
if (valueErr) return `${prefix}${valueErr}`
|
||||
} else if (rule.action === 'regex_replace') {
|
||||
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
|
||||
if (pathErr) return `${prefix}${pathErr}`
|
||||
if (!rule.pattern.trim()) return `${prefix}正则表达式不能为空`
|
||||
if (!rule.pattern.trim()) return `${prefix}${legacyT('正则表达式不能为空')}`
|
||||
try {
|
||||
new RegExp(rule.pattern.trim())
|
||||
} catch (err: unknown) {
|
||||
return `${prefix}正则表达式无效:${err instanceof Error ? err.message : String(err)}`
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
return locale.value === 'en-US'
|
||||
? `${prefix}Invalid regular expression: ${message}`
|
||||
: `${prefix}正则表达式无效:${message}`
|
||||
}
|
||||
const flags = rule.flags.trim()
|
||||
if (flags) {
|
||||
const validFlags = new Set(['i', 'm', 's'])
|
||||
for (const f of flags) {
|
||||
if (!validFlags.has(f)) return `${prefix}flags 仅允许 i/m/s,非法字符: ${f}`
|
||||
if (!validFlags.has(f)) return `${prefix}${legacyT('flags 仅允许 i/m/s,非法字符')}: ${f}`
|
||||
}
|
||||
}
|
||||
const count = rule.count.trim()
|
||||
if (count) {
|
||||
if (!isStrictNonNegativeIntegerString(count)) return `${prefix}替换次数必须是大于等于 0 的整数`
|
||||
if (!isStrictNonNegativeIntegerString(count)) return `${prefix}${legacyT('替换次数必须是大于等于 0 的整数')}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3058,7 +3091,7 @@ async function handleResetBodyRulesToDefault(endpoint: ProviderEndpoint) {
|
||||
try {
|
||||
const defaultRules = await loadDefaultBodyRulesForFormat(endpoint.api_format, true)
|
||||
if (!defaultRules.length) {
|
||||
showError('该端点没有默认请求体规则')
|
||||
showError(legacyT('该端点没有默认请求体规则'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3079,9 +3112,9 @@ async function handleResetBodyRulesToDefault(endpoint: ProviderEndpoint) {
|
||||
if (isEndpointRulesJsonMode(endpoint.id)) {
|
||||
refreshEndpointRulesJsonDraft(endpoint.id)
|
||||
}
|
||||
success('已重置请求体为默认规则,请点击保存生效')
|
||||
success(legacyT('已重置请求体为默认规则,请点击保存生效'))
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '重置失败'), '错误')
|
||||
showError(localizedApiError(error, '重置失败'), legacyT('错误'))
|
||||
} finally {
|
||||
resettingDefaultRulesEndpointId.value = null
|
||||
}
|
||||
@@ -3121,7 +3154,7 @@ function getHeaderValidationErrorForEndpoint(endpointId: string): string | null
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (!rule.enabled) continue
|
||||
const prefix = `第 ${i + 1} 条请求头规则:`
|
||||
const prefix = locale.value === 'en-US' ? `Header rule ${i + 1}: ` : `第 ${i + 1} 条请求头规则:`
|
||||
if (rule.action === 'set' || rule.action === 'drop') {
|
||||
const err = validateRuleKeyForEndpoint(endpointId, rule.key, i)
|
||||
if (err) return `${prefix}${err}`
|
||||
@@ -3156,7 +3189,7 @@ function validateResponseHeaderNameForEndpoint(endpointId: string, name: string,
|
||||
)
|
||||
)
|
||||
if (duplicate >= 0) {
|
||||
return field === 'from' ? '该响应头已被其他规则处理' : '响应头名称重复'
|
||||
return legacyT(field === 'from' ? '该响应头已被其他规则处理' : '响应头名称重复')
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -3167,7 +3200,7 @@ function getResponseHeaderValidationErrorForEndpoint(endpointId: string): string
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (!rule.enabled) continue
|
||||
const prefix = `第 ${i + 1} 条响应头规则:`
|
||||
const prefix = locale.value === 'en-US' ? `Response header rule ${i + 1}: ` : `第 ${i + 1} 条响应头规则:`
|
||||
if (rule.action === 'set' || rule.action === 'drop') {
|
||||
const err = validateResponseHeaderNameForEndpoint(endpointId, rule.key, i, 'key')
|
||||
if (err) return `${prefix}${err}`
|
||||
@@ -3274,20 +3307,20 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
|
||||
// 检查规则是否有验证错误
|
||||
const headerErr = getHeaderValidationErrorForEndpoint(endpoint.id)
|
||||
if (headerErr) {
|
||||
showError(headerErr)
|
||||
showError(legacyT(headerErr))
|
||||
return
|
||||
}
|
||||
|
||||
const responseHeaderErr = getResponseHeaderValidationErrorForEndpoint(endpoint.id)
|
||||
if (responseHeaderErr) {
|
||||
showError(responseHeaderErr)
|
||||
showError(legacyT(responseHeaderErr))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查请求体规则是否有验证错误
|
||||
const bodyErr = getBodyValidationErrorForEndpoint(endpoint.id)
|
||||
if (bodyErr) {
|
||||
showError(bodyErr)
|
||||
showError(legacyT(bodyErr))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3315,10 +3348,10 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
|
||||
if (Object.keys(payload).length === 0) return
|
||||
|
||||
await updateEndpoint(endpoint.id, payload)
|
||||
success('端点已更新')
|
||||
success(legacyT('端点已更新'))
|
||||
emit('endpointUpdated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '更新失败'), '错误')
|
||||
showError(localizedApiError(error, '更新失败'), legacyT('错误'))
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -3334,10 +3367,10 @@ async function handleToggleFormatConversion(endpoint: ProviderEndpoint) {
|
||||
await updateEndpoint(endpoint.id, {
|
||||
format_acceptance_config: newEnabled ? { enabled: true } : null,
|
||||
})
|
||||
success(newEnabled ? '已启用格式转换' : '已关闭格式转换')
|
||||
success(legacyT(newEnabled ? '已启用格式转换' : '已关闭格式转换'))
|
||||
emit('endpointUpdated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
showError(localizedApiError(error, '操作失败'), legacyT('错误'))
|
||||
} finally {
|
||||
togglingFormatEndpointId.value = null
|
||||
}
|
||||
@@ -3369,11 +3402,11 @@ function getUpstreamStreamButtonClass(endpoint: ProviderEndpoint): string {
|
||||
|
||||
// 获取上游流式按钮的提示文字
|
||||
function getUpstreamStreamTooltip(endpoint: ProviderEndpoint): string {
|
||||
if (isUpstreamStreamPolicyLocked(endpoint)) return '固定流式(Codex OpenAI Responses,已锁定)'
|
||||
if (isUpstreamStreamPolicyLocked(endpoint)) return legacyT('固定流式(Codex OpenAI Responses,已锁定)')
|
||||
const policy = getCurrentUpstreamStreamPolicy(endpoint)
|
||||
if (policy === 'force_stream') return '固定流式(点击切换为固定非流)'
|
||||
if (policy === 'force_non_stream') return '固定非流(点击切换为跟随请求)'
|
||||
return '跟随请求(点击切换为固定流式)'
|
||||
if (policy === 'force_stream') return legacyT('固定流式(点击切换为固定非流)')
|
||||
if (policy === 'force_non_stream') return legacyT('固定非流(点击切换为跟随请求)')
|
||||
return legacyT('跟随请求(点击切换为固定流式)')
|
||||
}
|
||||
|
||||
// 循环切换上游流式策略并直接保存
|
||||
@@ -3387,13 +3420,13 @@ async function handleCycleUpstreamStream(endpoint: ProviderEndpoint) {
|
||||
// 循环:auto -> force_stream -> force_non_stream -> auto
|
||||
if (currentPolicy === 'auto') {
|
||||
nextPolicy = 'force_stream'
|
||||
nextLabel = '固定流式'
|
||||
nextLabel = legacyT('固定流式')
|
||||
} else if (currentPolicy === 'force_stream') {
|
||||
nextPolicy = 'force_non_stream'
|
||||
nextLabel = '固定非流'
|
||||
nextLabel = legacyT('固定非流')
|
||||
} else {
|
||||
nextPolicy = 'auto'
|
||||
nextLabel = '跟随请求'
|
||||
nextLabel = legacyT('跟随请求')
|
||||
}
|
||||
|
||||
savingEndpointId.value = endpoint.id
|
||||
@@ -3417,10 +3450,10 @@ async function handleCycleUpstreamStream(endpoint: ProviderEndpoint) {
|
||||
endpointEditStates.value[endpoint.id].upstreamStreamPolicy = nextPolicy
|
||||
}
|
||||
|
||||
success(`已切换为${nextLabel}`)
|
||||
success(locale.value === 'en-US' ? `Switched to ${nextLabel}` : `已切换为${nextLabel}`)
|
||||
emit('endpointUpdated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
showError(localizedApiError(error, '操作失败'), legacyT('错误'))
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -3433,7 +3466,7 @@ async function handleAddEndpoint() {
|
||||
// 如果没有输入 base_url,使用按格式规范化后的提供商 website 作为默认值。
|
||||
const baseUrl = getNewEndpointBaseUrl()
|
||||
if (!baseUrl) {
|
||||
showError('请输入 Base URL')
|
||||
showError(legacyT('请输入 Base URL'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3446,12 +3479,14 @@ async function handleAddEndpoint() {
|
||||
custom_path: newEndpoint.value.custom_path || undefined,
|
||||
is_active: true,
|
||||
})
|
||||
success(`已添加 ${formatApiFormat(newEndpoint.value.api_format)} 端点`)
|
||||
success(locale.value === 'en-US'
|
||||
? `Added ${formatApiFormat(newEndpoint.value.api_format)} endpoint`
|
||||
: `已添加 ${formatApiFormat(newEndpoint.value.api_format)} 端点`)
|
||||
// 重置表单,保留 URL
|
||||
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
|
||||
emit('endpointCreated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '添加失败'), '错误')
|
||||
showError(localizedApiError(error, '添加失败'), legacyT('错误'))
|
||||
} finally {
|
||||
addingEndpoint.value = false
|
||||
}
|
||||
@@ -3463,10 +3498,10 @@ async function handleToggleEndpoint(endpoint: ProviderEndpoint) {
|
||||
try {
|
||||
const newStatus = !endpoint.is_active
|
||||
await updateEndpoint(endpoint.id, { is_active: newStatus })
|
||||
success(newStatus ? '端点已启用' : '端点已停用')
|
||||
success(legacyT(newStatus ? '端点已启用' : '端点已停用'))
|
||||
emit('endpointUpdated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
showError(localizedApiError(error, '操作失败'), legacyT('错误'))
|
||||
} finally {
|
||||
togglingEndpointId.value = null
|
||||
}
|
||||
@@ -3488,10 +3523,12 @@ async function confirmDeleteEndpoint() {
|
||||
|
||||
try {
|
||||
await deleteEndpoint(endpoint.id)
|
||||
success(`已删除 ${formatApiFormat(endpoint.api_format)} 端点`)
|
||||
success(locale.value === 'en-US'
|
||||
? `Deleted ${formatApiFormat(endpoint.api_format)} endpoint`
|
||||
: `已删除 ${formatApiFormat(endpoint.api_format)} 端点`)
|
||||
emit('endpointUpdated')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '删除失败'), '错误')
|
||||
showError(localizedApiError(error, '删除失败'), legacyT('错误'))
|
||||
} finally {
|
||||
deletingEndpointId.value = null
|
||||
endpointToDelete.value = null
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="isOpen"
|
||||
:title="isEditMode ? '编辑密钥' : '添加密钥'"
|
||||
:description="isEditMode ? '修改 API 密钥配置' : '为提供商添加新的 API 密钥'"
|
||||
:title="legacyT(isEditMode ? '编辑密钥' : '添加密钥')"
|
||||
:description="legacyT(isEditMode ? '修改 API 密钥配置' : '为提供商添加新的 API 密钥')"
|
||||
:icon="isEditMode ? SquarePen : Key"
|
||||
size="xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
@@ -15,13 +15,13 @@
|
||||
<!-- 基本信息 -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label :for="keyNameInputId">密钥名称 *</Label>
|
||||
<Label :for="keyNameInputId">{{ legacyT('密钥名称 *') }}</Label>
|
||||
<Input
|
||||
:id="keyNameInputId"
|
||||
v-model="form.name"
|
||||
:name="keyNameFieldName"
|
||||
required
|
||||
placeholder="例如:主 Key、备用 Key 1"
|
||||
:placeholder="legacyT('例如:主 Key、备用 Key 1')"
|
||||
maxlength="100"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
@@ -33,10 +33,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showAuthTypeSelector">
|
||||
<Label :for="authTypeSelectId">认证类型</Label>
|
||||
<Label :for="authTypeSelectId">{{ legacyT('认证类型') }}</Label>
|
||||
<Select v-model="form.auth_type">
|
||||
<SelectTrigger :id="authTypeSelectId">
|
||||
<SelectValue placeholder="选择认证类型" />
|
||||
<SelectValue :placeholder="legacyT('选择认证类型')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
@@ -61,9 +61,9 @@
|
||||
:reset-key="formNonce"
|
||||
accept=".json,.txt,application/json,text/plain"
|
||||
:multiple="false"
|
||||
drop-title="拖入 Service Account JSON 或点击选择"
|
||||
drop-hint="支持 .json / .txt,单文件导入"
|
||||
:manual-placeholder="editingKey ? '留空表示不修改,或粘贴完整的 Service Account JSON' : '粘贴完整的 Service Account JSON'"
|
||||
:drop-title="legacyT('拖入 Service Account JSON 或点击选择')"
|
||||
:drop-hint="legacyT('支持 .json / .txt,单文件导入')"
|
||||
:manual-placeholder="legacyT(editingKey ? '留空表示不修改,或粘贴完整的 Service Account JSON' : '粘贴完整的 Service Account JSON')"
|
||||
:manual-description="serviceAccountDescription"
|
||||
textarea-class="min-h-[160px] font-mono text-xs break-all !rounded-xl"
|
||||
@error="handleServiceAccountImportError"
|
||||
@@ -83,25 +83,25 @@
|
||||
v-if="editingKey && isRawSecretAuthType(form.auth_type)"
|
||||
class="text-xs text-muted-foreground mt-1"
|
||||
>
|
||||
留空表示不修改
|
||||
{{ legacyT('留空表示不修改') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注 -->
|
||||
<div>
|
||||
<Label for="note">备注</Label>
|
||||
<Label for="note">{{ legacyT('备注') }}</Label>
|
||||
<Input
|
||||
id="note"
|
||||
v-model="form.note"
|
||||
placeholder="可选的备注信息"
|
||||
:placeholder="legacyT('可选的备注信息')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- API 格式 & 认证方式 -->
|
||||
<div v-if="visibleApiFormats.length > 0">
|
||||
<div class="flex items-center gap-1 mb-1.5">
|
||||
<Label>支持的 API 格式 *</Label>
|
||||
<Label>{{ legacyT('支持的 API 格式 *') }}</Label>
|
||||
<span
|
||||
class="relative inline-flex"
|
||||
@mouseenter="apiFormatHelpHovered = true"
|
||||
@@ -110,8 +110,8 @@
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
title="API 格式说明"
|
||||
aria-label="API 格式说明"
|
||||
:title="legacyT('API 格式说明')"
|
||||
:aria-label="legacyT('API 格式说明')"
|
||||
:aria-expanded="apiFormatHelpVisible"
|
||||
@click.stop="toggleApiFormatHelp"
|
||||
@focus="apiFormatHelpHovered = true"
|
||||
@@ -125,7 +125,7 @@
|
||||
role="tooltip"
|
||||
class="absolute left-0 top-full z-[100] mt-1 w-80 rounded-md border bg-popover px-3 py-2 text-xs font-normal normal-case leading-5 tracking-normal text-popover-foreground shadow-md"
|
||||
>
|
||||
选择此密钥支持的 API 格式及对应认证方式。OpenAI 格式固定使用 Bearer Token;Claude / Gemini 格式可选 API Key 或 Bearer Token(如 Claude Code 应使用 Bearer Token)。
|
||||
{{ legacyT('选择此密钥支持的 API 格式及对应认证方式。OpenAI 格式固定使用 Bearer Token;Claude / Gemini 格式可选 API Key 或 Bearer Token(如 Claude Code 应使用 Bearer Token)。') }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -183,7 +183,7 @@
|
||||
<div
|
||||
v-if="canToggleAuthChannelMismatch(format)"
|
||||
class="flex items-center gap-1"
|
||||
title="允许客户端认证方式不一致时使用"
|
||||
:title="legacyT('允许客户端认证方式不一致时使用')"
|
||||
@click.stop
|
||||
>
|
||||
<Switch
|
||||
@@ -203,7 +203,7 @@
|
||||
<Label
|
||||
for="internal_priority"
|
||||
class="text-xs"
|
||||
>优先级</Label>
|
||||
>{{ legacyT('优先级') }}</Label>
|
||||
<Input
|
||||
id="internal_priority"
|
||||
v-model.number="form.internal_priority"
|
||||
@@ -212,51 +212,51 @@
|
||||
class="h-8"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
越小越优先
|
||||
{{ legacyT('越小越优先') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="rpm_limit"
|
||||
class="text-xs"
|
||||
>RPM 限制</Label>
|
||||
>{{ legacyT('RPM 限制') }}</Label>
|
||||
<Input
|
||||
id="rpm_limit"
|
||||
:model-value="form.rpm_limit ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
placeholder="自适应"
|
||||
:placeholder="legacyT('自适应')"
|
||||
class="h-8"
|
||||
@update:model-value="(v) => form.rpm_limit = parseNullableNumberInput(v, { min: 1, max: 10000 })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
留空自适应
|
||||
{{ legacyT('留空自适应') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="concurrent_limit"
|
||||
class="text-xs"
|
||||
>并发请求上限</Label>
|
||||
>{{ legacyT('并发请求上限') }}</Label>
|
||||
<Input
|
||||
id="concurrent_limit"
|
||||
:model-value="form.concurrent_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="不限制"
|
||||
:placeholder="legacyT('不限制')"
|
||||
class="h-8"
|
||||
@update:model-value="(v) => form.concurrent_limit = parseNullableNumberInput(v, { min: 0 })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
留空或 0 表示不限制
|
||||
{{ legacyT('留空或 0 表示不限制') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="cache_ttl_minutes"
|
||||
class="text-xs"
|
||||
>缓存 TTL</Label>
|
||||
>{{ legacyT('缓存 TTL') }}</Label>
|
||||
<Input
|
||||
id="cache_ttl_minutes"
|
||||
:model-value="form.cache_ttl_minutes ?? ''"
|
||||
@@ -267,14 +267,14 @@
|
||||
@update:model-value="(v) => form.cache_ttl_minutes = parseNumberInput(v, { min: 0, max: 60 }) ?? 5"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
分钟,0禁用
|
||||
{{ legacyT('分钟,0禁用') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="max_probe_interval_minutes"
|
||||
class="text-xs"
|
||||
>熔断探测</Label>
|
||||
>{{ legacyT('熔断探测') }}</Label>
|
||||
<Input
|
||||
id="max_probe_interval_minutes"
|
||||
:model-value="form.max_probe_interval_minutes ?? ''"
|
||||
@@ -286,7 +286,7 @@
|
||||
@update:model-value="(v) => form.max_probe_interval_minutes = parseNumberInput(v, { min: 0, max: 32 }) ?? 32"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
分钟,0-32
|
||||
{{ legacyT('分钟,0-32') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,9 +295,9 @@
|
||||
<div class="space-y-3 py-2 px-3 rounded-md border border-border/60 bg-muted/30">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<Label class="text-sm font-medium">自动获取上游可用模型</Label>
|
||||
<Label class="text-sm font-medium">{{ legacyT('自动获取上游可用模型') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
定时更新上游模型, 配合模型映射使用
|
||||
{{ legacyT('定时更新上游模型, 配合模型映射使用') }}
|
||||
</p>
|
||||
<p
|
||||
v-if="showAutoFetchWarning"
|
||||
@@ -316,15 +316,15 @@
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label class="text-xs">包含规则</Label>
|
||||
<Label class="text-xs">{{ legacyT('包含规则') }}</Label>
|
||||
<Input
|
||||
v-model="form.model_include_patterns_text"
|
||||
placeholder="gpt-*, claude-*, 留空包含全部"
|
||||
:placeholder="legacyT('gpt-*, claude-*, 留空包含全部')"
|
||||
class="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="text-xs">排除规则</Label>
|
||||
<Label class="text-xs">{{ legacyT('排除规则') }}</Label>
|
||||
<Input
|
||||
v-model="form.model_exclude_patterns_text"
|
||||
placeholder="*-preview, *-beta"
|
||||
@@ -333,7 +333,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
逗号分隔,支持 * ? 通配符,不区分大小写
|
||||
{{ legacyT('逗号分隔,支持 * ? 通配符,不区分大小写') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,13 +344,13 @@
|
||||
variant="outline"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
{{ legacyT('取消') }}
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="saving || !canSave"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ saving ? (isEditMode ? '保存中...' : '添加中...') : (isEditMode ? '保存' : '添加') }}
|
||||
{{ submitLabel }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -373,6 +373,7 @@ import {
|
||||
import { Key, SquarePen, CircleHelp } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput, parseNullableNumberInput } from '@/utils/form'
|
||||
import JsonImportInput from '@/components/common/JsonImportInput.vue'
|
||||
@@ -410,6 +411,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { legacyT, locale } = useI18n()
|
||||
|
||||
function isRawSecretAuthType(authType: string | null | undefined): authType is RawSecretAuthType {
|
||||
return authType === 'api_key' || authType === 'bearer'
|
||||
@@ -549,7 +551,7 @@ function toggleApiFormatHelp() {
|
||||
const authSecretLabel = computed(() => {
|
||||
if (form.value.auth_type === 'service_account') return 'Service Account JSON'
|
||||
if (form.value.auth_type === 'bearer') return 'Bearer Token'
|
||||
return 'API 密钥'
|
||||
return legacyT('API 密钥')
|
||||
})
|
||||
|
||||
const authSecretPlaceholder = computed(() =>
|
||||
@@ -618,11 +620,18 @@ function buildAllowAuthChannelMismatchFormatsPayload(): string[] {
|
||||
}
|
||||
|
||||
const serviceAccountDescription = computed(() => (
|
||||
props.editingKey
|
||||
legacyT(props.editingKey
|
||||
? '留空表示不修改;JSON 格式,包含 project_id、private_key 等字段'
|
||||
: 'JSON 格式,包含 project_id、private_key 等字段'
|
||||
: 'JSON 格式,包含 project_id、private_key 等字段')
|
||||
))
|
||||
|
||||
const submitLabel = computed(() => {
|
||||
if (saving.value) {
|
||||
return legacyT(isEditMode.value ? '保存中...' : '添加中...')
|
||||
}
|
||||
return legacyT(isEditMode.value ? '保存' : '添加')
|
||||
})
|
||||
|
||||
// 默认认证类型
|
||||
function getDefaultAuthType(): ProviderKeyFormAuthType {
|
||||
return authTypeOptions.value[0]?.value || 'api_key'
|
||||
@@ -653,7 +662,10 @@ const autoFetchWarningMessage = computed(() => {
|
||||
? props.editingKey.allowed_models
|
||||
: []
|
||||
if (models.length === 0) return ''
|
||||
return `当前 Key 模型权限存在以下模型:${models.map(model => `“${model}”`).join('、')},开启自动获取后将被覆盖`
|
||||
const formattedModels = models.map(model => locale.value === 'en-US' ? `"${model}"` : `“${model}”`).join(locale.value === 'en-US' ? ', ' : '、')
|
||||
return locale.value === 'en-US'
|
||||
? `Current key model permissions include these models: ${formattedModels}. Enabling auto fetch will overwrite them.`
|
||||
: `当前 Key 模型权限存在以下模型:${formattedModels},开启自动获取后将被覆盖`
|
||||
})
|
||||
|
||||
// 检查是否正在切换认证类型
|
||||
@@ -896,32 +908,32 @@ function parseAuthConfig(): Record<string, unknown> | null {
|
||||
}
|
||||
|
||||
function handleServiceAccountImportError(payload: { message: string, title?: string }) {
|
||||
showError(payload.message, payload.title || '错误')
|
||||
showError(payload.message, payload.title ? legacyT(payload.title) : legacyT('错误'))
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
// 必须有 providerId
|
||||
if (!props.providerId) {
|
||||
showError('无法保存:缺少提供商信息', '错误')
|
||||
showError(legacyT('无法保存:缺少提供商信息'), legacyT('错误'))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证认证信息
|
||||
if (form.value.auth_type === 'service_account') {
|
||||
if (!props.editingKey && !form.value.auth_config_text.trim()) {
|
||||
showError('请输入 Service Account JSON', '验证失败')
|
||||
showError(legacyT('请输入 Service Account JSON'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
// 验证 JSON 格式
|
||||
if (form.value.auth_config_text.trim()) {
|
||||
const parsed = parseAuthConfig()
|
||||
if (!parsed) {
|
||||
showError('Service Account JSON 格式无效', '验证失败')
|
||||
showError(legacyT('Service Account JSON 格式无效'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
// 验证必要字段
|
||||
if (!parsed.client_email || !parsed.private_key || !parsed.project_id) {
|
||||
showError('Service Account JSON 缺少必要字段 (client_email, private_key, project_id)', '验证失败')
|
||||
showError(legacyT('Service Account JSON 缺少必要字段 (client_email, private_key, project_id)'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -934,7 +946,7 @@ async function handleSave() {
|
||||
|
||||
// 验证至少选择一个 API 格式
|
||||
if (form.value.api_formats.length === 0) {
|
||||
showError('请至少选择一个 API 格式', '验证失败')
|
||||
showError(legacyT('请至少选择一个 API 格式'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -990,7 +1002,7 @@ async function handleSave() {
|
||||
}
|
||||
|
||||
await updateProviderKey(props.editingKey.id, updateData)
|
||||
success('密钥已更新', '成功')
|
||||
success(legacyT('密钥已更新'), legacyT('成功'))
|
||||
} else {
|
||||
// 新增模式
|
||||
await addProviderKey(props.providerId, {
|
||||
@@ -1013,7 +1025,7 @@ async function handleSave() {
|
||||
model_exclude_patterns: parsePatternText(form.value.model_exclude_patterns_text)
|
||||
})
|
||||
|
||||
success('密钥已添加', '成功')
|
||||
success(legacyT('密钥已添加'), legacyT('成功'))
|
||||
// 添加模式:不关闭对话框,只清除名称和密钥以便继续添加
|
||||
emit('saved')
|
||||
clearForNextAdd()
|
||||
@@ -1023,8 +1035,8 @@ async function handleSave() {
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '保存密钥失败')
|
||||
showError(errorMessage, '错误')
|
||||
const errorMessage = parseApiError(err, legacyT('保存密钥失败'))
|
||||
showError(errorMessage, legacyT('错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="isOpen"
|
||||
title="添加账号"
|
||||
:title="legacyT('添加账号')"
|
||||
:icon="UserPlus"
|
||||
size="md"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
@@ -18,7 +18,7 @@
|
||||
:class="selectedProxyNodeId
|
||||
? 'text-blue-500 bg-blue-500/10 hover:bg-blue-500/20'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'"
|
||||
:title="selectedProxyNodeId ? `代理: ${getSelectedNodeLabel()}` : '设置代理节点'"
|
||||
:title="selectedProxyNodeId ? `${legacyT('代理')}: ${getSelectedNodeLabel()}` : legacyT('设置代理节点')"
|
||||
>
|
||||
<Globe class="w-4 h-4" />
|
||||
</button>
|
||||
@@ -31,18 +31,18 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-xs font-medium">代理节点</span>
|
||||
<span class="text-xs font-medium">{{ legacyT('代理节点') }}</span>
|
||||
<span
|
||||
v-if="!proxyNodesStore.loading && proxyNodesStore.onlineNodes.length === 0"
|
||||
class="text-[10px] text-muted-foreground"
|
||||
>· 前往「模块管理 · 代理节点」添加</span>
|
||||
>· {{ legacyT('前往「模块管理 · 代理节点」添加') }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="selectedProxyNodeId"
|
||||
class="text-[10px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
@click="selectedProxyNodeId = ''; proxyPopoverOpen = false"
|
||||
>
|
||||
清除
|
||||
{{ legacyT('清除') }}
|
||||
</button>
|
||||
</div>
|
||||
<ProxyNodeSelect
|
||||
@@ -51,7 +51,7 @@
|
||||
@update:model-value="(v: string) => { selectedProxyNodeId = v; proxyPopoverOpen = false }"
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground">
|
||||
{{ selectedProxyNodeId ? `${providerCredentialActionLabel}、刷新、额度查询均走此代理` : '未设置,依次回退到提供商代理 → 系统代理' }}
|
||||
{{ selectedProxyNodeId ? proxyUsageDescription : legacyT('未设置,依次回退到提供商代理 → 系统代理') }}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
@@ -73,7 +73,7 @@
|
||||
]"
|
||||
@click="switchMode('oauth')"
|
||||
>
|
||||
{{ isDeviceBrowserProvider ? (isWindsurfProvider ? '浏览器登录' : '设备授权') : '获取授权' }}
|
||||
{{ authorizationModeLabel }}
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all"
|
||||
@@ -110,7 +110,7 @@
|
||||
: 'border-border text-muted-foreground hover:text-foreground hover:border-foreground/20'"
|
||||
@click="selectWindsurfLoginOption(opt.key)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
{{ legacyT(opt.label) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -124,10 +124,10 @@
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-destructive">
|
||||
{{ device.status === 'expired' ? '授权已过期' : '授权失败' }}
|
||||
{{ legacyT(device.status === 'expired' ? '授权已过期' : '授权失败') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ device.error || '请重试' }}
|
||||
{{ legacyT(device.error || '请重试') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -135,7 +135,7 @@
|
||||
variant="outline"
|
||||
@click="resetDevice"
|
||||
>
|
||||
重新开始
|
||||
{{ legacyT('重新开始') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto mb-3" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
正在准备登录...
|
||||
{{ legacyT('正在准备登录...') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +159,7 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center justify-center w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] font-semibold shrink-0">1</span>
|
||||
<span class="text-xs font-medium">前往登录</span>
|
||||
<span class="text-xs font-medium">{{ legacyT('前往登录') }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2 pl-6">
|
||||
<Button
|
||||
@@ -168,7 +168,7 @@
|
||||
@click="openDeviceVerificationUrl"
|
||||
>
|
||||
<ExternalLink class="w-3 h-3 mr-1" />
|
||||
打开
|
||||
{{ legacyT('打开') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -177,7 +177,7 @@
|
||||
@click="copyToClipboard(device.verification_uri_complete)"
|
||||
>
|
||||
<Copy class="w-3 h-3 mr-1" />
|
||||
复制
|
||||
{{ legacyT('复制') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!device.session_id"
|
||||
@@ -186,7 +186,7 @@
|
||||
:disabled="device.starting"
|
||||
@click="startDeviceAuth"
|
||||
>
|
||||
开始
|
||||
{{ legacyT('开始') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,7 +194,7 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center justify-center w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] font-semibold shrink-0">2</span>
|
||||
<span class="text-xs font-medium">粘贴回调 URL 或 token</span>
|
||||
<span class="text-xs font-medium">{{ legacyT('粘贴回调 URL 或 token') }}</span>
|
||||
</div>
|
||||
<div class="pl-6">
|
||||
<Textarea
|
||||
@@ -210,7 +210,7 @@
|
||||
class="pl-6 flex items-center gap-1.5 text-[11px] text-muted-foreground"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-[1.5px] border-primary/30 border-t-primary" />
|
||||
<span>会话剩余 {{ deviceCountdownFormatted }}</span>
|
||||
<span>{{ sessionRemainingText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,10 +253,10 @@
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-destructive">
|
||||
{{ device.status === 'expired' ? '授权已过期' : '授权失败' }}
|
||||
{{ legacyT(device.status === 'expired' ? '授权已过期' : '授权失败') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ device.error || '请重试' }}
|
||||
{{ legacyT(device.error || '请重试') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -264,7 +264,7 @@
|
||||
variant="outline"
|
||||
@click="resetDevice"
|
||||
>
|
||||
重新开始
|
||||
{{ legacyT('重新开始') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,7 +277,7 @@
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto mb-3" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
正在注册设备...
|
||||
{{ legacyT('正在注册设备...') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,7 +290,7 @@
|
||||
<div class="space-y-2 shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center justify-center w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] font-semibold shrink-0">1</span>
|
||||
<span class="text-xs font-medium">前往授权</span>
|
||||
<span class="text-xs font-medium">{{ legacyT('前往授权') }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2 pl-6">
|
||||
<Button
|
||||
@@ -299,7 +299,7 @@
|
||||
@click="openDeviceVerificationUrl"
|
||||
>
|
||||
<ExternalLink class="w-3 h-3 mr-1" />
|
||||
打开
|
||||
{{ legacyT('打开') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -308,7 +308,7 @@
|
||||
@click="copyToClipboard(device.verification_uri_complete)"
|
||||
>
|
||||
<Copy class="w-3 h-3 mr-1" />
|
||||
复制
|
||||
{{ legacyT('复制') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,7 +316,7 @@
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center justify-center w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] font-semibold shrink-0">2</span>
|
||||
<span class="text-xs font-medium">粘贴回调 URL</span>
|
||||
<span class="text-xs font-medium">{{ legacyT('粘贴回调 URL') }}</span>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 pl-6">
|
||||
<Textarea
|
||||
@@ -345,16 +345,16 @@
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium">
|
||||
在浏览器中完成授权
|
||||
{{ legacyT('在浏览器中完成授权') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
授权完成后此页面将自动更新
|
||||
{{ legacyT('授权完成后此页面将自动更新') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-[1.5px] border-primary/30 border-t-primary" />
|
||||
<span>剩余 {{ deviceCountdownFormatted }}</span>
|
||||
<span>{{ remainingText }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -364,7 +364,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<ShieldCheck class="w-3.5 h-3.5 text-primary" />
|
||||
<span class="text-[10px] text-muted-foreground">MFA 验证码</span>
|
||||
<span class="text-[10px] text-muted-foreground">{{ legacyT('MFA 验证码') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
@@ -372,7 +372,7 @@
|
||||
>{{ totp.code.value }}</span>
|
||||
<button
|
||||
class="p-1 rounded hover:bg-muted transition-colors"
|
||||
title="复制验证码"
|
||||
:title="legacyT('复制验证码')"
|
||||
@click="copyToClipboard(totp.code.value)"
|
||||
>
|
||||
<Copy class="w-3 h-3 text-muted-foreground" />
|
||||
@@ -401,7 +401,7 @@
|
||||
@click="openDeviceVerificationUrl"
|
||||
>
|
||||
<ExternalLink class="w-3.5 h-3.5 mr-1.5" />
|
||||
打开授权页面
|
||||
{{ legacyT('打开授权页面') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -423,14 +423,14 @@
|
||||
v-if="isSocialDeviceAuth"
|
||||
class="text-xs text-muted-foreground text-center"
|
||||
>
|
||||
授权后复制浏览器地址栏的 localhost 回调 URL。
|
||||
{{ legacyT('授权后复制浏览器地址栏的 localhost 回调 URL。') }}
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-else-if="device.auth_type === 'builder_id'"
|
||||
class="text-xs text-muted-foreground text-center"
|
||||
>
|
||||
使用个人 AWS Builder ID 进行设备授权,无需额外配置。
|
||||
{{ legacyT('使用个人 AWS Builder ID 进行设备授权,无需额外配置。') }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
@@ -458,7 +458,7 @@
|
||||
<ComboboxAnchor class="relative w-full">
|
||||
<ComboboxInput
|
||||
:display-value="() => device.region"
|
||||
placeholder="输入或选择 Region"
|
||||
:placeholder="legacyT('输入或选择 Region')"
|
||||
class="w-full h-8 px-2 pr-7 text-xs rounded-md border border-border bg-background font-mono focus:outline-none focus:ring-1 focus:ring-ring focus:relative focus:z-10"
|
||||
spellcheck="false"
|
||||
@input="(e: Event) => regionSearch = (e.target as HTMLInputElement).value"
|
||||
@@ -474,7 +474,7 @@
|
||||
>
|
||||
<ComboboxViewport>
|
||||
<ComboboxEmpty class="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
{{ awsRegionsLoaded ? '无匹配结果,回车使用自定义值' : '加载中...' }}
|
||||
{{ awsRegionsLoaded ? legacyT('无匹配结果,回车使用自定义值') : legacyT('加载中...') }}
|
||||
</ComboboxEmpty>
|
||||
<ComboboxItem
|
||||
v-for="r in filteredRegions"
|
||||
@@ -493,7 +493,7 @@
|
||||
</ComboboxRoot>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">TOTP Secret (可选, 2FA认证)</label>
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ legacyT('TOTP Secret (可选, 2FA认证)') }}</label>
|
||||
<input
|
||||
v-model="device.totp_secret"
|
||||
type="text"
|
||||
@@ -509,7 +509,7 @@
|
||||
:disabled="device.starting || (device.auth_type === 'identity_center' && !device.start_url.trim())"
|
||||
@click="startDeviceAuth"
|
||||
>
|
||||
{{ device.starting ? '正在准备授权...' : '开始授权' }}
|
||||
{{ device.starting ? legacyT('正在准备授权...') : legacyT('开始授权') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -525,7 +525,7 @@
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto mb-3" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
正在准备授权...
|
||||
{{ legacyT('正在准备授权...') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -534,7 +534,7 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center justify-center w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] font-semibold shrink-0">1</span>
|
||||
<span class="text-xs font-medium">前往授权</span>
|
||||
<span class="text-xs font-medium">{{ legacyT('前往授权') }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2 pl-6">
|
||||
<Button
|
||||
@@ -543,7 +543,7 @@
|
||||
@click="openAuthorizationUrl"
|
||||
>
|
||||
<ExternalLink class="w-3 h-3 mr-1" />
|
||||
打开
|
||||
{{ legacyT('打开') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -552,7 +552,7 @@
|
||||
@click="copyToClipboard(oauth.authorization_url)"
|
||||
>
|
||||
<Copy class="w-3 h-3 mr-1" />
|
||||
复制
|
||||
{{ legacyT('复制') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,7 +560,7 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center justify-center w-4 h-4 rounded-full bg-primary/10 text-primary text-[10px] font-semibold shrink-0">2</span>
|
||||
<span class="text-xs font-medium">粘贴回调 URL</span>
|
||||
<span class="text-xs font-medium">{{ legacyT('粘贴回调 URL') }}</span>
|
||||
</div>
|
||||
<div class="pl-6">
|
||||
<Textarea
|
||||
@@ -597,9 +597,9 @@
|
||||
: 'text-muted-foreground hover:text-foreground'"
|
||||
:disabled="importing"
|
||||
@click="setWindsurfImportMethod(method.key)"
|
||||
>
|
||||
{{ method.label }}
|
||||
</button>
|
||||
>
|
||||
{{ legacyT(method.label) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -607,7 +607,7 @@
|
||||
class="space-y-3"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">邮箱</label>
|
||||
<label class="text-xs font-medium">{{ legacyT('邮箱') }}</label>
|
||||
<input
|
||||
v-model="windsurfEmail"
|
||||
type="email"
|
||||
@@ -619,24 +619,24 @@
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">密码</label>
|
||||
<label class="text-xs font-medium">{{ legacyT('密码') }}</label>
|
||||
<input
|
||||
v-model="windsurfPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:disabled="importing"
|
||||
placeholder="Windsurf 密码"
|
||||
:placeholder="legacyT('Windsurf 密码')"
|
||||
class="w-full h-9 px-2.5 text-xs rounded-md border border-border bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:relative focus:z-10"
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">名称(可选)</label>
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ legacyT('名称(可选)') }}</label>
|
||||
<input
|
||||
v-model="windsurfAccountName"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="importing"
|
||||
placeholder="未填写时使用邮箱"
|
||||
:placeholder="legacyT('未填写时使用邮箱')"
|
||||
class="w-full h-9 px-2.5 text-xs rounded-md border border-border bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:relative focus:z-10"
|
||||
spellcheck="false"
|
||||
>
|
||||
@@ -677,8 +677,8 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span>进度 {{ importTask.processed }}/{{ importTask.total }}</span>
|
||||
<span>成功 {{ importTask.success }} · 失败 {{ importTask.failed }}</span>
|
||||
<span>{{ importProgressText(importTask) }}</span>
|
||||
<span>{{ importResultSummaryText(importTask) }}</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="importTaskMessageText"
|
||||
@@ -691,14 +691,14 @@
|
||||
class="space-y-1"
|
||||
>
|
||||
<p class="text-[11px] text-destructive">
|
||||
最近错误
|
||||
{{ legacyT('最近错误') }}
|
||||
</p>
|
||||
<p
|
||||
v-for="item in importTask.error_samples.slice(0, 3)"
|
||||
:key="`${item.index}-${item.error || item.status}`"
|
||||
class="text-[11px] text-destructive/90"
|
||||
>
|
||||
#{{ item.index + 1 }} {{ item.error || '导入失败' }}
|
||||
{{ importErrorSampleText(item) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -711,21 +711,21 @@
|
||||
variant="outline"
|
||||
@click="handleClose"
|
||||
>
|
||||
取消
|
||||
{{ legacyT('取消') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="mode === 'oauth' && showAuthorizationMode && !isDeviceBrowserProvider"
|
||||
:disabled="!canCompleteOAuth"
|
||||
@click="handleCompleteOAuth"
|
||||
>
|
||||
{{ oauth.completing ? '验证中...' : '验证' }}
|
||||
{{ oauth.completing ? legacyT('验证中...') : legacyT('验证') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="mode === 'oauth' && isManualDeviceCallbackMode"
|
||||
:disabled="!canCompleteDeviceAuth"
|
||||
@click="completeDeviceAuth"
|
||||
>
|
||||
{{ device.completing ? '验证中...' : '验证' }}
|
||||
{{ device.completing ? legacyT('验证中...') : legacyT('验证') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="mode === 'import'"
|
||||
@@ -756,6 +756,7 @@ import { useToast } from '@/composables/useToast'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useTotp } from '@/composables/useTotp'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useI18n } from '@/i18n'
|
||||
import {
|
||||
startProviderLevelOAuth,
|
||||
completeProviderLevelOAuth,
|
||||
@@ -788,6 +789,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
const { legacyT, locale } = useI18n()
|
||||
const proxyNodesStore = useProxyNodesStore()
|
||||
const totp = useTotp()
|
||||
|
||||
@@ -834,6 +836,14 @@ function getSelectedNodeLabel(): string {
|
||||
return node ? node.name : `${selectedProxyNodeId.value.slice(0, 8) }...`
|
||||
}
|
||||
|
||||
function isEnglishLocale(): boolean {
|
||||
return locale.value === 'en-US'
|
||||
}
|
||||
|
||||
function localizedApiError(error: unknown, fallback: string): string {
|
||||
return legacyT(parseApiError(error, fallback))
|
||||
}
|
||||
|
||||
// 模式
|
||||
type DialogMode = 'oauth' | 'import'
|
||||
const mode = ref<DialogMode>((props.providerType || '').toLowerCase() === 'grok' ? 'import' : 'oauth')
|
||||
@@ -959,9 +969,15 @@ const isManualDeviceCallbackPending = computed(() =>
|
||||
&& device.value.status === 'pending'
|
||||
)
|
||||
|
||||
const authorizationModeLabel = computed(() => {
|
||||
if (isWindsurfProvider.value) return legacyT('浏览器登录')
|
||||
if (isDeviceBrowserProvider.value) return legacyT('设备授权')
|
||||
return legacyT('获取授权')
|
||||
})
|
||||
|
||||
const deviceCallbackPlaceholder = computed(() =>
|
||||
isWindsurfProvider.value
|
||||
? `粘贴包含 token=...&state=... 的回调 URL;session token/apiKey 也可直接粘贴,普通 token 请用导入授权`
|
||||
? legacyT('粘贴包含 token=...&state=... 的回调 URL;session token/apiKey 也可直接粘贴,普通 token 请用导入授权')
|
||||
: `http://localhost:49153/oauth/callback?login_option=${device.value.auth_type}&code=...&state=...`
|
||||
)
|
||||
|
||||
@@ -972,6 +988,18 @@ const deviceCountdownFormatted = computed(() => {
|
||||
return `${min}:${String(sec).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
const sessionRemainingText = computed(() => (
|
||||
isEnglishLocale()
|
||||
? `Session remaining ${deviceCountdownFormatted.value}`
|
||||
: `会话剩余 ${deviceCountdownFormatted.value}`
|
||||
))
|
||||
|
||||
const remainingText = computed(() => (
|
||||
isEnglishLocale()
|
||||
? `${deviceCountdownFormatted.value} remaining`
|
||||
: `剩余 ${deviceCountdownFormatted.value}`
|
||||
))
|
||||
|
||||
const oauthBusy = computed(() =>
|
||||
oauth.value.starting || oauth.value.completing
|
||||
)
|
||||
@@ -997,33 +1025,42 @@ const canImport = computed(() => {
|
||||
return importText.value.trim().length > 0 && !importing.value
|
||||
})
|
||||
|
||||
const importModeLabel = computed(() => (isGrokProvider.value ? '导入账号' : '导入授权'))
|
||||
const importButtonLabel = computed(() => (isGrokProvider.value ? '导入账号' : '导入'))
|
||||
const importModeLabel = computed(() => legacyT(isGrokProvider.value ? '导入账号' : '导入授权'))
|
||||
const importButtonLabel = computed(() => legacyT(isGrokProvider.value ? '导入账号' : '导入'))
|
||||
const importDropTitle = computed(() => (
|
||||
isGrokProvider.value ? '拖入 Grok 账号文件或点击选择' : '拖入授权文件或点击选择'
|
||||
legacyT(isGrokProvider.value ? '拖入 Grok 账号文件或点击选择' : '拖入授权文件或点击选择')
|
||||
))
|
||||
const importDropHint = computed(() => (
|
||||
isGrokProvider.value ? '支持 .json / .txt,可多选、批量导入' : '支持 .json / .txt,可多选'
|
||||
legacyT(isGrokProvider.value ? '支持 .json / .txt,可多选、批量导入' : '支持 .json / .txt,可多选')
|
||||
))
|
||||
const importManualPlaceholder = computed(() => (
|
||||
isGrokProvider.value
|
||||
? '粘贴 Grok sso/session token,支持每行一个;或粘贴包含 token、sso_token、access_token、plan_type、pool_tier 的 JSON'
|
||||
? legacyT('粘贴 Grok sso/session token,支持每行一个;或粘贴包含 token、sso_token、access_token、plan_type、pool_tier 的 JSON')
|
||||
: isWindsurfProvider.value
|
||||
? '粘贴 show-auth-token Token、API key 或 JSON 内容'
|
||||
: '粘贴 Refresh Token / Access Token 或 JSON 内容'
|
||||
? legacyT('粘贴 show-auth-token Token、API key 或 JSON 内容')
|
||||
: legacyT('粘贴 Refresh Token / Access Token 或 JSON 内容')
|
||||
))
|
||||
const importManualDescription = computed(() => (
|
||||
isGrokProvider.value
|
||||
? 'plan_type / pool_tier 会作为账号套餐与能力特征保存,不是路由池选择。'
|
||||
? legacyT('plan_type / pool_tier 会作为账号套餐与能力特征保存,不是路由池选择。')
|
||||
: ''
|
||||
))
|
||||
const importPasteToggleText = computed(() => (
|
||||
isGrokProvider.value ? '或手动粘贴 Grok Token' : '或手动粘贴 Token'
|
||||
legacyT(isGrokProvider.value ? '或手动粘贴 Grok Token' : '或手动粘贴 Token')
|
||||
))
|
||||
const importFileToggleText = computed(() => (
|
||||
isGrokProvider.value ? '或选择 Grok Token 文件导入' : '或选择 JSON 文件导入'
|
||||
legacyT(isGrokProvider.value ? '或选择 Grok Token 文件导入' : '或选择 JSON 文件导入')
|
||||
))
|
||||
const providerCredentialActionLabel = computed(() => (isGrokProvider.value ? '导入' : '授权'))
|
||||
const proxyUsageDescription = computed(() => {
|
||||
if (isEnglishLocale()) {
|
||||
return isGrokProvider.value
|
||||
? 'Import, refresh, and quota queries use this proxy'
|
||||
: 'Authorization, refresh, and quota queries use this proxy'
|
||||
}
|
||||
return isGrokProvider.value
|
||||
? '导入、刷新、额度查询均走此代理'
|
||||
: '授权、刷新、额度查询均走此代理'
|
||||
})
|
||||
const isWindsurfEmailPasswordImport = computed(() =>
|
||||
isWindsurfProvider.value && windsurfImportMethod.value === 'email_password'
|
||||
)
|
||||
@@ -1031,19 +1068,35 @@ const isWindsurfEmailPasswordImport = computed(() =>
|
||||
const importButtonText = computed(() => {
|
||||
if (importing.value) {
|
||||
return importTask.value && !isWindsurfEmailPasswordImport.value
|
||||
? `导入中 ${importTask.value.progress_percent}%`
|
||||
: '导入中...'
|
||||
? (isEnglishLocale() ? `Importing ${importTask.value.progress_percent}%` : `导入中 ${importTask.value.progress_percent}%`)
|
||||
: legacyT('导入中...')
|
||||
}
|
||||
return isWindsurfEmailPasswordImport.value ? '登录并导入' : importButtonLabel.value
|
||||
return isWindsurfEmailPasswordImport.value ? legacyT('登录并导入') : importButtonLabel.value
|
||||
})
|
||||
|
||||
const importTaskMessageText = computed(() => {
|
||||
const message = importTask.value?.message?.trim()
|
||||
if (!message) return ''
|
||||
// 后端进度 message 已由“进度 x/y”展示,避免在导入中重复显示“处理中 x/y”。
|
||||
return redundantImportTaskMessagePattern.test(message) ? '' : message
|
||||
return redundantImportTaskMessagePattern.test(message) ? '' : legacyT(message)
|
||||
})
|
||||
|
||||
function importProgressText(task: OAuthBatchImportTaskStatusResponse): string {
|
||||
return isEnglishLocale()
|
||||
? `Progress ${task.processed}/${task.total}`
|
||||
: `进度 ${task.processed}/${task.total}`
|
||||
}
|
||||
|
||||
function importResultSummaryText(task: OAuthBatchImportTaskStatusResponse): string {
|
||||
return isEnglishLocale()
|
||||
? `Success ${task.success} · Failed ${task.failed}`
|
||||
: `成功 ${task.success} · 失败 ${task.failed}`
|
||||
}
|
||||
|
||||
function importErrorSampleText(item: OAuthBatchImportTaskStatusResponse['error_samples'][number]): string {
|
||||
return `#${item.index + 1} ${legacyT(item.error || '导入失败')}`
|
||||
}
|
||||
|
||||
function stopImportPolling() {
|
||||
if (importPollTimer) {
|
||||
clearTimeout(importPollTimer)
|
||||
@@ -1055,15 +1108,15 @@ function stopImportPolling() {
|
||||
function getImportTaskStatusText(status: OAuthBatchImportTaskStatus): string {
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
return '任务已提交'
|
||||
return legacyT('任务已提交')
|
||||
case 'processing':
|
||||
return '正在导入'
|
||||
return legacyT('正在导入')
|
||||
case 'completed':
|
||||
return '导入完成'
|
||||
return legacyT('导入完成')
|
||||
case 'failed':
|
||||
return '导入失败'
|
||||
return legacyT('导入失败')
|
||||
default:
|
||||
return '处理中'
|
||||
return legacyT('处理中')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,6 +1126,19 @@ function getOAuthSuccessMessage(
|
||||
): string {
|
||||
const email = typeof options?.email === 'string' ? options.email.trim() : ''
|
||||
const replaced = options?.replaced === true
|
||||
const actionText = legacyT(action)
|
||||
|
||||
if (isEnglishLocale()) {
|
||||
if (email) {
|
||||
return replaced
|
||||
? `${actionText} succeeded: ${email} (replaced existing account)`
|
||||
: `${actionText} succeeded: ${email}`
|
||||
}
|
||||
return replaced
|
||||
? `${actionText} succeeded; replaced existing account`
|
||||
: `${actionText} succeeded; account added`
|
||||
}
|
||||
|
||||
if (email) {
|
||||
return replaced
|
||||
? `${action}成功: ${email}(已替换旧账号)`
|
||||
@@ -1089,27 +1155,36 @@ function getBatchImportSuccessMessage(task: OAuthBatchImportTaskStatusResponse):
|
||||
const parts: string[] = []
|
||||
|
||||
if (createdCount > 0) {
|
||||
parts.push(`新增 ${createdCount} 个`)
|
||||
parts.push(isEnglishLocale() ? `${createdCount} added` : `新增 ${createdCount} 个`)
|
||||
}
|
||||
if (replacedCount > 0) {
|
||||
parts.push(`替换 ${replacedCount} 个`)
|
||||
parts.push(isEnglishLocale() ? `${replacedCount} replaced` : `替换 ${replacedCount} 个`)
|
||||
}
|
||||
if (task.failed > 0) {
|
||||
parts.push(`失败 ${task.failed} 个`)
|
||||
parts.push(isEnglishLocale() ? `${task.failed} failed` : `失败 ${task.failed} 个`)
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
if (isEnglishLocale()) {
|
||||
return task.failed > 0 ? `Batch import complete: ${task.failed} failed` : 'Batch import complete'
|
||||
}
|
||||
return task.failed > 0 ? `批量导入完成:失败 ${task.failed} 个` : '批量导入完成'
|
||||
}
|
||||
if (task.failed === 0 && createdCount > 0 && replacedCount === 0) {
|
||||
if (isEnglishLocale()) return `Batch import succeeded: ${createdCount} accounts added`
|
||||
return `批量导入成功:${createdCount} 个账号已添加`
|
||||
}
|
||||
if (task.failed === 0 && createdCount === 0 && replacedCount > 0) {
|
||||
if (isEnglishLocale()) return `Batch import succeeded: ${replacedCount} existing accounts replaced`
|
||||
return `批量导入成功:已替换 ${replacedCount} 个旧账号`
|
||||
}
|
||||
|
||||
const prefix = task.failed > 0 ? '批量导入完成' : '批量导入成功'
|
||||
return `${prefix}:${parts.join(',')}`
|
||||
const prefix = task.failed > 0
|
||||
? legacyT('批量导入完成')
|
||||
: legacyT('批量导入成功')
|
||||
return isEnglishLocale()
|
||||
? `${prefix}: ${parts.join(', ')}`
|
||||
: `${prefix}:${parts.join(',')}`
|
||||
}
|
||||
|
||||
function scheduleImportPoll(taskId: string, delayMs = 1200) {
|
||||
@@ -1135,7 +1210,7 @@ async function pollImportTaskStatus(taskId: string) {
|
||||
emit('saved')
|
||||
handleClose()
|
||||
} else {
|
||||
showError(task.error || '批量导入失败', '导入失败')
|
||||
showError(legacyT(task.error || '批量导入失败'), legacyT('导入失败'))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1143,7 +1218,7 @@ async function pollImportTaskStatus(taskId: string) {
|
||||
if (task.status === 'failed') {
|
||||
stopImportPolling()
|
||||
importing.value = false
|
||||
showError(task.error || task.message || '批量导入失败', '导入失败')
|
||||
showError(legacyT(task.error || task.message || '批量导入失败'), legacyT('导入失败'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1305,8 +1380,8 @@ async function initOAuth() {
|
||||
oauth.value.provider_type = resp.provider_type
|
||||
} catch (err: unknown) {
|
||||
if (requestId !== oauthInitRequestId) return
|
||||
const errorMessage = parseApiError(err, '初始化授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
const errorMessage = localizedApiError(err, '初始化授权失败')
|
||||
showError(errorMessage, legacyT('错误'))
|
||||
mode.value = 'import'
|
||||
} finally {
|
||||
if (requestId === oauthInitRequestId) {
|
||||
@@ -1331,8 +1406,8 @@ async function handleCompleteOAuth() {
|
||||
handleClose()
|
||||
} catch (err: unknown) {
|
||||
if (requestId !== oauthCompleteRequestId) return
|
||||
const errorMessage = parseApiError(err, '完成授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
const errorMessage = localizedApiError(err, '完成授权失败')
|
||||
showError(errorMessage, legacyT('错误'))
|
||||
} finally {
|
||||
if (requestId === oauthCompleteRequestId) {
|
||||
oauth.value.completing = false
|
||||
@@ -1618,7 +1693,7 @@ function decodeBase64Url(value: string): string {
|
||||
}
|
||||
|
||||
function handleImportInputError(payload: { message: string; title?: string }) {
|
||||
showError(payload.message, payload.title)
|
||||
showError(legacyT(payload.message), payload.title ? legacyT(payload.title) : undefined)
|
||||
}
|
||||
|
||||
function setWindsurfImportMethod(method: WindsurfImportMethod) {
|
||||
@@ -1633,7 +1708,7 @@ async function handleWindsurfEmailPasswordImport() {
|
||||
const email = windsurfEmail.value.trim()
|
||||
const password = windsurfPassword.value.trim()
|
||||
if (!email || !password) {
|
||||
showError('请输入邮箱和密码', '格式错误')
|
||||
showError(legacyT('请输入邮箱和密码'), legacyT('格式错误'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1649,8 +1724,8 @@ async function handleWindsurfEmailPasswordImport() {
|
||||
emit('saved')
|
||||
handleClose()
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '导入失败')
|
||||
showError(errorMessage, '错误')
|
||||
const errorMessage = localizedApiError(err, '导入失败')
|
||||
showError(errorMessage, legacyT('错误'))
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
@@ -1665,13 +1740,13 @@ async function handleImport() {
|
||||
|
||||
const inputText = importText.value.trim()
|
||||
if (!inputText) {
|
||||
showError('请输入凭据数据', '格式错误')
|
||||
showError(legacyT('请输入凭据数据'), legacyT('格式错误'))
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedCredentials = normalizeBatchImportCredentials(inputText)
|
||||
if (!normalizedCredentials.ok) {
|
||||
showError(normalizedCredentials.message, '格式错误')
|
||||
showError(legacyT(normalizedCredentials.message), legacyT('格式错误'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1708,7 +1783,7 @@ async function handleImport() {
|
||||
// 单条导入
|
||||
const parsed = parseImportText(normalizedCredentials.credentials)
|
||||
if (!parsed) {
|
||||
showError('无法解析输入内容,请检查格式', '格式错误')
|
||||
showError(legacyT('无法解析输入内容,请检查格式'), legacyT('格式错误'))
|
||||
return
|
||||
}
|
||||
const result = await importProviderRefreshToken(props.providerId, {
|
||||
@@ -1720,8 +1795,8 @@ async function handleImport() {
|
||||
handleClose()
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '导入失败')
|
||||
showError(errorMessage, '错误')
|
||||
const errorMessage = localizedApiError(err, '导入失败')
|
||||
showError(errorMessage, legacyT('错误'))
|
||||
} finally {
|
||||
if (!keepImporting) {
|
||||
importing.value = false
|
||||
@@ -1796,8 +1871,8 @@ async function startDeviceAuth() {
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (requestId !== deviceAuthRequestId || device.value.auth_type !== requestedAuthType) return
|
||||
const errorMessage = parseApiError(err, '发起设备授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
const errorMessage = localizedApiError(err, '发起设备授权失败')
|
||||
showError(errorMessage, legacyT('错误'))
|
||||
device.value.status = 'error'
|
||||
device.value.error = errorMessage
|
||||
} finally {
|
||||
@@ -1895,8 +1970,8 @@ async function pollDevice(withCallback = false) {
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (withCallback) {
|
||||
const errorMessage = parseApiError(err, '完成授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
const errorMessage = localizedApiError(err, '完成授权失败')
|
||||
showError(errorMessage, legacyT('错误'))
|
||||
}
|
||||
// 网络错误等,继续轮询
|
||||
if (!withCallback && !device.value.callback_required) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="internalOpen"
|
||||
title="优先级管理"
|
||||
description="拖拽调整顺序,点击序号可编辑(相同数字为同级),保存后自动切换对应的调度策略"
|
||||
:title="legacyT('优先级管理')"
|
||||
:description="legacyT('拖拽调整顺序,点击序号可编辑(相同数字为同级),保存后自动切换对应的调度策略')"
|
||||
:icon="ListOrdered"
|
||||
size="2xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
@@ -21,7 +21,7 @@
|
||||
@click="activeMainTab = 'provider'"
|
||||
>
|
||||
<Layers class="w-4 h-4" />
|
||||
<span>提供商优先</span>
|
||||
<span>{{ legacyT('提供商优先') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -34,7 +34,7 @@
|
||||
@click="activeMainTab = 'key'"
|
||||
>
|
||||
<Key class="w-4 h-4" />
|
||||
<span>Key 优先</span>
|
||||
<span>{{ legacyT('Key 优先') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
class="flex flex-col items-center justify-center py-20 text-muted-foreground"
|
||||
>
|
||||
<Layers class="w-10 h-10 mb-3 opacity-20" />
|
||||
<span class="text-sm">暂无提供商</span>
|
||||
<span class="text-sm">{{ legacyT('暂无提供商') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 提供商列表 -->
|
||||
@@ -98,7 +98,7 @@
|
||||
<div
|
||||
v-else
|
||||
class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
||||
title="点击编辑优先级,相同数字为同级"
|
||||
:title="legacyT('点击编辑优先级,相同数字为同级')"
|
||||
@click.stop="startEditProviderPriority(provider)"
|
||||
>
|
||||
{{ provider.provider_priority }}
|
||||
@@ -113,7 +113,7 @@
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1.5 h-5 shrink-0"
|
||||
>
|
||||
停用
|
||||
{{ legacyT('停用') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0 ml-2">
|
||||
@@ -156,7 +156,7 @@
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-2 border-muted border-t-primary" />
|
||||
<span class="text-xs text-muted-foreground">加载中...</span>
|
||||
<span class="text-xs text-muted-foreground">{{ legacyT('加载中...') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
class="flex flex-col items-center justify-center py-20 text-muted-foreground"
|
||||
>
|
||||
<Key class="w-10 h-10 mb-3 opacity-20" />
|
||||
<span class="text-sm">暂无 API Key</span>
|
||||
<span class="text-sm">{{ legacyT('暂无 API Key') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 左右布局:格式列表 + Key 列表 -->
|
||||
@@ -261,7 +261,7 @@
|
||||
<div
|
||||
v-else
|
||||
class="w-5 h-5 rounded bg-muted/50 flex items-center justify-center text-[11px] font-medium transition-colors text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary"
|
||||
title="点击编辑优先级"
|
||||
:title="legacyT('点击编辑优先级')"
|
||||
@click.stop="startEditKeyPriority(format, key)"
|
||||
>
|
||||
{{ key.priority }}
|
||||
@@ -281,28 +281,28 @@
|
||||
variant="outline"
|
||||
class="text-[9px] h-4 px-1 shrink-0"
|
||||
>
|
||||
号池
|
||||
{{ legacyT('号池') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="key.circuit_breaker_open"
|
||||
variant="destructive"
|
||||
class="text-[9px] h-4 px-1 shrink-0"
|
||||
>
|
||||
熔断
|
||||
{{ legacyT('熔断') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="!key.is_active && key.provider_active"
|
||||
variant="secondary"
|
||||
class="text-[9px] h-4 px-1 shrink-0"
|
||||
>
|
||||
停用
|
||||
{{ legacyT('停用') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<!-- 第二行:密钥脱敏 · Provider 名称 + Provider 级别状态 -->
|
||||
<div class="flex items-center gap-0 mt-0.5">
|
||||
<template v-if="key.is_pool_aggregate">
|
||||
<span class="text-[10px] text-muted-foreground/70 truncate">
|
||||
号池: {{ key.pool_active_key_count ?? 0 }}/{{ key.pool_key_count ?? 0 }}
|
||||
{{ poolKeySummary(key) }}
|
||||
</span>
|
||||
<template v-if="key.provider_type">
|
||||
<span class="text-[10px] text-muted-foreground/40 mx-1">·</span>
|
||||
@@ -319,7 +319,7 @@
|
||||
variant="secondary"
|
||||
class="text-[9px] h-4 px-1 shrink-0 ml-1"
|
||||
>
|
||||
停用
|
||||
{{ legacyT('停用') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,7 +341,7 @@
|
||||
--
|
||||
</div>
|
||||
<div class="text-[10px] text-muted-foreground tabular-nums">
|
||||
{{ key.is_pool_aggregate ? 'Pool' : (key.rate_multipliers?.[format] ?? 1) + 'x' }}
|
||||
{{ key.is_pool_aggregate ? legacyT('Pool') : (key.rate_multipliers?.[format] ?? 1) + 'x' }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 快捷启用/禁用开关 -->
|
||||
@@ -353,12 +353,12 @@
|
||||
? 'text-foreground/70 hover:bg-muted hover:text-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'"
|
||||
:title="key.is_pool_aggregate
|
||||
? '号池聚合项不支持在此单独开关'
|
||||
? legacyT('号池聚合项不支持在此单独开关')
|
||||
: !key.provider_active
|
||||
? 'Provider 停用'
|
||||
? legacyT('Provider 停用')
|
||||
: key.is_active
|
||||
? '点击停用'
|
||||
: '点击启用'"
|
||||
? legacyT('点击停用')
|
||||
: legacyT('点击启用')"
|
||||
:disabled="key.is_pool_aggregate || !key.provider_active"
|
||||
@click.stop="!key.is_pool_aggregate && toggleKeyActive(format, key)"
|
||||
>
|
||||
@@ -373,7 +373,7 @@
|
||||
class="flex flex-col items-center justify-center py-20 text-muted-foreground"
|
||||
>
|
||||
<Key class="w-10 h-10 mb-3 opacity-20" />
|
||||
<span class="text-sm">暂无 {{ format }} 格式的 Key</span>
|
||||
<span class="text-sm">{{ emptyFormatKeyText(format) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -386,10 +386,10 @@
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-xs text-muted-foreground whitespace-nowrap">
|
||||
当前模式: <span class="font-medium text-foreground/80">{{ activeMainTab === 'provider' ? '提供商优先' : 'Key 优先' }}</span>
|
||||
{{ legacyT('当前模式:') }} <span class="font-medium text-foreground/80">{{ activeMainTabLabel }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 pl-3 border-l border-border/60">
|
||||
<span class="text-xs text-muted-foreground">调度:</span>
|
||||
<span class="text-xs text-muted-foreground">{{ legacyT('调度:') }}</span>
|
||||
<div class="flex gap-0.5 p-0.5 bg-muted/40 rounded-md">
|
||||
<button
|
||||
type="button"
|
||||
@@ -399,10 +399,10 @@
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
]"
|
||||
title="优先使用已缓存的Provider,利用Prompt Cache"
|
||||
:title="legacyT('优先使用已缓存的Provider,利用Prompt Cache')"
|
||||
@click="schedulingMode = 'cache_affinity'"
|
||||
>
|
||||
缓存亲和
|
||||
{{ legacyT('缓存亲和') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -412,10 +412,10 @@
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
]"
|
||||
title="同优先级内随机轮换,不考虑缓存"
|
||||
:title="legacyT('同优先级内随机轮换,不考虑缓存')"
|
||||
@click="schedulingMode = 'load_balance'"
|
||||
>
|
||||
负载均衡
|
||||
{{ legacyT('负载均衡') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -425,10 +425,10 @@
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
]"
|
||||
title="严格按优先级顺序,不考虑缓存"
|
||||
:title="legacyT('严格按优先级顺序,不考虑缓存')"
|
||||
@click="schedulingMode = 'fixed_order'"
|
||||
>
|
||||
固定顺序
|
||||
{{ legacyT('固定顺序') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -444,7 +444,7 @@
|
||||
v-if="saving"
|
||||
class="w-3.5 h-3.5 mr-1.5 animate-spin"
|
||||
/>
|
||||
{{ saving ? '保存中' : '保存' }}
|
||||
{{ saving ? legacyT('保存中') : legacyT('保存') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -452,7 +452,7 @@
|
||||
class="min-w-[72px]"
|
||||
@click="close"
|
||||
>
|
||||
取消
|
||||
{{ legacyT('取消') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -468,6 +468,7 @@ import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { updateProvider, updateProviderKey } from '@/api/endpoints'
|
||||
import { getProvidersSummary, type ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
@@ -519,6 +520,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
// 内部状态
|
||||
const internalOpen = computed(() => props.modelValue)
|
||||
@@ -562,6 +564,22 @@ const schedulingMode = ref<'fixed_order' | 'load_balance' | 'cache_affinity'>('c
|
||||
// 余额数据缓存 {providerId: ActionResultResponse}
|
||||
const balanceCache = ref<Record<string, ActionResultResponse>>({})
|
||||
|
||||
const activeMainTabLabel = computed(() =>
|
||||
legacyT(activeMainTab.value === 'provider' ? '提供商优先' : 'Key 优先')
|
||||
)
|
||||
|
||||
function poolKeySummary(key: KeyWithMeta): string {
|
||||
return `${legacyT('号池')}: ${key.pool_active_key_count ?? 0}/${key.pool_key_count ?? 0}`
|
||||
}
|
||||
|
||||
function emptyFormatKeyText(format: string): string {
|
||||
return legacyT(`暂无 ${format} 格式的 Key`)
|
||||
}
|
||||
|
||||
function localizedApiError(error: unknown, fallback: string): string {
|
||||
return legacyT(parseApiError(error, fallback))
|
||||
}
|
||||
|
||||
// 类型守卫函数
|
||||
function isBalanceInfo(data: unknown): data is BalanceInfo {
|
||||
return data !== null && typeof data === 'object' && 'total_available' in data
|
||||
@@ -863,7 +881,7 @@ const PROVIDER_TYPE_LABELS: Record<string, string> = {
|
||||
|
||||
function formatProviderType(type?: string): string {
|
||||
if (!type) return ''
|
||||
return PROVIDER_TYPE_LABELS[type] || type
|
||||
return legacyT(PROVIDER_TYPE_LABELS[type] || type)
|
||||
}
|
||||
|
||||
function isPoolManagedProvider(providerId: string): boolean {
|
||||
@@ -928,7 +946,7 @@ function buildPoolAggregateItem(format: string, providerId: string, sourceKeys:
|
||||
? Number(poolPriorityRaw)
|
||||
: fallbackPriority
|
||||
|
||||
const providerName = provider?.name || sourceKeys[0]?.provider_name || '未知 Provider'
|
||||
const providerName = provider?.name || sourceKeys[0]?.provider_name || 'Unknown Provider'
|
||||
const activeKeyCount = sourceKeys.filter((k) => k.is_active).length
|
||||
const providerActive = provider?.is_active ?? sourceKeys.some((k) => k.provider_active)
|
||||
const healthCandidates = sourceKeys.map((k) => k.health_score).filter((v): v is number => v != null)
|
||||
@@ -1171,7 +1189,7 @@ async function loadKeysByFormat() {
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
originalKeyPriorityById = new Map()
|
||||
showError(parseApiError(err, '加载 Key 列表失败'), '错误')
|
||||
showError(localizedApiError(err, '加载 Key 列表失败'), legacyT('错误'))
|
||||
} finally {
|
||||
loadingKeys.value = false
|
||||
}
|
||||
@@ -1195,9 +1213,9 @@ async function toggleKeyActive(format: string, key: KeyWithMeta) {
|
||||
for (const fmt of Object.keys(keysByFormat.value)) {
|
||||
keysByFormat.value[fmt] = sortKeysByActiveAndPriority(keysByFormat.value[fmt])
|
||||
}
|
||||
success(newStatus ? 'Key 已启用' : 'Key 已停用')
|
||||
success(legacyT(newStatus ? 'Key 已启用' : 'Key 已停用'))
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
showError(localizedApiError(err, '操作失败'), legacyT('错误'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1539,7 +1557,7 @@ async function save() {
|
||||
)
|
||||
snapshotCurrentPriorityBaseline()
|
||||
|
||||
success('优先级已保存')
|
||||
success(legacyT('优先级已保存'))
|
||||
emit('saved')
|
||||
|
||||
// 提供商优先模式保存后关闭,Key 优先模式保存后保持打开方便继续调整
|
||||
@@ -1547,7 +1565,7 @@ async function save() {
|
||||
close()
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
showError(localizedApiError(err, '保存失败'), legacyT('错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="flex items-center gap-1.5 text-xs text-muted-foreground"
|
||||
>
|
||||
<Loader2 class="h-3 w-3 animate-spin" />
|
||||
<span>加载中...</span>
|
||||
<span>{{ legacyT('加载中...') }}</span>
|
||||
</div>
|
||||
<!-- 显示从上游 API 查询的余额 -->
|
||||
<div
|
||||
@@ -75,7 +75,7 @@
|
||||
<span
|
||||
class="text-[10px] text-amber-600 dark:text-amber-500"
|
||||
:title="getProviderCookieExpired(provider.id)?.message"
|
||||
>签到 Cookie 已失效</span>
|
||||
>{{ legacyT('签到 Cookie 已失效') }}</span>
|
||||
</div>
|
||||
<!-- 签到状态 -->
|
||||
<div
|
||||
@@ -86,12 +86,12 @@
|
||||
v-if="getProviderCheckin(provider.id)?.success !== false"
|
||||
class="text-[10px] text-muted-foreground/60"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>已签到</span>
|
||||
>{{ legacyT('已签到') }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-[10px] text-destructive/70"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>签到失败</span>
|
||||
>{{ legacyT('签到失败') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +133,9 @@ import Badge from '@/components/ui/badge.vue'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import { formatBillingType } from '@/utils/format'
|
||||
import type { BalanceExtraItem } from '@/features/providers/auth-templates'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
provider: ProviderWithEndpointsSummary
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<Card
|
||||
v-if="progress"
|
||||
class="border-primary/30 bg-primary/5"
|
||||
>
|
||||
<div class="px-5 py-4 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold text-foreground">
|
||||
{{ legacyT('正在删除提供商') }}: {{ progress.providerName }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-muted-foreground">
|
||||
{{ stageLabel }} · {{ legacyT(progress.message || '后台处理中') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0 text-right">
|
||||
<div class="text-xs font-medium text-primary">
|
||||
{{ overallPercent }}%
|
||||
</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
{{ completedUnits }}/{{ totalUnits }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ legacyT('总体进度') }}</span>
|
||||
<span>{{ completedUnits }}/{{ totalUnits }}</span>
|
||||
</div>
|
||||
<div class="h-2 rounded-full bg-primary/10 overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-primary transition-all duration-300"
|
||||
:style="{ width: `${overallPercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ legacyT('账号删除') }}</span>
|
||||
<span>{{ progress.deletedKeys }}/{{ progress.totalKeys || '...' }}</span>
|
||||
</div>
|
||||
<div class="h-2 rounded-full bg-primary/10 overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-primary/80 transition-all duration-300"
|
||||
:style="{ width: `${keysPercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ legacyT('端点删除') }}</span>
|
||||
<span>{{ progress.deletedEndpoints }}/{{ progress.totalEndpoints || '...' }}</span>
|
||||
</div>
|
||||
<div class="h-2 rounded-full bg-primary/10 overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-primary/60 transition-all duration-300"
|
||||
:style="{ width: `${endpointsPercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
export interface ProviderDeleteProgressView {
|
||||
providerName: string
|
||||
totalKeys: number
|
||||
deletedKeys: number
|
||||
totalEndpoints: number
|
||||
deletedEndpoints: number
|
||||
message: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
progress: ProviderDeleteProgressView | null
|
||||
stageLabel: string
|
||||
totalUnits: number
|
||||
completedUnits: number
|
||||
overallPercent: number
|
||||
keysPercent: number
|
||||
endpointsPercent: number
|
||||
}>()
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="sticky top-0 z-10 bg-background border-b px-4 sm:px-6 pt-4 sm:pt-6 pb-3 sm:pb-3">
|
||||
<div class="flex items-center justify-between gap-x-3 sm:gap-x-4 flex-wrap">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<h2 class="text-lg sm:text-xl font-bold truncate">
|
||||
{{ provider.name }}
|
||||
</h2>
|
||||
<Badge
|
||||
:variant="provider.is_active ? 'default' : 'secondary'"
|
||||
class="text-xs shrink-0"
|
||||
>
|
||||
{{ legacyT(provider.is_active ? '活跃' : '停用') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<span :title="formatConversionTitle">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="(provider.enable_format_conversion || systemFormatConversionEnabled) ? 'text-primary' : ''"
|
||||
:disabled="systemFormatConversionEnabled"
|
||||
@click="$emit('toggleFormatConversion')"
|
||||
>
|
||||
<Shuffle class="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
<span :title="legacyT(hasFailoverRules ? '已配置故障转移规则(点击编辑)' : '配置故障转移规则')">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="hasFailoverRules ? 'text-orange-500 dark:text-orange-400' : ''"
|
||||
@click="$emit('openFailoverRules')"
|
||||
>
|
||||
<GitBranch class="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
<Popover
|
||||
:open="providerProxyPopoverOpen"
|
||||
@update:open="$emit('update:providerProxyPopoverOpen', $event)"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="provider.proxy?.node_id ? 'text-blue-500' : ''"
|
||||
:disabled="savingProviderProxy"
|
||||
:title="provider.proxy?.node_id ? `${legacyT('代理')}: ${providerProxyNodeName}` : legacyT('设置代理节点')"
|
||||
>
|
||||
<Globe class="w-4 h-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
class="w-72 p-3"
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium">{{ legacyT('代理节点') }}</span>
|
||||
<Button
|
||||
v-if="provider.proxy?.node_id"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 px-2 text-[10px] text-muted-foreground"
|
||||
:disabled="savingProviderProxy"
|
||||
@click="$emit('clearProviderProxy')"
|
||||
>
|
||||
{{ legacyT('清除') }}
|
||||
</Button>
|
||||
</div>
|
||||
<ProxyNodeSelect
|
||||
:model-value="provider.proxy?.node_id || ''"
|
||||
trigger-class="h-8"
|
||||
@update:model-value="$emit('setProviderProxy', $event)"
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground">
|
||||
{{ legacyT(provider.proxy?.node_id ? '当前使用独立代理' : '未设置代理节点') }}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:title="legacyT('编辑提供商')"
|
||||
@click="$emit('edit', provider)"
|
||||
>
|
||||
<Edit class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:title="legacyT(provider.is_active ? '点击停用' : '点击启用')"
|
||||
@click="$emit('toggleStatus', provider)"
|
||||
>
|
||||
<Power class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:title="legacyT('关闭')"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="provider.website"
|
||||
class="-mt-0.5"
|
||||
>
|
||||
<a
|
||||
:href="provider.website"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-muted-foreground hover:text-primary hover:underline transition-colors truncate block"
|
||||
:title="provider.website"
|
||||
>{{ provider.website }}</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5 flex-wrap mt-3">
|
||||
<template v-if="loadingProviderEndpoints && endpoints.length === 0">
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Loader2 class="w-3.5 h-3.5 animate-spin" />
|
||||
{{ legacyT('加载端点中') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template
|
||||
v-for="endpoint in endpoints"
|
||||
:key="endpoint.id"
|
||||
>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-md border border-border bg-background hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors font-medium"
|
||||
:class="{ 'opacity-40': !endpoint.is_active }"
|
||||
:title="legacyT('编辑端点')"
|
||||
@click="$emit('editEndpoint', endpoint)"
|
||||
>{{ formatApiFormat(endpoint.api_format) }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-if="endpoints.length > 0"
|
||||
class="text-xs px-2 py-0.5 rounded-md border border-dashed border-border hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors text-muted-foreground"
|
||||
:title="legacyT('编辑端点')"
|
||||
@click="$emit('addEndpoint')"
|
||||
>{{ legacyT('编辑') }}</span>
|
||||
<Button
|
||||
v-else
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-7 text-xs"
|
||||
@click="$emit('addEndpoint')"
|
||||
>
|
||||
<Plus class="w-3 h-3 mr-1" />
|
||||
{{ legacyT('添加 API 端点') }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Edit, GitBranch, Globe, Loader2, Plus, Power, Shuffle, X } from 'lucide-vue-next'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import type { ProviderEndpoint, ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
provider: ProviderWithEndpointsSummary
|
||||
endpoints: ProviderEndpoint[]
|
||||
loadingProviderEndpoints: boolean
|
||||
systemFormatConversionEnabled: boolean
|
||||
hasFailoverRules: boolean
|
||||
providerProxyPopoverOpen: boolean
|
||||
providerProxyNodeName: string
|
||||
savingProviderProxy: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'toggleFormatConversion'): void
|
||||
(e: 'openFailoverRules'): void
|
||||
(e: 'update:providerProxyPopoverOpen', value: boolean): void
|
||||
(e: 'setProviderProxy', value: string): void
|
||||
(e: 'clearProviderProxy'): void
|
||||
(e: 'edit', provider: ProviderWithEndpointsSummary): void
|
||||
(e: 'toggleStatus', provider: ProviderWithEndpointsSummary): void
|
||||
(e: 'close'): void
|
||||
(e: 'editEndpoint', endpoint: ProviderEndpoint): void
|
||||
(e: 'addEndpoint'): void
|
||||
}>()
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
const formatConversionTitle = computed(() => {
|
||||
if (props.systemFormatConversionEnabled) return legacyT('系统级格式转换已启用')
|
||||
if (props.provider.enable_format_conversion) return legacyT('已启用格式转换(点击关闭)')
|
||||
return legacyT('启用格式转换')
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div class="mb-2 text-muted-foreground">
|
||||
{{ legacyT(hasActiveFilters ? '未找到匹配当前筛选条件的提供商' : '暂无提供商,点击右上角添加') }}
|
||||
</div>
|
||||
<Button
|
||||
v-if="hasActiveFilters"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="$emit('resetFilters')"
|
||||
>
|
||||
{{ legacyT('清除筛选') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
defineProps<{
|
||||
hasActiveFilters: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
resetFilters: []
|
||||
}>()
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
</script>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="internalOpen"
|
||||
:title="isEditMode ? '编辑提供商' : '添加提供商'"
|
||||
:description="isEditMode ? '更新提供商配置。API 端点和密钥需在详情页面单独管理。' : '创建新的提供商配置。创建后可以为其添加 API 端点和密钥。'"
|
||||
:title="legacyT(isEditMode ? '编辑提供商' : '添加提供商')"
|
||||
:description="legacyT(isEditMode ? '更新提供商配置。API 端点和密钥需在详情页面单独管理。' : '创建新的提供商配置。创建后可以为其添加 API 端点和密钥。')"
|
||||
:icon="isEditMode ? SquarePen : Server"
|
||||
size="xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
@@ -14,33 +14,33 @@
|
||||
<!-- 基本信息 -->
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
基本信息
|
||||
{{ legacyT('基本信息') }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="name">名称 *</Label>
|
||||
<Label for="name">{{ legacyT('名称 *') }}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
placeholder="例如: OpenAI 主账号"
|
||||
:placeholder="legacyT('例如: OpenAI 主账号')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label>提供商类型</Label>
|
||||
<Label>{{ legacyT('提供商类型') }}</Label>
|
||||
<Select
|
||||
v-model="form.provider_type"
|
||||
:disabled="isEditMode"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
<SelectValue :placeholder="legacyT('请选择')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<!-- 新建模式:允许自定义及各反代类型 -->
|
||||
<template v-if="!isEditMode">
|
||||
<SelectItem value="custom">
|
||||
自定义
|
||||
{{ legacyT('自定义') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="vertex_ai">
|
||||
Vertex AI
|
||||
@@ -49,7 +49,7 @@
|
||||
value="claude_code"
|
||||
disabled
|
||||
>
|
||||
ClaudeCode(暂不可用)
|
||||
{{ legacyT('ClaudeCode(暂不可用)') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="codex">
|
||||
Codex
|
||||
@@ -76,7 +76,7 @@
|
||||
<!-- 编辑模式:显示所有类型(兼容已有数据) -->
|
||||
<template v-else>
|
||||
<SelectItem value="custom">
|
||||
自定义
|
||||
{{ legacyT('自定义') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="vertex_ai">
|
||||
Vertex AI
|
||||
@@ -112,15 +112,15 @@
|
||||
v-if="!isEditMode && form.provider_type !== 'custom'"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
反代使用固定端点且不可修改
|
||||
{{ legacyT('反代使用固定端点且不可修改') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="website">主站链接</Label>
|
||||
<Label for="website">{{ legacyT('主站链接') }}</Label>
|
||||
<Input
|
||||
id="website"
|
||||
v-model="form.website"
|
||||
placeholder="https://example.com(可选)"
|
||||
:placeholder="legacyT('https://example.com(可选)')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,15 +130,15 @@
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
计费与限流
|
||||
{{ legacyT('计费与限流') }}
|
||||
</h3>
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
请求配置
|
||||
{{ legacyT('请求配置') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label>计费类型</Label>
|
||||
<Label>{{ legacyT('计费类型') }}</Label>
|
||||
<Select
|
||||
v-model="form.billing_type"
|
||||
>
|
||||
@@ -147,25 +147,25 @@
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly_quota">
|
||||
月卡额度
|
||||
{{ legacyT('月卡额度') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="pay_as_you_go">
|
||||
按量付费
|
||||
{{ legacyT('按量付费') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="free_tier">
|
||||
免费套餐
|
||||
{{ legacyT('免费套餐') }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>最大重试次数</Label>
|
||||
<Label>{{ legacyT('最大重试次数') }}</Label>
|
||||
<Input
|
||||
:model-value="form.max_retries ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999"
|
||||
placeholder="默认 2"
|
||||
:placeholder="legacyT('默认 2')"
|
||||
@update:model-value="(v) => form.max_retries = parseNumberInput(v)"
|
||||
/>
|
||||
</div>
|
||||
@@ -175,8 +175,8 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label>
|
||||
流式首字节超时
|
||||
<span class="text-xs text-muted-foreground">(秒)</span>
|
||||
{{ legacyT('流式首字节超时') }}
|
||||
<span class="text-xs text-muted-foreground">{{ legacyT('(秒)') }}</span>
|
||||
</Label>
|
||||
<Input
|
||||
:model-value="form.stream_first_byte_timeout ?? ''"
|
||||
@@ -190,8 +190,8 @@
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>
|
||||
非流式请求超时
|
||||
<span class="text-xs text-muted-foreground">(秒)</span>
|
||||
{{ legacyT('非流式请求超时') }}
|
||||
<span class="text-xs text-muted-foreground">{{ legacyT('(秒)') }}</span>
|
||||
</Label>
|
||||
<Input
|
||||
:model-value="form.request_timeout ?? ''"
|
||||
@@ -211,7 +211,7 @@
|
||||
class="grid grid-cols-2 gap-4 p-3 border rounded-lg bg-muted/50"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">周期额度 (USD)</Label>
|
||||
<Label class="text-xs">{{ legacyT('周期额度 (USD)') }}</Label>
|
||||
<Input
|
||||
:model-value="form.monthly_quota_usd ?? ''"
|
||||
type="number"
|
||||
@@ -221,7 +221,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">重置周期 (天)</Label>
|
||||
<Label class="text-xs">{{ legacyT('重置周期 (天)') }}</Label>
|
||||
<Input
|
||||
:model-value="form.quota_reset_day ?? ''"
|
||||
type="number"
|
||||
@@ -232,7 +232,7 @@
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">
|
||||
周期开始时间 <span class="text-red-500">*</span>
|
||||
{{ legacyT('周期开始时间') }} <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
v-model="form.quota_last_reset_at"
|
||||
@@ -240,7 +240,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">过期时间</Label>
|
||||
<Label class="text-xs">{{ legacyT('过期时间') }}</Label>
|
||||
<Input
|
||||
v-model="form.quota_expires_at"
|
||||
type="datetime-local"
|
||||
@@ -252,14 +252,14 @@
|
||||
<!-- 功能开关 -->
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
功能开关
|
||||
{{ legacyT('功能开关') }}
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">格式转换保持优先级</span>
|
||||
<span class="text-sm font-medium">{{ legacyT('格式转换保持优先级') }}</span>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
跨格式请求时保持原优先级排名,不降级到格式匹配的提供商之后
|
||||
{{ legacyT('跨格式请求时保持原优先级排名,不降级到格式匹配的提供商之后') }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -270,9 +270,9 @@
|
||||
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">号池调度模式</span>
|
||||
<span class="text-sm font-medium">{{ legacyT('号池调度模式') }}</span>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
启用后该提供商的密钥将由号池统一调度
|
||||
{{ legacyT('启用后该提供商的密钥将由号池统一调度') }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -286,9 +286,9 @@
|
||||
class="flex items-center justify-between p-3 border rounded-lg bg-muted/50"
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">模拟缓存模式</span>
|
||||
<span class="text-sm font-medium">{{ legacyT('模拟缓存模式') }}</span>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
启用后仅对 Kiro 请求模拟 prompt cache 读写计量。
|
||||
{{ legacyT('启用后仅对 Kiro 请求模拟 prompt cache 读写计量。') }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -299,9 +299,9 @@
|
||||
|
||||
<div class="flex items-center justify-between gap-4 p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">敏感信息保护</span>
|
||||
<span class="text-sm font-medium">{{ legacyT('敏感信息保护') }}</span>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
请前往模块管理-敏感信息保护中配置详细规则。
|
||||
{{ legacyT('请前往模块管理-敏感信息保护中配置详细规则。') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,13 +315,13 @@
|
||||
:disabled="loading"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
{{ legacyT('取消') }}
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="loading || !form.name"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存' : '创建') }}
|
||||
{{ submitLabel }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -344,6 +344,7 @@ import {
|
||||
import { Server, SquarePen } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { useI18n } from '@/i18n'
|
||||
import {
|
||||
createProvider,
|
||||
normalizePoolAdvancedConfig,
|
||||
@@ -368,6 +369,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { legacyT } = useI18n()
|
||||
const loading = ref(false)
|
||||
|
||||
// 内部状态
|
||||
@@ -381,6 +383,13 @@ const defaultPriority = computed(() => {
|
||||
return 100
|
||||
})
|
||||
|
||||
const submitLabel = computed(() => {
|
||||
if (loading.value) {
|
||||
return legacyT(isEditMode.value ? '保存中...' : '创建中...')
|
||||
}
|
||||
return legacyT(isEditMode.value ? '保存' : '创建')
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
@@ -494,24 +503,24 @@ watch(() => form.value.provider_type, () => {
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!isEditMode.value && form.value.provider_type === 'claude_code') {
|
||||
showError('ClaudeCode 提供商类型暂时禁用', '验证失败')
|
||||
showError(legacyT('ClaudeCode 提供商类型暂时禁用'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
|
||||
// 月卡类型必须设置周期开始时间
|
||||
if (form.value.billing_type === 'monthly_quota' && !form.value.quota_last_reset_at) {
|
||||
showError('月卡类型必须设置周期开始时间', '验证失败')
|
||||
showError(legacyT('月卡类型必须设置周期开始时间'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
|
||||
const quotaLastResetAt = dateTimeLocalToRfc3339(form.value.quota_last_reset_at)
|
||||
if (form.value.billing_type === 'monthly_quota' && !quotaLastResetAt) {
|
||||
showError('周期开始时间必须是合法时间', '验证失败')
|
||||
showError(legacyT('周期开始时间必须是合法时间'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
const quotaExpiresAt = dateTimeLocalToRfc3339(form.value.quota_expires_at)
|
||||
if (form.value.quota_expires_at && !quotaExpiresAt) {
|
||||
showError('过期时间必须是合法时间', '验证失败')
|
||||
showError(legacyT('过期时间必须是合法时间'), legacyT('验证失败'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -555,19 +564,19 @@ const handleSubmit = async () => {
|
||||
...basePayload,
|
||||
provider_priority: form.value.provider_priority,
|
||||
})
|
||||
success('提供商更新成功')
|
||||
success(legacyT('提供商更新成功'))
|
||||
emit('providerUpdated', updated)
|
||||
} else {
|
||||
// 创建提供商(优先级由后端自动置顶)
|
||||
await createProvider(basePayload)
|
||||
success('提供商已创建,请继续添加端点和密钥,或在优先级管理中调整顺序', '创建成功')
|
||||
success(legacyT('提供商已创建,请继续添加端点和密钥,或在优先级管理中调整顺序'), legacyT('创建成功'))
|
||||
emit('providerCreated')
|
||||
}
|
||||
|
||||
emit('update:modelValue', false)
|
||||
} catch (error: unknown) {
|
||||
const action = isEditMode.value ? '更新' : '创建'
|
||||
showError(parseApiError(error, `${action}提供商失败`), `${action}失败`)
|
||||
showError(parseApiError(error, legacyT(`${action}提供商失败`)), legacyT(`${action}失败`))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
:variant="provider.is_active ? 'success' : 'secondary'"
|
||||
class="text-xs shrink-0"
|
||||
>
|
||||
{{ provider.is_active ? '活跃' : '停用' }}
|
||||
{{ legacyT(provider.is_active ? '活跃' : '停用') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<!-- 内联编辑备注 (移动端) -->
|
||||
@@ -37,19 +37,19 @@
|
||||
v-model="localDescriptionValue"
|
||||
v-auto-focus
|
||||
class="flex-1 min-w-0 text-xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
placeholder="输入备注..."
|
||||
:placeholder="legacyT('输入备注...')"
|
||||
@keydown="handleDescriptionKeydown"
|
||||
>
|
||||
<button
|
||||
class="shrink-0 p-0.5 rounded hover:bg-muted text-primary"
|
||||
title="保存"
|
||||
:title="legacyT('保存')"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Check class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
class="shrink-0 p-0.5 rounded hover:bg-muted text-muted-foreground"
|
||||
title="取消"
|
||||
:title="legacyT('取消')"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<X class="w-3.5 h-3.5" />
|
||||
@@ -65,7 +65,7 @@
|
||||
v-else
|
||||
class="text-xs text-muted-foreground cursor-pointer hover:text-foreground/70 transition-colors"
|
||||
@click="handleStartEdit"
|
||||
>添加备注</span>
|
||||
>{{ legacyT('添加备注') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-0.5 shrink-0"
|
||||
@@ -75,7 +75,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="查看详情"
|
||||
:title="legacyT('查看详情')"
|
||||
@click="$emit('viewDetail', provider.id)"
|
||||
>
|
||||
<Eye class="h-3.5 w-3.5" />
|
||||
@@ -84,7 +84,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="编辑"
|
||||
:title="legacyT('编辑')"
|
||||
@click="$emit('editProvider', provider)"
|
||||
>
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
@@ -93,7 +93,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="扩展操作配置"
|
||||
:title="legacyT('扩展操作配置')"
|
||||
@click="$emit('openOpsConfig', provider)"
|
||||
>
|
||||
<KeyRound class="h-3.5 w-3.5" />
|
||||
@@ -131,31 +131,31 @@
|
||||
class="text-muted-foreground flex items-center gap-1"
|
||||
>
|
||||
<Loader2 class="h-3 w-3 animate-spin" />
|
||||
加载中...
|
||||
{{ legacyT('加载中...') }}
|
||||
</span>
|
||||
<!-- 余额(从上游 API 查询) -->
|
||||
<span
|
||||
v-else-if="provider.ops_configured && getProviderBalance(provider.id)"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
余额 <span class="font-semibold text-foreground/90">{{ formatBalanceDisplay(getProviderBalance(provider.id)) }}</span>
|
||||
{{ legacyT('余额') }} <span class="font-semibold text-foreground/90">{{ formatBalanceDisplay(getProviderBalance(provider.id)) }}</span>
|
||||
<!-- Cookie 失效警告 -->
|
||||
<span
|
||||
v-if="getProviderCookieExpired(provider.id)"
|
||||
class="ml-1 text-amber-600 dark:text-amber-500"
|
||||
:title="getProviderCookieExpired(provider.id)?.message"
|
||||
>签到 Cookie 已失效</span>
|
||||
>{{ legacyT('签到 Cookie 已失效') }}</span>
|
||||
<!-- 签到状态显示 -->
|
||||
<span
|
||||
v-else-if="getProviderCheckin(provider.id) && getProviderCheckin(provider.id)?.success !== false"
|
||||
class="ml-1 text-muted-foreground"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>已签到</span>
|
||||
>{{ legacyT('已签到') }}</span>
|
||||
<span
|
||||
v-else-if="getProviderCheckin(provider.id)?.success === false"
|
||||
class="ml-1 text-destructive/70"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>签到失败</span>
|
||||
>{{ legacyT('签到失败') }}</span>
|
||||
</span>
|
||||
<!-- 余额查询失败时显示错误 -->
|
||||
<span
|
||||
@@ -170,19 +170,19 @@
|
||||
v-else-if="provider.billing_type === 'monthly_quota'"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
配额 <span
|
||||
{{ legacyT('配额') }} <span
|
||||
class="font-semibold"
|
||||
:class="getQuotaUsedColorClass(provider)"
|
||||
>${{ (provider.monthly_used_usd ?? 0).toFixed(2) }}</span>/<span class="font-medium">${{ (provider.monthly_quota_usd ?? 0).toFixed(2) }}</span>
|
||||
</span>
|
||||
<span class="text-muted-foreground">
|
||||
端点 {{ provider.active_endpoints }}/{{ provider.total_endpoints }}
|
||||
{{ legacyT('端点') }} {{ provider.active_endpoints }}/{{ provider.total_endpoints }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{ getCredentialLabel(provider) }} {{ provider.active_keys }}/{{ provider.total_keys }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">
|
||||
模型 {{ provider.active_models }}/{{ provider.total_models }}
|
||||
{{ legacyT('模型') }} {{ provider.active_models }}/{{ provider.total_models }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
v-for="endpoint in sortEndpoints(provider.endpoint_health_details)"
|
||||
:key="endpoint.api_format"
|
||||
class="flex flex-col gap-1.5"
|
||||
:title="getEndpointTooltip(endpoint)"
|
||||
:title="getEndpointTooltip(endpoint, locale)"
|
||||
>
|
||||
<!-- 上排:缩写 + 百分比 -->
|
||||
<div class="flex items-center justify-between text-[10px] leading-none">
|
||||
@@ -240,6 +240,7 @@ import { type ProviderWithEndpointsSummary, formatApiFormatShort } from '@/api/e
|
||||
import { formatBillingType } from '@/utils/format'
|
||||
import { sortEndpoints, isEndpointAvailable, getEndpointDotColor, getEndpointTooltip } from '@/features/providers/composables/useEndpointStatus'
|
||||
import { isKeyManagedProviderType } from '../utils/providerTypeUtils'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
provider: ProviderWithEndpointsSummary
|
||||
@@ -270,6 +271,7 @@ const vAutoFocus = {
|
||||
}
|
||||
|
||||
const localDescriptionValue = ref('')
|
||||
const { legacyT, locale } = useI18n()
|
||||
|
||||
watch(
|
||||
() => props.editingDescriptionId,
|
||||
@@ -304,7 +306,7 @@ function handleDescriptionKeydown(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function getCredentialLabel(provider: ProviderWithEndpointsSummary): '账号' | '密钥' {
|
||||
return isKeyManagedProviderType(provider.provider_type) ? '密钥' : '账号'
|
||||
function getCredentialLabel(provider: ProviderWithEndpointsSummary): string {
|
||||
return legacyT(isKeyManagedProviderType(provider.provider_type) ? '密钥' : '账号')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<!-- 左侧:标题 -->
|
||||
<h3 class="text-sm sm:text-base font-semibold text-foreground shrink-0">
|
||||
提供商管理
|
||||
{{ legacyT('提供商管理') }}
|
||||
</h3>
|
||||
|
||||
<!-- 右侧:操作区 -->
|
||||
@@ -15,7 +15,7 @@
|
||||
id="provider-search"
|
||||
:model-value="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索提供商..."
|
||||
:placeholder="legacyT('搜索提供商...')"
|
||||
class="w-32 sm:w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
||||
@update:model-value="$emit('update:searchQuery', $event)"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@
|
||||
@update:model-value="$emit('update:filterStatus', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
<SelectValue :placeholder="legacyT('全部状态')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
@@ -36,7 +36,7 @@
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
{{ status.label }}
|
||||
{{ legacyT(status.label) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -49,7 +49,7 @@
|
||||
@update:model-value="$emit('update:filterApiFormat', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部格式" />
|
||||
<SelectValue :placeholder="legacyT('全部格式')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
@@ -57,7 +57,7 @@
|
||||
:key="fmt.value"
|
||||
:value="fmt.value"
|
||||
>
|
||||
{{ fmt.label }}
|
||||
{{ legacyT(fmt.label) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -70,7 +70,7 @@
|
||||
@update:model-value="$emit('update:filterModel', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-36 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部模型" />
|
||||
<SelectValue :placeholder="legacyT('全部模型')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
@@ -78,7 +78,7 @@
|
||||
:key="model.value"
|
||||
:value="model.value"
|
||||
>
|
||||
{{ model.label }}
|
||||
{{ legacyT(model.label) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -90,7 +90,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="重置筛选"
|
||||
:title="legacyT('重置筛选')"
|
||||
@click="$emit('resetFilters')"
|
||||
>
|
||||
<FilterX class="w-3.5 h-3.5" />
|
||||
@@ -101,10 +101,10 @@
|
||||
<!-- 调度策略 -->
|
||||
<button
|
||||
class="group inline-flex items-center gap-1.5 px-2.5 h-8 rounded-md border border-border/50 bg-muted/20 hover:bg-muted/40 hover:border-primary/40 transition-all duration-200 text-xs"
|
||||
title="点击调整调度策略"
|
||||
:title="legacyT('点击调整调度策略')"
|
||||
@click="$emit('openPriorityDialog')"
|
||||
>
|
||||
<span class="text-muted-foreground/80 hidden sm:inline">调度:</span>
|
||||
<span class="text-muted-foreground/80 hidden sm:inline">{{ legacyT('调度:') }}</span>
|
||||
<span class="font-medium text-foreground/90">{{ priorityModeLabel }}</span>
|
||||
<ChevronDown class="w-3 h-3 text-muted-foreground/70 group-hover:text-foreground transition-colors" />
|
||||
</button>
|
||||
@@ -116,7 +116,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="批量处理提供商"
|
||||
:title="legacyT('批量处理提供商')"
|
||||
:disabled="loading"
|
||||
@click="$emit('batchProcess')"
|
||||
>
|
||||
@@ -126,7 +126,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="新增提供商"
|
||||
:title="legacyT('新增提供商')"
|
||||
@click="$emit('addProvider')"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
@@ -151,6 +151,7 @@ import SelectContent from '@/components/ui/select-content.vue'
|
||||
import SelectItem from '@/components/ui/select-item.vue'
|
||||
import RefreshButton from '@/components/ui/refresh-button.vue'
|
||||
import type { FilterOption } from '@/features/providers/composables/useProviderFilters'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
defineProps<{
|
||||
searchQuery: string
|
||||
@@ -176,4 +177,6 @@ defineEmits<{
|
||||
'addProvider': []
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
</script>
|
||||
|
||||
@@ -31,19 +31,19 @@
|
||||
v-model="localDescriptionValue"
|
||||
v-auto-focus
|
||||
class="flex-1 min-w-0 text-xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
placeholder="输入备注..."
|
||||
:placeholder="legacyT('输入备注...')"
|
||||
@keydown="handleDescriptionKeydown"
|
||||
>
|
||||
<button
|
||||
class="shrink-0 p-0.5 rounded hover:bg-muted text-primary"
|
||||
title="保存"
|
||||
:title="legacyT('保存')"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Check class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
class="shrink-0 p-0.5 rounded hover:bg-muted text-muted-foreground"
|
||||
title="取消"
|
||||
:title="legacyT('取消')"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<X class="w-3.5 h-3.5" />
|
||||
@@ -59,7 +59,7 @@
|
||||
v-else
|
||||
class="text-xs text-muted-foreground cursor-pointer hover:text-foreground/70 transition-colors"
|
||||
@click="handleStartEdit"
|
||||
>添加备注</span>
|
||||
>{{ legacyT('添加备注') }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5">
|
||||
@@ -79,7 +79,7 @@
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5 text-center">
|
||||
<div class="inline-grid grid-cols-[1.75rem_1.75rem_1.75rem] gap-x-0.5 gap-y-0.5 text-xs text-left">
|
||||
<span class="text-muted-foreground/70">端点:</span>
|
||||
<span class="text-muted-foreground/70">{{ legacyT('端点:') }}</span>
|
||||
<span class="font-medium text-foreground/90 tabular-nums text-right">{{ provider.active_endpoints }}</span>
|
||||
<span class="text-muted-foreground/50 tabular-nums">/{{ provider.total_endpoints }}</span>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<span class="font-medium text-foreground/90 tabular-nums text-right">{{ provider.active_keys }}</span>
|
||||
<span class="text-muted-foreground/50 tabular-nums">/{{ provider.total_keys }}</span>
|
||||
|
||||
<span class="text-muted-foreground/70">模型:</span>
|
||||
<span class="text-muted-foreground/70">{{ legacyT('模型:') }}</span>
|
||||
<span class="font-medium text-foreground/90 tabular-nums text-right">{{ provider.active_models }}</span>
|
||||
<span class="text-muted-foreground/50 tabular-nums">/{{ provider.total_models }}</span>
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@
|
||||
v-for="endpoint in sortEndpoints(provider.endpoint_health_details)"
|
||||
:key="endpoint.api_format"
|
||||
class="flex flex-col gap-1.5"
|
||||
:title="getEndpointTooltip(endpoint)"
|
||||
:title="getEndpointTooltip(endpoint, locale)"
|
||||
>
|
||||
<!-- 上排:缩写 + 百分比 -->
|
||||
<div class="flex items-center justify-between text-[10px] leading-none">
|
||||
@@ -126,14 +126,14 @@
|
||||
<span
|
||||
v-else
|
||||
class="text-xs text-muted-foreground/50"
|
||||
>暂无端点</span>
|
||||
>{{ legacyT('暂无端点') }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5 text-center">
|
||||
<Badge
|
||||
:variant="provider.is_active ? 'success' : 'secondary'"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ provider.is_active ? '活跃' : '停用' }}
|
||||
{{ legacyT(provider.is_active ? '活跃' : '停用') }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
@@ -145,7 +145,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
|
||||
title="查看详情"
|
||||
:title="legacyT('查看详情')"
|
||||
@click="$emit('viewDetail', provider.id)"
|
||||
>
|
||||
<Eye class="h-3.5 w-3.5" />
|
||||
@@ -154,7 +154,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
|
||||
title="编辑提供商"
|
||||
:title="legacyT('编辑提供商')"
|
||||
@click="$emit('editProvider', provider)"
|
||||
>
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
@@ -163,7 +163,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
|
||||
title="扩展操作配置"
|
||||
:title="legacyT('扩展操作配置')"
|
||||
@click="$emit('openOpsConfig', provider)"
|
||||
>
|
||||
<KeyRound class="h-3.5 w-3.5" />
|
||||
@@ -172,7 +172,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
|
||||
:title="provider.is_active ? '停用提供商' : '启用提供商'"
|
||||
:title="legacyT(provider.is_active ? '停用提供商' : '启用提供商')"
|
||||
@click="$emit('toggleStatus', provider)"
|
||||
>
|
||||
<Power class="h-3.5 w-3.5" />
|
||||
@@ -181,7 +181,7 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-destructive"
|
||||
title="删除提供商"
|
||||
:title="legacyT('删除提供商')"
|
||||
@click="$emit('deleteProvider', provider)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
@@ -212,6 +212,7 @@ import ProviderBalanceCell from './ProviderBalanceCell.vue'
|
||||
import { type ProviderWithEndpointsSummary, formatApiFormatShort } from '@/api/endpoints'
|
||||
import { sortEndpoints, isEndpointAvailable, getEndpointDotColor, getEndpointTooltip } from '@/features/providers/composables/useEndpointStatus'
|
||||
import type { BalanceExtraItem } from '@/features/providers/auth-templates'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
provider: ProviderWithEndpointsSummary
|
||||
@@ -247,6 +248,7 @@ const vAutoFocus = {
|
||||
}
|
||||
|
||||
const localDescriptionValue = ref('')
|
||||
const { legacyT, locale } = useI18n()
|
||||
|
||||
// 当进入编辑模式时,同步 props 的 description
|
||||
watch(
|
||||
@@ -282,8 +284,8 @@ function handleDescriptionKeydown(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function getCredentialLabel(provider: ProviderWithEndpointsSummary): '账号' | '密钥' {
|
||||
function getCredentialLabel(provider: ProviderWithEndpointsSummary): string {
|
||||
const providerType = String(provider.provider_type || '').trim().toLowerCase()
|
||||
return providerType && providerType !== 'custom' ? '账号' : '密钥'
|
||||
return legacyT(providerType && providerType !== 'custom' ? '账号' : '密钥')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<SelectTrigger :class="triggerClass">
|
||||
<SelectValue
|
||||
:placeholder="proxyNodesStore.loading
|
||||
? '加载节点列表中...'
|
||||
? legacyT('加载节点列表中...')
|
||||
: nodeOptions.length === 0
|
||||
? '暂无可用节点'
|
||||
: '选择代理节点...'"
|
||||
? legacyT('暂无可用节点')
|
||||
: legacyT('选择代理节点...')"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
SelectItem,
|
||||
} from '@/components/ui'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { formatRegion } from '@/utils/region'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -49,6 +50,7 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const proxyNodesStore = useProxyNodesStore()
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
/** 在线节点 + 保留当前已选节点(可能已离线) */
|
||||
const nodeOptions = computed(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { EndpointHealthDetail } from '@/api/endpoints'
|
||||
import { defaultLocale, translateLegacyText, type Locale } from '@/i18n/messages'
|
||||
|
||||
// 端点状态枚举
|
||||
export type EndpointStatus = 'disabled' | 'no_keys' | 'keys_disabled' | 'available'
|
||||
@@ -75,23 +76,24 @@ export function getEndpointDotColor(endpoint: EndpointHealthDetail): string {
|
||||
/**
|
||||
* 端点提示文本
|
||||
*/
|
||||
export function getEndpointTooltip(endpoint: EndpointHealthDetail): string {
|
||||
export function getEndpointTooltip(endpoint: EndpointHealthDetail, locale: Locale = defaultLocale): string {
|
||||
const format = endpoint.api_format
|
||||
const status = getEndpointStatus(endpoint)
|
||||
const t = (value: string) => translateLegacyText(value, locale)
|
||||
|
||||
switch (status) {
|
||||
case 'disabled':
|
||||
return `${format}: 端点禁用`
|
||||
return `${format}: ${t('端点禁用')}`
|
||||
case 'no_keys':
|
||||
return `${format}: 未配置密钥`
|
||||
return `${format}: ${t('未配置密钥')}`
|
||||
case 'keys_disabled':
|
||||
return `${format}: 无可用密钥`
|
||||
return `${format}: ${t('无可用密钥')}`
|
||||
case 'available': {
|
||||
const score = endpoint.health_score
|
||||
if (score === undefined || score === null) {
|
||||
return `${format}: 暂无健康数据`
|
||||
return `${format}: ${t('暂无健康数据')}`
|
||||
}
|
||||
return `${format}: 健康度 ${(score * 100).toFixed(0)}%`
|
||||
return `${format}: ${t('健康度')} ${(score * 100).toFixed(0)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ProviderSummaryQuery } from '@/api/endpoints'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
export interface FilterOption {
|
||||
value: string
|
||||
@@ -9,20 +10,21 @@ export interface FilterOption {
|
||||
export function useProviderFilters(
|
||||
globalModels: () => { id: string; name: string }[],
|
||||
) {
|
||||
const { legacyT } = useI18n()
|
||||
// 搜索与筛选
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
const filterApiFormat = ref('all')
|
||||
const filterModel = ref('all')
|
||||
|
||||
const statusFilters: FilterOption[] = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'active', label: '活跃' },
|
||||
{ value: 'inactive', label: '停用' },
|
||||
]
|
||||
const statusFilters = computed<FilterOption[]>(() => [
|
||||
{ value: 'all', label: legacyT('全部状态') },
|
||||
{ value: 'active', label: legacyT('活跃') },
|
||||
{ value: 'inactive', label: legacyT('停用') },
|
||||
])
|
||||
|
||||
const apiFormatFilters: FilterOption[] = [
|
||||
{ value: 'all', label: '全部格式' },
|
||||
const apiFormatFilters = computed<FilterOption[]>(() => [
|
||||
{ value: 'all', label: legacyT('全部格式') },
|
||||
{ value: 'claude:messages', label: 'Claude Messages' },
|
||||
{ value: 'openai:chat', label: 'OpenAI Chat' },
|
||||
{ value: 'openai:responses', label: 'OpenAI Responses' },
|
||||
@@ -35,13 +37,13 @@ export function useProviderFilters(
|
||||
{ value: 'jina:rerank', label: 'Jina Rerank' },
|
||||
{ value: 'doubao:embedding', label: 'Doubao Embedding' },
|
||||
{ value: 'aliyun:multimodal_embedding', label: 'Aliyun Multimodal Embedding' },
|
||||
]
|
||||
])
|
||||
|
||||
const modelFilters = computed<FilterOption[]>(() => {
|
||||
const items = globalModels()
|
||||
.map(m => ({ value: m.id, label: m.name }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
return [{ value: 'all', label: '全部模型' }, ...items]
|
||||
return [{ value: 'all', label: legacyT('全部模型') }, ...items]
|
||||
})
|
||||
|
||||
const hasActiveFilters = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user