feat(distribution): 分站前台按独立商城呈现并支持首页文案自定义
分站前台原本对顾客暴露分销身份,且登录等页面写死了主站品牌, 与「一手独立商城」的定位不符;同时首页文案全部硬编码,站长无法按自己口径调整。 去分销身份(顾客可见处): - 头部品牌区、首页主视觉、移动端认证卡去掉「官方分销/官方分销认证/官方分销站」标识 - 会员徽标「分销专享」→「会员专享」,「所属商城」兜底「专属分销站/商城」→「官方商城」 - 订单页说明改为「当前为【X】商城,仅展示在本站产生的订单记录」,保留订单按站隔离的必要说明 - 无公告时的兜底跑马灯、轮播角标去掉分销措辞 - 清理随之失效的 pc-brand-badge / mobile-brand-badge 样式 品牌跟随站点配置(即可自定义): - PC/移动登录页、移动注册页、加桌面引导页的品牌改为 站点名称 + Logo(无 Logo 取站名首字), 不再写死主站「大锤商行」;主站行为不变(新增 store getter brandMark:分站取站名首字,主站仍为「锤」) 首页文案可配置: - 新增迁移 000076 与 distribution_sites.storefront_text_json (tagline 头部副标题 / hero_badge 主视觉徽标 / hero_desc 主视觉描述 / card_sub 移动端卡片副标题) - 分销后台「品牌装修 → 首页文案」可编辑,占位提示即默认值,留空或整段清空回落内置默认 - 贯通 UpdateSiteBranding → 公开站点信息接口(含 Redis 缓存失效)→ 前台 store 逐字段合并默认值 - 装修预览同步展示站长填写的副标题与徽标 测试: - 后端补装修字段写入/公开接口透出/清空回落断言 - 前端新增 storefrontText 单测(默认合并、覆盖、空白与非法类型回落)
This commit is contained in:
@@ -4,29 +4,31 @@ import "time"
|
||||
|
||||
// DistributionSite 表示一个独立分销网站主体。
|
||||
type DistributionSite struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
SiteCode string `gorm:"size:64;not null;uniqueIndex" json:"site_code"`
|
||||
SiteName string `gorm:"size:128;not null" json:"site_name"`
|
||||
Domain string `gorm:"size:255;not null;uniqueIndex" json:"domain"`
|
||||
Status string `gorm:"size:16;not null;default:'active';index" json:"status"`
|
||||
DisplayAccountEnabled bool `gorm:"not null;default:true" json:"display_account_enabled"`
|
||||
LogoURL string `gorm:"size:512;not null;default:''" json:"logo_url"`
|
||||
FaviconURL string `gorm:"size:512;not null;default:''" json:"favicon_url"`
|
||||
BannersJSON string `gorm:"type:text" json:"banners_json"`
|
||||
AnnouncementsJSON string `gorm:"type:text" json:"announcements_json"`
|
||||
FooterInfoJSON string `gorm:"type:text" json:"footer_info_json"`
|
||||
AWRecycleJSON string `gorm:"type:text" json:"aw_recycle_json"`
|
||||
CooperationFeedbackJSON string `gorm:"type:text" json:"cooperation_feedback_json"`
|
||||
DefaultSupportGroupID *uint64 `gorm:"index" json:"default_support_group_id,omitempty"`
|
||||
ListingQrcodeURL string `gorm:"size:512;not null;default:''" json:"listing_qrcode_url"`
|
||||
SMSConfigJSON string `gorm:"type:text" json:"-"`
|
||||
ReceiveRatioDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_ratio_delta"`
|
||||
ReceiveCostDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_cost_delta"`
|
||||
TaxEnabled bool `gorm:"not null;default:false" json:"tax_enabled"`
|
||||
TaxRateBPS int64 `gorm:"not null;default:100" json:"tax_rate_bps"`
|
||||
SettlementHoldDays int `gorm:"not null;default:7" json:"settlement_hold_days"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
SiteCode string `gorm:"size:64;not null;uniqueIndex" json:"site_code"`
|
||||
SiteName string `gorm:"size:128;not null" json:"site_name"`
|
||||
Domain string `gorm:"size:255;not null;uniqueIndex" json:"domain"`
|
||||
Status string `gorm:"size:16;not null;default:'active';index" json:"status"`
|
||||
DisplayAccountEnabled bool `gorm:"not null;default:true" json:"display_account_enabled"`
|
||||
LogoURL string `gorm:"size:512;not null;default:''" json:"logo_url"`
|
||||
FaviconURL string `gorm:"size:512;not null;default:''" json:"favicon_url"`
|
||||
BannersJSON string `gorm:"type:text" json:"banners_json"`
|
||||
AnnouncementsJSON string `gorm:"type:text" json:"announcements_json"`
|
||||
FooterInfoJSON string `gorm:"type:text" json:"footer_info_json"`
|
||||
AWRecycleJSON string `gorm:"type:text" json:"aw_recycle_json"`
|
||||
CooperationFeedbackJSON string `gorm:"type:text" json:"cooperation_feedback_json"`
|
||||
// StorefrontTextJSON 首页文案(tagline/hero_badge/hero_desc/card_sub),空字段继承前台默认。
|
||||
StorefrontTextJSON string `gorm:"type:text" json:"storefront_text_json"`
|
||||
DefaultSupportGroupID *uint64 `gorm:"index" json:"default_support_group_id,omitempty"`
|
||||
ListingQrcodeURL string `gorm:"size:512;not null;default:''" json:"listing_qrcode_url"`
|
||||
SMSConfigJSON string `gorm:"type:text" json:"-"`
|
||||
ReceiveRatioDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_ratio_delta"`
|
||||
ReceiveCostDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"receive_cost_delta"`
|
||||
TaxEnabled bool `gorm:"not null;default:false" json:"tax_enabled"`
|
||||
TaxRateBPS int64 `gorm:"not null;default:100" json:"tax_rate_bps"`
|
||||
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" }
|
||||
|
||||
@@ -16,6 +16,7 @@ type SiteDTO struct {
|
||||
FooterInfoJSON string `json:"footer_info_json"`
|
||||
AWRecycleJSON string `json:"aw_recycle_json"`
|
||||
CooperationFeedbackJSON string `json:"cooperation_feedback_json"`
|
||||
StorefrontTextJSON string `json:"storefront_text_json"`
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
||||
ListingQrcodeURL string `json:"listing_qrcode_url"`
|
||||
ReceiveRatioDelta float64 `json:"receive_ratio_delta"`
|
||||
@@ -260,6 +261,7 @@ type PublicSiteInfoDTO struct {
|
||||
BannersJSON string `json:"banners_json"`
|
||||
AnnouncementsJSON string `json:"announcements_json"`
|
||||
FooterInfoJSON string `json:"footer_info_json"`
|
||||
StorefrontTextJSON string `json:"storefront_text_json"`
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
||||
ReceiveRatioDelta float64 `json:"receive_ratio_delta"`
|
||||
ReceiveCostDelta float64 `json:"receive_cost_delta"`
|
||||
@@ -428,6 +430,7 @@ type UpdateSiteBrandingRequest struct {
|
||||
FooterInfoJSON *string `json:"footer_info_json"`
|
||||
AWRecycleJSON *string `json:"aw_recycle_json"`
|
||||
CooperationFeedbackJSON *string `json:"cooperation_feedback_json"`
|
||||
StorefrontTextJSON *string `json:"storefront_text_json"`
|
||||
ListingQrcodeURL *string `json:"listing_qrcode_url"`
|
||||
}
|
||||
|
||||
|
||||
@@ -887,6 +887,7 @@ func siteDTO(row model.DistributionSite) SiteDTO {
|
||||
FooterInfoJSON: row.FooterInfoJSON,
|
||||
AWRecycleJSON: row.AWRecycleJSON,
|
||||
CooperationFeedbackJSON: row.CooperationFeedbackJSON,
|
||||
StorefrontTextJSON: row.StorefrontTextJSON,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
@@ -2657,6 +2658,7 @@ func (r *Repository) GetPublicSiteInfo(ctx context.Context, code, domain string)
|
||||
BannersJSON: site.BannersJSON,
|
||||
AnnouncementsJSON: site.AnnouncementsJSON,
|
||||
FooterInfoJSON: site.FooterInfoJSON,
|
||||
StorefrontTextJSON: site.StorefrontTextJSON,
|
||||
ReceiveRatioDelta: site.ReceiveRatioDelta,
|
||||
ReceiveCostDelta: site.ReceiveCostDelta,
|
||||
}
|
||||
@@ -2744,6 +2746,10 @@ func (r *Repository) UpdateSiteBranding(ctx context.Context, adminID, siteID uin
|
||||
site.CooperationFeedbackJSON = strings.TrimSpace(*req.CooperationFeedbackJSON)
|
||||
updates["cooperation_feedback_json"] = site.CooperationFeedbackJSON
|
||||
}
|
||||
if req.StorefrontTextJSON != nil {
|
||||
site.StorefrontTextJSON = strings.TrimSpace(*req.StorefrontTextJSON)
|
||||
updates["storefront_text_json"] = site.StorefrontTextJSON
|
||||
}
|
||||
if req.ListingQrcodeURL != nil {
|
||||
site.ListingQrcodeURL = strings.TrimSpace(*req.ListingQrcodeURL)
|
||||
updates["listing_qrcode_url"] = site.ListingQrcodeURL
|
||||
|
||||
@@ -637,12 +637,14 @@ func TestDistributionBrandingAndMarkup(t *testing.T) {
|
||||
banners := `[{"image_url":"https://cdn.example.com/b1.jpg"}]`
|
||||
announcements := `[{"title":"开业大吉"}]`
|
||||
footer := `{"copyright":"2026 Brand"}`
|
||||
storefrontText := `{"tagline":"天域商行 · 专注回收","hero_badge":"正品保障"}`
|
||||
updated, err := repo.UpdateSiteBranding(t.Context(), 0, site.ID, UpdateSiteBrandingRequest{
|
||||
LogoURL: &logo,
|
||||
FaviconURL: &favicon,
|
||||
BannersJSON: &banners,
|
||||
AnnouncementsJSON: &announcements,
|
||||
FooterInfoJSON: &footer,
|
||||
LogoURL: &logo,
|
||||
FaviconURL: &favicon,
|
||||
BannersJSON: &banners,
|
||||
AnnouncementsJSON: &announcements,
|
||||
FooterInfoJSON: &footer,
|
||||
StorefrontTextJSON: &storefrontText,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSiteBranding failed: %v", err)
|
||||
@@ -650,6 +652,30 @@ func TestDistributionBrandingAndMarkup(t *testing.T) {
|
||||
if updated.LogoURL != "https://cdn.example.com/logo.png" || updated.FaviconURL != "https://cdn.example.com/favicon.ico" {
|
||||
t.Fatalf("unexpected branding: %+v", updated)
|
||||
}
|
||||
if updated.StorefrontTextJSON != storefrontText {
|
||||
t.Fatalf("storefront_text_json = %q, want %q", updated.StorefrontTextJSON, storefrontText)
|
||||
}
|
||||
|
||||
// 公开站点信息必须透出首页文案,前台才能读到站长自定义内容。
|
||||
publicInfo, err := repo.GetPublicSiteInfo(t.Context(), site.SiteCode, "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetPublicSiteInfo failed: %v", err)
|
||||
}
|
||||
if publicInfo.StorefrontTextJSON != storefrontText {
|
||||
t.Fatalf("public storefront_text_json = %q, want %q", publicInfo.StorefrontTextJSON, storefrontText)
|
||||
}
|
||||
|
||||
// 传空字符串表示清除自定义文案,前台回落内置默认值。
|
||||
emptyText := ""
|
||||
textCleared, err := repo.UpdateSiteBranding(t.Context(), 0, site.ID, UpdateSiteBrandingRequest{
|
||||
StorefrontTextJSON: &emptyText,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("clearing storefront text failed: %v", err)
|
||||
}
|
||||
if textCleared.StorefrontTextJSON != "" {
|
||||
t.Fatalf("storefront_text_json should be cleared, got %q", textCleared.StorefrontTextJSON)
|
||||
}
|
||||
|
||||
qrURL := "/api/files/object?key=qrcode/brand-site.png"
|
||||
qrUpdated, err := repo.UpdateSite(t.Context(), 0, site.ID, UpdateSiteRequest{ListingQrcodeURL: &qrURL})
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 分站首页文案(站长可在分销后台「品牌装修」自定义)。JSON 字段:
|
||||
-- tagline 头部品牌区副标题(PC + 移动)
|
||||
-- hero_badge 首页主视觉徽标(PC Hero + 移动认证卡片)
|
||||
-- hero_desc 首页主视觉描述(PC)
|
||||
-- card_sub 移动端认证卡片副标题
|
||||
-- 空值或空字符串表示沿用前台内置默认文案,因此不影响存量站点。
|
||||
ALTER TABLE distribution_sites
|
||||
ADD COLUMN storefront_text_json TEXT NULL COMMENT '分站首页文案 JSON(tagline/hero_badge/hero_desc/card_sub),空字段继承默认' AFTER cooperation_feedback_json;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE distribution_sites
|
||||
DROP COLUMN storefront_text_json;
|
||||
@@ -129,6 +129,7 @@ export interface Site {
|
||||
footer_info_json?: string
|
||||
aw_recycle_json?: string
|
||||
cooperation_feedback_json?: string
|
||||
storefront_text_json?: string
|
||||
listing_qrcode_url?: string
|
||||
receive_ratio_delta?: number
|
||||
receive_cost_delta?: number
|
||||
@@ -651,6 +652,7 @@ export interface SiteBrandingPayload {
|
||||
footer_info_json?: string
|
||||
aw_recycle_json?: string
|
||||
cooperation_feedback_json?: string
|
||||
storefront_text_json?: string
|
||||
listing_qrcode_url?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,21 @@ interface AnnouncementItem {
|
||||
content?: string
|
||||
}
|
||||
|
||||
interface StorefrontTextForm {
|
||||
tagline: string
|
||||
hero_badge: string
|
||||
hero_desc: string
|
||||
card_sub: string
|
||||
}
|
||||
|
||||
// 与前台 shared/utils/storefrontText.ts 的默认文案保持一致(留空即回落到这些值)。
|
||||
const DEFAULT_STOREFRONT_TEXT: StorefrontTextForm = {
|
||||
tagline: '安全交易 · 全程留痕',
|
||||
hero_badge: '品质保障',
|
||||
hero_desc: '账号均经过严格验号与安全质检,支持自动交付与全程留痕,为您提供安全无忧的租号体验。',
|
||||
card_sub: '严格验号 · 统一质检交付 · 专属独立订单',
|
||||
}
|
||||
|
||||
interface FooterInfo {
|
||||
copyright?: string
|
||||
icp?: string
|
||||
@@ -121,6 +136,9 @@ export default function SiteSettings() {
|
||||
const [faviconUrl, setFaviconUrl] = useState<string>('')
|
||||
const [banners, setBanners] = useState<BannerItem[]>([])
|
||||
const [announcements, setAnnouncements] = useState<AnnouncementItem[]>([])
|
||||
const [storefrontText, setStorefrontText] = useState<StorefrontTextForm>({
|
||||
...DEFAULT_STOREFRONT_TEXT,
|
||||
})
|
||||
const [footerInfo, setFooterInfo] = useState<FooterInfo>({
|
||||
copyright: '',
|
||||
icp: '',
|
||||
@@ -187,6 +205,20 @@ export default function SiteSettings() {
|
||||
} catch {
|
||||
setFooterInfo({})
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = found.storefront_text_json
|
||||
? (JSON.parse(found.storefront_text_json) as Partial<StorefrontTextForm>)
|
||||
: {}
|
||||
setStorefrontText({
|
||||
tagline: parsed.tagline?.trim() || '',
|
||||
hero_badge: parsed.hero_badge?.trim() || '',
|
||||
hero_desc: parsed.hero_desc?.trim() || '',
|
||||
card_sub: parsed.card_sub?.trim() || '',
|
||||
})
|
||||
} catch {
|
||||
setStorefrontText({ tagline: '', hero_badge: '', hero_desc: '', card_sub: '' })
|
||||
}
|
||||
setReceiveCostDelta(Math.max(0, found.receive_cost_delta || 0))
|
||||
const aw = parseHomeEntry(found.aw_recycle_json, defaultAWEntry)
|
||||
setAwUseMain(aw.useMain)
|
||||
@@ -468,6 +500,17 @@ export default function SiteSettings() {
|
||||
)
|
||||
}
|
||||
|
||||
/** 全部留空时清除该配置(前台回落默认文案),否则按 JSON 保存。 */
|
||||
function storefrontTextFilled(value: StorefrontTextForm): string {
|
||||
const filled = {
|
||||
tagline: value.tagline.trim(),
|
||||
hero_badge: value.hero_badge.trim(),
|
||||
hero_desc: value.hero_desc.trim(),
|
||||
card_sub: value.card_sub.trim(),
|
||||
}
|
||||
return Object.values(filled).some(Boolean) ? JSON.stringify(filled) : ''
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!siteID) return
|
||||
setSaving(true)
|
||||
@@ -480,6 +523,7 @@ export default function SiteSettings() {
|
||||
footer_info_json: JSON.stringify(footerInfo),
|
||||
aw_recycle_json: awUseMain ? '' : JSON.stringify(awEntry),
|
||||
cooperation_feedback_json: coopUseMain ? '' : JSON.stringify(coopEntry),
|
||||
storefront_text_json: storefrontTextFilled(storefrontText),
|
||||
listing_qrcode_url: listingQrcode.trim(),
|
||||
}
|
||||
const updated = await updateSiteBranding(siteID, payload)
|
||||
@@ -1001,6 +1045,58 @@ export default function SiteSettings() {
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'storefront_text',
|
||||
label: '首页文案',
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<Typography.Text type="secondary">
|
||||
留空则使用默认文案(占位提示即默认值)。这些文案出现在商城头部与首页主视觉,可按本站品牌口径自由填写。
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Typography.Text strong>头部品牌区副标题</Typography.Text>
|
||||
<Input
|
||||
placeholder={DEFAULT_STOREFRONT_TEXT.tagline}
|
||||
value={storefrontText.tagline}
|
||||
onChange={(e) =>
|
||||
setStorefrontText({ ...storefrontText, tagline: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text strong>首页主视觉徽标</Typography.Text>
|
||||
<Input
|
||||
placeholder={DEFAULT_STOREFRONT_TEXT.hero_badge}
|
||||
value={storefrontText.hero_badge}
|
||||
onChange={(e) =>
|
||||
setStorefrontText({ ...storefrontText, hero_badge: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text strong>首页主视觉描述</Typography.Text>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder={DEFAULT_STOREFRONT_TEXT.hero_desc}
|
||||
value={storefrontText.hero_desc}
|
||||
onChange={(e) =>
|
||||
setStorefrontText({ ...storefrontText, hero_desc: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text strong>移动端认证卡片副标题</Typography.Text>
|
||||
<Input
|
||||
placeholder={DEFAULT_STOREFRONT_TEXT.card_sub}
|
||||
value={storefrontText.card_sub}
|
||||
onChange={(e) =>
|
||||
setStorefrontText({ ...storefrontText, card_sub: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'footer',
|
||||
label: '页脚与客服备案',
|
||||
@@ -1126,11 +1222,18 @@ export default function SiteSettings() {
|
||||
{logoUrl ? (
|
||||
<img src={logoUrl} alt="Logo" style={{ height: 26, objectFit: 'contain' }} />
|
||||
) : (
|
||||
<strong style={{ fontSize: 14, color: '#1677ff' }}>
|
||||
{currentSite?.site_name || 'HFB 分销商城'}
|
||||
</strong>
|
||||
<span style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<strong style={{ fontSize: 14, color: '#1677ff' }}>
|
||||
{currentSite?.site_name || 'HFB 分销商城'}
|
||||
</strong>
|
||||
<small style={{ fontSize: 10, color: '#94a3b8' }}>
|
||||
{storefrontText.tagline.trim() || DEFAULT_STOREFRONT_TEXT.tagline}
|
||||
</small>
|
||||
</span>
|
||||
)}
|
||||
<Tag color="blue">分销前台</Tag>
|
||||
<Tag color="blue">
|
||||
{storefrontText.hero_badge.trim() || DEFAULT_STOREFRONT_TEXT.hero_badge}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* Simulated Announcements Marquee */}
|
||||
|
||||
@@ -867,3 +867,8 @@ distribution.hao858.com {
|
||||
- 生产与测试口径一致:结算批次入账、站点提现与冷却期自动释放不再按 `APP_ENV` 冻结(原 `liveMoneyMovementEnabled` 生产保护已移除)。
|
||||
- 新建站点的默认结算冷却天数由系统配置 `distribution.default_settlement_hold_days` 统一控制(默认 7 天,0=立即释放,最大 365),管理端可在系统配置页调整;站点级 `settlement_hold_days` 仍可单独覆盖。
|
||||
- `distribution-admin` 基于 React + TypeScript + Vite + Ant Design,覆盖经营大盘、双视图资产管理、选品货架、订单中心、品牌装修与钱包明细。
|
||||
- 分站前台统一按「一手独立商城」呈现:不向顾客暴露分销身份(无「官方分销」标识),
|
||||
登录/注册页与首页品牌区的站名、Logo 均取自站点配置。
|
||||
- 首页文案支持站长自定义:`distribution_sites.storefront_text_json`
|
||||
(`tagline` 头部副标题 / `hero_badge` 主视觉徽标 / `hero_desc` 主视觉描述 / `card_sub` 移动端卡片副标题),
|
||||
在分销后台「品牌装修 → 首页文案」编辑;字段留空或整段为空时回落到前台内置默认文案。
|
||||
|
||||
@@ -6,10 +6,12 @@ import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { fetchAuthCaptcha, sendSmsCode, type AuthCaptcha } from '@/features/auth/api/auth'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const router = useRouter()
|
||||
const session = useSessionStore()
|
||||
const siteStore = useDistributionSiteStore()
|
||||
const loading = ref(false)
|
||||
const sending = ref(false)
|
||||
const captchaLoading = ref(false)
|
||||
@@ -177,8 +179,9 @@ const modeSubtitle = computed(() =>
|
||||
<div class="login-card">
|
||||
<div class="login-left">
|
||||
<div class="brand-lockup">
|
||||
<div class="brand-logo">锤</div>
|
||||
<strong>大锤商行</strong>
|
||||
<img v-if="siteStore.logoUrl" :src="siteStore.logoUrl" alt="" class="brand-logo-img" />
|
||||
<div v-else class="brand-logo">{{ siteStore.brandMark }}</div>
|
||||
<strong>{{ siteStore.siteName }}</strong>
|
||||
</div>
|
||||
<div class="login-hero">
|
||||
<h2>安全租号<br />畅享游戏</h2>
|
||||
@@ -188,7 +191,7 @@ const modeSubtitle = computed(() =>
|
||||
</p>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<span>© 哈夫币租号平台</span>
|
||||
<span>© {{ siteStore.isDistribution ? siteStore.siteName : '哈夫币租号平台' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -488,6 +491,14 @@ const modeSubtitle = computed(() =>
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.brand-logo-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 8px 24px rgba(23, 35, 61, 0.12);
|
||||
}
|
||||
|
||||
.brand-lockup strong {
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -5,12 +5,14 @@ import { showToast, showDialog } from 'vant'
|
||||
|
||||
import { fetchAuthCaptcha, type AuthCaptcha } from '@/features/auth/api/auth'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
import { useSmsCountdown } from '@/shared/composables/useSmsCountdown'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const session = useSessionStore()
|
||||
const siteStore = useDistributionSiteStore()
|
||||
const loading = ref(false)
|
||||
const agreed = ref(false)
|
||||
const captchaLoading = ref(false)
|
||||
@@ -116,8 +118,9 @@ async function handlePasswordLogin() {
|
||||
|
||||
<section class="auth-body">
|
||||
<div class="brand-lockup">
|
||||
<div class="brand-logo">锤</div>
|
||||
<strong>大锤商行</strong>
|
||||
<img v-if="siteStore.logoUrl" :src="siteStore.logoUrl" alt="" class="brand-logo-img" />
|
||||
<div v-else class="brand-logo">{{ siteStore.brandMark }}</div>
|
||||
<strong>{{ siteStore.siteName }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
@@ -293,6 +296,14 @@ async function handlePasswordLogin() {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.brand-logo-img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 10px 28px rgba(23, 35, 61, 0.12);
|
||||
}
|
||||
|
||||
.brand-lockup strong {
|
||||
color: #0f172a;
|
||||
font-size: 20px;
|
||||
|
||||
@@ -359,7 +359,7 @@ function resolveAvatarURL(url: string | undefined | null) {
|
||||
<section v-if="siteStore.isDistribution" class="growth-card is-distribution">
|
||||
<div>
|
||||
<span>所属商城</span>
|
||||
<strong>{{ siteStore.siteName || '专属分销商城' }}</strong>
|
||||
<strong>{{ siteStore.siteName || '官方商城' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>会员特权</span>
|
||||
|
||||
@@ -5,12 +5,14 @@ import { showToast } from 'vant'
|
||||
|
||||
import { fetchAuthCaptcha, type AuthCaptcha } from '@/features/auth/api/auth'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
import { useSmsCountdown } from '@/shared/composables/useSmsCountdown'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const session = useSessionStore()
|
||||
const siteStore = useDistributionSiteStore()
|
||||
const loading = ref(false)
|
||||
const agreed = ref(false)
|
||||
const captchaLoading = ref(false)
|
||||
@@ -95,8 +97,9 @@ async function handleRegister() {
|
||||
|
||||
<section class="auth-body">
|
||||
<div class="brand-lockup">
|
||||
<div class="brand-logo">锤</div>
|
||||
<strong>大锤商行</strong>
|
||||
<img v-if="siteStore.logoUrl" :src="siteStore.logoUrl" alt="" class="brand-logo-img" />
|
||||
<div v-else class="brand-logo">{{ siteStore.brandMark }}</div>
|
||||
<strong>{{ siteStore.siteName }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
@@ -267,6 +270,14 @@ async function handleRegister() {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.brand-logo-img {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 14px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 14px 32px rgba(23, 35, 61, 0.12);
|
||||
}
|
||||
|
||||
.brand-lockup strong {
|
||||
display: block;
|
||||
color: #111827;
|
||||
|
||||
@@ -3,8 +3,10 @@ import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import shareMenuIcon from '@/assets/share-menu-icon.png'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
const router = useRouter()
|
||||
const siteStore = useDistributionSiteStore()
|
||||
|
||||
const homeUrl = computed(() => `${window.location.origin}/m`)
|
||||
|
||||
@@ -32,18 +34,22 @@ async function copyHomeUrl() {
|
||||
|
||||
<section class="guide-hero">
|
||||
<div class="guide-hero-copy">
|
||||
<span>大锤商行</span>
|
||||
<span>{{ siteStore.siteName }}</span>
|
||||
<h2>如何添加到手机</h2>
|
||||
<p>添加后可从桌面直接进入,找号、下单和查看消息更顺手。</p>
|
||||
</div>
|
||||
<div class="hammer-badge" aria-hidden="true">锤</div>
|
||||
<div class="hammer-badge" aria-hidden="true">{{ siteStore.brandMark }}</div>
|
||||
</section>
|
||||
|
||||
<section class="guide-step">
|
||||
<div class="step-index">1</div>
|
||||
<div class="step-body">
|
||||
<h3>点击浏览器「分享/共享」</h3>
|
||||
<p>请先用 Safari、Chrome 或系统浏览器打开大锤商行,在底部工具栏或菜单里找到入口。</p>
|
||||
<p>
|
||||
请先用 Safari、Chrome 或系统浏览器打开{{
|
||||
siteStore.siteName
|
||||
}},在底部工具栏或菜单里找到入口。
|
||||
</p>
|
||||
<div class="browser-demo" aria-label="浏览器分享或共享按钮示意">
|
||||
<div class="demo-nav">
|
||||
<span>AA</span>
|
||||
|
||||
@@ -51,7 +51,7 @@ const displayAnnouncements = computed(() => {
|
||||
return siteStore.announcements
|
||||
}
|
||||
return [
|
||||
`【${siteStore.siteName}】官方认证独立分销站:账号实时同步主站,官方质检,安全秒交接!`,
|
||||
`【${siteStore.siteName}】精选三角洲账号:官方验号,安全秒交接,售后有保障!`,
|
||||
...announcements.value,
|
||||
]
|
||||
}
|
||||
@@ -63,7 +63,7 @@ const displayBanners = computed<HomeBannerSlide[]>(() => {
|
||||
return siteStore.banners.map(b => ({
|
||||
eyebrow: siteStore.siteName,
|
||||
title: b.title || '精选游戏账号 · 随租随玩',
|
||||
badge: '分销精选',
|
||||
badge: '本店精选',
|
||||
pill: '官方保障',
|
||||
tone: 'gold',
|
||||
image_url: b.image_url,
|
||||
@@ -441,17 +441,15 @@ if (!restored) {
|
||||
<HomeAnnouncement :announcements="displayAnnouncements" />
|
||||
|
||||
<main class="home-content">
|
||||
<!-- 分销站专属标识 Banner -->
|
||||
<!-- 商城专属标识 Banner -->
|
||||
<div v-if="siteStore.isDistribution" class="dist-portal-banner">
|
||||
<div class="dist-portal-content">
|
||||
<div class="dist-portal-tag">
|
||||
<span class="dist-pulse"></span>
|
||||
官方分销认证
|
||||
{{ siteStore.storefrontText.hero_badge }}
|
||||
</div>
|
||||
<h2 class="dist-portal-title">{{ siteStore.siteName }} 专区</h2>
|
||||
<p class="dist-portal-desc">
|
||||
全站上架账号均由平台统一质检与自动交付,订单数据独立隔离,为您提供安全无忧的租号体验。
|
||||
</p>
|
||||
<h2 class="dist-portal-title">{{ siteStore.siteName }}</h2>
|
||||
<p class="dist-portal-desc">{{ siteStore.storefrontText.hero_desc }}</p>
|
||||
</div>
|
||||
<div class="dist-portal-badges">
|
||||
<div class="dist-badge-item">
|
||||
|
||||
@@ -71,19 +71,6 @@
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.mobile-brand-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #ff6a00;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
border: 1px solid rgba(255, 106, 0, 0.25);
|
||||
border-radius: 999px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.mobile-brand small {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -90,7 +90,7 @@ const announcements = ref<string[]>(defaultHomeAnnouncements)
|
||||
const displayAnnouncements = computed(() => {
|
||||
if (siteStore.isDistribution) {
|
||||
return [
|
||||
`【${siteStore.siteName}】官方认证分销站,账号实时同步主站,售后有保障!`,
|
||||
`【${siteStore.siteName}】精选三角洲账号:官方验号,安全秒交接,售后有保障!`,
|
||||
...announcements.value,
|
||||
]
|
||||
}
|
||||
@@ -841,9 +841,10 @@ if (!restoreHomeConfigFromCache()) {
|
||||
<div>
|
||||
<div class="mobile-brand-title-row">
|
||||
<strong>{{ siteStore.siteName }}</strong>
|
||||
<span v-if="siteStore.isDistribution" class="mobile-brand-badge">官方分销</span>
|
||||
</div>
|
||||
<small>{{ siteStore.isDistribution ? '官方分销·平台保障' : '哈夫币租号' }}</small>
|
||||
<small>{{
|
||||
siteStore.isDistribution ? siteStore.storefrontText.tagline : '哈夫币租号'
|
||||
}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -898,13 +899,13 @@ if (!restoreHomeConfigFromCache()) {
|
||||
<!-- ========== Content 区域 ========== -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<section class="mobile-content">
|
||||
<!-- 分销站移动端专属认证卡片 -->
|
||||
<!-- 商城移动端专属认证卡片 -->
|
||||
<div v-if="siteStore.isDistribution" class="mobile-dist-card">
|
||||
<div class="dist-card-top">
|
||||
<span class="dist-card-badge">官方分销站</span>
|
||||
<span class="dist-card-badge">{{ siteStore.storefrontText.hero_badge }}</span>
|
||||
<strong class="dist-card-name">{{ siteStore.siteName }}</strong>
|
||||
</div>
|
||||
<p class="dist-card-sub">官方平台实时同步 · 统一质检交付 · 专属独立订单</p>
|
||||
<p class="dist-card-sub">{{ siteStore.storefrontText.card_sub }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Banner 轮播 -->
|
||||
|
||||
@@ -313,7 +313,7 @@ function bizTypeLabel(bizType: BizType) {
|
||||
<span class="orders-total-badge">{{ unifiedOrders.length }} 笔订单</span>
|
||||
</div>
|
||||
<p v-if="siteStore.isDistribution" class="hero-subtitle">
|
||||
当前为【{{ siteStore.siteName }}】专属分销商城,仅展示在此分销站产生的订单记录
|
||||
当前为【{{ siteStore.siteName }}】商城,仅展示在本站产生的订单记录
|
||||
</p>
|
||||
<p v-else class="hero-subtitle">租赁与撞车订单统一管理 · 实时交接与进度追踪</p>
|
||||
</div>
|
||||
|
||||
@@ -81,20 +81,6 @@
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.pc-brand-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #ff6a00;
|
||||
background: rgba(255, 106, 0, 0.1);
|
||||
border: 1px solid rgba(255, 106, 0, 0.25);
|
||||
border-radius: 999px;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.pc-brand-sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
|
||||
@@ -158,10 +158,9 @@ async function handleSupportClick() {
|
||||
<div class="pc-brand-info">
|
||||
<div class="pc-brand-title-row">
|
||||
<span class="pc-brand-text">{{ siteStore.siteName }}</span>
|
||||
<span v-if="siteStore.isDistribution" class="pc-brand-badge">官方分销</span>
|
||||
</div>
|
||||
<small class="pc-brand-sub">{{
|
||||
siteStore.isDistribution ? '官方分销·平台保障' : '哈夫币租号'
|
||||
siteStore.isDistribution ? siteStore.storefrontText.tagline : '哈夫币租号'
|
||||
}}</small>
|
||||
</div>
|
||||
</RouterLink>
|
||||
@@ -232,7 +231,7 @@ async function handleSupportClick() {
|
||||
</span>
|
||||
<span class="pc-user-name">{{ session.displayName }}</span>
|
||||
<span v-if="siteStore.isDistribution" class="pc-growth-badge is-distribution">
|
||||
分销专享
|
||||
会员专享
|
||||
</span>
|
||||
<span v-else class="pc-growth-badge" :class="`is-${growthBadgeTone}`">
|
||||
{{ session.renterGrowthLevelName }}
|
||||
@@ -254,7 +253,7 @@ async function handleSupportClick() {
|
||||
>
|
||||
<div>
|
||||
<span>所属商城</span>
|
||||
<strong>{{ siteStore.siteName || '专属分销站' }}</strong>
|
||||
<strong>{{ siteStore.siteName || '官方商城' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>会员权益</span>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { defaultStorefrontText, mergeStorefrontText } from '../storefrontText'
|
||||
|
||||
describe('mergeStorefrontText', () => {
|
||||
it('未配置时全部回落默认文案', () => {
|
||||
expect(mergeStorefrontText()).toEqual(defaultStorefrontText)
|
||||
expect(mergeStorefrontText(null)).toEqual(defaultStorefrontText)
|
||||
expect(mergeStorefrontText({})).toEqual(defaultStorefrontText)
|
||||
})
|
||||
|
||||
it('站长配置覆盖对应字段,其余保持默认', () => {
|
||||
const merged = mergeStorefrontText({ tagline: '天域商行 · 专注回收', hero_badge: '正品保障' })
|
||||
expect(merged.tagline).toBe('天域商行 · 专注回收')
|
||||
expect(merged.hero_badge).toBe('正品保障')
|
||||
expect(merged.hero_desc).toBe(defaultStorefrontText.hero_desc)
|
||||
expect(merged.card_sub).toBe(defaultStorefrontText.card_sub)
|
||||
})
|
||||
|
||||
it('空白配置按未配置处理(不允许出现空文案)', () => {
|
||||
const merged = mergeStorefrontText({ tagline: ' ', hero_desc: '' })
|
||||
expect(merged.tagline).toBe(defaultStorefrontText.tagline)
|
||||
expect(merged.hero_desc).toBe(defaultStorefrontText.hero_desc)
|
||||
})
|
||||
|
||||
it('去掉首尾空白并忽略非法类型', () => {
|
||||
const merged = mergeStorefrontText({
|
||||
tagline: ' 安全交易 · 全程留痕 ',
|
||||
hero_badge: 123 as unknown as string,
|
||||
})
|
||||
expect(merged.tagline).toBe('安全交易 · 全程留痕')
|
||||
expect(merged.hero_badge).toBe(defaultStorefrontText.hero_badge)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 分站首页文案(站长可在分销后台「品牌装修」自定义,留空则用这里的默认值)。
|
||||
*
|
||||
* 这些位置原本是写死的分销标识(如「官方分销认证」),改成可配置后:
|
||||
* - 各站可以写自己的口号,前台看起来是独立商城
|
||||
* - 不配置时行为与默认口径一致
|
||||
*/
|
||||
export interface StorefrontText {
|
||||
/** 头部品牌区副标题(PC + 移动) */
|
||||
tagline: string
|
||||
/** 首页主视觉徽标(PC Hero + 移动认证卡片) */
|
||||
hero_badge: string
|
||||
/** 首页主视觉描述(PC) */
|
||||
hero_desc: string
|
||||
/** 移动端认证卡片副标题 */
|
||||
card_sub: string
|
||||
}
|
||||
|
||||
export const defaultStorefrontText: StorefrontText = {
|
||||
tagline: '安全交易 · 全程留痕',
|
||||
hero_badge: '品质保障',
|
||||
hero_desc: '账号均经过严格验号与安全质检,支持自动交付与全程留痕,为您提供安全无忧的租号体验。',
|
||||
card_sub: '严格验号 · 统一质检交付 · 专属独立订单',
|
||||
}
|
||||
|
||||
/** 逐字段合并站长配置:未配置或配置为空的字段回落到默认文案。 */
|
||||
export function mergeStorefrontText(raw?: Partial<StorefrontText> | null): StorefrontText {
|
||||
return {
|
||||
tagline: readText(raw?.tagline, defaultStorefrontText.tagline),
|
||||
hero_badge: readText(raw?.hero_badge, defaultStorefrontText.hero_badge),
|
||||
hero_desc: readText(raw?.hero_desc, defaultStorefrontText.hero_desc),
|
||||
card_sub: readText(raw?.card_sub, defaultStorefrontText.card_sub),
|
||||
}
|
||||
}
|
||||
|
||||
function readText(value: unknown, fallback: string): string {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : fallback
|
||||
}
|
||||
@@ -2,6 +2,11 @@ import { defineStore } from 'pinia'
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import type { ApiResponse } from '@/shared/types/types'
|
||||
import { normalizeAnnouncements } from '@/shared/utils/announcements'
|
||||
import {
|
||||
defaultStorefrontText,
|
||||
mergeStorefrontText,
|
||||
type StorefrontText,
|
||||
} from '@/shared/utils/storefrontText'
|
||||
|
||||
export interface DistributionBanner {
|
||||
image_url: string
|
||||
@@ -28,6 +33,7 @@ export interface DistributionSiteInfo {
|
||||
banners_json?: string
|
||||
announcements_json?: string
|
||||
footer_info_json?: string
|
||||
storefront_text_json?: string
|
||||
receive_ratio_delta?: number
|
||||
receive_cost_delta?: number
|
||||
}
|
||||
@@ -74,12 +80,20 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
|
||||
banners: [] as DistributionBanner[],
|
||||
announcements: [] as string[],
|
||||
footerInfo: null as DistributionFooterInfo | null,
|
||||
storefrontText: { ...defaultStorefrontText } as StorefrontText,
|
||||
receiveRatioDelta: 0,
|
||||
receiveCostDelta: 0,
|
||||
initialized: false,
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
/** 品牌标识字符:分站取站名首字,主站沿用「锤」。 */
|
||||
brandMark(state): string {
|
||||
return state.isDistribution && state.siteName ? state.siteName.slice(0, 1) : '锤'
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
async initSite() {
|
||||
const envCode = ((import.meta.env.VITE_DISTRIBUTION_SITE_CODE as string) || '').trim()
|
||||
@@ -181,6 +195,16 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
|
||||
this.footerInfo = null
|
||||
}
|
||||
|
||||
if (info.storefront_text_json) {
|
||||
try {
|
||||
this.storefrontText = mergeStorefrontText(JSON.parse(info.storefront_text_json))
|
||||
} catch {
|
||||
this.storefrontText = { ...defaultStorefrontText }
|
||||
}
|
||||
} else {
|
||||
this.storefrontText = { ...defaultStorefrontText }
|
||||
}
|
||||
|
||||
this.receiveRatioDelta = Math.max(0, Number(info.receive_ratio_delta || 0))
|
||||
this.receiveCostDelta = Math.max(0, Number(info.receive_cost_delta || 0))
|
||||
|
||||
@@ -221,6 +245,7 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
|
||||
this.banners = []
|
||||
this.announcements = []
|
||||
this.footerInfo = null
|
||||
this.storefrontText = { ...defaultStorefrontText }
|
||||
this.receiveRatioDelta = 0
|
||||
this.receiveCostDelta = 0
|
||||
sessionStorage.removeItem('hfb_site_code')
|
||||
|
||||
Reference in New Issue
Block a user