feat(distribution): 分站品牌装修可配置首页入口并上传图片
分站可单独配置回收、合作反馈和客服二维码,未配置时继承主站;Logo、图标和轮播支持直接上传。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,27 +4,29 @@ import "time"
|
|||||||
|
|
||||||
// DistributionSite 表示一个独立分销网站主体。
|
// DistributionSite 表示一个独立分销网站主体。
|
||||||
type DistributionSite struct {
|
type DistributionSite struct {
|
||||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||||
SiteCode string `gorm:"size:64;not null;uniqueIndex" json:"site_code"`
|
SiteCode string `gorm:"size:64;not null;uniqueIndex" json:"site_code"`
|
||||||
SiteName string `gorm:"size:128;not null" json:"site_name"`
|
SiteName string `gorm:"size:128;not null" json:"site_name"`
|
||||||
Domain string `gorm:"size:255;not null;uniqueIndex" json:"domain"`
|
Domain string `gorm:"size:255;not null;uniqueIndex" json:"domain"`
|
||||||
Status string `gorm:"size:16;not null;default:'active';index" json:"status"`
|
Status string `gorm:"size:16;not null;default:'active';index" json:"status"`
|
||||||
DisplayAccountEnabled bool `gorm:"not null;default:true" json:"display_account_enabled"`
|
DisplayAccountEnabled bool `gorm:"not null;default:true" json:"display_account_enabled"`
|
||||||
LogoURL string `gorm:"size:512;not null;default:''" json:"logo_url"`
|
LogoURL string `gorm:"size:512;not null;default:''" json:"logo_url"`
|
||||||
FaviconURL string `gorm:"size:512;not null;default:''" json:"favicon_url"`
|
FaviconURL string `gorm:"size:512;not null;default:''" json:"favicon_url"`
|
||||||
BannersJSON string `gorm:"type:text" json:"banners_json"`
|
BannersJSON string `gorm:"type:text" json:"banners_json"`
|
||||||
AnnouncementsJSON string `gorm:"type:text" json:"announcements_json"`
|
AnnouncementsJSON string `gorm:"type:text" json:"announcements_json"`
|
||||||
FooterInfoJSON string `gorm:"type:text" json:"footer_info_json"`
|
FooterInfoJSON string `gorm:"type:text" json:"footer_info_json"`
|
||||||
DefaultSupportGroupID *uint64 `gorm:"index" json:"default_support_group_id,omitempty"`
|
AWRecycleJSON string `gorm:"type:text" json:"aw_recycle_json"`
|
||||||
ListingQrcodeURL string `gorm:"size:512;not null;default:''" json:"listing_qrcode_url"`
|
CooperationFeedbackJSON string `gorm:"type:text" json:"cooperation_feedback_json"`
|
||||||
SMSConfigJSON string `gorm:"type:text" json:"-"`
|
DefaultSupportGroupID *uint64 `gorm:"index" json:"default_support_group_id,omitempty"`
|
||||||
ReceiveRatioDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_ratio_delta"`
|
ListingQrcodeURL string `gorm:"size:512;not null;default:''" json:"listing_qrcode_url"`
|
||||||
ReceiveCostDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_cost_delta"`
|
SMSConfigJSON string `gorm:"type:text" json:"-"`
|
||||||
TaxEnabled bool `gorm:"not null;default:false" json:"tax_enabled"`
|
ReceiveRatioDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_ratio_delta"`
|
||||||
TaxRateBPS int64 `gorm:"not null;default:100" json:"tax_rate_bps"`
|
ReceiveCostDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_cost_delta"`
|
||||||
SettlementHoldDays int `gorm:"not null;default:7" json:"settlement_hold_days"`
|
TaxEnabled bool `gorm:"not null;default:false" json:"tax_enabled"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
TaxRateBPS int64 `gorm:"not null;default:100" json:"tax_rate_bps"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
SettlementHoldDays int `gorm:"not null;default:7" json:"settlement_hold_days"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (DistributionSite) TableName() string { return "distribution_sites" }
|
func (DistributionSite) TableName() string { return "distribution_sites" }
|
||||||
|
|||||||
@@ -356,7 +356,12 @@ func resolveListingQrCode(tx *gorm.DB, siteID uint64) (*model.ChatQrCode, string
|
|||||||
if qrcode != nil {
|
if qrcode != nil {
|
||||||
return qrcode, qrcode.ImageURL, nil
|
return qrcode, qrcode.ImageURL, nil
|
||||||
}
|
}
|
||||||
return nil, "", nil
|
// 分站没有固定码、也没有自己的码池时,沿用主站二维码池。
|
||||||
|
mainQR, err := fetchUnusedQrCode(tx)
|
||||||
|
if err != nil || mainQR == nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return mainQR, mainQR.ImageURL, nil
|
||||||
}
|
}
|
||||||
qrcode, err := fetchUnusedQrCodeForSite(tx, nil)
|
qrcode, err := fetchUnusedQrCodeForSite(tx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,30 +3,32 @@ package distribution
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type SiteDTO struct {
|
type SiteDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
SiteCode string `json:"site_code"`
|
SiteCode string `json:"site_code"`
|
||||||
SiteName string `json:"site_name"`
|
SiteName string `json:"site_name"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
DisplayAccountEnabled bool `json:"display_account_enabled"`
|
DisplayAccountEnabled bool `json:"display_account_enabled"`
|
||||||
LogoURL string `json:"logo_url"`
|
LogoURL string `json:"logo_url"`
|
||||||
FaviconURL string `json:"favicon_url"`
|
FaviconURL string `json:"favicon_url"`
|
||||||
BannersJSON string `json:"banners_json"`
|
BannersJSON string `json:"banners_json"`
|
||||||
AnnouncementsJSON string `json:"announcements_json"`
|
AnnouncementsJSON string `json:"announcements_json"`
|
||||||
FooterInfoJSON string `json:"footer_info_json"`
|
FooterInfoJSON string `json:"footer_info_json"`
|
||||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
AWRecycleJSON string `json:"aw_recycle_json"`
|
||||||
ListingQrcodeURL string `json:"listing_qrcode_url"`
|
CooperationFeedbackJSON string `json:"cooperation_feedback_json"`
|
||||||
ReceiveRatioDelta float64 `json:"receive_ratio_delta"`
|
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
||||||
ReceiveCostDelta float64 `json:"receive_cost_delta"`
|
ListingQrcodeURL string `json:"listing_qrcode_url"`
|
||||||
TaxEnabled bool `json:"tax_enabled"`
|
ReceiveRatioDelta float64 `json:"receive_ratio_delta"`
|
||||||
TaxRateBPS int64 `json:"tax_rate_bps"`
|
ReceiveCostDelta float64 `json:"receive_cost_delta"`
|
||||||
SettlementHoldDays int `json:"settlement_hold_days"`
|
TaxEnabled bool `json:"tax_enabled"`
|
||||||
AdminUserID *uint64 `json:"admin_user_id,omitempty"`
|
TaxRateBPS int64 `json:"tax_rate_bps"`
|
||||||
AdminUsername string `json:"admin_username,omitempty"`
|
SettlementHoldDays int `json:"settlement_hold_days"`
|
||||||
AdminNickname string `json:"admin_nickname,omitempty"`
|
AdminUserID *uint64 `json:"admin_user_id,omitempty"`
|
||||||
MemberType string `json:"member_type,omitempty"`
|
AdminUsername string `json:"admin_username,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
AdminNickname string `json:"admin_nickname,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
MemberType string `json:"member_type,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResetSiteAdminPasswordRequest struct {
|
type ResetSiteAdminPasswordRequest struct {
|
||||||
@@ -418,11 +420,14 @@ type UpdateReceivedListingStatusRequest struct {
|
|||||||
|
|
||||||
// 站点品牌与装修 DTO。指针字段未传则保持原值,避免空字符串把装修清掉。
|
// 站点品牌与装修 DTO。指针字段未传则保持原值,避免空字符串把装修清掉。
|
||||||
type UpdateSiteBrandingRequest struct {
|
type UpdateSiteBrandingRequest struct {
|
||||||
LogoURL *string `json:"logo_url"`
|
LogoURL *string `json:"logo_url"`
|
||||||
FaviconURL *string `json:"favicon_url"`
|
FaviconURL *string `json:"favicon_url"`
|
||||||
BannersJSON *string `json:"banners_json"`
|
BannersJSON *string `json:"banners_json"`
|
||||||
AnnouncementsJSON *string `json:"announcements_json"`
|
AnnouncementsJSON *string `json:"announcements_json"`
|
||||||
FooterInfoJSON *string `json:"footer_info_json"`
|
FooterInfoJSON *string `json:"footer_info_json"`
|
||||||
|
AWRecycleJSON *string `json:"aw_recycle_json"`
|
||||||
|
CooperationFeedbackJSON *string `json:"cooperation_feedback_json"`
|
||||||
|
ListingQrcodeURL *string `json:"listing_qrcode_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自主加价 DTO:增加该站「1 元兑换哈夫币」比例,不是人民币。
|
// 自主加价 DTO:增加该站「1 元兑换哈夫币」比例,不是人民币。
|
||||||
|
|||||||
@@ -855,26 +855,28 @@ func (r *Repository) UpdateRuleStatus(ctx context.Context, adminID, id uint64, s
|
|||||||
|
|
||||||
func siteDTO(row model.DistributionSite) SiteDTO {
|
func siteDTO(row model.DistributionSite) SiteDTO {
|
||||||
return SiteDTO{
|
return SiteDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
SiteCode: row.SiteCode,
|
SiteCode: row.SiteCode,
|
||||||
SiteName: row.SiteName,
|
SiteName: row.SiteName,
|
||||||
Domain: row.Domain,
|
Domain: row.Domain,
|
||||||
Status: row.Status,
|
Status: row.Status,
|
||||||
DisplayAccountEnabled: row.DisplayAccountEnabled,
|
DisplayAccountEnabled: row.DisplayAccountEnabled,
|
||||||
DefaultSupportGroupID: row.DefaultSupportGroupID,
|
DefaultSupportGroupID: row.DefaultSupportGroupID,
|
||||||
ListingQrcodeURL: row.ListingQrcodeURL,
|
ListingQrcodeURL: row.ListingQrcodeURL,
|
||||||
ReceiveRatioDelta: row.ReceiveRatioDelta,
|
ReceiveRatioDelta: row.ReceiveRatioDelta,
|
||||||
ReceiveCostDelta: row.ReceiveCostDelta,
|
ReceiveCostDelta: row.ReceiveCostDelta,
|
||||||
TaxEnabled: row.TaxEnabled,
|
TaxEnabled: row.TaxEnabled,
|
||||||
TaxRateBPS: row.TaxRateBPS,
|
TaxRateBPS: row.TaxRateBPS,
|
||||||
SettlementHoldDays: row.SettlementHoldDays,
|
SettlementHoldDays: row.SettlementHoldDays,
|
||||||
LogoURL: row.LogoURL,
|
LogoURL: row.LogoURL,
|
||||||
FaviconURL: row.FaviconURL,
|
FaviconURL: row.FaviconURL,
|
||||||
BannersJSON: row.BannersJSON,
|
BannersJSON: row.BannersJSON,
|
||||||
AnnouncementsJSON: row.AnnouncementsJSON,
|
AnnouncementsJSON: row.AnnouncementsJSON,
|
||||||
FooterInfoJSON: row.FooterInfoJSON,
|
FooterInfoJSON: row.FooterInfoJSON,
|
||||||
CreatedAt: row.CreatedAt,
|
AWRecycleJSON: row.AWRecycleJSON,
|
||||||
UpdatedAt: row.UpdatedAt,
|
CooperationFeedbackJSON: row.CooperationFeedbackJSON,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2700,6 +2702,18 @@ func (r *Repository) UpdateSiteBranding(ctx context.Context, adminID, siteID uin
|
|||||||
site.FooterInfoJSON = strings.TrimSpace(*req.FooterInfoJSON)
|
site.FooterInfoJSON = strings.TrimSpace(*req.FooterInfoJSON)
|
||||||
updates["footer_info_json"] = site.FooterInfoJSON
|
updates["footer_info_json"] = site.FooterInfoJSON
|
||||||
}
|
}
|
||||||
|
if req.AWRecycleJSON != nil {
|
||||||
|
site.AWRecycleJSON = strings.TrimSpace(*req.AWRecycleJSON)
|
||||||
|
updates["aw_recycle_json"] = site.AWRecycleJSON
|
||||||
|
}
|
||||||
|
if req.CooperationFeedbackJSON != nil {
|
||||||
|
site.CooperationFeedbackJSON = strings.TrimSpace(*req.CooperationFeedbackJSON)
|
||||||
|
updates["cooperation_feedback_json"] = site.CooperationFeedbackJSON
|
||||||
|
}
|
||||||
|
if req.ListingQrcodeURL != nil {
|
||||||
|
site.ListingQrcodeURL = strings.TrimSpace(*req.ListingQrcodeURL)
|
||||||
|
updates["listing_qrcode_url"] = site.ListingQrcodeURL
|
||||||
|
}
|
||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
if err := r.db.WithContext(ctx).Model(&site).Updates(updates).Error; err != nil {
|
if err := r.db.WithContext(ctx).Model(&site).Updates(updates).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) {
|
|||||||
!strings.HasPrefix(key, "mohong/") &&
|
!strings.HasPrefix(key, "mohong/") &&
|
||||||
!strings.HasPrefix(key, "crash/") &&
|
!strings.HasPrefix(key, "crash/") &&
|
||||||
!strings.HasPrefix(key, "aw-recycle/") &&
|
!strings.HasPrefix(key, "aw-recycle/") &&
|
||||||
!strings.HasPrefix(key, "cooperation-feedback/") {
|
!strings.HasPrefix(key, "cooperation-feedback/") &&
|
||||||
|
!strings.HasPrefix(key, "support-qrcode/") {
|
||||||
response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问")
|
response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ func normalizeContentType(contentType string, data []byte) string {
|
|||||||
|
|
||||||
func fileURLForScene(scene string, key string) string {
|
func fileURLForScene(scene string, key string) string {
|
||||||
fileURL := "/api/files/object?key=" + url.QueryEscape(key)
|
fileURL := "/api/files/object?key=" + url.QueryEscape(key)
|
||||||
if scene == "home-banner" || scene == "avatar" || scene == "announcement" || scene == "mohong" || scene == "crash" || scene == "aw-recycle" || scene == "cooperation-feedback" {
|
if scene == "home-banner" || scene == "avatar" || scene == "announcement" || scene == "mohong" || scene == "crash" || scene == "aw-recycle" || scene == "cooperation-feedback" || scene == "support-qrcode" {
|
||||||
fileURL = "/api/public/files/object?key=" + url.QueryEscape(key)
|
fileURL = "/api/public/files/object?key=" + url.QueryEscape(key)
|
||||||
}
|
}
|
||||||
return fileURL
|
return fileURL
|
||||||
@@ -118,7 +118,7 @@ func fileURLForScene(scene string, key string) string {
|
|||||||
func normalizeScene(scene string) string {
|
func normalizeScene(scene string) string {
|
||||||
scene = strings.TrimSpace(strings.ToLower(scene))
|
scene = strings.TrimSpace(strings.ToLower(scene))
|
||||||
switch scene {
|
switch scene {
|
||||||
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "mohong", "crash", "aw-recycle", "cooperation-feedback", "manual-disbursement", "operating-expense":
|
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "support-qrcode", "mohong", "crash", "aw-recycle", "cooperation-feedback", "manual-disbursement", "operating-expense":
|
||||||
return scene
|
return scene
|
||||||
case "listing_screenshot", "listing-screenshot":
|
case "listing_screenshot", "listing-screenshot":
|
||||||
return "listing"
|
return "listing"
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ func TestCooperationFeedbackSceneUsesPublicFileURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSupportQrcodeSceneUsesPublicFileURL(t *testing.T) {
|
||||||
|
if got := normalizeScene("support-qrcode"); got != "support-qrcode" {
|
||||||
|
t.Fatalf("normalize support-qrcode scene = %q", got)
|
||||||
|
}
|
||||||
|
got := fileURLForScene("support-qrcode", "support-qrcode/2026/09/25/example.png")
|
||||||
|
if !strings.HasPrefix(got, "/api/public/files/object?") {
|
||||||
|
t.Fatalf("support-qrcode file url = %q, want public file endpoint", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeSceneListingScreenshotAndOperatingExpense(t *testing.T) {
|
func TestNormalizeSceneListingScreenshotAndOperatingExpense(t *testing.T) {
|
||||||
if got := normalizeScene("listing_screenshot"); got != "listing" {
|
if got := normalizeScene("listing_screenshot"); got != "listing" {
|
||||||
t.Fatalf("normalize listing_screenshot scene = %q, want listing", got)
|
t.Fatalf("normalize listing_screenshot scene = %q, want listing", got)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/middleware"
|
"hfb_sys/backend/internal/middleware"
|
||||||
|
"hfb_sys/backend/internal/sitecode"
|
||||||
"hfb_sys/backend/pkg/response"
|
"hfb_sys/backend/pkg/response"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -82,7 +83,7 @@ func (h *Handler) HomeAnnouncements(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) HomeConfig(c *gin.Context) {
|
func (h *Handler) HomeConfig(c *gin.Context) {
|
||||||
config, err := h.service.HomeConfig(c.Request.Context())
|
config, err := h.service.HomeConfig(c.Request.Context(), sitecode.FromRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeConfigError(c, err)
|
writeConfigError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package systemconfig
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseHomeEntryOverrideInheritsWhenEmpty(t *testing.T) {
|
||||||
|
if _, ok := parseAWRecycleOverride(" "); ok {
|
||||||
|
t.Fatal("空配置应继承主站")
|
||||||
|
}
|
||||||
|
if _, ok := parseCooperationFeedbackOverride(""); ok {
|
||||||
|
t.Fatal("空配置应继承主站")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHomeEntryOverrideUsesSiteJSON(t *testing.T) {
|
||||||
|
aw, ok := parseAWRecycleOverride(`{"title":"分站回收","subtitle":"看图","image_url":"/aw.png","enabled":true}`)
|
||||||
|
if !ok || aw.Title != "分站回收" || aw.ImageURL != "/aw.png" {
|
||||||
|
t.Fatalf("回收覆盖 = %+v ok=%v", aw, ok)
|
||||||
|
}
|
||||||
|
coop, ok := parseCooperationFeedbackOverride(`{"title":"分站合作","subtitle":"联系","image_url":"/coop.png","enabled":false}`)
|
||||||
|
if !ok || coop.Title != "分站合作" || coop.Enabled {
|
||||||
|
t.Fatalf("合作覆盖 = %+v ok=%v", coop, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ package systemconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) HomeAnnouncements(ctx context.Context) (*HomeAnnouncementsDTO, error) {
|
func (s *Service) HomeAnnouncements(ctx context.Context) (*HomeAnnouncementsDTO, error) {
|
||||||
@@ -16,7 +19,7 @@ func (s *Service) HomeAnnouncements(ctx context.Context) (*HomeAnnouncementsDTO,
|
|||||||
return &HomeAnnouncementsDTO{Items: items}, nil
|
return &HomeAnnouncementsDTO{Items: items}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HomeConfig(ctx context.Context) (*HomeConfigDTO, error) {
|
func (s *Service) HomeConfig(ctx context.Context, siteCode string) (*HomeConfigDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
}
|
}
|
||||||
@@ -40,13 +43,61 @@ func (s *Service) HomeConfig(ctx context.Context) (*HomeConfigDTO, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &HomeConfigDTO{
|
config := &HomeConfigDTO{
|
||||||
Announcements: announcements,
|
Announcements: announcements,
|
||||||
Banners: banners,
|
Banners: banners,
|
||||||
PublishOptions: publishOptions,
|
PublishOptions: publishOptions,
|
||||||
AWRecycle: awRecycle,
|
AWRecycle: awRecycle,
|
||||||
CooperationFeedback: cooperationFeedback,
|
CooperationFeedback: cooperationFeedback,
|
||||||
}, nil
|
}
|
||||||
|
s.applySiteHomeOverrides(ctx, siteCode, config)
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applySiteHomeOverrides 用分站自己的首页入口覆盖主站配置。字段为空表示继续继承主站。
|
||||||
|
func (s *Service) applySiteHomeOverrides(ctx context.Context, siteCode string, config *HomeConfigDTO) {
|
||||||
|
siteCode = strings.TrimSpace(siteCode)
|
||||||
|
if s == nil || s.repo == nil || s.repo.db == nil || config == nil || siteCode == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var site model.DistributionSite
|
||||||
|
err := s.repo.db.WithContext(ctx).
|
||||||
|
Select("aw_recycle_json", "cooperation_feedback_json").
|
||||||
|
Where("site_code = ? AND status = ?", siteCode, "active").
|
||||||
|
First(&site).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if item, ok := parseAWRecycleOverride(site.AWRecycleJSON); ok {
|
||||||
|
config.AWRecycle = item
|
||||||
|
}
|
||||||
|
if item, ok := parseCooperationFeedbackOverride(site.CooperationFeedbackJSON); ok {
|
||||||
|
config.CooperationFeedback = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAWRecycleOverride(raw string) (AWRecycleConfig, bool) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return AWRecycleConfig{}, false
|
||||||
|
}
|
||||||
|
var item AWRecycleConfig
|
||||||
|
if err := json.Unmarshal([]byte(raw), &item); err != nil {
|
||||||
|
return AWRecycleConfig{}, false
|
||||||
|
}
|
||||||
|
return normalizeAWRecycle(item), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCooperationFeedbackOverride(raw string) (CooperationFeedbackConfig, bool) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return CooperationFeedbackConfig{}, false
|
||||||
|
}
|
||||||
|
var item CooperationFeedbackConfig
|
||||||
|
if err := json.Unmarshal([]byte(raw), &item); err != nil {
|
||||||
|
return CooperationFeedbackConfig{}, false
|
||||||
|
}
|
||||||
|
return normalizeCooperationFeedback(item), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) homeAnnouncements(ctx context.Context) ([]string, error) {
|
func (s *Service) homeAnnouncements(ctx context.Context) ([]string, error) {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
-- 分站首页入口。空字符串表示继承主站 mobile.aw_recycle / mobile.cooperation_feedback。
|
||||||
|
ALTER TABLE distribution_sites
|
||||||
|
ADD COLUMN aw_recycle_json TEXT NULL COMMENT '分站 AW炫彩回收 JSON,空则继承主站' AFTER footer_info_json,
|
||||||
|
ADD COLUMN cooperation_feedback_json TEXT NULL COMMENT '分站合作与反馈 JSON,空则继承主站' AFTER aw_recycle_json;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
|
||||||
|
ALTER TABLE distribution_sites
|
||||||
|
DROP COLUMN cooperation_feedback_json,
|
||||||
|
DROP COLUMN aw_recycle_json;
|
||||||
@@ -127,6 +127,8 @@ export interface Site {
|
|||||||
banners_json?: string
|
banners_json?: string
|
||||||
announcements_json?: string
|
announcements_json?: string
|
||||||
footer_info_json?: string
|
footer_info_json?: string
|
||||||
|
aw_recycle_json?: string
|
||||||
|
cooperation_feedback_json?: string
|
||||||
listing_qrcode_url?: string
|
listing_qrcode_url?: string
|
||||||
receive_ratio_delta?: number
|
receive_ratio_delta?: number
|
||||||
receive_cost_delta?: number
|
receive_cost_delta?: number
|
||||||
@@ -646,6 +648,19 @@ export interface SiteBrandingPayload {
|
|||||||
banners_json?: string
|
banners_json?: string
|
||||||
announcements_json?: string
|
announcements_json?: string
|
||||||
footer_info_json?: string
|
footer_info_json?: string
|
||||||
|
aw_recycle_json?: string
|
||||||
|
cooperation_feedback_json?: string
|
||||||
|
listing_qrcode_url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadSiteImage(file: File, scene: string): Promise<string> {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('file', file)
|
||||||
|
form.append('scene', scene)
|
||||||
|
const resp = await api.post<ApiResponse<{ url: string }>>('/admin/files/upload', form, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
return extract(resp).url
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSiteBranding(siteID: number, payload: SiteBrandingPayload): Promise<Site> {
|
export async function updateSiteBranding(siteID: number, payload: SiteBrandingPayload): Promise<Site> {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Alert,
|
Alert,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
|
Upload,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
fetchSiteSMSConfig,
|
fetchSiteSMSConfig,
|
||||||
updateSiteSMSConfig,
|
updateSiteSMSConfig,
|
||||||
testSiteSMSConfig,
|
testSiteSMSConfig,
|
||||||
|
uploadSiteImage,
|
||||||
type Site,
|
type Site,
|
||||||
type SiteBrandingPayload,
|
type SiteBrandingPayload,
|
||||||
type SiteSMSConfig,
|
type SiteSMSConfig,
|
||||||
@@ -66,6 +68,46 @@ interface FooterInfo {
|
|||||||
contact?: string
|
contact?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HomeEntryConfig {
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
image_url: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAWEntry: HomeEntryConfig = {
|
||||||
|
title: 'AW炫彩回收',
|
||||||
|
subtitle: '点击查看回收说明',
|
||||||
|
image_url: '',
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultCoopEntry: HomeEntryConfig = {
|
||||||
|
title: '合作与反馈',
|
||||||
|
subtitle: '点击查看联系方式',
|
||||||
|
image_url: '',
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHomeEntry(raw: string | undefined, fallback: HomeEntryConfig) {
|
||||||
|
const text = (raw || '').trim()
|
||||||
|
if (!text) return { useMain: true, value: fallback }
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text) as Partial<HomeEntryConfig>
|
||||||
|
return {
|
||||||
|
useMain: false,
|
||||||
|
value: {
|
||||||
|
title: parsed.title || fallback.title,
|
||||||
|
subtitle: parsed.subtitle || fallback.subtitle,
|
||||||
|
image_url: parsed.image_url || '',
|
||||||
|
enabled: parsed.enabled !== false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { useMain: true, value: fallback }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function SiteSettings() {
|
export default function SiteSettings() {
|
||||||
const member = useMember()
|
const member = useMember()
|
||||||
const [sites, setSites] = useState<Site[]>([])
|
const [sites, setSites] = useState<Site[]>([])
|
||||||
@@ -85,6 +127,11 @@ export default function SiteSettings() {
|
|||||||
contact: '',
|
contact: '',
|
||||||
})
|
})
|
||||||
const [receiveCostDelta, setReceiveCostDelta] = useState(0)
|
const [receiveCostDelta, setReceiveCostDelta] = useState(0)
|
||||||
|
const [awUseMain, setAwUseMain] = useState(true)
|
||||||
|
const [awEntry, setAwEntry] = useState<HomeEntryConfig>(defaultAWEntry)
|
||||||
|
const [coopUseMain, setCoopUseMain] = useState(true)
|
||||||
|
const [coopEntry, setCoopEntry] = useState<HomeEntryConfig>(defaultCoopEntry)
|
||||||
|
const [listingQrcode, setListingQrcode] = useState('')
|
||||||
const [savingRatio, setSavingRatio] = useState(false)
|
const [savingRatio, setSavingRatio] = useState(false)
|
||||||
|
|
||||||
// SMS config state
|
// SMS config state
|
||||||
@@ -141,6 +188,13 @@ export default function SiteSettings() {
|
|||||||
setFooterInfo({})
|
setFooterInfo({})
|
||||||
}
|
}
|
||||||
setReceiveCostDelta(Math.max(0, found.receive_cost_delta || 0))
|
setReceiveCostDelta(Math.max(0, found.receive_cost_delta || 0))
|
||||||
|
const aw = parseHomeEntry(found.aw_recycle_json, defaultAWEntry)
|
||||||
|
setAwUseMain(aw.useMain)
|
||||||
|
setAwEntry(aw.value)
|
||||||
|
const coop = parseHomeEntry(found.cooperation_feedback_json, defaultCoopEntry)
|
||||||
|
setCoopUseMain(coop.useMain)
|
||||||
|
setCoopEntry(coop.value)
|
||||||
|
setListingQrcode(found.listing_qrcode_url || '')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sms = await fetchSiteSMSConfig(id)
|
const sms = await fetchSiteSMSConfig(id)
|
||||||
@@ -193,7 +247,7 @@ export default function SiteSettings() {
|
|||||||
setBanners([
|
setBanners([
|
||||||
...banners,
|
...banners,
|
||||||
{
|
{
|
||||||
image_url: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=1200&q=80',
|
image_url: '',
|
||||||
title: '热门推荐活动',
|
title: '热门推荐活动',
|
||||||
link: '',
|
link: '',
|
||||||
},
|
},
|
||||||
@@ -349,6 +403,71 @@ export default function SiteSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadEntryImage(file: File, scene: string, apply: (url: string) => void) {
|
||||||
|
try {
|
||||||
|
const url = await uploadSiteImage(file, scene)
|
||||||
|
apply(url)
|
||||||
|
message.success('图片已上传,保存后才会在前台生效')
|
||||||
|
} catch {
|
||||||
|
message.error('图片上传失败')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEntryEditor(
|
||||||
|
useMain: boolean,
|
||||||
|
setUseMain: (value: boolean) => void,
|
||||||
|
entry: HomeEntryConfig,
|
||||||
|
setEntry: (value: HomeEntryConfig) => void,
|
||||||
|
scene: string,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography.Text>继承主站配置</Typography.Text>
|
||||||
|
<Switch checked={useMain} onChange={setUseMain} />
|
||||||
|
</div>
|
||||||
|
{useMain ? (
|
||||||
|
<Typography.Text type="secondary">当前展示主站的标题、说明和图片。</Typography.Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
placeholder="标题"
|
||||||
|
value={entry.title}
|
||||||
|
onChange={(e) => setEntry({ ...entry, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="副标题"
|
||||||
|
value={entry.subtitle}
|
||||||
|
onChange={(e) => setEntry({ ...entry, subtitle: e.target.value })}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography.Text>前台显示该入口</Typography.Text>
|
||||||
|
<Switch checked={entry.enabled} onChange={(checked) => setEntry({ ...entry, enabled: checked })} />
|
||||||
|
</div>
|
||||||
|
{entry.image_url ? (
|
||||||
|
<Image src={entry.image_url} width={160} style={{ objectFit: 'contain' }} />
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">尚未上传图片</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Space>
|
||||||
|
<Upload
|
||||||
|
accept="image/*"
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={(file) => uploadEntryImage(file, scene, (url) => setEntry({ ...entry, image_url: url }))}
|
||||||
|
>
|
||||||
|
<Button icon={<PictureOutlined />}>上传图片</Button>
|
||||||
|
</Upload>
|
||||||
|
<Button disabled={!entry.image_url} onClick={() => setEntry({ ...entry, image_url: '' })}>
|
||||||
|
清除图片
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!siteID) return
|
if (!siteID) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -359,6 +478,9 @@ export default function SiteSettings() {
|
|||||||
banners_json: JSON.stringify(banners),
|
banners_json: JSON.stringify(banners),
|
||||||
announcements_json: JSON.stringify(announcements),
|
announcements_json: JSON.stringify(announcements),
|
||||||
footer_info_json: JSON.stringify(footerInfo),
|
footer_info_json: JSON.stringify(footerInfo),
|
||||||
|
aw_recycle_json: awUseMain ? '' : JSON.stringify(awEntry),
|
||||||
|
cooperation_feedback_json: coopUseMain ? '' : JSON.stringify(coopEntry),
|
||||||
|
listing_qrcode_url: listingQrcode.trim(),
|
||||||
}
|
}
|
||||||
const updated = await updateSiteBranding(siteID, payload)
|
const updated = await updateSiteBranding(siteID, payload)
|
||||||
setCurrentSite(updated)
|
setCurrentSite(updated)
|
||||||
@@ -379,7 +501,7 @@ export default function SiteSettings() {
|
|||||||
品牌与站点可视化装修
|
品牌与站点可视化装修
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
可视化配置当前分销独立站的 Logo、首页轮播海报、跑马灯公告及页脚客服信息。
|
可视化配置当前分销独立站的 Logo、首页入口、客服二维码、轮播海报和页脚信息。未单独配置的回收与合作入口继续使用主站。
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Space>
|
||||||
@@ -606,28 +728,64 @@ export default function SiteSettings() {
|
|||||||
children: (
|
children: (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong>站点 Logo 图片链接:</Typography.Text>
|
<Typography.Text strong>站点 Logo:</Typography.Text>
|
||||||
<Input
|
<Space direction="vertical" style={{ width: '100%', marginTop: 6 }} size="small">
|
||||||
placeholder="https://.../logo.png"
|
{logoUrl ? (
|
||||||
value={logoUrl}
|
<Image src={logoUrl} height={48} style={{ objectFit: 'contain' }} />
|
||||||
onChange={(e) => setLogoUrl(e.target.value)}
|
) : (
|
||||||
allowClear
|
<Typography.Text type="secondary">尚未上传 Logo</Typography.Text>
|
||||||
style={{ marginTop: 6 }}
|
)}
|
||||||
/>
|
<Space>
|
||||||
|
<Upload
|
||||||
|
accept="image/*"
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={(file) => uploadEntryImage(file, 'avatar', setLogoUrl)}
|
||||||
|
>
|
||||||
|
<Button icon={<PictureOutlined />}>上传 Logo</Button>
|
||||||
|
</Upload>
|
||||||
|
<Button disabled={!logoUrl} onClick={() => setLogoUrl('')}>
|
||||||
|
清除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<Input
|
||||||
|
placeholder="或粘贴图片链接 https://.../logo.png"
|
||||||
|
value={logoUrl}
|
||||||
|
onChange={(e) => setLogoUrl(e.target.value)}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
|
||||||
建议尺寸 240x60 像素,透明底色 PNG 或 SVG 格式
|
建议尺寸 240x60 像素,透明底色 PNG
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong>浏览器标签页 Favicon 图标:</Typography.Text>
|
<Typography.Text strong>浏览器标签页 Favicon:</Typography.Text>
|
||||||
<Input
|
<Space direction="vertical" style={{ width: '100%', marginTop: 6 }} size="small">
|
||||||
placeholder="https://.../favicon.ico"
|
{faviconUrl ? (
|
||||||
value={faviconUrl}
|
<Image src={faviconUrl} width={32} height={32} style={{ objectFit: 'contain' }} />
|
||||||
onChange={(e) => setFaviconUrl(e.target.value)}
|
) : (
|
||||||
allowClear
|
<Typography.Text type="secondary">尚未上传图标</Typography.Text>
|
||||||
style={{ marginTop: 6 }}
|
)}
|
||||||
/>
|
<Space>
|
||||||
|
<Upload
|
||||||
|
accept="image/*"
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={(file) => uploadEntryImage(file, 'avatar', setFaviconUrl)}
|
||||||
|
>
|
||||||
|
<Button icon={<PictureOutlined />}>上传图标</Button>
|
||||||
|
</Upload>
|
||||||
|
<Button disabled={!faviconUrl} onClick={() => setFaviconUrl('')}>
|
||||||
|
清除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<Input
|
||||||
|
placeholder="或粘贴图片链接 https://.../favicon.png"
|
||||||
|
value={faviconUrl}
|
||||||
|
onChange={(e) => setFaviconUrl(e.target.value)}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
|
||||||
浏览器选项卡显示的小图标,建议 32x32 像素
|
浏览器选项卡显示的小图标,建议 32x32 像素
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@@ -693,8 +851,21 @@ export default function SiteSettings() {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={18}>
|
<Col span={18}>
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
|
<Space>
|
||||||
|
<Upload
|
||||||
|
accept="image/*"
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={(file) =>
|
||||||
|
uploadEntryImage(file, 'home-banner', (url) =>
|
||||||
|
handleUpdateBanner(idx, 'image_url', url),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button icon={<PictureOutlined />}>上传横幅</Button>
|
||||||
|
</Upload>
|
||||||
|
</Space>
|
||||||
<Input
|
<Input
|
||||||
placeholder="输入图片 URL (https://...)"
|
placeholder="或粘贴图片链接 https://..."
|
||||||
value={item.image_url}
|
value={item.image_url}
|
||||||
onChange={(e) => handleUpdateBanner(idx, 'image_url', e.target.value)}
|
onChange={(e) => handleUpdateBanner(idx, 'image_url', e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -777,6 +948,59 @@ export default function SiteSettings() {
|
|||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'entries',
|
||||||
|
label: '首页入口与客服二维码',
|
||||||
|
children: (
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message="回收和合作入口默认继承主站。关掉继承后,只影响当前分销站首页。客服二维码用于号主发布后进群;留空时先用本站二维码池,没有再使用主站二维码池。"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong>AW炫彩回收</Typography.Text>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
{renderEntryEditor(awUseMain, setAwUseMain, awEntry, setAwEntry, 'aw-recycle')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong>合作与反馈</Typography.Text>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
{renderEntryEditor(
|
||||||
|
coopUseMain,
|
||||||
|
setCoopUseMain,
|
||||||
|
coopEntry,
|
||||||
|
setCoopEntry,
|
||||||
|
'cooperation-feedback',
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong>客服二维码</Typography.Text>
|
||||||
|
<Space direction="vertical" style={{ width: '100%', marginTop: 8 }} size="small">
|
||||||
|
{listingQrcode ? (
|
||||||
|
<Image src={listingQrcode} width={160} style={{ objectFit: 'contain' }} />
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">未配置,发布群使用主站二维码池。</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Space>
|
||||||
|
<Upload
|
||||||
|
accept="image/*"
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={(file) => uploadEntryImage(file, 'support-qrcode', setListingQrcode)}
|
||||||
|
>
|
||||||
|
<Button icon={<PictureOutlined />}>上传二维码</Button>
|
||||||
|
</Upload>
|
||||||
|
<Button disabled={!listingQrcode} onClick={() => setListingQrcode('')}>
|
||||||
|
恢复主站
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'footer',
|
key: 'footer',
|
||||||
label: '页脚与客服备案',
|
label: '页脚与客服备案',
|
||||||
|
|||||||
Reference in New Issue
Block a user