From 65ad7df4f46a6bb22d81752bc491e0366caa3ada Mon Sep 17 00:00:00 2001 From: SMGDev Date: Tue, 23 Jun 2026 05:10:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(payment):=20=E4=BF=AE=E5=A4=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E8=BF=94=E5=9B=9E=E7=A9=BAsupported=5Ftypes=E6=97=B6?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E6=8F=90=E4=BE=9B=E5=95=86=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E6=B6=88=E5=A4=B1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一处理后端返回的null类型supported_types,将其标准化为空数组,避免调用includes()时报错导致卡片无法显示,同时修复多处相关的类型判断逻辑 --- .../payment/PaymentProviderDialog.vue | 4 +- .../src/components/payment/ProviderCard.vue | 2 +- frontend/src/views/admin/SettingsView.vue | 19 ++++-- .../admin/__tests__/SettingsView.spec.ts | 64 +++++++++++++++++++ 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/payment/PaymentProviderDialog.vue b/frontend/src/components/payment/PaymentProviderDialog.vue index b6085ed066..c8ebadd517 100644 --- a/frontend/src/components/payment/PaymentProviderDialog.vue +++ b/frontend/src/components/payment/PaymentProviderDialog.vue @@ -658,7 +658,9 @@ function reset(defaultKey: string) { function loadProvider(provider: ProviderInstance) { form.name = provider.name form.provider_key = provider.provider_key - form.supported_types = provider.supported_types + form.supported_types = Array.isArray(provider.supported_types) + ? [...provider.supported_types] + : [] form.enabled = provider.enabled // Coerce to a valid value for this provider. Guards against stale data // (e.g. "popup" written by an older client) showing up as an unselected diff --git a/frontend/src/components/payment/ProviderCard.vue b/frontend/src/components/payment/ProviderCard.vue index e64d5d5ec9..5cf52c28ca 100644 --- a/frontend/src/components/payment/ProviderCard.vue +++ b/frontend/src/components/payment/ProviderCard.vue @@ -104,6 +104,6 @@ const modeLabel = computed(() => { }) function isSelected(type: string): boolean { - return props.provider.supported_types.includes(type) + return Array.isArray(props.provider.supported_types) && props.provider.supported_types.includes(type) } diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 5ae9869f0d..dbdea7e961 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -9777,7 +9777,15 @@ async function loadProviders() { providersLoading.value = true; try { const res = await adminAPI.payment.getProviders(); - providers.value = res.data || []; + // Normalize supported_types: backend returns null when the list is empty + // (Go nil slice → JSON null). Without this, ProviderCard's isSelected() + // throws TypeError on null.includes(), causing the card to vanish. + providers.value = (res.data || []).map((p) => ({ + ...p, + supported_types: Array.isArray(p.supported_types) + ? p.supported_types + : [], + })); } catch (err: unknown) { appStore.showError(extractI18nErrorMessage(err, t, "payment.errors", t("common.error"))); } finally { @@ -9871,9 +9879,12 @@ async function handleToggleField( } async function handleToggleType(provider: ProviderInstance, type: string) { - const updated = provider.supported_types.includes(type) - ? provider.supported_types.filter((t) => t !== type) - : [...provider.supported_types, type]; + const currentTypes = Array.isArray(provider.supported_types) + ? provider.supported_types + : []; + const updated = currentTypes.includes(type) + ? currentTypes.filter((t) => t !== type) + : [...currentTypes, type]; const conflict = findProviderEnablementConflict({ id: provider.id, provider_key: provider.provider_key, diff --git a/frontend/src/views/admin/__tests__/SettingsView.spec.ts b/frontend/src/views/admin/__tests__/SettingsView.spec.ts index 4327eb1869..eaf5adfe71 100644 --- a/frontend/src/views/admin/__tests__/SettingsView.spec.ts +++ b/frontend/src/views/admin/__tests__/SettingsView.spec.ts @@ -793,6 +793,70 @@ describe("admin SettingsView payment visible method controls", () => { expect(paymentHelpImageUpload?.attributes("data-upload-label")).toBe("上传图片"); expect(paymentHelpImageUpload?.attributes("data-remove-label")).toBe("移除"); }); + + it("normalizes null supported_types from API so provider card stays visible", async () => { + // Backend returns null for supported_types when the list is empty + // (Go nil slice → JSON null). Without normalization, ProviderCard's + // isSelected() throws TypeError on null.includes(), causing the card + // to vanish from the list. + const providerWithNullTypes = { + id: 42, + provider_key: "easypay", + name: "EasyPay", + config: {}, + supported_types: null as unknown as string[], + enabled: true, + payment_mode: "", + refund_enabled: false, + allow_user_refund: false, + limits: "", + sort_order: 0, + }; + getProviders.mockReset(); + getProviders.mockResolvedValue({ data: [providerWithNullTypes] }); + + let receivedProviders: Array> = []; + const PaymentProviderListCapture = defineComponent({ + props: { + providers: { + type: Array, + default: () => [], + }, + }, + setup(props) { + receivedProviders = props.providers as Array>; + return () => h("div", { class: "provider-list-capture" }); + }, + }); + + const wrapper = mount(SettingsView, { + global: { + stubs: { + AppLayout: AppLayoutStub, + Select: SelectStub, + Toggle: ToggleStub, + Icon: true, + ConfirmDialog: true, + PaymentProviderList: PaymentProviderListCapture, + PaymentProviderDialog: true, + GroupBadge: true, + GroupOptionItem: true, + ProxySelector: true, + ImageUpload: ImageUploadStub, + BackupSettings: true, + }, + }, + }); + + await flushPromises(); + await openPaymentTab(wrapper); + + // The provider should still be in the list + expect(receivedProviders.length).toBe(1); + // supported_types should be normalized to an empty array, not null + expect(Array.isArray(receivedProviders[0].supported_types)).toBe(true); + expect(receivedProviders[0].supported_types).toEqual([]); + }); }); describe("admin SettingsView wechat connect controls", () => {