>(() => ({
diff --git a/frontend/src/components/user/profile/ProfilePasswordForm.vue b/frontend/src/components/user/profile/ProfilePasswordForm.vue
index d44cac68..e906973d 100644
--- a/frontend/src/components/user/profile/ProfilePasswordForm.vue
+++ b/frontend/src/components/user/profile/ProfilePasswordForm.vue
@@ -50,12 +50,6 @@
autocomplete="new-password"
class="input"
/>
-
- {{ t('profile.passwordsNotMatch') }}
-
diff --git a/frontend/src/components/user/profile/TotpDisableDialog.vue b/frontend/src/components/user/profile/TotpDisableDialog.vue
index cd93764c..c5e9afe7 100644
--- a/frontend/src/components/user/profile/TotpDisableDialog.vue
+++ b/frontend/src/components/user/profile/TotpDisableDialog.vue
@@ -63,11 +63,6 @@
/>
-
-
- {{ error }}
-
-
@@ -104,7 +99,6 @@ const appStore = useAppStore()
const methodLoading = ref(true)
const verificationMethod = ref<'email' | 'password'>('password')
const loading = ref(false)
-const error = ref('')
const sendingCode = ref(false)
const codeCooldown = ref(0)
const cooldownTimer = ref | null>(null)
@@ -164,7 +158,6 @@ const handleDisable = async () => {
if (!canSubmit.value) return
loading.value = true
- error.value = ''
try {
const request = verificationMethod.value === 'email'
@@ -175,7 +168,7 @@ const handleDisable = async () => {
appStore.showSuccess(t('profile.totp.disableSuccess'))
emit('success')
} catch (err: any) {
- error.value = err.response?.data?.message || t('profile.totp.disableFailed')
+ appStore.showError(err.response?.data?.message || t('profile.totp.disableFailed'))
} finally {
loading.value = false
}
diff --git a/frontend/src/components/user/profile/TotpSetupModal.vue b/frontend/src/components/user/profile/TotpSetupModal.vue
index b544e75b..312ecf7f 100644
--- a/frontend/src/components/user/profile/TotpSetupModal.vue
+++ b/frontend/src/components/user/profile/TotpSetupModal.vue
@@ -61,10 +61,6 @@
-
- {{ verifyError }}
-
-
{{ t('common.cancel') }}
@@ -151,10 +147,6 @@
-
- {{ error }}
-
-
{{ t('common.back') }}
@@ -195,7 +187,6 @@ const step = ref(0)
const methodLoading = ref(true)
const verificationMethod = ref<'email' | 'password'>('password')
const verifyForm = ref({ emailCode: '', password: '' })
-const verifyError = ref('')
const sendingCode = ref(false)
const codeCooldown = ref(0)
const cooldownTimer = ref | null>(null)
@@ -203,7 +194,6 @@ const cooldownTimer = ref | null>(null)
const setupLoading = ref(false)
const setupData = ref(null)
const verifying = ref(false)
-const error = ref('')
const code = ref(['', '', '', '', '', ''])
const inputRefs = ref<(HTMLInputElement | null)[]>([])
const qrCodeDataUrl = ref('')
@@ -361,7 +351,6 @@ const handleSendCode = async () => {
const handleVerifyAndSetup = async () => {
setupLoading.value = true
- verifyError.value = ''
try {
const request = verificationMethod.value === 'email'
@@ -371,7 +360,7 @@ const handleVerifyAndSetup = async () => {
setupData.value = await totpAPI.initiateSetup(request)
step.value = 1
} catch (err: any) {
- verifyError.value = err.response?.data?.message || t('profile.totp.setupFailed')
+ appStore.showError(err.response?.data?.message || t('profile.totp.setupFailed'))
} finally {
setupLoading.value = false
}
@@ -382,7 +371,6 @@ const handleVerify = async () => {
if (totpCode.length !== 6 || !setupData.value) return
verifying.value = true
- error.value = ''
try {
await totpAPI.enable({
@@ -392,7 +380,7 @@ const handleVerify = async () => {
appStore.showSuccess(t('profile.totp.enableSuccess'))
emit('success')
} catch (err: any) {
- error.value = err.response?.data?.message || t('profile.totp.verifyFailed')
+ appStore.showError(err.response?.data?.message || t('profile.totp.verifyFailed'))
code.value = ['', '', '', '', '', '']
nextTick(() => {
inputRefs.value[0]?.focus()
diff --git a/frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts b/frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts
new file mode 100644
index 00000000..b7b22966
--- /dev/null
+++ b/frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts
@@ -0,0 +1,79 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+import ProfilePasswordForm from '@/components/user/profile/ProfilePasswordForm.vue'
+
+const { changePasswordMock, showSuccessMock, showErrorMock } = vi.hoisted(() => ({
+ changePasswordMock: vi.fn(),
+ showSuccessMock: vi.fn(),
+ showErrorMock: vi.fn()
+}))
+
+vi.mock('@/api', () => ({
+ userAPI: {
+ changePassword: changePasswordMock
+ }
+}))
+
+vi.mock('@/stores/app', () => ({
+ useAppStore: () => ({
+ showSuccess: showSuccessMock,
+ showError: showErrorMock
+ })
+}))
+
+vi.mock('vue-i18n', async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ useI18n: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ 'profile.changePassword': 'Change Password',
+ 'profile.currentPassword': 'Current Password',
+ 'profile.newPassword': 'New Password',
+ 'profile.confirmNewPassword': 'Confirm New Password',
+ 'profile.passwordHint': 'Password must be at least 8 characters long',
+ 'profile.changingPassword': 'Changing...',
+ 'profile.changePasswordButton': 'Change Password',
+ 'profile.passwordsNotMatch': 'New passwords do not match',
+ 'profile.passwordTooShort': 'Password must be at least 8 characters long',
+ 'profile.passwordChangeSuccess': 'Password changed successfully',
+ 'profile.passwordChangeFailed': 'Failed to change password'
+ }
+ return translations[key] ?? key
+ }
+ })
+ }
+})
+
+describe('ProfilePasswordForm', () => {
+ it('shows validation failures as toast messages instead of inline errors', async () => {
+ const wrapper = mount(ProfilePasswordForm)
+
+ await wrapper.get('#old_password').setValue('old-password')
+ await wrapper.get('#new_password').setValue('new-password')
+ await wrapper.get('#confirm_password').setValue('different-password')
+ await wrapper.get('form').trigger('submit.prevent')
+
+ expect(changePasswordMock).not.toHaveBeenCalled()
+ expect(showErrorMock).toHaveBeenCalledWith('New passwords do not match')
+ expect(wrapper.find('.input-error-text').exists()).toBe(false)
+ })
+
+ it('shows API failures as toast messages', async () => {
+ changePasswordMock.mockRejectedValue({
+ response: { data: { detail: 'backend failure' } }
+ })
+
+ const wrapper = mount(ProfilePasswordForm)
+
+ await wrapper.get('#old_password').setValue('old-password')
+ await wrapper.get('#new_password').setValue('new-password')
+ await wrapper.get('#confirm_password').setValue('new-password')
+ await wrapper.get('form').trigger('submit.prevent')
+
+ expect(changePasswordMock).toHaveBeenCalledWith('old-password', 'new-password')
+ expect(showErrorMock).toHaveBeenCalledWith('backend failure')
+ expect(wrapper.find('.input-error-text').exists()).toBe(false)
+ })
+})
diff --git a/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts b/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts
index 0259f902..44d9fcf9 100644
--- a/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts
+++ b/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts
@@ -7,7 +7,10 @@ const mocks = vi.hoisted(() => ({
showSuccess: vi.fn(),
showError: vi.fn(),
getVerificationMethod: vi.fn(),
- sendVerifyCode: vi.fn()
+ sendVerifyCode: vi.fn(),
+ initiateSetup: vi.fn(),
+ enable: vi.fn(),
+ disable: vi.fn()
}))
vi.mock('vue-i18n', () => ({
@@ -27,9 +30,9 @@ vi.mock('@/api', () => ({
totpAPI: {
getVerificationMethod: mocks.getVerificationMethod,
sendVerifyCode: mocks.sendVerifyCode,
- initiateSetup: vi.fn(),
- enable: vi.fn(),
- disable: vi.fn()
+ initiateSetup: mocks.initiateSetup,
+ enable: mocks.enable,
+ disable: mocks.disable
}
}))
@@ -49,9 +52,19 @@ describe('TOTP 弹窗定时器清理', () => {
mocks.showError.mockReset()
mocks.getVerificationMethod.mockReset()
mocks.sendVerifyCode.mockReset()
+ mocks.initiateSetup.mockReset()
+ mocks.enable.mockReset()
+ mocks.disable.mockReset()
mocks.getVerificationMethod.mockResolvedValue({ method: 'email' })
mocks.sendVerifyCode.mockResolvedValue({ success: true })
+ mocks.initiateSetup.mockResolvedValue({
+ qr_code_url: 'otpauth://totp/Sub2API:test?secret=ABC123',
+ secret: 'ABC123',
+ setup_token: 'setup-token'
+ })
+ mocks.enable.mockResolvedValue({ success: true })
+ mocks.disable.mockResolvedValue({ success: true })
setIntervalSpy = vi.spyOn(window, 'setInterval').mockImplementation(((handler: TimerHandler) => {
void handler
@@ -105,4 +118,40 @@ describe('TOTP 弹窗定时器清理', () => {
expect(clearIntervalSpy).toHaveBeenCalledWith(timerId)
})
+
+ it('TotpSetupModal 失败时改用 toast 并不渲染内联错误', async () => {
+ mocks.getVerificationMethod.mockResolvedValue({ method: 'password' })
+ mocks.initiateSetup.mockRejectedValue({
+ response: { data: { message: 'setup failed' } }
+ })
+
+ const wrapper = mount(TotpSetupModal)
+ await flushPromises()
+
+ await wrapper.get('input[type="password"]').setValue('correct horse battery staple')
+ await wrapper.get('button[type="button"].btn-primary').trigger('click')
+ await flushPromises()
+
+ expect(mocks.showError).toHaveBeenCalledWith('setup failed')
+ expect(wrapper.text()).not.toContain('setup failed')
+ expect(wrapper.find('.bg-red-50').exists()).toBe(false)
+ })
+
+ it('TotpDisableDialog 失败时改用 toast 并不渲染内联错误', async () => {
+ mocks.getVerificationMethod.mockResolvedValue({ method: 'password' })
+ mocks.disable.mockRejectedValue({
+ response: { data: { message: 'disable failed' } }
+ })
+
+ const wrapper = mount(TotpDisableDialog)
+ await flushPromises()
+
+ await wrapper.get('input[type="password"]').setValue('correct horse battery staple')
+ await wrapper.get('form').trigger('submit.prevent')
+ await flushPromises()
+
+ expect(mocks.showError).toHaveBeenCalledWith('disable failed')
+ expect(wrapper.text()).not.toContain('disable failed')
+ expect(wrapper.find('.bg-red-50').exists()).toBe(false)
+ })
})
diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue
index d3683fcd..ef4a13e8 100644
--- a/frontend/src/views/admin/SettingsView.vue
+++ b/frontend/src/views/admin/SettingsView.vue
@@ -1372,30 +1372,20 @@
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
- {{ localText("微信登录", "WeChat Connect") }}
+ {{ t("admin.settings.wechatConnect.title") }}
- {{
- localText(
- "用于微信开放平台或公众号/小程序的第三方登录配置。",
- "Third-party login configuration for WeChat Open Platform or Official Account / Mini Program.",
- )
- }}
+ {{ t("admin.settings.wechatConnect.description") }}
@@ -1470,29 +1446,19 @@
- {{ localText("模式", "Mode") }}
+ {{ t("admin.settings.wechatConnect.modeLabel") }}
- {{
- localText(
- "非微信环境使用开放平台",
- "Use Open outside WeChat",
- )
- }}
+ {{ t("admin.settings.wechatConnect.openModeLabel") }}
- {{
- localText(
- "浏览器不在微信内时,自动走开放平台扫码授权。",
- "Use Open Platform QR authorization outside the WeChat browser.",
- )
- }}
+ {{ t("admin.settings.wechatConnect.openModeHint") }}
- {{
- localText(
- "微信环境使用公众号",
- "Use MP inside WeChat",
- )
- }}
+ {{ t("admin.settings.wechatConnect.mpModeLabel") }}
- {{
- localText(
- "浏览器在微信内时,自动走公众号授权。",
- "Use Official Account authorization inside the WeChat browser.",
- )
- }}
+ {{ t("admin.settings.wechatConnect.mpModeHint") }}
- {{ localText("回调地址", "Redirect URL") }}
+ {{ t("admin.settings.wechatConnect.redirectUrlLabel") }}
@@ -2215,15 +2151,10 @@
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
- {{ localText("认证来源默认值", "Auth Source Defaults") }}
+ {{ t("admin.settings.authSourceDefaults.title") }}
- {{
- localText(
- "按注册来源配置新用户默认余额、并发、订阅与授权策略。",
- "Configure per-source default balance, concurrency, subscriptions, and grant rules.",
- )
- }}
+ {{ t("admin.settings.authSourceDefaults.description") }}
@@ -2232,20 +2163,10 @@
>
- {{
- localText(
- "第三方注册强制补充邮箱",
- "Require email on third-party signup",
- )
- }}
+ {{ t("admin.settings.authSourceDefaults.requireEmailLabel") }}
- {{
- localText(
- "启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。",
- "When enabled, Linux DO, OIDC, and WeChat signups must provide an email before account creation.",
- )
- }}
+ {{ t("admin.settings.authSourceDefaults.requireEmailHint") }}
@@ -2280,12 +2201,7 @@
class="mt-4 space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
>
- {{
- localText(
- "以下默认值会在该来源注册新用户时发放;首次绑定时授权仅作用于已有账号绑定该来源。",
- "These defaults apply when a new user registers through this source. Grant on first bind only applies when an existing user binds this source.",
- )
- }}
+ {{ t("admin.settings.authSourceDefaults.enabledHint") }}
@@ -2331,19 +2247,12 @@
- {{
- localText("首次绑定时授权", "Grant on first bind")
- }}
+ {{ t("admin.settings.authSourceDefaults.grantOnFirstBindLabel") }}
- {{
- localText(
- "已有账号首次绑定该来源时发放默认权益。",
- "Grant default entitlements when an existing user first binds this source.",
- )
- }}
+ {{ t("admin.settings.authSourceDefaults.grantOnFirstBindHint") }}
- {{ localText("默认订阅", "Default subscriptions") }}
+ {{ t("admin.settings.authSourceDefaults.defaultSubscriptionsLabel") }}
- {{
- localText(
- "仅对当前认证来源生效,未配置时不追加来源专属订阅。",
- "Applies only to this auth source. Leave empty to skip source-specific subscriptions.",
- )
- }}
+ {{ t("admin.settings.authSourceDefaults.defaultSubscriptionsHint") }}
- {{
- localText(
- "当前来源未配置专属默认订阅。",
- "No source-specific default subscriptions configured.",
- )
- }}
+ {{ t("admin.settings.authSourceDefaults.noSourceSubscriptions") }}
@@ -2621,18 +2520,12 @@
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
- localText(
- "OpenAI 实验调度策略",
- "OpenAI experimental scheduler policy",
- )
+ t("admin.settings.openaiExperimentalScheduler.title")
}}
{{
- localText(
- "默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。",
- "Disabled by default. When enabled, this only changes the gateway's experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability.",
- )
+ t("admin.settings.openaiExperimentalScheduler.description")
}}
@@ -4131,20 +4024,16 @@
class="font-medium text-gray-900 dark:text-white"
>
{{
- localText(
- `${visibleMethod.title} 可见方式`,
- `${visibleMethod.title} visible method`,
- )
+ t("admin.settings.paymentVisibleMethods.methodLabel", {
+ title: visibleMethod.title,
+ })
}}
{{
- localText(
- "控制前台结算页是否展示该方式,以及展示时使用的来源键。",
- "Controls whether checkout shows this method and which source key it exposes.",
- )
+ t("admin.settings.paymentVisibleMethods.methodHint")
}}
@@ -4163,7 +4052,7 @@
- {{ localText("支付来源", "Payment source") }}
+ {{ t("admin.settings.paymentVisibleMethods.sourceLabel") }}
{{
- localText(
- "启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。",
- "Choose an explicit source before enabling the method. Not configured methods are not exposed.",
- )
+ t("admin.settings.paymentVisibleMethods.sourceHint")
}}
@@ -5028,35 +4914,23 @@ const authSourceDefaults = reactive(
const authSourceDefaultsMeta = computed(() => [
{
source: "email" as AuthSourceType,
- title: localText("邮箱注册", "Email signup"),
- description: localText(
- "适用于邮箱密码注册的新用户默认配额。",
- "Default quota grants for email-password signups.",
- ),
+ title: t("admin.settings.authSourceDefaults.sources.email.title"),
+ description: t("admin.settings.authSourceDefaults.sources.email.description"),
},
{
source: "linuxdo" as AuthSourceType,
- title: localText("Linux DO 登录", "Linux DO signup"),
- description: localText(
- "适用于 Linux DO 第三方注册的新用户默认配额。",
- "Default quota grants for Linux DO signups.",
- ),
+ title: t("admin.settings.authSourceDefaults.sources.linuxdo.title"),
+ description: t("admin.settings.authSourceDefaults.sources.linuxdo.description"),
},
{
source: "oidc" as AuthSourceType,
- title: localText("OIDC 登录", "OIDC signup"),
- description: localText(
- "适用于 OIDC 第三方注册的新用户默认配额。",
- "Default quota grants for OIDC signups.",
- ),
+ title: t("admin.settings.authSourceDefaults.sources.oidc.title"),
+ description: t("admin.settings.authSourceDefaults.sources.oidc.description"),
},
{
source: "wechat" as AuthSourceType,
- title: localText("微信登录", "WeChat signup"),
- description: localText(
- "适用于微信第三方注册的新用户默认配额。",
- "Default quota grants for WeChat signups.",
- ),
+ title: t("admin.settings.authSourceDefaults.sources.wechat.title"),
+ description: t("admin.settings.authSourceDefaults.sources.wechat.description"),
},
]);
@@ -5130,10 +5004,9 @@ function validatePaymentVisibleMethodSelections(): boolean {
}
appStore.showError(
- localText(
- `${visibleMethod.title} 已启用,请先选择支付来源`,
- `Select a payment source before enabling ${visibleMethod.title}`,
- ),
+ t("admin.settings.paymentVisibleMethods.sourceRequiredError", {
+ title: visibleMethod.title,
+ }),
);
return false;
}
@@ -5479,10 +5352,7 @@ async function setAndCopyWeChatRedirectUrl() {
form.wechat_connect_redirect_url = url;
await copyToClipboard(
url,
- localText(
- "已使用当前站点生成回调地址并复制到剪贴板",
- "Redirect URL generated and copied to clipboard",
- ),
+ t("admin.settings.wechatConnect.redirectUrlSetAndCopied"),
);
}
diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue
index 39c9b377..ea67f695 100644
--- a/frontend/src/views/admin/UsersView.vue
+++ b/frontend/src/views/admin/UsersView.vue
@@ -700,7 +700,7 @@ const getAttributeValue = (userId: number, attrId: number): string => {
// All possible columns (for column settings)
const allColumns = computed(() => [
{ key: 'email', label: t('admin.users.columns.user'), sortable: true },
- { key: 'id', label: 'ID', sortable: true },
+ { key: 'id', label: t('admin.users.columns.id'), sortable: true },
{ key: 'username', label: t('admin.users.columns.username'), sortable: true },
{ key: 'notes', label: t('admin.users.columns.notes'), sortable: false },
// Dynamic attribute columns
diff --git a/frontend/src/views/admin/__tests__/SettingsView.spec.ts b/frontend/src/views/admin/__tests__/SettingsView.spec.ts
index 16a36d61..ee998971 100644
--- a/frontend/src/views/admin/__tests__/SettingsView.spec.ts
+++ b/frontend/src/views/admin/__tests__/SettingsView.spec.ts
@@ -93,10 +93,61 @@ vi.mock("@/utils/apiError", () => ({
vi.mock("vue-i18n", async () => {
const actual = await vi.importActual("vue-i18n");
+ const translations: Record = {
+ "admin.settings.wechatConnect.title": "微信登录",
+ "admin.settings.wechatConnect.description": "用于微信开放平台或公众号/小程序的第三方登录配置。",
+ "admin.settings.wechatConnect.enabledLabel": "启用微信登录",
+ "admin.settings.wechatConnect.enabledHint": "开启后可使用微信第三方登录回调与授权配置。",
+ "admin.settings.wechatConnect.appIdLabel": "AppID",
+ "admin.settings.wechatConnect.appIdPlaceholder": "微信开放平台 AppID",
+ "admin.settings.wechatConnect.appSecretLabel": "AppSecret",
+ "admin.settings.wechatConnect.appSecretConfiguredPlaceholder": "密钥已配置,留空以保留当前值。",
+ "admin.settings.wechatConnect.appSecretPlaceholder": "微信开放平台 AppSecret",
+ "admin.settings.wechatConnect.appSecretConfiguredHint": "密钥已配置,留空以保留当前值。",
+ "admin.settings.wechatConnect.appSecretHint": "填写后会覆盖当前微信密钥。",
+ "admin.settings.wechatConnect.modeLabel": "模式",
+ "admin.settings.wechatConnect.openModeLabel": "非微信环境使用开放平台",
+ "admin.settings.wechatConnect.openModeHint": "浏览器不在微信内时,自动走开放平台扫码授权。",
+ "admin.settings.wechatConnect.mpModeLabel": "微信环境使用公众号",
+ "admin.settings.wechatConnect.mpModeHint": "浏览器在微信内时,自动走公众号授权。",
+ "admin.settings.wechatConnect.redirectUrlLabel": "回调地址",
+ "admin.settings.wechatConnect.redirectUrlPlaceholder": "https://your-site.com/api/v1/auth/oauth/wechat/callback",
+ "admin.settings.wechatConnect.generateAndCopy": "使用当前站点生成并复制",
+ "admin.settings.wechatConnect.redirectUrlSetAndCopied": "已使用当前站点生成回调地址并复制到剪贴板",
+ "admin.settings.wechatConnect.frontendRedirectUrlLabel": "前端回调地址",
+ "admin.settings.wechatConnect.frontendRedirectUrlPlaceholder": "/auth/wechat/callback",
+ "admin.settings.wechatConnect.frontendRedirectUrlHint": "通常用于前端路由回调地址,需与后端配置保持一致。",
+ "admin.settings.authSourceDefaults.title": "认证来源默认值",
+ "admin.settings.authSourceDefaults.description": "按注册来源配置新用户默认余额、并发、订阅与授权策略。",
+ "admin.settings.authSourceDefaults.requireEmailLabel": "第三方注册强制补充邮箱",
+ "admin.settings.authSourceDefaults.requireEmailHint": "启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。",
+ "admin.settings.authSourceDefaults.enabledHint": "以下默认值会在该来源注册新用户时发放;首次绑定时授权仅作用于已有账号绑定该来源。",
+ "admin.settings.authSourceDefaults.sources.email.title": "邮箱注册",
+ "admin.settings.authSourceDefaults.sources.email.description": "适用于邮箱密码注册的新用户默认配额。",
+ "admin.settings.authSourceDefaults.sources.linuxdo.title": "Linux DO 登录",
+ "admin.settings.authSourceDefaults.sources.linuxdo.description": "适用于 Linux DO 第三方注册的新用户默认配额。",
+ "admin.settings.authSourceDefaults.sources.oidc.title": "OIDC 登录",
+ "admin.settings.authSourceDefaults.sources.oidc.description": "适用于 OIDC 第三方注册的新用户默认配额。",
+ "admin.settings.authSourceDefaults.sources.wechat.title": "微信登录",
+ "admin.settings.authSourceDefaults.sources.wechat.description": "适用于微信第三方注册的新用户默认配额。",
+ "admin.settings.authSourceDefaults.grantOnFirstBindLabel": "首次绑定时授权",
+ "admin.settings.authSourceDefaults.grantOnFirstBindHint": "已有账号首次绑定该来源时发放默认权益。",
+ "admin.settings.authSourceDefaults.defaultSubscriptionsLabel": "默认订阅",
+ "admin.settings.authSourceDefaults.defaultSubscriptionsHint": "仅对当前认证来源生效,未配置时不追加来源专属订阅。",
+ "admin.settings.authSourceDefaults.noSourceSubscriptions": "当前来源未配置专属默认订阅。",
+ "admin.settings.paymentVisibleMethods.methodLabel": "{title} 可见方式",
+ "admin.settings.paymentVisibleMethods.methodHint": "控制前台结算页是否展示该方式,以及展示时使用的来源键。",
+ "admin.settings.paymentVisibleMethods.sourceLabel": "支付来源",
+ "admin.settings.paymentVisibleMethods.sourceHint": "启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。",
+ "admin.settings.paymentVisibleMethods.sourceRequiredError": "{title} 已启用,请先选择支付来源。",
+ "admin.settings.openaiExperimentalScheduler.title": "OpenAI 实验调度策略",
+ "admin.settings.openaiExperimentalScheduler.description": "默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。",
+ };
return {
...actual,
useI18n: () => ({
- t: (key: string) => key,
+ t: (key: string, params?: Record) =>
+ (translations[key] ?? key).replace(/\{(\w+)\}/g, (_, token) => params?.[token] ?? `{${token}}`),
locale: ref("zh-CN"),
}),
};