frontend: normalize profile and admin i18n cleanup
This commit is contained in:
@@ -92,7 +92,7 @@ const avatarQualitySteps = [0.92, 0.84, 0.76, 0.68, 0.6, 0.52, 0.44, 0.36]
|
|||||||
const avatarDraft = ref('')
|
const avatarDraft = ref('')
|
||||||
const avatarSaving = ref(false)
|
const avatarSaving = ref(false)
|
||||||
|
|
||||||
const displayName = computed(() => props.user?.username?.trim() || props.user?.email?.trim() || 'User')
|
const displayName = computed(() => props.user?.username?.trim() || props.user?.email?.trim() || t('profile.user'))
|
||||||
const avatarInitial = computed(() => displayName.value.charAt(0).toUpperCase() || 'U')
|
const avatarInitial = computed(() => displayName.value.charAt(0).toUpperCase() || 'U')
|
||||||
const avatarPreviewUrl = computed(() => avatarDraft.value.trim() || props.user?.avatar_url?.trim() || '')
|
const avatarPreviewUrl = computed(() => avatarDraft.value.trim() || props.user?.avatar_url?.trim() || '')
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,11 @@
|
|||||||
<span
|
<span
|
||||||
:class="['badge', user?.status === 'active' ? 'badge-success' : 'badge-danger']"
|
:class="['badge', user?.status === 'active' ? 'badge-success' : 'badge-danger']"
|
||||||
>
|
>
|
||||||
{{ user?.status }}
|
{{
|
||||||
|
user?.status === 'active'
|
||||||
|
? t('common.active')
|
||||||
|
: t('common.disabled')
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +84,7 @@ const props = defineProps<{
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const avatarUrl = computed(() => props.user?.avatar_url?.trim() || '')
|
const avatarUrl = computed(() => props.user?.avatar_url?.trim() || '')
|
||||||
const displayName = computed(() => props.user?.username?.trim() || props.user?.email?.trim() || 'User')
|
const displayName = computed(() => props.user?.username?.trim() || props.user?.email?.trim() || t('profile.user'))
|
||||||
const avatarInitial = computed(() => displayName.value.charAt(0).toUpperCase() || 'U')
|
const avatarInitial = computed(() => displayName.value.charAt(0).toUpperCase() || 'U')
|
||||||
|
|
||||||
const providerLabels = computed<Record<UserAuthProvider, string>>(() => ({
|
const providerLabels = computed<Record<UserAuthProvider, string>>(() => ({
|
||||||
|
|||||||
@@ -50,12 +50,6 @@
|
|||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
class="input"
|
class="input"
|
||||||
/>
|
/>
|
||||||
<p
|
|
||||||
v-if="form.new_password && form.confirm_password && form.new_password !== form.confirm_password"
|
|
||||||
class="input-error-text"
|
|
||||||
>
|
|
||||||
{{ t('profile.passwordsNotMatch') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-4">
|
<div class="flex justify-end pt-4">
|
||||||
|
|||||||
@@ -63,11 +63,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error -->
|
|
||||||
<div v-if="error" class="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-end gap-3 pt-4">
|
<div class="flex justify-end gap-3 pt-4">
|
||||||
<button type="button" class="btn btn-secondary" @click="$emit('close')">
|
<button type="button" class="btn btn-secondary" @click="$emit('close')">
|
||||||
@@ -104,7 +99,6 @@ const appStore = useAppStore()
|
|||||||
const methodLoading = ref(true)
|
const methodLoading = ref(true)
|
||||||
const verificationMethod = ref<'email' | 'password'>('password')
|
const verificationMethod = ref<'email' | 'password'>('password')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
|
||||||
const sendingCode = ref(false)
|
const sendingCode = ref(false)
|
||||||
const codeCooldown = ref(0)
|
const codeCooldown = ref(0)
|
||||||
const cooldownTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
const cooldownTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
@@ -164,7 +158,6 @@ const handleDisable = async () => {
|
|||||||
if (!canSubmit.value) return
|
if (!canSubmit.value) return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = verificationMethod.value === 'email'
|
const request = verificationMethod.value === 'email'
|
||||||
@@ -175,7 +168,7 @@ const handleDisable = async () => {
|
|||||||
appStore.showSuccess(t('profile.totp.disableSuccess'))
|
appStore.showSuccess(t('profile.totp.disableSuccess'))
|
||||||
emit('success')
|
emit('success')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err.response?.data?.message || t('profile.totp.disableFailed')
|
appStore.showError(err.response?.data?.message || t('profile.totp.disableFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="verifyError" class="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
|
||||||
{{ verifyError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end gap-3 pt-4">
|
<div class="flex justify-end gap-3 pt-4">
|
||||||
<button type="button" class="btn btn-secondary" @click="$emit('close')">
|
<button type="button" class="btn btn-secondary" @click="$emit('close')">
|
||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
@@ -151,10 +147,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button type="button" class="btn btn-secondary" @click="step = 1">
|
<button type="button" class="btn btn-secondary" @click="step = 1">
|
||||||
{{ t('common.back') }}
|
{{ t('common.back') }}
|
||||||
@@ -195,7 +187,6 @@ const step = ref(0)
|
|||||||
const methodLoading = ref(true)
|
const methodLoading = ref(true)
|
||||||
const verificationMethod = ref<'email' | 'password'>('password')
|
const verificationMethod = ref<'email' | 'password'>('password')
|
||||||
const verifyForm = ref({ emailCode: '', password: '' })
|
const verifyForm = ref({ emailCode: '', password: '' })
|
||||||
const verifyError = ref('')
|
|
||||||
const sendingCode = ref(false)
|
const sendingCode = ref(false)
|
||||||
const codeCooldown = ref(0)
|
const codeCooldown = ref(0)
|
||||||
const cooldownTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
const cooldownTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
@@ -203,7 +194,6 @@ const cooldownTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
|||||||
const setupLoading = ref(false)
|
const setupLoading = ref(false)
|
||||||
const setupData = ref<TotpSetupResponse | null>(null)
|
const setupData = ref<TotpSetupResponse | null>(null)
|
||||||
const verifying = ref(false)
|
const verifying = ref(false)
|
||||||
const error = ref('')
|
|
||||||
const code = ref<string[]>(['', '', '', '', '', ''])
|
const code = ref<string[]>(['', '', '', '', '', ''])
|
||||||
const inputRefs = ref<(HTMLInputElement | null)[]>([])
|
const inputRefs = ref<(HTMLInputElement | null)[]>([])
|
||||||
const qrCodeDataUrl = ref('')
|
const qrCodeDataUrl = ref('')
|
||||||
@@ -361,7 +351,6 @@ const handleSendCode = async () => {
|
|||||||
|
|
||||||
const handleVerifyAndSetup = async () => {
|
const handleVerifyAndSetup = async () => {
|
||||||
setupLoading.value = true
|
setupLoading.value = true
|
||||||
verifyError.value = ''
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = verificationMethod.value === 'email'
|
const request = verificationMethod.value === 'email'
|
||||||
@@ -371,7 +360,7 @@ const handleVerifyAndSetup = async () => {
|
|||||||
setupData.value = await totpAPI.initiateSetup(request)
|
setupData.value = await totpAPI.initiateSetup(request)
|
||||||
step.value = 1
|
step.value = 1
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
verifyError.value = err.response?.data?.message || t('profile.totp.setupFailed')
|
appStore.showError(err.response?.data?.message || t('profile.totp.setupFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
setupLoading.value = false
|
setupLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -382,7 +371,6 @@ const handleVerify = async () => {
|
|||||||
if (totpCode.length !== 6 || !setupData.value) return
|
if (totpCode.length !== 6 || !setupData.value) return
|
||||||
|
|
||||||
verifying.value = true
|
verifying.value = true
|
||||||
error.value = ''
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await totpAPI.enable({
|
await totpAPI.enable({
|
||||||
@@ -392,7 +380,7 @@ const handleVerify = async () => {
|
|||||||
appStore.showSuccess(t('profile.totp.enableSuccess'))
|
appStore.showSuccess(t('profile.totp.enableSuccess'))
|
||||||
emit('success')
|
emit('success')
|
||||||
} catch (err: any) {
|
} 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 = ['', '', '', '', '', '']
|
code.value = ['', '', '', '', '', '']
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
inputRefs.value[0]?.focus()
|
inputRefs.value[0]?.focus()
|
||||||
|
|||||||
@@ -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<typeof import('vue-i18n')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useI18n: () => ({
|
||||||
|
t: (key: string) => {
|
||||||
|
const translations: Record<string, string> = {
|
||||||
|
'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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,7 +7,10 @@ const mocks = vi.hoisted(() => ({
|
|||||||
showSuccess: vi.fn(),
|
showSuccess: vi.fn(),
|
||||||
showError: vi.fn(),
|
showError: vi.fn(),
|
||||||
getVerificationMethod: vi.fn(),
|
getVerificationMethod: vi.fn(),
|
||||||
sendVerifyCode: vi.fn()
|
sendVerifyCode: vi.fn(),
|
||||||
|
initiateSetup: vi.fn(),
|
||||||
|
enable: vi.fn(),
|
||||||
|
disable: vi.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('vue-i18n', () => ({
|
vi.mock('vue-i18n', () => ({
|
||||||
@@ -27,9 +30,9 @@ vi.mock('@/api', () => ({
|
|||||||
totpAPI: {
|
totpAPI: {
|
||||||
getVerificationMethod: mocks.getVerificationMethod,
|
getVerificationMethod: mocks.getVerificationMethod,
|
||||||
sendVerifyCode: mocks.sendVerifyCode,
|
sendVerifyCode: mocks.sendVerifyCode,
|
||||||
initiateSetup: vi.fn(),
|
initiateSetup: mocks.initiateSetup,
|
||||||
enable: vi.fn(),
|
enable: mocks.enable,
|
||||||
disable: vi.fn()
|
disable: mocks.disable
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -49,9 +52,19 @@ describe('TOTP 弹窗定时器清理', () => {
|
|||||||
mocks.showError.mockReset()
|
mocks.showError.mockReset()
|
||||||
mocks.getVerificationMethod.mockReset()
|
mocks.getVerificationMethod.mockReset()
|
||||||
mocks.sendVerifyCode.mockReset()
|
mocks.sendVerifyCode.mockReset()
|
||||||
|
mocks.initiateSetup.mockReset()
|
||||||
|
mocks.enable.mockReset()
|
||||||
|
mocks.disable.mockReset()
|
||||||
|
|
||||||
mocks.getVerificationMethod.mockResolvedValue({ method: 'email' })
|
mocks.getVerificationMethod.mockResolvedValue({ method: 'email' })
|
||||||
mocks.sendVerifyCode.mockResolvedValue({ success: true })
|
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) => {
|
setIntervalSpy = vi.spyOn(window, 'setInterval').mockImplementation(((handler: TimerHandler) => {
|
||||||
void handler
|
void handler
|
||||||
@@ -105,4 +118,40 @@ describe('TOTP 弹窗定时器清理', () => {
|
|||||||
|
|
||||||
expect(clearIntervalSpy).toHaveBeenCalledWith(timerId)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1372,30 +1372,20 @@
|
|||||||
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{{ localText("微信登录", "WeChat Connect") }}
|
{{ t("admin.settings.wechatConnect.title") }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.description") }}
|
||||||
localText(
|
|
||||||
"用于微信开放平台或公众号/小程序的第三方登录配置。",
|
|
||||||
"Third-party login configuration for WeChat Open Platform or Official Account / Mini Program.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-5 p-6">
|
<div class="space-y-5 p-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||||
localText("启用微信登录", "Enable WeChat Connect")
|
t("admin.settings.wechatConnect.enabledLabel")
|
||||||
}}</label>
|
}}</label>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.enabledHint") }}
|
||||||
localText(
|
|
||||||
"开启后可使用微信第三方登录回调与授权配置。",
|
|
||||||
"Enable this to configure WeChat OAuth callbacks and authorization.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -1413,16 +1403,14 @@
|
|||||||
<label
|
<label
|
||||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{{ localText("AppID", "App ID") }}
|
{{ t("admin.settings.wechatConnect.appIdLabel") }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
data-testid="wechat-connect-app-id"
|
data-testid="wechat-connect-app-id"
|
||||||
v-model="form.wechat_connect_app_id"
|
v-model="form.wechat_connect_app_id"
|
||||||
type="text"
|
type="text"
|
||||||
class="input font-mono text-sm"
|
class="input font-mono text-sm"
|
||||||
:placeholder="
|
:placeholder="t('admin.settings.wechatConnect.appIdPlaceholder')"
|
||||||
localText('微信开放平台 AppID', 'WeChat App ID')
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1430,7 +1418,7 @@
|
|||||||
<label
|
<label
|
||||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{{ localText("AppSecret", "App Secret") }}
|
{{ t("admin.settings.wechatConnect.appSecretLabel") }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
data-testid="wechat-connect-app-secret"
|
data-testid="wechat-connect-app-secret"
|
||||||
@@ -1439,27 +1427,15 @@
|
|||||||
class="input font-mono text-sm"
|
class="input font-mono text-sm"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
form.wechat_connect_app_secret_configured
|
form.wechat_connect_app_secret_configured
|
||||||
? localText(
|
? t('admin.settings.wechatConnect.appSecretConfiguredPlaceholder')
|
||||||
'密钥已配置,留空以保留当前值。',
|
: t('admin.settings.wechatConnect.appSecretPlaceholder')
|
||||||
'Secret configured. Leave empty to keep the current value.',
|
|
||||||
)
|
|
||||||
: localText(
|
|
||||||
'微信开放平台 AppSecret',
|
|
||||||
'WeChat App Secret',
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{
|
||||||
form.wechat_connect_app_secret_configured
|
form.wechat_connect_app_secret_configured
|
||||||
? localText(
|
? t('admin.settings.wechatConnect.appSecretConfiguredHint')
|
||||||
"密钥已配置,留空以保留当前值。",
|
: t('admin.settings.wechatConnect.appSecretHint')
|
||||||
"Secret configured. Leave empty to keep the current value.",
|
|
||||||
)
|
|
||||||
: localText(
|
|
||||||
"填写后会覆盖当前微信密钥。",
|
|
||||||
"Enter a new secret to replace the current WeChat credential.",
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1470,29 +1446,19 @@
|
|||||||
<label
|
<label
|
||||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{{ localText("模式", "Mode") }}
|
{{ t("admin.settings.wechatConnect.modeLabel") }}
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
|
class="flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-900 dark:text-white">
|
<div class="font-medium text-gray-900 dark:text-white">
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.openModeLabel") }}
|
||||||
localText(
|
|
||||||
"非微信环境使用开放平台",
|
|
||||||
"Use Open outside WeChat",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.openModeHint") }}
|
||||||
localText(
|
|
||||||
"浏览器不在微信内时,自动走开放平台扫码授权。",
|
|
||||||
"Use Open Platform QR authorization outside the WeChat browser.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -1506,22 +1472,12 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-900 dark:text-white">
|
<div class="font-medium text-gray-900 dark:text-white">
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.mpModeLabel") }}
|
||||||
localText(
|
|
||||||
"微信环境使用公众号",
|
|
||||||
"Use MP inside WeChat",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.mpModeHint") }}
|
||||||
localText(
|
|
||||||
"浏览器在微信内时,自动走公众号授权。",
|
|
||||||
"Use Official Account authorization inside the WeChat browser.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -1536,19 +1492,14 @@
|
|||||||
<label
|
<label
|
||||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{{ localText("回调地址", "Redirect URL") }}
|
{{ t("admin.settings.wechatConnect.redirectUrlLabel") }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
data-testid="wechat-connect-redirect-url"
|
data-testid="wechat-connect-redirect-url"
|
||||||
v-model="form.wechat_connect_redirect_url"
|
v-model="form.wechat_connect_redirect_url"
|
||||||
type="url"
|
type="url"
|
||||||
class="input font-mono text-sm"
|
class="input font-mono text-sm"
|
||||||
:placeholder="
|
:placeholder="t('admin.settings.wechatConnect.redirectUrlPlaceholder')"
|
||||||
localText(
|
|
||||||
'https://your-site.com/api/v1/auth/oauth/wechat/callback',
|
|
||||||
'https://your-site.com/api/v1/auth/oauth/wechat/callback',
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
|
class="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
|
||||||
@@ -1558,12 +1509,7 @@
|
|||||||
class="btn btn-secondary btn-sm w-fit"
|
class="btn btn-secondary btn-sm w-fit"
|
||||||
@click="setAndCopyWeChatRedirectUrl"
|
@click="setAndCopyWeChatRedirectUrl"
|
||||||
>
|
>
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.generateAndCopy") }}
|
||||||
localText(
|
|
||||||
"使用当前站点生成并复制",
|
|
||||||
"Generate & Copy (current site)",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</button>
|
</button>
|
||||||
<code
|
<code
|
||||||
v-if="wechatRedirectUrlSuggestion"
|
v-if="wechatRedirectUrlSuggestion"
|
||||||
@@ -1579,27 +1525,17 @@
|
|||||||
<label
|
<label
|
||||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{{ localText("前端回调地址", "Frontend redirect URL") }}
|
{{ t("admin.settings.wechatConnect.frontendRedirectUrlLabel") }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
data-testid="wechat-connect-frontend-redirect-url"
|
data-testid="wechat-connect-frontend-redirect-url"
|
||||||
v-model="form.wechat_connect_frontend_redirect_url"
|
v-model="form.wechat_connect_frontend_redirect_url"
|
||||||
type="text"
|
type="text"
|
||||||
class="input font-mono text-sm"
|
class="input font-mono text-sm"
|
||||||
:placeholder="
|
:placeholder="t('admin.settings.wechatConnect.frontendRedirectUrlPlaceholder')"
|
||||||
localText(
|
|
||||||
'/auth/wechat/callback',
|
|
||||||
'/auth/wechat/callback',
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{ t("admin.settings.wechatConnect.frontendRedirectUrlHint") }}
|
||||||
localText(
|
|
||||||
"通常用于前端路由回调地址,需与后端配置保持一致。",
|
|
||||||
"Usually the frontend route callback path; keep it aligned with the backend.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2215,15 +2151,10 @@
|
|||||||
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{{ localText("认证来源默认值", "Auth Source Defaults") }}
|
{{ t("admin.settings.authSourceDefaults.title") }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.description") }}
|
||||||
localText(
|
|
||||||
"按注册来源配置新用户默认余额、并发、订阅与授权策略。",
|
|
||||||
"Configure per-source default balance, concurrency, subscriptions, and grant rules.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-6 p-6">
|
<div class="space-y-6 p-6">
|
||||||
@@ -2232,20 +2163,10 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="font-medium text-gray-900 dark:text-white">
|
<label class="font-medium text-gray-900 dark:text-white">
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.requireEmailLabel") }}
|
||||||
localText(
|
|
||||||
"第三方注册强制补充邮箱",
|
|
||||||
"Require email on third-party signup",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</label>
|
</label>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.requireEmailHint") }}
|
||||||
localText(
|
|
||||||
"启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。",
|
|
||||||
"When enabled, Linux DO, OIDC, and WeChat signups must provide an email before account creation.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle v-model="form.force_email_on_third_party_signup" />
|
<Toggle v-model="form.force_email_on_third_party_signup" />
|
||||||
@@ -2280,12 +2201,7 @@
|
|||||||
class="mt-4 space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
|
class="mt-4 space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||||
>
|
>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.enabledHint") }}
|
||||||
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.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
@@ -2331,19 +2247,12 @@
|
|||||||
<label
|
<label
|
||||||
class="font-medium text-gray-900 dark:text-white"
|
class="font-medium text-gray-900 dark:text-white"
|
||||||
>
|
>
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.grantOnFirstBindLabel") }}
|
||||||
localText("首次绑定时授权", "Grant on first bind")
|
|
||||||
}}
|
|
||||||
</label>
|
</label>
|
||||||
<p
|
<p
|
||||||
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.grantOnFirstBindHint") }}
|
||||||
localText(
|
|
||||||
"已有账号首次绑定该来源时发放默认权益。",
|
|
||||||
"Grant default entitlements when an existing user first binds this source.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -2359,15 +2268,10 @@
|
|||||||
<label
|
<label
|
||||||
class="font-medium text-gray-900 dark:text-white"
|
class="font-medium text-gray-900 dark:text-white"
|
||||||
>
|
>
|
||||||
{{ localText("默认订阅", "Default subscriptions") }}
|
{{ t("admin.settings.authSourceDefaults.defaultSubscriptionsLabel") }}
|
||||||
</label>
|
</label>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.defaultSubscriptionsHint") }}
|
||||||
localText(
|
|
||||||
"仅对当前认证来源生效,未配置时不追加来源专属订阅。",
|
|
||||||
"Applies only to this auth source. Leave empty to skip source-specific subscriptions.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -2391,12 +2295,7 @@
|
|||||||
"
|
"
|
||||||
class="rounded border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500 dark:border-dark-600 dark:text-gray-400"
|
class="rounded border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500 dark:border-dark-600 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{
|
{{ t("admin.settings.authSourceDefaults.noSourceSubscriptions") }}
|
||||||
localText(
|
|
||||||
"当前来源未配置专属默认订阅。",
|
|
||||||
"No source-specific default subscriptions configured.",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
@@ -2621,18 +2520,12 @@
|
|||||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
localText(
|
t("admin.settings.openaiExperimentalScheduler.title")
|
||||||
"OpenAI 实验调度策略",
|
|
||||||
"OpenAI experimental scheduler policy",
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</label>
|
</label>
|
||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{
|
||||||
localText(
|
t("admin.settings.openaiExperimentalScheduler.description")
|
||||||
"默认关闭。开启后仅影响本网关在 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.",
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -4131,20 +4024,16 @@
|
|||||||
class="font-medium text-gray-900 dark:text-white"
|
class="font-medium text-gray-900 dark:text-white"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
localText(
|
t("admin.settings.paymentVisibleMethods.methodLabel", {
|
||||||
`${visibleMethod.title} 可见方式`,
|
title: visibleMethod.title,
|
||||||
`${visibleMethod.title} visible method`,
|
})
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</label>
|
</label>
|
||||||
<p
|
<p
|
||||||
class="mt-1 text-sm text-gray-500 dark:text-gray-400"
|
class="mt-1 text-sm text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
localText(
|
t("admin.settings.paymentVisibleMethods.methodHint")
|
||||||
"控制前台结算页是否展示该方式,以及展示时使用的来源键。",
|
|
||||||
"Controls whether checkout shows this method and which source key it exposes.",
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -4163,7 +4052,7 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label class="input-label">
|
<label class="input-label">
|
||||||
{{ localText("支付来源", "Payment source") }}
|
{{ t("admin.settings.paymentVisibleMethods.sourceLabel") }}
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
:model-value="
|
:model-value="
|
||||||
@@ -4184,10 +4073,7 @@
|
|||||||
/>
|
/>
|
||||||
<p class="mt-1.5 text-xs text-gray-400">
|
<p class="mt-1.5 text-xs text-gray-400">
|
||||||
{{
|
{{
|
||||||
localText(
|
t("admin.settings.paymentVisibleMethods.sourceHint")
|
||||||
"启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。",
|
|
||||||
"Choose an explicit source before enabling the method. Not configured methods are not exposed.",
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -5028,35 +4914,23 @@ const authSourceDefaults = reactive<AuthSourceDefaultsState>(
|
|||||||
const authSourceDefaultsMeta = computed(() => [
|
const authSourceDefaultsMeta = computed(() => [
|
||||||
{
|
{
|
||||||
source: "email" as AuthSourceType,
|
source: "email" as AuthSourceType,
|
||||||
title: localText("邮箱注册", "Email signup"),
|
title: t("admin.settings.authSourceDefaults.sources.email.title"),
|
||||||
description: localText(
|
description: t("admin.settings.authSourceDefaults.sources.email.description"),
|
||||||
"适用于邮箱密码注册的新用户默认配额。",
|
|
||||||
"Default quota grants for email-password signups.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "linuxdo" as AuthSourceType,
|
source: "linuxdo" as AuthSourceType,
|
||||||
title: localText("Linux DO 登录", "Linux DO signup"),
|
title: t("admin.settings.authSourceDefaults.sources.linuxdo.title"),
|
||||||
description: localText(
|
description: t("admin.settings.authSourceDefaults.sources.linuxdo.description"),
|
||||||
"适用于 Linux DO 第三方注册的新用户默认配额。",
|
|
||||||
"Default quota grants for Linux DO signups.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "oidc" as AuthSourceType,
|
source: "oidc" as AuthSourceType,
|
||||||
title: localText("OIDC 登录", "OIDC signup"),
|
title: t("admin.settings.authSourceDefaults.sources.oidc.title"),
|
||||||
description: localText(
|
description: t("admin.settings.authSourceDefaults.sources.oidc.description"),
|
||||||
"适用于 OIDC 第三方注册的新用户默认配额。",
|
|
||||||
"Default quota grants for OIDC signups.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "wechat" as AuthSourceType,
|
source: "wechat" as AuthSourceType,
|
||||||
title: localText("微信登录", "WeChat signup"),
|
title: t("admin.settings.authSourceDefaults.sources.wechat.title"),
|
||||||
description: localText(
|
description: t("admin.settings.authSourceDefaults.sources.wechat.description"),
|
||||||
"适用于微信第三方注册的新用户默认配额。",
|
|
||||||
"Default quota grants for WeChat signups.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -5130,10 +5004,9 @@ function validatePaymentVisibleMethodSelections(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appStore.showError(
|
appStore.showError(
|
||||||
localText(
|
t("admin.settings.paymentVisibleMethods.sourceRequiredError", {
|
||||||
`${visibleMethod.title} 已启用,请先选择支付来源`,
|
title: visibleMethod.title,
|
||||||
`Select a payment source before enabling ${visibleMethod.title}`,
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -5479,10 +5352,7 @@ async function setAndCopyWeChatRedirectUrl() {
|
|||||||
form.wechat_connect_redirect_url = url;
|
form.wechat_connect_redirect_url = url;
|
||||||
await copyToClipboard(
|
await copyToClipboard(
|
||||||
url,
|
url,
|
||||||
localText(
|
t("admin.settings.wechatConnect.redirectUrlSetAndCopied"),
|
||||||
"已使用当前站点生成回调地址并复制到剪贴板",
|
|
||||||
"Redirect URL generated and copied to clipboard",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -700,7 +700,7 @@ const getAttributeValue = (userId: number, attrId: number): string => {
|
|||||||
// All possible columns (for column settings)
|
// All possible columns (for column settings)
|
||||||
const allColumns = computed<Column[]>(() => [
|
const allColumns = computed<Column[]>(() => [
|
||||||
{ key: 'email', label: t('admin.users.columns.user'), sortable: true },
|
{ 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: 'username', label: t('admin.users.columns.username'), sortable: true },
|
||||||
{ key: 'notes', label: t('admin.users.columns.notes'), sortable: false },
|
{ key: 'notes', label: t('admin.users.columns.notes'), sortable: false },
|
||||||
// Dynamic attribute columns
|
// Dynamic attribute columns
|
||||||
|
|||||||
@@ -93,10 +93,61 @@ vi.mock("@/utils/apiError", () => ({
|
|||||||
|
|
||||||
vi.mock("vue-i18n", async () => {
|
vi.mock("vue-i18n", async () => {
|
||||||
const actual = await vi.importActual<typeof import("vue-i18n")>("vue-i18n");
|
const actual = await vi.importActual<typeof import("vue-i18n")>("vue-i18n");
|
||||||
|
const translations: Record<string, string> = {
|
||||||
|
"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 {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useI18n: () => ({
|
useI18n: () => ({
|
||||||
t: (key: string) => key,
|
t: (key: string, params?: Record<string, string>) =>
|
||||||
|
(translations[key] ?? key).replace(/\{(\w+)\}/g, (_, token) => params?.[token] ?? `{${token}}`),
|
||||||
locale: ref("zh-CN"),
|
locale: ref("zh-CN"),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user