From a6cd294ab639e7c54b2b6e01334e61967167a80f Mon Sep 17 00:00:00 2001 From: erio Date: Mon, 6 Apr 2026 14:51:14 +0800 Subject: [PATCH] feat(frontend): enrich payment settings with provider management inline - Move payment config out of orders submenu (now 3 items: dashboard, orders, plans) - Embed provider management directly in SettingsView Payment tab - Add missing fields: product name prefix/suffix with preview, load balance strategy - Add full provider CRUD dialog (EasyPay/Alipay/Wxpay/Stripe config fields) --- frontend/src/components/layout/AppSidebar.vue | 1 - frontend/src/i18n/locales/en.ts | 22 +- frontend/src/i18n/locales/zh.ts | 22 +- frontend/src/views/admin/SettingsView.vue | 219 +++++++++++++++++- 4 files changed, 251 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 37509a7eb0..ca84337830 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -683,7 +683,6 @@ const adminNavItems = computed((): NavItem[] => { children: [ { path: '/admin/orders/dashboard', label: t('nav.paymentDashboard'), icon: ChartIcon }, { path: '/admin/orders', label: t('nav.orderManagement'), icon: OrderIcon }, - { path: '/admin/orders/config', label: t('nav.paymentConfig'), icon: CogIcon }, { path: '/admin/orders/plans', label: t('nav.paymentPlans'), icon: CreditCardIcon }, ], }, diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 417cfec403..55b5391a2e 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4344,7 +4344,27 @@ export default { helpText: 'Help Text', helpImageUrl: 'Help Image URL', manageProviders: 'Manage Providers', - balancePaymentDisabled: 'Balance payment is disabled', + balancePaymentDisabled: 'Disable Balance Recharge', + productNamePrefix: 'Product Name Prefix', + productNameSuffix: 'Product Name Suffix', + preview: 'Preview', + loadBalanceStrategy: 'Load Balance Strategy', + strategyRoundRobin: 'Round Robin', + strategyLeastAmount: 'Least Daily Amount', + providerManagement: 'Provider Management', + providerManagementDesc: 'Manage payment provider instances', + createProvider: 'Add Provider', + editProvider: 'Edit Provider', + deleteProvider: 'Delete Provider', + deleteProviderConfirm: 'Are you sure you want to delete this provider?', + providerName: 'Provider Name', + providerKey: 'Provider Type', + selectProviderKey: 'Select Provider Type', + providerConfig: 'Credentials', + noProviders: 'No provider instances configured', + supportedTypes: 'Supported Payment Types', + supportedTypesHint: 'Comma-separated, e.g. alipay,wxpay', + refundEnabled: 'Allow Refund', }, smtp: { title: 'SMTP Settings', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index cbf969bf27..47cd3918f4 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4517,7 +4517,27 @@ export default { helpText: '帮助文本', helpImageUrl: '帮助图片链接', manageProviders: '管理服务商', - balancePaymentDisabled: '余额支付已禁用', + balancePaymentDisabled: '禁用余额充值', + productNamePrefix: '商品名前缀', + productNameSuffix: '商品名后缀', + preview: '预览', + loadBalanceStrategy: '负载均衡策略', + strategyRoundRobin: '轮询', + strategyLeastAmount: '最少金额', + providerManagement: '服务商管理', + providerManagementDesc: '管理支付服务商实例', + createProvider: '添加服务商', + editProvider: '编辑服务商', + deleteProvider: '删除服务商', + deleteProviderConfirm: '确定要删除此服务商吗?', + providerName: '服务商名称', + providerKey: '服务商类型', + selectProviderKey: '选择服务商类型', + providerConfig: '凭证配置', + noProviders: '暂无服务商实例', + supportedTypes: '支持的支付方式', + supportedTypesHint: '逗号分隔,如 alipay,wxpay', + refundEnabled: '允许退款', }, smtp: { title: 'SMTP 设置', diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 6cf8ed6de2..26e9f991ae 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1640,13 +1640,14 @@
- +

{{ t('admin.settings.payment.title') }}

{{ t('admin.settings.payment.description') }}

+
@@ -1655,31 +1656,146 @@
+ +
+
+
+
+

{{ t('admin.settings.payment.providerManagement') }}

+

{{ t('admin.settings.payment.providerManagementDesc') }}

+
+
+ + +
+
+
+
+
+
+
+
+
+
+ +
+
+

{{ provider.name }}

+

{{ providerKeyLabel(provider.provider_key) }}

+
+
+ {{ provider.enabled ? t('common.enabled') : t('common.disabled') }} +
+
+
+ {{ t('admin.settings.payment.supportedTypes') }}: + {{ provider.supported_types || '-' }} +
+
+ {{ t('admin.settings.payment.refundEnabled') }}: + {{ provider.refund_enabled ? 'Yes' : 'No' }} +
+
+
+ + +
+
+
+
+ +

{{ t('admin.settings.payment.noProviders') }}

+ +
+
+ +
+ + + +
+
+
+

{{ t('admin.settings.payment.supportedTypesHint') }}

+
+
+
+
+
+

{{ t('admin.settings.payment.providerConfig') }}

+ + + + +
{{ t('admin.settings.payment.selectProviderKey') }}
+
+ + + +
@@ -1946,9 +2062,12 @@ import type { DefaultSubscriptionSetting } from '@/api/admin/settings' import type { AdminGroup } from '@/types' +import type { ProviderInstance } from '@/types/payment' import AppLayout from '@/components/layout/AppLayout.vue' import Icon from '@/components/icons/Icon.vue' import Select from '@/components/common/Select.vue' +import BaseDialog from '@/components/common/BaseDialog.vue' +import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import GroupBadge from '@/components/common/GroupBadge.vue' import GroupOptionItem from '@/components/common/GroupOptionItem.vue' import Toggle from '@/components/common/Toggle.vue' @@ -2066,6 +2185,9 @@ type SettingsForm = SystemSettings & { payment_enabled_types: string payment_help_image_url: string payment_help_text: string + payment_product_name_prefix: string + payment_product_name_suffix: string + payment_load_balance_strategy: string } const form = reactive({ @@ -2089,7 +2211,7 @@ const form = reactive({ home_content: '', backend_mode_enabled: false, hide_ccs_import_button: false, - payment_enabled: false, payment_min_amount: 1, payment_max_amount: 10000, payment_daily_limit: 50000, payment_max_pending_orders: 3, payment_order_timeout_minutes: 30, payment_balance_disabled: false, payment_enabled_types: '', payment_help_image_url: '', payment_help_text: '', + payment_enabled: false, payment_min_amount: 1, payment_max_amount: 10000, payment_daily_limit: 50000, payment_max_pending_orders: 3, payment_order_timeout_minutes: 30, payment_balance_disabled: false, payment_enabled_types: '', payment_help_image_url: '', payment_help_text: '', payment_product_name_prefix: '', payment_product_name_suffix: '', payment_load_balance_strategy: 'round-robin', sora_client_enabled: false, custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>, custom_endpoints: [] as Array<{name: string; endpoint: string; description: string}>, @@ -2311,6 +2433,9 @@ async function loadSettings() { form.payment_order_timeout_minutes = paymentConfig.order_timeout_minutes form.payment_balance_disabled = paymentConfig.balance_disabled form.payment_enabled_types = (paymentConfig.enabled_payment_types || []).join(',') + form.payment_product_name_prefix = paymentConfig.product_name_prefix || '' + form.payment_product_name_suffix = paymentConfig.product_name_suffix || '' + form.payment_load_balance_strategy = paymentConfig.load_balance_strategy || 'round-robin' } catch (error: any) { loadFailed.value = true appStore.showError( @@ -2454,7 +2579,10 @@ async function saveSettings() { max_pending_orders: form.payment_max_pending_orders, order_timeout_minutes: form.payment_order_timeout_minutes, balance_disabled: form.payment_balance_disabled, - enabled_payment_types: enabledTypes + enabled_payment_types: enabledTypes, + product_name_prefix: form.payment_product_name_prefix, + product_name_suffix: form.payment_product_name_suffix, + load_balance_strategy: form.payment_load_balance_strategy, } const [updated] = await Promise.all([ @@ -2756,6 +2884,76 @@ async function saveBetaPolicySettings() { } } +// ==================== Provider Management ==================== + +const providersLoading = ref(false) +const providerSaving = ref(false) +const providers = ref([]) +const showProviderDialog = ref(false) +const showDeleteProviderDialog = ref(false) +const editingProvider = ref(null) +const deletingProviderId = ref(null) + +const providerForm = reactive({ + name: '', provider_key: 'easypay', supported_types: '', enabled: true, refund_enabled: false, +}) +const providerConfig = reactive({ + pid: '', pkey: '', apiBase: '', notifyUrl: '', returnUrl: '', + cid: '', cidAlipay: '', cidWxpay: '', + appId: '', privateKey: '', publicKey: '', + mchId: '', apiV3Key: '', publicKeyId: '', certSerial: '', + secretKey: '', publishableKey: '', webhookSecret: '', +}) +const providerKeyOptions = [ + { value: 'easypay', label: 'EasyPay' }, + { value: 'alipay', label: 'Alipay (Direct)' }, + { value: 'wxpay', label: 'WeChat Pay (Direct)' }, + { value: 'stripe', label: 'Stripe' }, +] +const loadBalanceOptions = computed(() => [ + { value: 'round-robin', label: t('admin.settings.payment.strategyRoundRobin') }, + { value: 'least-amount', label: t('admin.settings.payment.strategyLeastAmount') }, +]) +function providerKeyLabel(key: string): string { + return providerKeyOptions.find(o => o.value === key)?.label || key +} +async function loadProviders() { + providersLoading.value = true + try { const res = await adminAPI.payment.getProviders(); providers.value = res.data || [] } + catch (err: unknown) { appStore.showError(err instanceof Error ? err.message : String(err)) } + finally { providersLoading.value = false } +} +function resetProviderForm() { + providerForm.name = ''; providerForm.provider_key = 'easypay'; providerForm.supported_types = '' + providerForm.enabled = true; providerForm.refund_enabled = false + Object.keys(providerConfig).forEach(k => { (providerConfig as any)[k] = '' }) +} +function openCreateProvider() { editingProvider.value = null; resetProviderForm(); showProviderDialog.value = true } +function openEditProvider(provider: ProviderInstance) { + editingProvider.value = provider + providerForm.name = provider.name; providerForm.provider_key = provider.provider_key + providerForm.supported_types = provider.supported_types; providerForm.enabled = provider.enabled + providerForm.refund_enabled = provider.refund_enabled + Object.keys(providerConfig).forEach(k => { (providerConfig as any)[k] = '' }) + showProviderDialog.value = true +} +async function handleSaveProvider() { + providerSaving.value = true + try { + const data = { ...providerForm, config: { ...providerConfig } } as any + if (editingProvider.value) { await adminAPI.payment.updateProvider(editingProvider.value.id, data) } + else { await adminAPI.payment.createProvider(data) } + appStore.showSuccess(t('common.saved')); showProviderDialog.value = false; loadProviders() + } catch (err: unknown) { appStore.showError(err instanceof Error ? err.message : String(err)) } + finally { providerSaving.value = false } +} +function confirmDeleteProvider(provider: ProviderInstance) { deletingProviderId.value = provider.id; showDeleteProviderDialog.value = true } +async function handleDeleteProvider() { + if (!deletingProviderId.value) return + try { await adminAPI.payment.deleteProvider(deletingProviderId.value); appStore.showSuccess(t('common.deleted')); showDeleteProviderDialog.value = false; loadProviders() } + catch (err: unknown) { appStore.showError(err instanceof Error ? err.message : String(err)) } +} + onMounted(() => { loadSettings() loadSubscriptionGroups() @@ -2764,6 +2962,7 @@ onMounted(() => { loadStreamTimeoutSettings() loadRectifierSettings() loadBetaPolicySettings() + loadProviders() })