feat(purchase): 增加购买订阅 iframe 页面与配置
- 新增 /purchase 页面(iframe + 新窗口兜底) - 管理员系统设置可配置开关与URL - 非 simple mode 才在侧边栏展示入口
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,
|
||||||
@@ -119,14 +121,16 @@ type UpdateSettingsRequest struct {
|
|||||||
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
||||||
|
|
||||||
// OEM设置
|
// OEM设置
|
||||||
SiteName string `json:"site_name"`
|
SiteName string `json:"site_name"`
|
||||||
SiteLogo string `json:"site_logo"`
|
SiteLogo string `json:"site_logo"`
|
||||||
SiteSubtitle string `json:"site_subtitle"`
|
SiteSubtitle string `json:"site_subtitle"`
|
||||||
APIBaseURL string `json:"api_base_url"`
|
APIBaseURL string `json:"api_base_url"`
|
||||||
ContactInfo string `json:"contact_info"`
|
ContactInfo string `json:"contact_info"`
|
||||||
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
|
||||||
@@ -255,42 +287,44 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settings := &service.SystemSettings{
|
settings := &service.SystemSettings{
|
||||||
RegistrationEnabled: req.RegistrationEnabled,
|
RegistrationEnabled: req.RegistrationEnabled,
|
||||||
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
||||||
PromoCodeEnabled: req.PromoCodeEnabled,
|
PromoCodeEnabled: req.PromoCodeEnabled,
|
||||||
PasswordResetEnabled: req.PasswordResetEnabled,
|
PasswordResetEnabled: req.PasswordResetEnabled,
|
||||||
TotpEnabled: req.TotpEnabled,
|
TotpEnabled: req.TotpEnabled,
|
||||||
SMTPHost: req.SMTPHost,
|
SMTPHost: req.SMTPHost,
|
||||||
SMTPPort: req.SMTPPort,
|
SMTPPort: req.SMTPPort,
|
||||||
SMTPUsername: req.SMTPUsername,
|
SMTPUsername: req.SMTPUsername,
|
||||||
SMTPPassword: req.SMTPPassword,
|
SMTPPassword: req.SMTPPassword,
|
||||||
SMTPFrom: req.SMTPFrom,
|
SMTPFrom: req.SMTPFrom,
|
||||||
SMTPFromName: req.SMTPFromName,
|
SMTPFromName: req.SMTPFromName,
|
||||||
SMTPUseTLS: req.SMTPUseTLS,
|
SMTPUseTLS: req.SMTPUseTLS,
|
||||||
TurnstileEnabled: req.TurnstileEnabled,
|
TurnstileEnabled: req.TurnstileEnabled,
|
||||||
TurnstileSiteKey: req.TurnstileSiteKey,
|
TurnstileSiteKey: req.TurnstileSiteKey,
|
||||||
TurnstileSecretKey: req.TurnstileSecretKey,
|
TurnstileSecretKey: req.TurnstileSecretKey,
|
||||||
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
|
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
|
||||||
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
||||||
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
||||||
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
|
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
|
||||||
SiteName: req.SiteName,
|
SiteName: req.SiteName,
|
||||||
SiteLogo: req.SiteLogo,
|
SiteLogo: req.SiteLogo,
|
||||||
SiteSubtitle: req.SiteSubtitle,
|
SiteSubtitle: req.SiteSubtitle,
|
||||||
APIBaseURL: req.APIBaseURL,
|
APIBaseURL: req.APIBaseURL,
|
||||||
ContactInfo: req.ContactInfo,
|
ContactInfo: req.ContactInfo,
|
||||||
DocURL: req.DocURL,
|
DocURL: req.DocURL,
|
||||||
HomeContent: req.HomeContent,
|
HomeContent: req.HomeContent,
|
||||||
HideCcsImportButton: req.HideCcsImportButton,
|
HideCcsImportButton: req.HideCcsImportButton,
|
||||||
DefaultConcurrency: req.DefaultConcurrency,
|
PurchaseSubscriptionEnabled: purchaseEnabled,
|
||||||
DefaultBalance: req.DefaultBalance,
|
PurchaseSubscriptionURL: purchaseURL,
|
||||||
EnableModelFallback: req.EnableModelFallback,
|
DefaultConcurrency: req.DefaultConcurrency,
|
||||||
FallbackModelAnthropic: req.FallbackModelAnthropic,
|
DefaultBalance: req.DefaultBalance,
|
||||||
FallbackModelOpenAI: req.FallbackModelOpenAI,
|
EnableModelFallback: req.EnableModelFallback,
|
||||||
FallbackModelGemini: req.FallbackModelGemini,
|
FallbackModelAnthropic: req.FallbackModelAnthropic,
|
||||||
FallbackModelAntigravity: req.FallbackModelAntigravity,
|
FallbackModelOpenAI: req.FallbackModelOpenAI,
|
||||||
EnableIdentityPatch: req.EnableIdentityPatch,
|
FallbackModelGemini: req.FallbackModelGemini,
|
||||||
IdentityPatchPrompt: req.IdentityPatchPrompt,
|
FallbackModelAntigravity: req.FallbackModelAntigravity,
|
||||||
|
EnableIdentityPatch: req.EnableIdentityPatch,
|
||||||
|
IdentityPatchPrompt: req.IdentityPatchPrompt,
|
||||||
OpsMonitoringEnabled: func() bool {
|
OpsMonitoringEnabled: func() bool {
|
||||||
if req.OpsMonitoringEnabled != nil {
|
if req.OpsMonitoringEnabled != nil {
|
||||||
return *req.OpsMonitoringEnabled
|
return *req.OpsMonitoringEnabled
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ type SystemSettings struct {
|
|||||||
LinuxDoConnectClientSecretConfigured bool `json:"linuxdo_connect_client_secret_configured"`
|
LinuxDoConnectClientSecretConfigured bool `json:"linuxdo_connect_client_secret_configured"`
|
||||||
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
||||||
|
|
||||||
SiteName string `json:"site_name"`
|
SiteName string `json:"site_name"`
|
||||||
SiteLogo string `json:"site_logo"`
|
SiteLogo string `json:"site_logo"`
|
||||||
SiteSubtitle string `json:"site_subtitle"`
|
SiteSubtitle string `json:"site_subtitle"`
|
||||||
APIBaseURL string `json:"api_base_url"`
|
APIBaseURL string `json:"api_base_url"`
|
||||||
ContactInfo string `json:"contact_info"`
|
ContactInfo string `json:"contact_info"`
|
||||||
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"`
|
||||||
@@ -57,23 +59,25 @@ type SystemSettings struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PublicSettings struct {
|
type PublicSettings struct {
|
||||||
RegistrationEnabled bool `json:"registration_enabled"`
|
RegistrationEnabled bool `json:"registration_enabled"`
|
||||||
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
||||||
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
||||||
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
||||||
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
||||||
TurnstileEnabled bool `json:"turnstile_enabled"`
|
TurnstileEnabled bool `json:"turnstile_enabled"`
|
||||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||||
SiteName string `json:"site_name"`
|
SiteName string `json:"site_name"`
|
||||||
SiteLogo string `json:"site_logo"`
|
SiteLogo string `json:"site_logo"`
|
||||||
SiteSubtitle string `json:"site_subtitle"`
|
SiteSubtitle string `json:"site_subtitle"`
|
||||||
APIBaseURL string `json:"api_base_url"`
|
APIBaseURL string `json:"api_base_url"`
|
||||||
ContactInfo string `json:"contact_info"`
|
ContactInfo string `json:"contact_info"`
|
||||||
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"`
|
||||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||||
Version string `json:"version"`
|
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||||
|
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||||
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamTimeoutSettings 流超时处理配置 DTO
|
// StreamTimeoutSettings 流超时处理配置 DTO
|
||||||
|
|||||||
@@ -32,21 +32,24 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.PublicSettings{
|
response.Success(c, dto.PublicSettings{
|
||||||
RegistrationEnabled: settings.RegistrationEnabled,
|
RegistrationEnabled: settings.RegistrationEnabled,
|
||||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||||
TurnstileEnabled: settings.TurnstileEnabled,
|
TotpEnabled: settings.TotpEnabled,
|
||||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
TurnstileEnabled: settings.TurnstileEnabled,
|
||||||
SiteName: settings.SiteName,
|
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||||
SiteLogo: settings.SiteLogo,
|
SiteName: settings.SiteName,
|
||||||
SiteSubtitle: settings.SiteSubtitle,
|
SiteLogo: settings.SiteLogo,
|
||||||
APIBaseURL: settings.APIBaseURL,
|
SiteSubtitle: settings.SiteSubtitle,
|
||||||
ContactInfo: settings.ContactInfo,
|
APIBaseURL: settings.APIBaseURL,
|
||||||
DocURL: settings.DocURL,
|
ContactInfo: settings.ContactInfo,
|
||||||
HomeContent: settings.HomeContent,
|
DocURL: settings.DocURL,
|
||||||
HideCcsImportButton: settings.HideCcsImportButton,
|
HomeContent: settings.HomeContent,
|
||||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
HideCcsImportButton: settings.HideCcsImportButton,
|
||||||
Version: h.version,
|
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||||
|
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||||
|
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||||
|
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": ""
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,14 +98,16 @@ const (
|
|||||||
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
|
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
|
||||||
|
|
||||||
// OEM设置
|
// OEM设置
|
||||||
SettingKeySiteName = "site_name" // 网站名称
|
SettingKeySiteName = "site_name" // 网站名称
|
||||||
SettingKeySiteLogo = "site_logo" // 网站Logo (base64)
|
SettingKeySiteLogo = "site_logo" // 网站Logo (base64)
|
||||||
SettingKeySiteSubtitle = "site_subtitle" // 网站副标题
|
SettingKeySiteSubtitle = "site_subtitle" // 网站副标题
|
||||||
SettingKeyAPIBaseURL = "api_base_url" // API端点地址(用于客户端配置和导入)
|
SettingKeyAPIBaseURL = "api_base_url" // API端点地址(用于客户端配置和导入)
|
||||||
SettingKeyContactInfo = "contact_info" // 客服联系方式
|
SettingKeyContactInfo = "contact_info" // 客服联系方式
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,22 +95,24 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
passwordResetEnabled := emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true"
|
passwordResetEnabled := emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true"
|
||||||
|
|
||||||
return &PublicSettings{
|
return &PublicSettings{
|
||||||
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
||||||
EmailVerifyEnabled: emailVerifyEnabled,
|
EmailVerifyEnabled: emailVerifyEnabled,
|
||||||
PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
|
PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
|
||||||
PasswordResetEnabled: passwordResetEnabled,
|
PasswordResetEnabled: passwordResetEnabled,
|
||||||
TotpEnabled: settings[SettingKeyTotpEnabled] == "true",
|
TotpEnabled: settings[SettingKeyTotpEnabled] == "true",
|
||||||
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
||||||
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
||||||
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
||||||
SiteLogo: settings[SettingKeySiteLogo],
|
SiteLogo: settings[SettingKeySiteLogo],
|
||||||
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
||||||
APIBaseURL: settings[SettingKeyAPIBaseURL],
|
APIBaseURL: settings[SettingKeyAPIBaseURL],
|
||||||
ContactInfo: settings[SettingKeyContactInfo],
|
ContactInfo: settings[SettingKeyContactInfo],
|
||||||
DocURL: settings[SettingKeyDocURL],
|
DocURL: settings[SettingKeyDocURL],
|
||||||
HomeContent: settings[SettingKeyHomeContent],
|
HomeContent: settings[SettingKeyHomeContent],
|
||||||
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
||||||
LinuxDoOAuthEnabled: linuxDoEnabled,
|
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
|
||||||
|
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
|
||||||
|
LinuxDoOAuthEnabled: linuxDoEnabled,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,41 +137,45 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|||||||
|
|
||||||
// Return a struct that matches the frontend's expected format
|
// Return a struct that matches the frontend's expected format
|
||||||
return &struct {
|
return &struct {
|
||||||
RegistrationEnabled bool `json:"registration_enabled"`
|
RegistrationEnabled bool `json:"registration_enabled"`
|
||||||
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
||||||
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
||||||
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
||||||
TotpEnabled bool `json:"totp_enabled"`
|
TotpEnabled bool `json:"totp_enabled"`
|
||||||
TurnstileEnabled bool `json:"turnstile_enabled"`
|
TurnstileEnabled bool `json:"turnstile_enabled"`
|
||||||
TurnstileSiteKey string `json:"turnstile_site_key,omitempty"`
|
TurnstileSiteKey string `json:"turnstile_site_key,omitempty"`
|
||||||
SiteName string `json:"site_name"`
|
SiteName string `json:"site_name"`
|
||||||
SiteLogo string `json:"site_logo,omitempty"`
|
SiteLogo string `json:"site_logo,omitempty"`
|
||||||
SiteSubtitle string `json:"site_subtitle,omitempty"`
|
SiteSubtitle string `json:"site_subtitle,omitempty"`
|
||||||
APIBaseURL string `json:"api_base_url,omitempty"`
|
APIBaseURL string `json:"api_base_url,omitempty"`
|
||||||
ContactInfo string `json:"contact_info,omitempty"`
|
ContactInfo string `json:"contact_info,omitempty"`
|
||||||
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"`
|
||||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||||
Version string `json:"version,omitempty"`
|
PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"`
|
||||||
|
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
}{
|
}{
|
||||||
RegistrationEnabled: settings.RegistrationEnabled,
|
RegistrationEnabled: settings.RegistrationEnabled,
|
||||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||||
TotpEnabled: settings.TotpEnabled,
|
TotpEnabled: settings.TotpEnabled,
|
||||||
TurnstileEnabled: settings.TurnstileEnabled,
|
TurnstileEnabled: settings.TurnstileEnabled,
|
||||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||||
SiteName: settings.SiteName,
|
SiteName: settings.SiteName,
|
||||||
SiteLogo: settings.SiteLogo,
|
SiteLogo: settings.SiteLogo,
|
||||||
SiteSubtitle: settings.SiteSubtitle,
|
SiteSubtitle: settings.SiteSubtitle,
|
||||||
APIBaseURL: settings.APIBaseURL,
|
APIBaseURL: settings.APIBaseURL,
|
||||||
ContactInfo: settings.ContactInfo,
|
ContactInfo: settings.ContactInfo,
|
||||||
DocURL: settings.DocURL,
|
DocURL: settings.DocURL,
|
||||||
HomeContent: settings.HomeContent,
|
HomeContent: settings.HomeContent,
|
||||||
HideCcsImportButton: settings.HideCcsImportButton,
|
HideCcsImportButton: settings.HideCcsImportButton,
|
||||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||||
Version: s.version,
|
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||||
|
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||||
|
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)
|
||||||
@@ -352,15 +362,17 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|||||||
|
|
||||||
// 初始化默认设置
|
// 初始化默认设置
|
||||||
defaults := map[string]string{
|
defaults := map[string]string{
|
||||||
SettingKeyRegistrationEnabled: "true",
|
SettingKeyRegistrationEnabled: "true",
|
||||||
SettingKeyEmailVerifyEnabled: "false",
|
SettingKeyEmailVerifyEnabled: "false",
|
||||||
SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
|
SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
|
||||||
SettingKeySiteName: "Sub2API",
|
SettingKeySiteName: "Sub2API",
|
||||||
SettingKeySiteLogo: "",
|
SettingKeySiteLogo: "",
|
||||||
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
SettingKeyPurchaseSubscriptionEnabled: "false",
|
||||||
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
SettingKeyPurchaseSubscriptionURL: "",
|
||||||
SettingKeySMTPPort: "587",
|
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
||||||
SettingKeySMTPUseTLS: "false",
|
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
||||||
|
SettingKeySMTPPort: "587",
|
||||||
|
SettingKeySMTPUseTLS: "false",
|
||||||
// Model fallback defaults
|
// Model fallback defaults
|
||||||
SettingKeyEnableModelFallback: "false",
|
SettingKeyEnableModelFallback: "false",
|
||||||
SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
|
SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
|
||||||
@@ -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]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析整数类型
|
// 解析整数类型
|
||||||
|
|||||||
@@ -28,14 +28,16 @@ type SystemSettings struct {
|
|||||||
LinuxDoConnectClientSecretConfigured bool
|
LinuxDoConnectClientSecretConfigured bool
|
||||||
LinuxDoConnectRedirectURL string
|
LinuxDoConnectRedirectURL string
|
||||||
|
|
||||||
SiteName string
|
SiteName string
|
||||||
SiteLogo string
|
SiteLogo string
|
||||||
SiteSubtitle string
|
SiteSubtitle string
|
||||||
APIBaseURL string
|
APIBaseURL string
|
||||||
ContactInfo string
|
ContactInfo string
|
||||||
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,8 +76,12 @@ type PublicSettings struct {
|
|||||||
DocURL string
|
DocURL string
|
||||||
HomeContent string
|
HomeContent string
|
||||||
HideCcsImportButton bool
|
HideCcsImportButton bool
|
||||||
LinuxDoOAuthEnabled bool
|
|
||||||
Version string
|
PurchaseSubscriptionEnabled bool
|
||||||
|
PurchaseSubscriptionURL string
|
||||||
|
|
||||||
|
LinuxDoOAuthEnabled bool
|
||||||
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
|
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
|
||||||
|
|||||||
@@ -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