+ type="number" step="any" min="0" class="input text-sm" :placeholder="t('admin.channels.form.pricePlaceholder')" />
@@ -254,9 +254,9 @@ const emit = defineEmits<{
const collapsed = ref(props.entry.models.length > 0)
const billingModeOptions = computed(() => [
- { value: 'token', label: 'Token' },
- { value: 'per_request', label: t('admin.channels.billingMode.perRequest', '按次') },
- { value: 'image', label: t('admin.channels.billingMode.image', '图片(按次)') }
+ { value: 'token', label: t('admin.channels.billingMode.token') },
+ { value: 'per_request', label: t('admin.channels.billingMode.perRequest') },
+ { value: 'image', label: t('admin.channels.billingMode.image') }
])
const billingModeLabel = computed(() => {
diff --git a/frontend/src/components/admin/channel/__tests__/types.spec.ts b/frontend/src/components/admin/channel/__tests__/types.spec.ts
index 9fd8e066e0..04758012f1 100644
--- a/frontend/src/components/admin/channel/__tests__/types.spec.ts
+++ b/frontend/src/components/admin/channel/__tests__/types.spec.ts
@@ -16,6 +16,10 @@ function makeInterval(over: Partial): IntervalFormEntry {
}
}
+function t(key: string, params?: Record): string {
+ return `${key}${params ? ` ${JSON.stringify(params)}` : ''}`
+}
+
describe('validateIntervals', () => {
describe('token mode', () => {
it('rejects unbounded interval that is not last', () => {
@@ -23,7 +27,7 @@ describe('validateIntervals', () => {
makeInterval({ min_tokens: 0, max_tokens: null, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 200000, max_tokens: 500000, input_price: 2, output_price: 2 }),
]
- expect(validateIntervals(intervals, 'token')).toMatch(/无上限/)
+ expect(validateIntervals(intervals, 'token', t)).toContain('unboundedLast')
})
it('accepts unbounded interval at the end', () => {
@@ -31,7 +35,7 @@ describe('validateIntervals', () => {
makeInterval({ min_tokens: 0, max_tokens: 200000, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 200000, max_tokens: null, input_price: 2, output_price: 2 }),
]
- expect(validateIntervals(intervals, 'token')).toBeNull()
+ expect(validateIntervals(intervals, 'token', t)).toBeNull()
})
it('rejects overlapping intervals', () => {
@@ -39,15 +43,15 @@ describe('validateIntervals', () => {
makeInterval({ min_tokens: 0, max_tokens: 250000, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 200000, max_tokens: 500000, input_price: 2, output_price: 2 }),
]
- expect(validateIntervals(intervals, 'token')).toMatch(/重叠/)
+ expect(validateIntervals(intervals, 'token', t)).toContain('overlap')
})
- it('defaults mode to token when omitted', () => {
+ it('rejects unbounded interval in token mode', () => {
const intervals: IntervalFormEntry[] = [
makeInterval({ min_tokens: 0, max_tokens: null, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 100, max_tokens: 200, input_price: 2, output_price: 2 }),
]
- expect(validateIntervals(intervals)).toMatch(/无上限/)
+ expect(validateIntervals(intervals, 'token', t)).toContain('unboundedLast')
})
})
@@ -58,22 +62,22 @@ describe('validateIntervals', () => {
makeInterval({ tier_label: '2K', per_request_price: 0.06 }),
makeInterval({ tier_label: '4K', per_request_price: 0.08 }),
]
- expect(validateIntervals(intervals, 'image')).toBeNull()
- expect(validateIntervals(intervals, 'per_request')).toBeNull()
+ expect(validateIntervals(intervals, 'image', t)).toBeNull()
+ expect(validateIntervals(intervals, 'per_request', t)).toBeNull()
})
it('still rejects negative prices', () => {
const intervals: IntervalFormEntry[] = [
makeInterval({ tier_label: '1K', per_request_price: -1 }),
]
- expect(validateIntervals(intervals, 'image')).toMatch(/不能为负数/)
+ expect(validateIntervals(intervals, 'image', t)).toContain('negativePrice')
})
it('still rejects max <= min on a single tier', () => {
const intervals: IntervalFormEntry[] = [
makeInterval({ tier_label: '1K', min_tokens: 100, max_tokens: 50, per_request_price: 0.04 }),
]
- expect(validateIntervals(intervals, 'image')).toMatch(/必须大于/)
+ expect(validateIntervals(intervals, 'image', t)).toContain('maxGreaterThanMin')
})
})
})
diff --git a/frontend/src/components/admin/channel/types.ts b/frontend/src/components/admin/channel/types.ts
index dd9dcd1a52..bda497a172 100644
--- a/frontend/src/components/admin/channel/types.ts
+++ b/frontend/src/components/admin/channel/types.ts
@@ -1,5 +1,7 @@
import type { BillingMode, PricingInterval } from '@/api/admin/channels'
+type TranslateFn = (key: string, params?: Record) => string
+
export interface IntervalFormEntry {
min_tokens: number
max_tokens: number | null
@@ -124,7 +126,8 @@ export function findModelConflict(models: string[]): [string, string] | null {
*/
export function validateIntervals(
intervals: IntervalFormEntry[],
- mode: BillingMode = 'token',
+ mode: BillingMode,
+ t: TranslateFn,
): string | null {
if (!intervals || intervals.length === 0) return null
@@ -132,58 +135,97 @@ export function validateIntervals(
const sorted = [...intervals].sort((a, b) => a.min_tokens - b.min_tokens)
for (let i = 0; i < sorted.length; i++) {
- const err = validateSingleInterval(sorted[i], i)
+ const err = validateSingleInterval(sorted[i], i, t)
if (err) return err
}
// per_request / image 模式按 tier_label 匹配,不做 token 区间重叠校验
if (mode !== 'token') return null
- return checkIntervalOverlap(sorted)
+ return checkIntervalOverlap(sorted, t)
}
-function validateSingleInterval(iv: IntervalFormEntry, idx: number): string | null {
+function intervalValidationMessage(
+ t: TranslateFn,
+ key: string,
+ params: Record,
+): string {
+ return t(`admin.channels.intervalValidation.${key}`, params)
+}
+
+function intervalPriceLabel(t: TranslateFn, key: string): string {
+ return t(`admin.channels.intervalValidation.price.${key}`)
+}
+
+function validateSingleInterval(iv: IntervalFormEntry, idx: number, t: TranslateFn): string | null {
+ const index = idx + 1
if (iv.min_tokens < 0) {
- return `区间 #${idx + 1}: 最小 token 数 (${iv.min_tokens}) 不能为负数`
+ return intervalValidationMessage(
+ t,
+ 'negativeMin',
+ { index, value: iv.min_tokens },
+ )
}
if (iv.max_tokens != null) {
if (iv.max_tokens <= 0) {
- return `区间 #${idx + 1}: 最大 token 数 (${iv.max_tokens}) 必须大于 0`
+ return intervalValidationMessage(
+ t,
+ 'maxPositive',
+ { index, value: iv.max_tokens },
+ )
}
if (iv.max_tokens <= iv.min_tokens) {
- return `区间 #${idx + 1}: 最大 token 数 (${iv.max_tokens}) 必须大于最小 token 数 (${iv.min_tokens})`
+ return intervalValidationMessage(
+ t,
+ 'maxGreaterThanMin',
+ { index, max: iv.max_tokens, min: iv.min_tokens },
+ )
}
}
- return validateIntervalPrices(iv, idx)
+ return validateIntervalPrices(iv, idx, t)
}
-function validateIntervalPrices(iv: IntervalFormEntry, idx: number): string | null {
+function validateIntervalPrices(iv: IntervalFormEntry, idx: number, t: TranslateFn): string | null {
+ const index = idx + 1
const prices: [string, number | string | null][] = [
- ['输入价格', iv.input_price],
- ['输出价格', iv.output_price],
- ['缓存写入价格', iv.cache_write_price],
- ['缓存读取价格', iv.cache_read_price],
- ['单次价格', iv.per_request_price],
+ ['inputPrice', iv.input_price],
+ ['outputPrice', iv.output_price],
+ ['cacheWritePrice', iv.cache_write_price],
+ ['cacheReadPrice', iv.cache_read_price],
+ ['perRequestPrice', iv.per_request_price],
]
- for (const [name, val] of prices) {
+ for (const [key, val] of prices) {
if (val != null && val !== '' && Number(val) < 0) {
- return `区间 #${idx + 1}: ${name}不能为负数`
+ const field = intervalPriceLabel(t, key)
+ return intervalValidationMessage(
+ t,
+ 'negativePrice',
+ { index, field },
+ )
}
}
return null
}
-function checkIntervalOverlap(sorted: IntervalFormEntry[]): string | null {
+function checkIntervalOverlap(sorted: IntervalFormEntry[], t: TranslateFn): string | null {
for (let i = 0; i < sorted.length; i++) {
// 无上限区间必须是最后一个
if (sorted[i].max_tokens == null && i < sorted.length - 1) {
- return `区间 #${i + 1}: 无上限区间(最大 token 数为空)只能是最后一个`
+ return intervalValidationMessage(
+ t,
+ 'unboundedLast',
+ { index: i + 1 },
+ )
}
if (i === 0) continue
const prev = sorted[i - 1]
// (min, max] 语义:前一个区间上界 > 当前区间下界则重叠
if (prev.max_tokens == null || prev.max_tokens > sorted[i].min_tokens) {
const prevMax = prev.max_tokens == null ? '∞' : String(prev.max_tokens)
- return `区间 #${i} 和 #${i + 1} 重叠:前一个区间上界 (${prevMax}) 大于当前区间下界 (${sorted[i].min_tokens})`
+ return intervalValidationMessage(
+ t,
+ 'overlap',
+ { previousIndex: i, currentIndex: i + 1, previousMax: prevMax, currentMin: sorted[i].min_tokens },
+ )
}
}
return null
diff --git a/frontend/src/components/auth/DingTalkOAuthSection.vue b/frontend/src/components/auth/DingTalkOAuthSection.vue
index 9003225d78..55b919e659 100644
--- a/frontend/src/components/auth/DingTalkOAuthSection.vue
+++ b/frontend/src/components/auth/DingTalkOAuthSection.vue
@@ -19,7 +19,7 @@
font-weight="bold"
fill="white"
text-anchor="middle"
- >钉
+ >D
{{ t('auth.dingtalk.signIn') }}
diff --git a/frontend/src/components/auth/LoginAgreementPrompt.vue b/frontend/src/components/auth/LoginAgreementPrompt.vue
index dd71cbdcbc..f73a3b1c4c 100644
--- a/frontend/src/components/auth/LoginAgreementPrompt.vue
+++ b/frontend/src/components/auth/LoginAgreementPrompt.vue
@@ -17,7 +17,7 @@
for="login-agreement-consent"
class="cursor-pointer text-gray-700 dark:text-dark-200"
>
- 我已阅读并同意
+ {{ t('legal.loginAgreementPrompt.checkboxPrefix') }}
{{ doc.title }}
- 、
+ {{ t('legal.loginAgreementPrompt.documentSeparator') }}
@@ -42,9 +42,9 @@
-
继续登录前需要先同意最新条款。
+
{{ t('legal.loginAgreementPrompt.noticeTitle') }}
- 未同意前,账号密码输入和快捷登录会保持禁用。
+ {{ t('legal.loginAgreementPrompt.noticeDescription') }}