diff --git a/frontend/src/api/__tests__/settings.paymentVisibleMethods.spec.ts b/frontend/src/api/__tests__/settings.paymentVisibleMethods.spec.ts index 3b1a373f..ad355afe 100644 --- a/frontend/src/api/__tests__/settings.paymentVisibleMethods.spec.ts +++ b/frontend/src/api/__tests__/settings.paymentVisibleMethods.spec.ts @@ -27,8 +27,8 @@ describe('admin settings payment visible method helpers', () => { expect(getPaymentVisibleMethodSourceOptions('alipay')).toEqual([ { value: '', - labelZh: '自动路由', - labelEn: 'Automatic routing', + labelZh: '未配置', + labelEn: 'Not configured', }, { value: 'official_alipay', @@ -45,8 +45,8 @@ describe('admin settings payment visible method helpers', () => { expect(getPaymentVisibleMethodSourceOptions('wxpay')).toEqual([ { value: '', - labelZh: '自动路由', - labelEn: 'Automatic routing', + labelZh: '未配置', + labelEn: 'Not configured', }, { value: 'official_wxpay', diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 505fcdca..235bda7b 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -44,12 +44,12 @@ const PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS: Record< PaymentVisibleMethodSourceOption[] > = { alipay: [ - { value: '', labelZh: '自动路由', labelEn: 'Automatic routing' }, + { value: '', labelZh: '未配置', labelEn: 'Not configured' }, { value: 'official_alipay', labelZh: '支付宝官方', labelEn: 'Official Alipay' }, { value: 'easypay_alipay', labelZh: '易支付支付宝', labelEn: 'EasyPay Alipay' }, ], wxpay: [ - { value: '', labelZh: '自动路由', labelEn: 'Automatic routing' }, + { value: '', labelZh: '未配置', labelEn: 'Not configured' }, { value: 'official_wxpay', labelZh: '微信官方', labelEn: 'Official WeChat Pay' }, { value: 'easypay_wxpay', labelZh: '易支付微信', labelEn: 'EasyPay WeChat Pay' }, ], diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 92dcc519..b7158d9b 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -663,6 +663,12 @@ const adminNavItems = computed((): NavItem[] => { ? [{ path: '/admin/ops', label: t('nav.ops'), icon: ChartIcon }] : []), { path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true }, + { + path: '/admin/users/auth-identity-migration-reports', + label: 'Migration Reports', + icon: UsersIcon, + hideInSimpleMode: true + }, { path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true }, { path: '/admin/channels', label: t('nav.channels', '渠道管理'), icon: ChannelIcon, hideInSimpleMode: true }, { path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true }, diff --git a/frontend/src/components/layout/__tests__/AppSidebar.spec.ts b/frontend/src/components/layout/__tests__/AppSidebar.spec.ts index 118c7615..915a67f8 100644 --- a/frontend/src/components/layout/__tests__/AppSidebar.spec.ts +++ b/frontend/src/components/layout/__tests__/AppSidebar.spec.ts @@ -30,3 +30,9 @@ describe('AppSidebar header styles', () => { expect(sidebarBrandBlockMatch?.[0]).not.toContain('overflow: hidden;') }) }) + +describe('AppSidebar admin navigation', () => { + it('includes a visible entry for auth identity migration reports', () => { + expect(componentSource).toContain("'/admin/users/auth-identity-migration-reports'") + }) +}) diff --git a/frontend/src/views/admin/AuthIdentityMigrationReportsView.vue b/frontend/src/views/admin/AuthIdentityMigrationReportsView.vue index 5aeb6b28..35c232b6 100644 --- a/frontend/src/views/admin/AuthIdentityMigrationReportsView.vue +++ b/frontend/src/views/admin/AuthIdentityMigrationReportsView.vue @@ -318,6 +318,7 @@ const pagination = reactive({ pageSize: 20, total: 0, }) +const knownReportTypes = ref([]) const columns: Column[] = [ { key: 'status', label: text('状态', 'Status') }, @@ -330,12 +331,16 @@ const columns: Column[] = [ ] const reportTypeOptions = computed(() => - Object.entries(summary.value.by_type) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([value, count]) => ({ - value, - label: `${value} (${count})`, - })) + knownReportTypes.value + .slice() + .sort((left, right) => left.localeCompare(right)) + .map((value) => { + const count = summary.value.by_type[value] + return { + value, + label: count === undefined ? value : `${value} (${count})`, + } + }) ) const canResolve = computed(() => @@ -347,10 +352,22 @@ const canResolve = computed(() => ) ) +const mergeKnownReportTypes = (...values: Array) => { + const merged = new Set(knownReportTypes.value) + for (const value of values) { + const normalized = value?.trim() + if (normalized) { + merged.add(normalized) + } + } + knownReportTypes.value = Array.from(merged) +} + const loadSummary = async () => { summaryLoading.value = true try { summary.value = await adminAPI.users.getAuthIdentityMigrationReportSummary() + mergeKnownReportTypes(...Object.keys(summary.value.by_type)) } catch (error) { console.error('Failed to load auth identity migration report summary:', error) appStore.showError(text('加载 migration reports 汇总失败', 'Failed to load migration report summary')) @@ -370,6 +387,7 @@ const loadReports = async () => { reports.value = response.items pagination.total = response.total + mergeKnownReportTypes(filters.reportType, ...response.items.map((report) => report.report_type)) if (selectedReport.value) { const refreshed = response.items.find((report) => report.id === selectedReport.value?.id) ?? null diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 8a042e70..9fb8da41 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -2728,8 +2728,8 @@

{{ localText( - '留空表示自动路由;仅允许当前系统支持的官方或易支付来源。', - 'Leave blank for automatic routing. Only supported official or EasyPay sources are allowed.' + '启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。', + 'Choose an explicit source before enabling the method. Not configured methods are not exposed.' ) }}

@@ -3450,6 +3450,28 @@ function setPaymentVisibleMethodSource( form.payment_visible_method_wxpay_source = normalized } +function validatePaymentVisibleMethodSelections(): boolean { + for (const visibleMethod of paymentVisibleMethodCards.value) { + if (!getPaymentVisibleMethodEnabled(visibleMethod.key)) { + continue + } + + if (getPaymentVisibleMethodSource(visibleMethod.key)) { + continue + } + + appStore.showError( + localText( + `${visibleMethod.title} 已启用,请先选择支付来源`, + `Select a payment source before enabling ${visibleMethod.title}` + ) + ) + return false + } + + return true +} + // Proxies for web search emulation ProxySelector const webSearchProxies = ref([]) @@ -3979,6 +4001,10 @@ async function saveSettings() { } } + if (!validatePaymentVisibleMethodSelections()) { + return + } + // Validate URL fields — novalidate disables browser-native checks, so we validate here const isValidHttpUrl = (url: string): boolean => { if (!url) return true diff --git a/frontend/src/views/admin/__tests__/AuthIdentityMigrationReportsView.spec.ts b/frontend/src/views/admin/__tests__/AuthIdentityMigrationReportsView.spec.ts index 5e6b0ae0..406baaf1 100644 --- a/frontend/src/views/admin/__tests__/AuthIdentityMigrationReportsView.spec.ts +++ b/frontend/src/views/admin/__tests__/AuthIdentityMigrationReportsView.spec.ts @@ -240,4 +240,21 @@ describe('AuthIdentityMigrationReportsView', () => { reportType: '', }) }) + + it('keeps report type filter options available from list data when summary fails', async () => { + getAuthIdentityMigrationReportSummary.mockRejectedValueOnce(new Error('summary failed')) + listAuthIdentityMigrationReports.mockResolvedValueOnce(listResponse) + + const wrapper = mountView() + + await flushPromises() + + const options = wrapper + .get('[data-test="report-type-filter"]') + .findAll('option') + .map((node) => node.element.value) + + expect(showError).toHaveBeenCalled() + expect(options).toContain('oidc_synthetic_email_requires_manual_recovery') + }) }) diff --git a/frontend/src/views/admin/__tests__/SettingsView.spec.ts b/frontend/src/views/admin/__tests__/SettingsView.spec.ts index f20170e9..b6f8ab17 100644 --- a/frontend/src/views/admin/__tests__/SettingsView.spec.ts +++ b/frontend/src/views/admin/__tests__/SettingsView.spec.ts @@ -449,4 +449,27 @@ describe('admin SettingsView payment visible method controls', () => { }) ) }) + + it('blocks saving when a visible payment method is enabled without a source', async () => { + const wrapper = mountView() + + await flushPromises() + await openPaymentTab(wrapper) + + const paymentSourceSelects = wrapper + .findAll('select.select-stub') + .filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder'))) + + const alipaySelect = paymentSourceSelects.find( + (node) => node.attributes('data-placeholder') === 'alipay' + ) + + await alipaySelect?.setValue('') + await wrapper.find('form').trigger('submit.prevent') + await flushPromises() + + expect(updateSettings).not.toHaveBeenCalled() + expect(showError).toHaveBeenCalled() + expect(String(showError.mock.calls.at(-1)?.[0] ?? '')).toContain('支付来源') + }) })