Merge pull request #409 from DuckyProject/feat/purchase-subscription-iframe
feat(purchase): 增加购买订阅 iframe 页面与配置
This commit is contained in:
@@ -73,6 +73,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|||||||
DocURL: settings.DocURL,
|
DocURL: settings.DocURL,
|
||||||
HomeContent: settings.HomeContent,
|
HomeContent: settings.HomeContent,
|
||||||
HideCcsImportButton: settings.HideCcsImportButton,
|
HideCcsImportButton: settings.HideCcsImportButton,
|
||||||
|
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||||
|
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||||
DefaultConcurrency: settings.DefaultConcurrency,
|
DefaultConcurrency: settings.DefaultConcurrency,
|
||||||
DefaultBalance: settings.DefaultBalance,
|
DefaultBalance: settings.DefaultBalance,
|
||||||
EnableModelFallback: settings.EnableModelFallback,
|
EnableModelFallback: settings.EnableModelFallback,
|
||||||
@@ -127,6 +129,8 @@ type UpdateSettingsRequest struct {
|
|||||||
DocURL string `json:"doc_url"`
|
DocURL string `json:"doc_url"`
|
||||||
HomeContent string `json:"home_content"`
|
HomeContent string `json:"home_content"`
|
||||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||||
|
PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"`
|
||||||
|
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
DefaultConcurrency int `json:"default_concurrency"`
|
DefaultConcurrency int `json:"default_concurrency"`
|
||||||
@@ -242,6 +246,34 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// “购买订阅”页面配置验证
|
||||||
|
purchaseEnabled := previousSettings.PurchaseSubscriptionEnabled
|
||||||
|
if req.PurchaseSubscriptionEnabled != nil {
|
||||||
|
purchaseEnabled = *req.PurchaseSubscriptionEnabled
|
||||||
|
}
|
||||||
|
purchaseURL := previousSettings.PurchaseSubscriptionURL
|
||||||
|
if req.PurchaseSubscriptionURL != nil {
|
||||||
|
purchaseURL = strings.TrimSpace(*req.PurchaseSubscriptionURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// - 启用时要求 URL 合法且非空
|
||||||
|
// - 禁用时允许为空;若提供了 URL 也做基本校验,避免误配置
|
||||||
|
if purchaseEnabled {
|
||||||
|
if purchaseURL == "" {
|
||||||
|
response.BadRequest(c, "Purchase Subscription URL is required when enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
|
||||||
|
response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if purchaseURL != "" {
|
||||||
|
if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
|
||||||
|
response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ops metrics collector interval validation (seconds).
|
// Ops metrics collector interval validation (seconds).
|
||||||
if req.OpsMetricsIntervalSeconds != nil {
|
if req.OpsMetricsIntervalSeconds != nil {
|
||||||
v := *req.OpsMetricsIntervalSeconds
|
v := *req.OpsMetricsIntervalSeconds
|
||||||
@@ -282,6 +314,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
DocURL: req.DocURL,
|
DocURL: req.DocURL,
|
||||||
HomeContent: req.HomeContent,
|
HomeContent: req.HomeContent,
|
||||||
HideCcsImportButton: req.HideCcsImportButton,
|
HideCcsImportButton: req.HideCcsImportButton,
|
||||||
|
PurchaseSubscriptionEnabled: purchaseEnabled,
|
||||||
|
PurchaseSubscriptionURL: purchaseURL,
|
||||||
DefaultConcurrency: req.DefaultConcurrency,
|
DefaultConcurrency: req.DefaultConcurrency,
|
||||||
DefaultBalance: req.DefaultBalance,
|
DefaultBalance: req.DefaultBalance,
|
||||||
EnableModelFallback: req.EnableModelFallback,
|
EnableModelFallback: req.EnableModelFallback,
|
||||||
@@ -360,6 +394,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
DocURL: updatedSettings.DocURL,
|
DocURL: updatedSettings.DocURL,
|
||||||
HomeContent: updatedSettings.HomeContent,
|
HomeContent: updatedSettings.HomeContent,
|
||||||
HideCcsImportButton: updatedSettings.HideCcsImportButton,
|
HideCcsImportButton: updatedSettings.HideCcsImportButton,
|
||||||
|
PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
|
||||||
|
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
|
||||||
DefaultConcurrency: updatedSettings.DefaultConcurrency,
|
DefaultConcurrency: updatedSettings.DefaultConcurrency,
|
||||||
DefaultBalance: updatedSettings.DefaultBalance,
|
DefaultBalance: updatedSettings.DefaultBalance,
|
||||||
EnableModelFallback: updatedSettings.EnableModelFallback,
|
EnableModelFallback: updatedSettings.EnableModelFallback,
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type SystemSettings struct {
|
|||||||
DocURL string `json:"doc_url"`
|
DocURL string `json:"doc_url"`
|
||||||
HomeContent string `json:"home_content"`
|
HomeContent string `json:"home_content"`
|
||||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||||
|
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||||
|
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||||
|
|
||||||
DefaultConcurrency int `json:"default_concurrency"`
|
DefaultConcurrency int `json:"default_concurrency"`
|
||||||
DefaultBalance float64 `json:"default_balance"`
|
DefaultBalance float64 `json:"default_balance"`
|
||||||
@@ -72,6 +74,8 @@ type PublicSettings struct {
|
|||||||
DocURL string `json:"doc_url"`
|
DocURL string `json:"doc_url"`
|
||||||
HomeContent string `json:"home_content"`
|
HomeContent string `json:"home_content"`
|
||||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||||
|
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||||
|
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
|||||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||||
|
TotpEnabled: settings.TotpEnabled,
|
||||||
TurnstileEnabled: settings.TurnstileEnabled,
|
TurnstileEnabled: settings.TurnstileEnabled,
|
||||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||||
SiteName: settings.SiteName,
|
SiteName: settings.SiteName,
|
||||||
@@ -46,6 +47,8 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
|||||||
DocURL: settings.DocURL,
|
DocURL: settings.DocURL,
|
||||||
HomeContent: settings.HomeContent,
|
HomeContent: settings.HomeContent,
|
||||||
HideCcsImportButton: settings.HideCcsImportButton,
|
HideCcsImportButton: settings.HideCcsImportButton,
|
||||||
|
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||||
|
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||||
Version: h.version,
|
Version: h.version,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -489,7 +489,9 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"enable_identity_patch": true,
|
"enable_identity_patch": true,
|
||||||
"identity_patch_prompt": "",
|
"identity_patch_prompt": "",
|
||||||
"home_content": "",
|
"home_content": "",
|
||||||
"hide_ccs_import_button": false
|
"hide_ccs_import_button": false,
|
||||||
|
"purchase_subscription_enabled": false,
|
||||||
|
"purchase_subscription_url": ""
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ const (
|
|||||||
SettingKeyDocURL = "doc_url" // 文档链接
|
SettingKeyDocURL = "doc_url" // 文档链接
|
||||||
SettingKeyHomeContent = "home_content" // 首页内容(支持 Markdown/HTML,或 URL 作为 iframe src)
|
SettingKeyHomeContent = "home_content" // 首页内容(支持 Markdown/HTML,或 URL 作为 iframe src)
|
||||||
SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮
|
SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮
|
||||||
|
SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示“购买订阅”页面入口
|
||||||
|
SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // “购买订阅”页面 URL(作为 iframe src)
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量
|
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
SettingKeyDocURL,
|
SettingKeyDocURL,
|
||||||
SettingKeyHomeContent,
|
SettingKeyHomeContent,
|
||||||
SettingKeyHideCcsImportButton,
|
SettingKeyHideCcsImportButton,
|
||||||
|
SettingKeyPurchaseSubscriptionEnabled,
|
||||||
|
SettingKeyPurchaseSubscriptionURL,
|
||||||
SettingKeyLinuxDoConnectEnabled,
|
SettingKeyLinuxDoConnectEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +110,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
DocURL: settings[SettingKeyDocURL],
|
DocURL: settings[SettingKeyDocURL],
|
||||||
HomeContent: settings[SettingKeyHomeContent],
|
HomeContent: settings[SettingKeyHomeContent],
|
||||||
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
||||||
|
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
|
||||||
|
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
|
||||||
LinuxDoOAuthEnabled: linuxDoEnabled,
|
LinuxDoOAuthEnabled: linuxDoEnabled,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -148,6 +152,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|||||||
DocURL string `json:"doc_url,omitempty"`
|
DocURL string `json:"doc_url,omitempty"`
|
||||||
HomeContent string `json:"home_content,omitempty"`
|
HomeContent string `json:"home_content,omitempty"`
|
||||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||||
|
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||||
|
PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"`
|
||||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
}{
|
}{
|
||||||
@@ -166,6 +172,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|||||||
DocURL: settings.DocURL,
|
DocURL: settings.DocURL,
|
||||||
HomeContent: settings.HomeContent,
|
HomeContent: settings.HomeContent,
|
||||||
HideCcsImportButton: settings.HideCcsImportButton,
|
HideCcsImportButton: settings.HideCcsImportButton,
|
||||||
|
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||||
|
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||||
Version: s.version,
|
Version: s.version,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -217,6 +225,8 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
|
|||||||
updates[SettingKeyDocURL] = settings.DocURL
|
updates[SettingKeyDocURL] = settings.DocURL
|
||||||
updates[SettingKeyHomeContent] = settings.HomeContent
|
updates[SettingKeyHomeContent] = settings.HomeContent
|
||||||
updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton)
|
updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton)
|
||||||
|
updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled)
|
||||||
|
updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
|
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
|
||||||
@@ -357,6 +367,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|||||||
SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
|
SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
|
||||||
SettingKeySiteName: "Sub2API",
|
SettingKeySiteName: "Sub2API",
|
||||||
SettingKeySiteLogo: "",
|
SettingKeySiteLogo: "",
|
||||||
|
SettingKeyPurchaseSubscriptionEnabled: "false",
|
||||||
|
SettingKeyPurchaseSubscriptionURL: "",
|
||||||
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
||||||
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
||||||
SettingKeySMTPPort: "587",
|
SettingKeySMTPPort: "587",
|
||||||
@@ -407,6 +419,8 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
DocURL: settings[SettingKeyDocURL],
|
DocURL: settings[SettingKeyDocURL],
|
||||||
HomeContent: settings[SettingKeyHomeContent],
|
HomeContent: settings[SettingKeyHomeContent],
|
||||||
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
||||||
|
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
|
||||||
|
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析整数类型
|
// 解析整数类型
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type SystemSettings struct {
|
|||||||
DocURL string
|
DocURL string
|
||||||
HomeContent string
|
HomeContent string
|
||||||
HideCcsImportButton bool
|
HideCcsImportButton bool
|
||||||
|
PurchaseSubscriptionEnabled bool
|
||||||
|
PurchaseSubscriptionURL string
|
||||||
|
|
||||||
DefaultConcurrency int
|
DefaultConcurrency int
|
||||||
DefaultBalance float64
|
DefaultBalance float64
|
||||||
@@ -74,6 +76,10 @@ type PublicSettings struct {
|
|||||||
DocURL string
|
DocURL string
|
||||||
HomeContent string
|
HomeContent string
|
||||||
HideCcsImportButton bool
|
HideCcsImportButton bool
|
||||||
|
|
||||||
|
PurchaseSubscriptionEnabled bool
|
||||||
|
PurchaseSubscriptionURL string
|
||||||
|
|
||||||
LinuxDoOAuthEnabled bool
|
LinuxDoOAuthEnabled bool
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface SystemSettings {
|
|||||||
doc_url: string
|
doc_url: string
|
||||||
home_content: string
|
home_content: string
|
||||||
hide_ccs_import_button: boolean
|
hide_ccs_import_button: boolean
|
||||||
|
purchase_subscription_enabled: boolean
|
||||||
|
purchase_subscription_url: string
|
||||||
// SMTP settings
|
// SMTP settings
|
||||||
smtp_host: string
|
smtp_host: string
|
||||||
smtp_port: number
|
smtp_port: number
|
||||||
@@ -81,6 +83,8 @@ export interface UpdateSettingsRequest {
|
|||||||
doc_url?: string
|
doc_url?: string
|
||||||
home_content?: string
|
home_content?: string
|
||||||
hide_ccs_import_button?: boolean
|
hide_ccs_import_button?: boolean
|
||||||
|
purchase_subscription_enabled?: boolean
|
||||||
|
purchase_subscription_url?: string
|
||||||
smtp_host?: string
|
smtp_host?: string
|
||||||
smtp_port?: number
|
smtp_port?: number
|
||||||
smtp_username?: string
|
smtp_username?: string
|
||||||
|
|||||||
@@ -421,6 +421,16 @@ const userNavItems = computed(() => {
|
|||||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||||
|
...(appStore.cachedPublicSettings?.purchase_subscription_enabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: '/purchase',
|
||||||
|
label: t('nav.buySubscription'),
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
hideInSimpleMode: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
||||||
]
|
]
|
||||||
@@ -433,6 +443,16 @@ const personalNavItems = computed(() => {
|
|||||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||||
|
...(appStore.cachedPublicSettings?.purchase_subscription_enabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: '/purchase',
|
||||||
|
label: t('nav.buySubscription'),
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
hideInSimpleMode: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export default {
|
|||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
github: 'GitHub',
|
github: 'GitHub',
|
||||||
mySubscriptions: 'My Subscriptions',
|
mySubscriptions: 'My Subscriptions',
|
||||||
|
buySubscription: 'Purchase Subscription',
|
||||||
docs: 'Docs'
|
docs: 'Docs'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2894,6 +2895,17 @@ export default {
|
|||||||
hideCcsImportButton: 'Hide CCS Import Button',
|
hideCcsImportButton: 'Hide CCS Import Button',
|
||||||
hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page'
|
hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page'
|
||||||
},
|
},
|
||||||
|
purchase: {
|
||||||
|
title: 'Purchase Page',
|
||||||
|
description: 'Show a "Purchase Subscription" entry in the sidebar and open the configured URL in an iframe',
|
||||||
|
enabled: 'Show Purchase Entry',
|
||||||
|
enabledHint: 'Only shown in standard mode (not simple mode)',
|
||||||
|
url: 'Purchase URL',
|
||||||
|
urlPlaceholder: 'https://example.com/purchase',
|
||||||
|
urlHint: 'Must be an absolute http(s) URL',
|
||||||
|
iframeWarning:
|
||||||
|
'⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Open in new tab" alternative.'
|
||||||
|
},
|
||||||
smtp: {
|
smtp: {
|
||||||
title: 'SMTP Settings',
|
title: 'SMTP Settings',
|
||||||
description: 'Configure email sending for verification codes',
|
description: 'Configure email sending for verification codes',
|
||||||
@@ -3039,6 +3051,18 @@ export default {
|
|||||||
retry: 'Retry'
|
retry: 'Retry'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Purchase Subscription Page
|
||||||
|
purchase: {
|
||||||
|
title: 'Purchase Subscription',
|
||||||
|
description: 'Purchase a subscription via the embedded page',
|
||||||
|
openInNewTab: 'Open in new tab',
|
||||||
|
notEnabledTitle: 'Feature not enabled',
|
||||||
|
notEnabledDesc: 'The administrator has not enabled the purchase page. Please contact admin.',
|
||||||
|
notConfiguredTitle: 'Purchase URL not configured',
|
||||||
|
notConfiguredDesc:
|
||||||
|
'The administrator enabled the entry but has not configured a purchase URL. Please contact admin.'
|
||||||
|
},
|
||||||
|
|
||||||
// User Subscriptions Page
|
// User Subscriptions Page
|
||||||
userSubscriptions: {
|
userSubscriptions: {
|
||||||
title: 'My Subscriptions',
|
title: 'My Subscriptions',
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export default {
|
|||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
github: 'GitHub',
|
github: 'GitHub',
|
||||||
mySubscriptions: '我的订阅',
|
mySubscriptions: '我的订阅',
|
||||||
|
buySubscription: '购买订阅',
|
||||||
docs: '文档'
|
docs: '文档'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -3045,6 +3046,17 @@ export default {
|
|||||||
hideCcsImportButton: '隐藏 CCS 导入按钮',
|
hideCcsImportButton: '隐藏 CCS 导入按钮',
|
||||||
hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮'
|
hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮'
|
||||||
},
|
},
|
||||||
|
purchase: {
|
||||||
|
title: '购买订阅页面',
|
||||||
|
description: '在侧边栏展示“购买订阅”入口,并在页面内通过 iframe 打开指定链接',
|
||||||
|
enabled: '显示购买订阅入口',
|
||||||
|
enabledHint: '仅在标准模式(非简单模式)下展示',
|
||||||
|
url: '购买页面 URL',
|
||||||
|
urlPlaceholder: 'https://example.com/purchase',
|
||||||
|
urlHint: '必须是完整的 http(s) 链接',
|
||||||
|
iframeWarning:
|
||||||
|
'⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用“新窗口打开”。'
|
||||||
|
},
|
||||||
smtp: {
|
smtp: {
|
||||||
title: 'SMTP 设置',
|
title: 'SMTP 设置',
|
||||||
description: '配置用于发送验证码的邮件服务',
|
description: '配置用于发送验证码的邮件服务',
|
||||||
@@ -3189,6 +3201,17 @@ export default {
|
|||||||
retry: '重试'
|
retry: '重试'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Purchase Subscription Page
|
||||||
|
purchase: {
|
||||||
|
title: '购买订阅',
|
||||||
|
description: '通过内嵌页面完成订阅购买',
|
||||||
|
openInNewTab: '新窗口打开',
|
||||||
|
notEnabledTitle: '该功能未开启',
|
||||||
|
notEnabledDesc: '管理员暂未开启购买订阅入口,请联系管理员。',
|
||||||
|
notConfiguredTitle: '购买链接未配置',
|
||||||
|
notConfiguredDesc: '管理员已开启入口,但尚未配置购买订阅链接,请联系管理员。'
|
||||||
|
},
|
||||||
|
|
||||||
// User Subscriptions Page
|
// User Subscriptions Page
|
||||||
userSubscriptions: {
|
userSubscriptions: {
|
||||||
title: '我的订阅',
|
title: '我的订阅',
|
||||||
|
|||||||
@@ -175,6 +175,18 @@ const routes: RouteRecordRaw[] = [
|
|||||||
descriptionKey: 'userSubscriptions.description'
|
descriptionKey: 'userSubscriptions.description'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/purchase',
|
||||||
|
name: 'PurchaseSubscription',
|
||||||
|
component: () => import('@/views/user/PurchaseSubscriptionView.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAdmin: false,
|
||||||
|
title: 'Purchase Subscription',
|
||||||
|
titleKey: 'purchase.title',
|
||||||
|
descriptionKey: 'purchase.description'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ==================== Admin Routes ====================
|
// ==================== Admin Routes ====================
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -324,6 +324,8 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
doc_url: docUrl.value,
|
doc_url: docUrl.value,
|
||||||
home_content: '',
|
home_content: '',
|
||||||
hide_ccs_import_button: false,
|
hide_ccs_import_button: false,
|
||||||
|
purchase_subscription_enabled: false,
|
||||||
|
purchase_subscription_url: '',
|
||||||
linuxdo_oauth_enabled: false,
|
linuxdo_oauth_enabled: false,
|
||||||
version: siteVersion.value
|
version: siteVersion.value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export interface PublicSettings {
|
|||||||
doc_url: string
|
doc_url: string
|
||||||
home_content: string
|
home_content: string
|
||||||
hide_ccs_import_button: boolean
|
hide_ccs_import_button: boolean
|
||||||
|
purchase_subscription_enabled: boolean
|
||||||
|
purchase_subscription_url: string
|
||||||
linuxdo_oauth_enabled: boolean
|
linuxdo_oauth_enabled: boolean
|
||||||
version: string
|
version: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -935,6 +935,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Purchase Subscription Page -->
|
||||||
|
<div class="card">
|
||||||
|
<div 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">
|
||||||
|
{{ t('admin.settings.purchase.title') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.settings.purchase.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6 p-6">
|
||||||
|
<!-- Enable Toggle -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||||
|
t('admin.settings.purchase.enabled')
|
||||||
|
}}</label>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.settings.purchase.enabledHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="form.purchase_subscription_enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('admin.settings.purchase.url') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.purchase_subscription_url"
|
||||||
|
type="url"
|
||||||
|
class="input font-mono text-sm"
|
||||||
|
:placeholder="t('admin.settings.purchase.urlPlaceholder')"
|
||||||
|
/>
|
||||||
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.settings.purchase.urlHint') }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
{{ t('admin.settings.purchase.iframeWarning') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Send Test Email - Only show when email verification is enabled -->
|
<!-- Send Test Email - Only show when email verification is enabled -->
|
||||||
<div v-if="form.email_verify_enabled" class="card">
|
<div v-if="form.email_verify_enabled" class="card">
|
||||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||||
@@ -1083,6 +1128,8 @@ const form = reactive<SettingsForm>({
|
|||||||
doc_url: '',
|
doc_url: '',
|
||||||
home_content: '',
|
home_content: '',
|
||||||
hide_ccs_import_button: false,
|
hide_ccs_import_button: false,
|
||||||
|
purchase_subscription_enabled: false,
|
||||||
|
purchase_subscription_url: '',
|
||||||
smtp_host: '',
|
smtp_host: '',
|
||||||
smtp_port: 587,
|
smtp_port: 587,
|
||||||
smtp_username: '',
|
smtp_username: '',
|
||||||
@@ -1208,6 +1255,8 @@ async function saveSettings() {
|
|||||||
doc_url: form.doc_url,
|
doc_url: form.doc_url,
|
||||||
home_content: form.home_content,
|
home_content: form.home_content,
|
||||||
hide_ccs_import_button: form.hide_ccs_import_button,
|
hide_ccs_import_button: form.hide_ccs_import_button,
|
||||||
|
purchase_subscription_enabled: form.purchase_subscription_enabled,
|
||||||
|
purchase_subscription_url: form.purchase_subscription_url,
|
||||||
smtp_host: form.smtp_host,
|
smtp_host: form.smtp_host,
|
||||||
smtp_port: form.smtp_port,
|
smtp_port: form.smtp_port,
|
||||||
smtp_username: form.smtp_username,
|
smtp_username: form.smtp_username,
|
||||||
|
|||||||
121
frontend/src/views/user/PurchaseSubscriptionView.vue
Normal file
121
frontend/src/views/user/PurchaseSubscriptionView.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<AppLayout>
|
||||||
|
<div class="purchase-page-layout">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('purchase.title') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||||
|
{{ t('purchase.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
v-if="isValidUrl"
|
||||||
|
:href="purchaseUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
>
|
||||||
|
<Icon name="externalLink" size="sm" class="mr-1.5" :stroke-width="2" />
|
||||||
|
{{ t('purchase.openInNewTab') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex-1 min-h-0 overflow-hidden">
|
||||||
|
<div v-if="loading" class="flex h-full items-center justify-center py-12">
|
||||||
|
<div
|
||||||
|
class="h-8 w-8 animate-spin rounded-full border-2 border-primary-500 border-t-transparent"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="!purchaseEnabled"
|
||||||
|
class="flex h-full items-center justify-center p-10 text-center"
|
||||||
|
>
|
||||||
|
<div class="max-w-md">
|
||||||
|
<div
|
||||||
|
class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 dark:bg-dark-700"
|
||||||
|
>
|
||||||
|
<Icon name="creditCard" size="lg" class="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('purchase.notEnabledTitle') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">
|
||||||
|
{{ t('purchase.notEnabledDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="!isValidUrl"
|
||||||
|
class="flex h-full items-center justify-center p-10 text-center"
|
||||||
|
>
|
||||||
|
<div class="max-w-md">
|
||||||
|
<div
|
||||||
|
class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 dark:bg-dark-700"
|
||||||
|
>
|
||||||
|
<Icon name="link" size="lg" class="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('purchase.notConfiguredTitle') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">
|
||||||
|
{{ t('purchase.notConfiguredDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<iframe v-else :src="purchaseUrl" class="h-full w-full border-0" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAppStore } from '@/stores'
|
||||||
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const purchaseEnabled = computed(() => {
|
||||||
|
return appStore.cachedPublicSettings?.purchase_subscription_enabled ?? false
|
||||||
|
})
|
||||||
|
|
||||||
|
const purchaseUrl = computed(() => {
|
||||||
|
return (appStore.cachedPublicSettings?.purchase_subscription_url || '').trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValidUrl = computed(() => {
|
||||||
|
const url = purchaseUrl.value
|
||||||
|
return url.startsWith('http://') || url.startsWith('https://')
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (appStore.publicSettingsLoaded) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await appStore.fetchPublicSettings()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.purchase-page-layout {
|
||||||
|
@apply flex flex-col gap-6;
|
||||||
|
height: calc(100vh - 64px - 4rem); /* 减去 header + lg:p-8 的上下padding */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
Reference in New Issue
Block a user