diff --git a/admin-web/src/features/admin/components/HomeBrandBarDialog.vue b/admin-web/src/features/admin/components/HomeBrandBarDialog.vue new file mode 100644 index 0000000..cdb687c --- /dev/null +++ b/admin-web/src/features/admin/components/HomeBrandBarDialog.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/admin-web/src/features/admin/views/AdminSystemConfigsView.vue b/admin-web/src/features/admin/views/AdminSystemConfigsView.vue index 7656feb..4c83717 100644 --- a/admin-web/src/features/admin/views/AdminSystemConfigsView.vue +++ b/admin-web/src/features/admin/views/AdminSystemConfigsView.vue @@ -38,6 +38,7 @@ import PublishOptionsDialog from '../components/PublishOptionsDialog.vue' import SalePriceDialog from '../components/SalePriceDialog.vue' import HomeAnnouncementsDialog from '../components/HomeAnnouncementsDialog.vue' import HomeBannersDialog from '../components/HomeBannersDialog.vue' +import HomeBrandBarDialog from '../components/HomeBrandBarDialog.vue' import AwRecycleDialog from '../components/AwRecycleDialog.vue' import { defaultAWRecycleConfig, @@ -92,6 +93,7 @@ const salePriceVisible = ref(false) const qualityOwnerVisible = ref(false) const announcementsVisible = ref(false) const bannersVisible = ref(false) +const brandBarVisible = ref(false) const awRecycleVisible = ref(false) const cooperationFeedbackVisible = ref(false) const listingPublishAgreementsVisible = ref(false) @@ -134,6 +136,9 @@ const cooperationFeedbackConfig = computed( const homeEntriesEnabledConfig = computed( () => configs.value.find(item => item.key === 'mobile.home_entries_enabled') || null ) +const homeBrandBarConfig = computed( + () => configs.value.find(item => item.key === 'mobile.home_brand_bar') || null +) const homeEntriesDisplayEnabled = computed(() => { const raw = homeEntriesEnabledConfig.value?.value.trim().toLowerCase() || '' return raw === 'true' || raw === '1' @@ -185,6 +190,7 @@ const regularConfigs = computed(() => item.key !== 'listing.publish_cooldown_minutes' && item.key !== 'mobile.home_announcements' && item.key !== 'mobile.home_banners' && + item.key !== 'mobile.home_brand_bar' && item.key !== 'mobile.aw_recycle' && item.key !== 'mobile.cooperation_feedback' && item.key !== 'mobile.home_entries_enabled' && @@ -368,6 +374,8 @@ function openEdit(row: SystemConfig) { announcementsVisible.value = true } else if (row.key === 'mobile.home_banners') { bannersVisible.value = true + } else if (row.key === 'mobile.home_brand_bar') { + brandBarVisible.value = true } else if (row.key === 'mobile.aw_recycle') { awRecycleVisible.value = true } else if (row.key === 'mobile.cooperation_feedback') { @@ -749,6 +757,7 @@ function formatConfigValue(row: SystemConfig) { v-if=" homeAnnouncementsConfig || homeBannersConfig || + homeBrandBarConfig || awRecycleConfig || cooperationFeedbackConfig || homeEntriesEnabledConfig @@ -760,7 +769,7 @@ function formatConfigValue(row: SystemConfig) {

Home Content

首页运营配置

管理首页公告、轮播图、AW炫彩回收与合作反馈入口展示图,保存后 + >管理首页公告、轮播图、品牌条文案、AW炫彩回收与合作反馈入口展示图,保存后 PC/移动端从接口读取。 @@ -771,6 +780,9 @@ function formatConfigValue(row: SystemConfig) { 编辑轮播图 + 编辑品牌文案 编辑 AW炫彩回收 @@ -824,7 +836,10 @@ function formatConfigValue(row: SystemConfig) { 更新 {{ formatHomeConfigStatus( - cooperationFeedbackConfig || awRecycleConfig || homeBannersConfig, + homeBrandBarConfig || + cooperationFeedbackConfig || + awRecycleConfig || + homeBannersConfig, '未初始化' ) }} @@ -1087,6 +1102,13 @@ function formatConfigValue(row: SystemConfig) { @saved="loadConfigs" /> + + ): MobileHomeC : defaultCooperationFeedbackConfig.enabled, } + const brandRaw = config?.brand_bar + const brandBar: HomeBrandBarConfig = { + eyebrow: + typeof brandRaw?.eyebrow === 'string' && brandRaw.eyebrow.trim() + ? brandRaw.eyebrow.trim() + : defaultHomeBrandBar.eyebrow, + title: + typeof brandRaw?.title === 'string' && brandRaw.title.trim() + ? brandRaw.title.trim() + : defaultHomeBrandBar.title, + subtitle: + typeof brandRaw?.subtitle === 'string' && brandRaw.subtitle.trim() + ? brandRaw.subtitle.trim() + : defaultHomeBrandBar.subtitle, + } + return { announcements: announcements.length ? announcements : defaultHomeAnnouncements, banners: banners.length ? banners : defaultHomeBanners, @@ -329,6 +360,7 @@ export function mergeHomeConfig(config?: Partial): MobileHomeC typeof config?.home_entries_enabled === 'boolean' ? config.home_entries_enabled : defaultHomeEntriesEnabled, + brand_bar: brandBar, } } diff --git a/backend/internal/modules/systemconfig/distribution.go b/backend/internal/modules/systemconfig/distribution.go index 60d9187..d48a4ed 100644 --- a/backend/internal/modules/systemconfig/distribution.go +++ b/backend/internal/modules/systemconfig/distribution.go @@ -36,6 +36,8 @@ func validateConfigValue(key, value string) error { return ErrInvalidConfig } return nil + case homeBrandBarConfigKey: + return validateHomeBrandBar(value) default: return nil } diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index 727fac6..8860f33 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -37,6 +37,15 @@ type HomeConfigDTO struct { CooperationFeedback CooperationFeedbackConfig `json:"cooperation_feedback"` // HomeEntriesEnabled 首页业务入口总开关:关闭后 PC/移动端同时隐藏回收与合作入口。 HomeEntriesEnabled bool `json:"home_entries_enabled"` + // BrandBar 首页品牌条三行文案,后台可配置;留空时回退默认文案。 + BrandBar HomeBrandBarConfig `json:"brand_bar"` +} + +// HomeBrandBarConfig 首页品牌条文案:小标签 / 主标题 / 服务承诺。 +type HomeBrandBarConfig struct { + Eyebrow string `json:"eyebrow"` + Title string `json:"title"` + Subtitle string `json:"subtitle"` } // AWRecycleConfig 首页「AW炫彩回收」入口:点击展示可配置图片。 diff --git a/backend/internal/modules/systemconfig/home_brand_bar.go b/backend/internal/modules/systemconfig/home_brand_bar.go new file mode 100644 index 0000000..233a2ac --- /dev/null +++ b/backend/internal/modules/systemconfig/home_brand_bar.go @@ -0,0 +1,81 @@ +package systemconfig + +import ( + "context" + "encoding/json" + "strings" + "unicode/utf8" +) + +const homeBrandBarConfigKey = "mobile.home_brand_bar" + +// 首页品牌条单行文案上限:防止后台输入超长文本撑破 PC/移动端布局。 +const ( + homeBrandBarEyebrowMaxRunes = 60 + homeBrandBarTitleMaxRunes = 20 + homeBrandBarSubtitleMaxRunes = 80 +) + +func defaultHomeBrandBar() HomeBrandBarConfig { + return HomeBrandBarConfig{ + Eyebrow: "JIEYOU TRADING HOUSE · 账号租赁", + Title: "解忧柜选", + Subtitle: "实名审核 · 流程留痕 · 账单透明 · 争议可申诉", + } +} + +func defaultHomeBrandBarConfigValue() string { + raw, err := json.Marshal(defaultHomeBrandBar()) + if err != nil { + return `{"eyebrow":"JIEYOU TRADING HOUSE · 账号租赁","title":"解忧柜选","subtitle":"实名审核 · 流程留痕 · 账单透明 · 争议可申诉"}` + } + return string(raw) +} + +// normalizeHomeBrandBar 去除首尾空白;空字段回退默认文案,避免前台出现空白品牌条。 +func normalizeHomeBrandBar(item HomeBrandBarConfig) HomeBrandBarConfig { + def := defaultHomeBrandBar() + eyebrow := strings.TrimSpace(item.Eyebrow) + if eyebrow == "" { + eyebrow = def.Eyebrow + } + title := strings.TrimSpace(item.Title) + if title == "" { + title = def.Title + } + subtitle := strings.TrimSpace(item.Subtitle) + if subtitle == "" { + subtitle = def.Subtitle + } + return HomeBrandBarConfig{ + Eyebrow: eyebrow, + Title: title, + Subtitle: subtitle, + } +} + +func validateHomeBrandBar(value string) error { + var item HomeBrandBarConfig + if err := json.Unmarshal([]byte(value), &item); err != nil { + return ErrInvalidConfig + } + item = normalizeHomeBrandBar(item) + if utf8.RuneCountInString(item.Eyebrow) > homeBrandBarEyebrowMaxRunes || + utf8.RuneCountInString(item.Title) > homeBrandBarTitleMaxRunes || + utf8.RuneCountInString(item.Subtitle) > homeBrandBarSubtitleMaxRunes { + return ErrInvalidConfig + } + return nil +} + +func (s *Service) homeBrandBar(ctx context.Context) (HomeBrandBarConfig, error) { + value, err := s.repo.FindValue(ctx, homeBrandBarConfigKey) + if err != nil { + return defaultHomeBrandBar(), err + } + item := defaultHomeBrandBar() + if err := json.Unmarshal([]byte(value), &item); err != nil { + item = defaultHomeBrandBar() + } + return normalizeHomeBrandBar(item), nil +} diff --git a/backend/internal/modules/systemconfig/home_brand_bar_test.go b/backend/internal/modules/systemconfig/home_brand_bar_test.go new file mode 100644 index 0000000..f2276d3 --- /dev/null +++ b/backend/internal/modules/systemconfig/home_brand_bar_test.go @@ -0,0 +1,83 @@ +package systemconfig + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestHomeBrandBarConfigRegistered(t *testing.T) { + value := "" + for _, item := range defaultConfigs { + if item.Key == homeBrandBarConfigKey { + value = item.Value + break + } + } + if value != defaultHomeBrandBarConfigValue() { + t.Fatalf("defaultConfigs 中 %s 的默认值 = %q, want %q", homeBrandBarConfigKey, value, defaultHomeBrandBarConfigValue()) + } + visible := false + for _, key := range adminVisibleConfigKeys { + if key == homeBrandBarConfigKey { + visible = true + break + } + } + if !visible { + t.Fatalf("adminVisibleConfigKeys 缺少 %s", homeBrandBarConfigKey) + } + var parsed HomeBrandBarConfig + if err := json.Unmarshal([]byte(value), &parsed); err != nil { + t.Fatalf("默认值不是合法 JSON: %v", err) + } + if parsed != defaultHomeBrandBar() { + t.Fatalf("默认值解析结果 = %+v, want %+v", parsed, defaultHomeBrandBar()) + } +} + +func TestNormalizeHomeBrandBar(t *testing.T) { + got := normalizeHomeBrandBar(HomeBrandBarConfig{ + Eyebrow: " 测试标签 ", + Title: " ", + Subtitle: "", + }) + def := defaultHomeBrandBar() + if got.Eyebrow != "测试标签" { + t.Fatalf("Eyebrow = %q, want %q", got.Eyebrow, "测试标签") + } + if got.Title != def.Title { + t.Fatalf("空标题应回退默认值, got %q want %q", got.Title, def.Title) + } + if got.Subtitle != def.Subtitle { + t.Fatalf("空副标题应回退默认值, got %q want %q", got.Subtitle, def.Subtitle) + } +} + +func TestValidateHomeBrandBar(t *testing.T) { + valid := []string{ + `{"eyebrow":"JIEYOU TRADING HOUSE · 账号租赁","title":"解忧柜选","subtitle":"实名审核 · 流程留痕 · 账单透明 · 争议可申诉"}`, + `{}`, + `{"eyebrow":"","title":"","subtitle":""}`, + `{"eyebrow":"A","title":"柜选","subtitle":"透明"}`, + } + for _, value := range valid { + if err := validateConfigValue(homeBrandBarConfigKey, value); err != nil { + t.Fatalf("validateConfigValue(%q) = %v, want nil", value, err) + } + } + + invalid := []string{ + ``, + `not-json`, + `[]`, + `{"title":"` + strings.Repeat("超", homeBrandBarTitleMaxRunes+1) + `"}`, + `{"eyebrow":"` + strings.Repeat("长", homeBrandBarEyebrowMaxRunes+1) + `"}`, + `{"subtitle":"` + strings.Repeat("长", homeBrandBarSubtitleMaxRunes+1) + `"}`, + } + for _, value := range invalid { + if err := validateConfigValue(homeBrandBarConfigKey, value); err != ErrInvalidConfig { + t.Fatalf("validateConfigValue(%q) = %v, want ErrInvalidConfig", value, err) + } + } +} diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 6384916..3ac0f6e 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -57,6 +57,7 @@ var defaultConfigs = []defaultConfig{ {Key: awRecycleConfigKey, Value: defaultAWRecycleConfigValue(), Description: "首页 AW炫彩回收入口配置 JSON(标题/副标题/展示图片)"}, {Key: cooperationFeedbackConfigKey, Value: defaultCooperationFeedbackConfigValue(), Description: "首页 合作与反馈入口配置 JSON(标题/副标题/展示图片)"}, {Key: homeEntriesEnabledConfigKey, Value: defaultHomeEntriesEnabledConfigValue(), Description: "首页 AW炫彩回收/合作与反馈入口总开关(关闭后 PC/移动端均隐藏)"}, + {Key: homeBrandBarConfigKey, Value: defaultHomeBrandBarConfigValue(), Description: "首页品牌条文案 JSON(小标签/主标题/服务承诺)"}, {Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"}, {Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"}, {Key: DistributionSettlementHoldDaysKey, Value: defaultDistributionSettlementHoldDaysValue(), Description: "新建分销站点默认结算冷却天数(0=立即释放,最大 365)"}, @@ -87,6 +88,7 @@ var adminVisibleConfigKeys = []string{ "mobile.aw_recycle", "mobile.cooperation_feedback", "mobile.home_entries_enabled", + homeBrandBarConfigKey, "order.agreements", "order.pending_payment_timeout_minutes", "order.return_overdue_grace_minutes", diff --git a/backend/internal/modules/systemconfig/service_home.go b/backend/internal/modules/systemconfig/service_home.go index 01c5e0e..d798eb4 100644 --- a/backend/internal/modules/systemconfig/service_home.go +++ b/backend/internal/modules/systemconfig/service_home.go @@ -55,6 +55,10 @@ func (s *Service) HomeConfig(ctx context.Context, siteCode string) (*HomeConfigD if err != nil { return nil, err } + brandBar, err := s.homeBrandBar(ctx) + if err != nil { + return nil, err + } config := &HomeConfigDTO{ Announcements: announcements, Banners: banners, @@ -62,6 +66,7 @@ func (s *Service) HomeConfig(ctx context.Context, siteCode string) (*HomeConfigD AWRecycle: awRecycle, CooperationFeedback: cooperationFeedback, HomeEntriesEnabled: homeEntriesEnabled, + BrandBar: brandBar, } s.applySiteHomeOverrides(ctx, siteCode, config) return config, nil diff --git a/frontend/src/features/home/api.ts b/frontend/src/features/home/api.ts index 554e1ca..33761df 100644 --- a/frontend/src/features/home/api.ts +++ b/frontend/src/features/home/api.ts @@ -34,6 +34,12 @@ export interface HomeBannerItem { mobile?: HomeBannerVisual } +export interface HomeBrandBarConfig { + eyebrow: string + title: string + subtitle: string +} + export interface HomeConfig { announcements: string[] banners: HomeBannerItem[] @@ -41,6 +47,15 @@ export interface HomeConfig { aw_recycle: HomeEntryConfig cooperation_feedback: HomeEntryConfig home_entries_enabled: boolean + /** 首页品牌条三行文案,后台可配置;旧接口未返回时前台回退默认值。 */ + brand_bar?: HomeBrandBarConfig +} + +/** 首页品牌条默认文案,与后端 mobile.home_brand_bar 默认值保持一致。 */ +export const fallbackBrandBar: HomeBrandBarConfig = { + eyebrow: 'JIEYOU TRADING HOUSE · 账号租赁', + title: '解忧柜选', + subtitle: '实名审核 · 流程留痕 · 账单透明 · 争议可申诉', } export const fallbackBanners: HomeBannerItem[] = [ diff --git a/frontend/src/features/listings/ListingListPage.tsx b/frontend/src/features/listings/ListingListPage.tsx index 76620d9..a1bdb4f 100644 --- a/frontend/src/features/listings/ListingListPage.tsx +++ b/frontend/src/features/listings/ListingListPage.tsx @@ -25,6 +25,7 @@ import { import { Link } from 'react-router-dom' import { BrandLogo } from '@/components/BrandLogo' +import { fallbackBrandBar, useHomeConfig } from '@/features/home/api' import { HomePortalRow } from '@/features/home/HomePortalRow' import { useListings } from '@/features/listings/api' import { @@ -296,6 +297,9 @@ export function ListingListPage() { localStorage.setItem('jieyou:listings:view', mode) } + const homeConfig = useHomeConfig() + const brandBar = homeConfig.data?.brand_bar ?? fallbackBrandBar + const publishOptions = useQuery({ queryKey: ['listing-publish-options'], queryFn: fetchListingPublishOptions, @@ -384,11 +388,9 @@ export function ListingListPage() {
- JIEYOU TRADING HOUSE · 账号租赁 - 解忧柜选 - - 实名审核 · 流程留痕 · 账单透明 · 争议可申诉 - + {brandBar.eyebrow} + {brandBar.title} + {brandBar.subtitle}
banner.enabled !== false) .slice(0, 4) + const brandBar = homeConfig.data?.brand_bar ?? fallbackBrandBar const runBannerAction = (banner: HomeBannerItem) => { const action = banner.mobile?.action @@ -469,7 +471,7 @@ export function MobileHomePage() {
- 解忧柜选 + {brandBar.title} 今天想租什么账号?
规则 diff --git a/frontend/src/styles/storefront-home.css b/frontend/src/styles/storefront-home.css index 49599a7..b8062fe 100644 --- a/frontend/src/styles/storefront-home.css +++ b/frontend/src/styles/storefront-home.css @@ -255,25 +255,35 @@ flex-direction: column; gap: 1px; min-width: 232px; + max-width: 360px; } .home-market-bar__eyebrow { + max-width: 100%; + overflow: hidden; color: var(--ink-muted); font-size: 10.5px; letter-spacing: 0.18em; + text-overflow: ellipsis; + white-space: nowrap; } .home-market-bar__title { + max-width: 100%; + overflow: hidden; color: var(--brand-ink); font-family: 'Noto Serif SC', 'Songti SC', serif; font-size: 22px; letter-spacing: 0.05em; line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; } .home-market-bar__trust { color: var(--ink-muted); font-size: 11px; + line-height: 1.4; } .home-market-bar__search {