diff --git a/internal/httpapi/portal.go b/internal/httpapi/portal.go index 9e71b94..ad0ebe5 100644 --- a/internal/httpapi/portal.go +++ b/internal/httpapi/portal.go @@ -11,8 +11,11 @@ import ( ) var publicContentKeys = map[string]struct{}{ - "privacy_policy": {}, - "user_agreement": {}, + "privacy_policy": {}, + "user_agreement": {}, + "platform_intro": {}, + "platform_introduction": {}, + "frequently_asked_questions": {}, } func (a *API) home(w http.ResponseWriter, r *http.Request) { diff --git a/internal/portal/service.go b/internal/portal/service.go index e069b89..91c31a1 100644 --- a/internal/portal/service.go +++ b/internal/portal/service.go @@ -53,6 +53,7 @@ type Notice struct { ID int64 `json:"id"` Title string `json:"title"` Content string `json:"content"` + BodyHTML string `json:"body_html,omitempty"` SourceType string `json:"source_type"` StartAt string `json:"start_at"` EndAt string `json:"end_at"` @@ -207,13 +208,16 @@ func (s *Service) Notices(ctx context.Context, tenantID, memberID int64, page, p } func (s *Service) PopupNotice(ctx context.Context, tenantID, memberID int64) (*Notice, error) { - row, err := s.repository.LatestPopupNotice(ctx, tenantID, memberID) - if err != nil || row == nil { + row, err := s.repository.PublishedContent(ctx, tenantID, "home_popup_content") + if err != nil { return nil, err } - return &Notice{ID: row.ID, Title: row.Title, Content: row.Content, - SourceType: noticeSourceName(row.SourceType), StartAt: row.StartAt, EndAt: row.EndAt, - CreatedAt: row.CreatedAt}, nil + content := strings.TrimSpace(row.BodyText) + if content == "" { + content = strings.TrimSpace(row.Summary) + } + return &Notice{ID: 0, Title: row.Title, Content: content, BodyHTML: row.BodyHTML, + SourceType: "managed_content", CreatedAt: row.PublishedAt}, nil } func (s *Service) MarkNoticeRead(ctx context.Context, tenantID, memberID, noticeID int64) error { @@ -225,6 +229,20 @@ func (s *Service) Content(ctx context.Context, tenantID int64, key string) (*Con if !contentKeyPattern.MatchString(key) { return nil, ErrContentKeyInvalid } + requestedKey := key + titleOverride := "" + switch key { + case "platform_intro", "user_agreement": + key = "platform_introduction" + if requestedKey == "user_agreement" { + titleOverride = "用户协议" + } + case "privacy_policy": + // 当前后台只维护原型的 12 条正式文本,隐私条款沿用平台协议正文, + // 避免为了前端路由额外制造一条无法在原型中对应的初始化数据。 + key = "platform_introduction" + titleOverride = "隐私政策" + } row, err := s.repository.PublishedContent(ctx, tenantID, key) if err != nil { return nil, err @@ -233,7 +251,11 @@ func (s *Service) Content(ctx context.Context, tenantID int64, key string) (*Con if row.AssetIDs.Valid && strings.TrimSpace(row.AssetIDs.String) != "" { _ = json.Unmarshal([]byte(row.AssetIDs.String), &assetIDs) } - return &Content{ContentKey: row.ContentKey, ContentType: row.ContentType, Title: row.Title, + title := row.Title + if titleOverride != "" { + title = titleOverride + } + return &Content{ContentKey: requestedKey, ContentType: row.ContentType, Title: title, Summary: row.Summary, BodyHTML: row.BodyHTML, BodyText: row.BodyText, CoverURL: row.CoverURL, AssetIDs: assetIDs, Version: row.Version, PublishedAt: row.PublishedAt}, nil } diff --git a/internal/portal/service_test.go b/internal/portal/service_test.go index afc4e1b..4934e98 100644 --- a/internal/portal/service_test.go +++ b/internal/portal/service_test.go @@ -203,15 +203,29 @@ func TestNoticesRejectsOversizedPage(t *testing.T) { } } -func TestPopupNoticeMapsLatestUnreadNotice(t *testing.T) { - service := New(fakeRepository{notices: []store.MemberNotice{{ - ID: 9, Title: "公告通知", Content: "新活动已上线", SourceType: 1, - }}}) +func TestPopupNoticeUsesManagedContent(t *testing.T) { + service := New(fakeRepository{content: &store.Content{ + ContentKey: "home_popup_content", Title: "公告通知", BodyText: "新活动已上线", + PublishedAt: "2026-10-16T08:00:00Z", + }}) notice, err := service.PopupNotice(context.Background(), 1, 7) if err != nil { t.Fatal(err) } - if notice == nil || notice.ID != 9 || notice.Content != "新活动已上线" || notice.SourceType != "real" { + if notice == nil || notice.ID != 0 || notice.Content != "新活动已上线" || notice.SourceType != "managed_content" { t.Fatalf("unexpected popup notice: %+v", notice) } } + +func TestContentAliasesPublicAgreementRoutesToManagedText(t *testing.T) { + service := New(fakeRepository{content: &store.Content{ + ContentKey: "platform_introduction", Title: "平台简介", BodyText: "服务条款正文", + }}) + content, err := service.Content(context.Background(), 1, "user_agreement") + if err != nil { + t.Fatal(err) + } + if content.ContentKey != "user_agreement" || content.Title != "用户协议" || content.BodyText != "服务条款正文" { + t.Fatalf("unexpected aliased content: %+v", content) + } +} diff --git a/web/src/mobile/components/AnnouncementPopup.vue b/web/src/mobile/components/AnnouncementPopup.vue index 32c2307..775c46d 100644 --- a/web/src/mobile/components/AnnouncementPopup.vue +++ b/web/src/mobile/components/AnnouncementPopup.vue @@ -15,7 +15,8 @@
- {{ notice?.content }} +
+
@@ -33,6 +34,7 @@ id: number; title: string; content: string; + body_html?: string; } const props = defineProps<{ diff --git a/web/src/mobile/styles.css b/web/src/mobile/styles.css index af8c7ba..ac06954 100644 --- a/web/src/mobile/styles.css +++ b/web/src/mobile/styles.css @@ -375,12 +375,45 @@ body { background: var(--m-bg); } .m-home-hero { + position: relative; min-height: 305px; padding: calc(34px + env(safe-area-inset-top)) 30px 26px; color: #fff; background-color: #1b1711; background-size: cover; background-position: center; + transition: background-image 0.35s ease; +} +.m-home-banner-link { + position: absolute; + inset: 0; + z-index: 0; + border: 0; + background: transparent; + cursor: pointer; +} +.m-home-hero > :not(.m-home-banner-link) { + position: relative; + z-index: 1; +} +.m-home-banner-dots { + display: flex; + justify-content: center; + gap: 6px; + margin-top: 13px; +} +.m-home-banner-dots button { + width: 7px; + height: 7px; + padding: 0; + border: 0; + border-radius: 999px; + background: rgba(255, 255, 255, 0.55); + transition: width 0.2s ease, background 0.2s ease; +} +.m-home-banner-dots button.active { + width: 20px; + background: #fff; } .m-search { height: 48px; @@ -2371,6 +2404,25 @@ body { font-size: 17px; line-height: 1.65; } +.m-managed-rich-text { + color: inherit; + font-size: inherit; + line-height: 1.75; + overflow-wrap: anywhere; +} +.m-managed-rich-text :where(h1, h2, h3, h4) { + margin: 1em 0 0.55em; + color: inherit; + line-height: 1.45; +} +.m-managed-rich-text :where(p, ul, ol) { + margin: 0 0 0.75em; +} +.m-managed-rich-text :where(img) { + max-width: 100%; + height: auto; + border-radius: 8px; +} .m-agency-page { padding: 0 18px 32px; @@ -2610,6 +2662,7 @@ body { gap: 22px; margin-top: 20px; font-size: 18px; + flex-wrap: wrap; } .m-rules-banner strong { font-size: 31px; @@ -2621,10 +2674,12 @@ body { .m-rules-row > span:first-child, .m-rules-row > span:last-child { font-size: 10px; - white-space: nowrap; + overflow-wrap: anywhere; } .m-rule-level { gap: 1px; + flex-direction: column; + white-space: normal; } .m-rule-level img { width: 22px; diff --git a/web/src/mobile/views/AgencyView.vue b/web/src/mobile/views/AgencyView.vue index 83f1ffe..f1b1cf5 100644 --- a/web/src/mobile/views/AgencyView.vue +++ b/web/src/mobile/views/AgencyView.vue @@ -7,9 +7,12 @@ 邀请好友成为平台代理
-

1什么是平台代理?

-

平台用户可通过推荐新人成为平台代理,代理能够获得额外动态奖励。

-

被推荐用户获得每日佣金后,代理按对应层级比例获得奖励。

+

1{{ content?.title || '什么是平台代理?' }}

+
+
@@ -48,12 +51,22 @@ diff --git a/web/src/mobile/views/FormView.vue b/web/src/mobile/views/FormView.vue index 9ca6a00..29e8f2c 100644 --- a/web/src/mobile/views/FormView.vue +++ b/web/src/mobile/views/FormView.vue @@ -213,7 +213,8 @@

{{ content.title }}

{{ content.summary }}

{{ content.body_text || '暂无正文' }}
{{ content.body_text || '暂无正文' }}
+
+
+ +
@@ -82,7 +99,7 @@ diff --git a/web/src/mobile/views/PlatformIntroView.vue b/web/src/mobile/views/PlatformIntroView.vue index 49a4df5..044aa8f 100644 --- a/web/src/mobile/views/PlatformIntroView.vue +++ b/web/src/mobile/views/PlatformIntroView.vue @@ -2,47 +2,76 @@
-
- OLONS PLATFORM -

平台简介

-

{{ platformIntroLead }}

-
{{ platformIntroSections.length }}服务条款{{ platformFAQs.length }}常见问题
-
+ + +
diff --git a/web/src/mobile/views/RulesView.vue b/web/src/mobile/views/RulesView.vue index 01e4c6f..4aab899 100644 --- a/web/src/mobile/views/RulesView.vue +++ b/web/src/mobile/views/RulesView.vue @@ -5,47 +5,93 @@ > -
- 充值赠送金 -

充值500元赠送20元

-
- -
-
- 充值金额会员等级佣金比例每日代购额外奖励单次提现 + +
+ {{ entryLevel.name }}等级配置 +

升级价格{{ yuan(entryLevel.upgrade_amount) }}赠送金额{{ yuan(entryLevel.gift_amount) }}

-
- {{ level.amount }} - {{ level.name }} - {{ level.rate }} - {{ level.orders }} - {{ level.bonus }} - {{ level.withdrawal }} -
-
-
-

03 规则补充说明

-
    -
  • 达到对应单次充值门槛,赠送所列永久会员等级。
  • -
  • 每日代购数量、额外奖励及提现额度以对应档位为准。
  • -
  • 星耀、至尊档位原文佣金比例前后不一致,需核实。
  • -
  • 奖励领取条件、实际结算及提现限制请以正式规则为准。
  • -
-
+
+
+ 升级价格会员等级订单佣金每日接单赠送金额每日提现次数 +
+
+ ≥ {{ yuan(level.upgrade_amount) }} + {{ level.name }} + {{ percent(level.commission_rate) }} + {{ level.daily_order_limit }}单 + {{ yuan(level.gift_amount) }} + {{ level.daily_withdraw_limit }}次 +
+
+ +
+

03 {{ ruleContent?.title || '规则补充说明' }}

+
+
    +
  • 本页展示后台当前生效的会员等级配置,后台调整后重新进入页面即可查看新规则。
  • +
  • 订单佣金按订单金额 × 匹配时会员等级的佣金比例计算,实际金额以订单详情为准。
  • +
  • 每日接单、每日提现均为次数限制;赠送金额为等级配置,实际入账以账户明细为准。
  • +
+
+