Files
hfb_sys/distribution-admin/src/pages/SiteSettings.tsx
T
yml2213 22fcaa5945 feat(distribution): 分站收号压价解耦买家价(方案 B 新口径)
核心语义反转:receive_cost_delta 为压价点数,收号比例 = 主站参考比例 R + 点数,
点数越大号主收号价越低;买家售价锚定主站参考比例推导,不再随收号价联动。

- 迁移 000072:distribution_sites 新增 receive_cost_delta(旧 receive_ratio_delta 保留只读)
- receive_pricing:新增 Normalize/ApplySiteReceiveCostDelta;分站定价买家侧改用
  R 锚点(referenceSellerCoinBasePrice),快照写入 site_receive_cost_delta 与 pricing_model 标记
- site_receive:新增 resolveSiteReceiveCostDelta 读取链路,旧 ResolveReceiveRatioDelta 保留
- 分成不变:压价差经结算层 profit 自动进入 35/40/25 三方分成池;
  货架自主加价仍 100% 归分站,两条边界均以主站为锚
- 权限对齐旧字段:主站全局管理员与分站操作员均可设置
- 前端三端同步:主站 pricing.ts/usePricingCalculator 买卖解耦试算、
  主站后台弹窗改「收号压价」价格语义文案、分销后台 SiteSettings 同步
- 测试:单测锚定 R=43 时 cost_delta=3 → 收号 217.4 / 卖价 263.2 不变 / markup 池 30.6→54.9
2026-09-17 15:54:56 +08:00

1012 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState, useCallback } from 'react'
import {
Card,
Typography,
Space,
Select,
message,
Button,
Form,
Input,
InputNumber,
Row,
Col,
Divider,
Image,
Tag,
Tabs,
Collapse,
Switch,
Alert,
Popconfirm,
} from 'antd'
import {
SaveOutlined,
PictureOutlined,
NotificationOutlined,
EyeOutlined,
PlusOutlined,
DeleteOutlined,
InfoCircleOutlined,
SkinOutlined,
GlobalOutlined,
MessageOutlined,
SendOutlined,
} from '@ant-design/icons'
import {
listSites,
updateSiteBranding,
updateSite,
getMe,
fetchSiteSMSConfig,
updateSiteSMSConfig,
testSiteSMSConfig,
type Site,
type SiteBrandingPayload,
type SiteSMSConfig,
type UpdateSiteSMSConfigPayload,
type TestSiteSMSConfigPayload,
} from '../api'
import { useMember } from '../MemberContext'
interface BannerItem {
image_url: string
title?: string
link?: string
}
interface AnnouncementItem {
title: string
content?: string
}
interface FooterInfo {
copyright?: string
icp?: string
contact?: string
}
export default function SiteSettings() {
const member = useMember()
const [sites, setSites] = useState<Site[]>([])
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [currentSite, setCurrentSite] = useState<Site | null>(null)
// Visual state for customizer
const [logoUrl, setLogoUrl] = useState<string>('')
const [faviconUrl, setFaviconUrl] = useState<string>('')
const [banners, setBanners] = useState<BannerItem[]>([])
const [announcements, setAnnouncements] = useState<AnnouncementItem[]>([])
const [footerInfo, setFooterInfo] = useState<FooterInfo>({
copyright: '',
icp: '',
contact: '',
})
const [receiveCostDelta, setReceiveCostDelta] = useState(0)
const [savingRatio, setSavingRatio] = useState(false)
// SMS config state
const [smsConfig, setSmsConfig] = useState<SiteSMSConfig>({
enabled: false,
provider: 'aliyun',
access_key_id: '',
masked_secret: '',
has_secret: false,
endpoint: 'dysmsapi.aliyuncs.com',
sign_name: '',
login_template_code: '',
})
const [smsSecretInput, setSmsSecretInput] = useState('')
const [savingSMS, setSavingSMS] = useState(false)
const [testPhone, setTestPhone] = useState('')
const [testingSMS, setTestingSMS] = useState(false)
const [testCooldown, setTestCooldown] = useState(0)
useEffect(() => {
if (testCooldown <= 0) return
const timer = setInterval(() => {
setTestCooldown((prev) => (prev > 0 ? prev - 1 : 0))
}, 1000)
return () => clearInterval(timer)
}, [testCooldown])
const loadSite = useCallback(async (id: number) => {
setLoading(true)
try {
const rows = await listSites()
setSites(rows)
const found = rows.find((s) => s.id === id)
if (found) {
setCurrentSite(found)
setLogoUrl(found.logo_url || '')
setFaviconUrl(found.favicon_url || '')
try {
setBanners(found.banners_json ? JSON.parse(found.banners_json) : [])
} catch {
setBanners([])
}
try {
setAnnouncements(found.announcements_json ? JSON.parse(found.announcements_json) : [])
} catch {
setAnnouncements([])
}
try {
setFooterInfo(found.footer_info_json ? JSON.parse(found.footer_info_json) : {})
} catch {
setFooterInfo({})
}
setReceiveCostDelta(Math.max(0, found.receive_cost_delta || 0))
try {
const sms = await fetchSiteSMSConfig(id)
setSmsConfig(sms)
setSmsSecretInput('')
} catch {
// ignore
}
}
} catch {
message.error('加载站点设置失败')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
async function init() {
let currentMember = member
if (!currentMember) {
try {
currentMember = await getMe()
} catch {
// ignore
}
}
const activeSiteID = currentMember?.site_id
if (activeSiteID) {
setSiteID(activeSiteID)
void loadSite(activeSiteID)
}
if (currentMember?.is_superadmin) {
try {
const rows = await listSites()
setSites(rows)
if (!activeSiteID && rows.length > 0) {
setSiteID(rows[0].id)
void loadSite(rows[0].id)
}
} catch {
// ignore
}
}
}
void init()
}, [member, loadSite])
function handleAddBanner() {
setBanners([
...banners,
{
image_url: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=1200&q=80',
title: '热门推荐活动',
link: '',
},
])
}
function handleUpdateBanner(index: number, field: keyof BannerItem, value: string) {
const next = [...banners]
next[index] = { ...next[index], [field]: value }
setBanners(next)
}
function handleDeleteBanner(index: number) {
setBanners(banners.filter((_, i) => i !== index))
}
function handleAddAnnouncement() {
setAnnouncements([
...announcements,
{
title: '平台全新上线精选商品,免押畅玩!',
content: '',
},
])
}
function handleUpdateAnnouncement(index: number, field: keyof AnnouncementItem, value: string) {
const next = [...announcements]
next[index] = { ...next[index], [field]: value }
setAnnouncements(next)
}
function handleDeleteAnnouncement(index: number) {
setAnnouncements(announcements.filter((_, i) => i !== index))
}
async function handleSaveReceiveRatio() {
if (!siteID) return
setSavingRatio(true)
try {
const updated = await updateSite(siteID, { receive_cost_delta: receiveCostDelta })
setCurrentSite(updated)
setReceiveCostDelta(Math.max(0, updated.receive_cost_delta || 0))
message.success('收号压价已保存,本站收号价比主站更低,售价与主站一致')
} catch (error: unknown) {
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
message.error(respMessage || '保存收号比例失败')
} finally {
setSavingRatio(false)
}
}
async function handleSaveSMS() {
if (!siteID) return
if (smsConfig.enabled) {
if (!smsConfig.access_key_id?.trim()) {
message.warning('启用独立短信时,阿里云 AccessKey ID 不能为空')
return
}
if (!smsConfig.has_secret && !smsSecretInput.trim()) {
message.warning('启用独立短信时,阿里云 AccessKey Secret 不能为空')
return
}
if (!smsConfig.sign_name?.trim()) {
message.warning('启用独立短信时,短信签名不能为空')
return
}
if (!smsConfig.login_template_code?.trim()) {
message.warning('启用独立短信时,登录验证码模板 Code 不能为空')
return
}
}
setSavingSMS(true)
try {
const payload: UpdateSiteSMSConfigPayload = {
enabled: smsConfig.enabled,
provider: smsConfig.provider || 'aliyun',
access_key_id: smsConfig.access_key_id.trim(),
endpoint: smsConfig.endpoint.trim() || 'dysmsapi.aliyuncs.com',
sign_name: smsConfig.sign_name.trim(),
login_template_code: smsConfig.login_template_code.trim(),
}
if (smsSecretInput.trim()) {
payload.access_key_secret = smsSecretInput.trim()
}
const updated = await updateSiteSMSConfig(siteID, payload)
setSmsConfig(updated)
setSmsSecretInput('')
message.success('短信服务配置保存成功')
} catch (err: unknown) {
const msg =
err && typeof err === 'object' && 'response' in err
? (err as { response?: { data?: { message?: string } } }).response?.data?.message
: '保存短信配置失败'
message.error(msg || '保存短信配置失败')
} finally {
setSavingSMS(false)
}
}
async function handleTestSMS() {
if (!siteID) return
const phone = testPhone.trim()
if (!phone) {
message.warning('请输入接收测试短信的手机号')
return
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
message.warning('手机号格式不正确,请输入有效的11位手机号码')
return
}
if (!smsConfig.access_key_id?.trim()) {
message.warning('请先填写阿里云 AccessKey ID')
return
}
if (!smsConfig.has_secret && !smsSecretInput.trim()) {
message.warning('请先填写阿里云 AccessKey Secret')
return
}
if (!smsConfig.sign_name?.trim()) {
message.warning('请先填写短信签名')
return
}
if (!smsConfig.login_template_code?.trim()) {
message.warning('请先填写登录验证码模板 Code')
return
}
setTestingSMS(true)
try {
const payload: TestSiteSMSConfigPayload = {
phone,
enabled: true,
provider: smsConfig.provider || 'aliyun',
access_key_id: smsConfig.access_key_id.trim(),
endpoint: smsConfig.endpoint.trim() || 'dysmsapi.aliyuncs.com',
sign_name: smsConfig.sign_name.trim(),
login_template_code: smsConfig.login_template_code.trim(),
}
if (smsSecretInput.trim()) {
payload.access_key_secret = smsSecretInput.trim()
}
await testSiteSMSConfig(siteID, payload)
message.success('测试短信发送成功,请查收手机短信')
setTestCooldown(60)
} catch (err: unknown) {
const msg =
err && typeof err === 'object' && 'response' in err
? (err as { response?: { data?: { message?: string } } }).response?.data?.message
: '测试短信发送失败'
message.error(msg || '测试短信发送失败')
} finally {
setTestingSMS(false)
}
}
async function handleSave() {
if (!siteID) return
setSaving(true)
try {
const payload: SiteBrandingPayload = {
logo_url: logoUrl.trim(),
favicon_url: faviconUrl.trim(),
banners_json: JSON.stringify(banners),
announcements_json: JSON.stringify(announcements),
footer_info_json: JSON.stringify(footerInfo),
}
const updated = await updateSiteBranding(siteID, payload)
setCurrentSite(updated)
message.success('分销站点品牌与装修配置保存成功!')
} catch (error: unknown) {
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
message.error(respMessage || '保存装修配置失败')
} finally {
setSaving(false)
}
}
return (
<Space direction="vertical" className="page" style={{ width: '100%' }} size="middle">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Typography.Title level={3} style={{ marginBottom: 4 }}>
品牌与站点可视化装修
</Typography.Title>
<Typography.Text type="secondary">
可视化配置当前分销独立站的 Logo、首页轮播海报、跑马灯公告及页脚客服信息。
</Typography.Text>
</div>
<Space>
{member?.is_superadmin && sites.length > 1 && (
<Select
placeholder="切换分销网站"
style={{ width: 220 }}
value={siteID}
options={sites.map((site) => ({ label: `${site.site_name} (${site.site_code})`, value: site.id }))}
onChange={(value) => {
setSiteID(value)
void loadSite(value)
}}
/>
)}
{currentSite?.site_code && (
<Button
icon={<EyeOutlined />}
onClick={() => {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
const targetUrl = isLocal
? `http://${window.location.hostname}:5189/?site=${currentSite.site_code}`
: currentSite.domain
? `https://${currentSite.domain}`
: `/?site=${currentSite.site_code}`
window.open(targetUrl, '_blank')
}}
>
预览分销前台
</Button>
)}
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}>
保存全部装修
</Button>
</Space>
</div>
<Card loading={loading}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<Typography.Title level={5} style={{ marginBottom: 4 }}>
本站收号压价
</Typography.Title>
<Typography.Text type="secondary">
压价点数越大,号主在本站拿到的收号价越低。买家售价保持与主站一致,压价差进入三方分成(本站 35% / 卖出方 40% / 平台 25%)。0 表示与主站同价收号。
</Typography.Text>
</div>
<Space wrap align="center">
<Typography.Text>收号价比主站低</Typography.Text>
<InputNumber
min={0}
max={20}
step={0.1}
precision={1}
value={receiveCostDelta}
onChange={(value) => setReceiveCostDelta(Math.max(0, Number(value || 0)))}
addonAfter="点"
style={{ width: 160 }}
/>
<Button type="primary" icon={<SaveOutlined />} loading={savingRatio} onClick={() => void handleSaveReceiveRatio()}>
保存收号压价
</Button>
</Space>
</Space>
</Card>
<Card loading={loading}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<Space align="center">
<MessageOutlined style={{ fontSize: 18, color: '#1677ff' }} />
<Typography.Title level={5} style={{ margin: 0 }}>
独立短信验证码配置
</Typography.Title>
</Space>
<Typography.Paragraph type="secondary" style={{ marginTop: 4, marginBottom: 0 }}>
开启后,本分站访客登录/注册时将调用分站专属的阿里云短信模板与签名,并直接计入本站自己的阿里云短信额度;未开启时自动回退使用主站默认短信通道。
</Typography.Paragraph>
</div>
<Alert
type={smsConfig.enabled ? 'info' : 'warning'}
showIcon
message={
smsConfig.enabled
? '当前已启用独立短信服务:分站登录验证码将消耗您配置的阿里云额度,展示您的专属短信签名。'
: '当前未启用独立短信服务:分站将免费使用主站平台额度发送验证码(展示主站默认签名)。'
}
/>
<Form layout="vertical">
<Row gutter={16}>
<Col xs={24} sm={12}>
<Form.Item label="启用独立短信服务">
<Space>
<Switch
checked={smsConfig.enabled}
onChange={(val) => setSmsConfig({ ...smsConfig, enabled: val })}
/>
<Typography.Text type="secondary">
{smsConfig.enabled ? '已开启(优先使用分站凭据)' : '已停用(回退使用主站额度)'}
</Typography.Text>
</Space>
</Form.Item>
</Col>
<Col xs={24} sm={12}>
<Form.Item label="短信服务商">
<Select
value={smsConfig.provider || 'aliyun'}
onChange={(val) => setSmsConfig({ ...smsConfig, provider: val })}
options={[{ label: '阿里云短信服务 (Aliyun Dysms)', value: 'aliyun' }]}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={12}>
<Form.Item label="短信签名 (SignName)" required={smsConfig.enabled} tooltip="在阿里云短信控制台申请并已通过审核的签名,例如:【XX租号】">
<Input
placeholder="例如:XX租号"
value={smsConfig.sign_name}
onChange={(e) => setSmsConfig({ ...smsConfig, sign_name: e.target.value })}
/>
</Form.Item>
</Col>
<Col xs={24} sm={12}>
<Form.Item label="验证码模板 Code (TemplateCode)" required={smsConfig.enabled} tooltip="在阿里云申请的验证码模板,必须包含 ${code} 变量,例如:SMS_123456789">
<Input
placeholder="例如:SMS_123456789"
value={smsConfig.login_template_code}
onChange={(e) => setSmsConfig({ ...smsConfig, login_template_code: e.target.value })}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={12}>
<Form.Item label="阿里云 AccessKey ID" required={smsConfig.enabled} tooltip="具备 Dysms 权限的阿里云 RAM 用户 AccessKey ID">
<Input
placeholder="例如:LTAI5t..."
value={smsConfig.access_key_id}
onChange={(e) => setSmsConfig({ ...smsConfig, access_key_id: e.target.value })}
/>
</Form.Item>
</Col>
<Col xs={24} sm={12}>
<Form.Item
label="阿里云 AccessKey Secret"
required={smsConfig.enabled && !smsConfig.has_secret}
tooltip={smsConfig.has_secret ? `已保存密钥 (${smsConfig.masked_secret}),如需更换请输入新密钥,留空则保持不变` : '请输入阿里云 AccessKey Secret'}
>
<Input.Password
placeholder={smsConfig.has_secret ? '已设置密钥,留空保持不变' : '请输入 AccessKey Secret'}
value={smsSecretInput}
onChange={(e) => setSmsSecretInput(e.target.value)}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={12}>
<Form.Item label="API Endpoint (可选)" tooltip="默认 dysmsapi.aliyuncs.com">
<Input
placeholder="dysmsapi.aliyuncs.com"
value={smsConfig.endpoint}
onChange={(e) => setSmsConfig({ ...smsConfig, endpoint: e.target.value })}
/>
</Form.Item>
</Col>
<Col xs={24} sm={12}>
<Form.Item label="连通性测试">
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="输入手机号测试接收"
value={testPhone}
onChange={(e) => setTestPhone(e.target.value)}
/>
<Popconfirm
title="确认发送测试短信?"
description="将向填写的手机号发送一条随机验证码,并消耗该站点的阿里云短信额度。"
okText="确认发送"
cancelText="取消"
disabled={testingSMS || testCooldown > 0}
onConfirm={() => void handleTestSMS()}
>
<Button
icon={<SendOutlined />}
loading={testingSMS}
disabled={testCooldown > 0}
>
{testCooldown > 0 ? `重新发送(${testCooldown}s)` : '发送测试'}
</Button>
</Popconfirm>
</Space.Compact>
</Form.Item>
</Col>
</Row>
<Button
type="primary"
icon={<SaveOutlined />}
loading={savingSMS}
onClick={() => void handleSaveSMS()}
>
保存短信配置
</Button>
</Form>
</Space>
</Card>
<Row gutter={24}>
{/* Left Visual Form Customizer */}
<Col xs={24} lg={14}>
<Card loading={loading}>
<Tabs
defaultActiveKey="branding"
items={[
{
key: 'branding',
label: '品牌基础资料',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<Typography.Text strong>站点 Logo 图片链接:</Typography.Text>
<Input
placeholder="https://.../logo.png"
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
allowClear
style={{ marginTop: 6 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
建议尺寸 240x60 像素,透明底色 PNG 或 SVG 格式
</Typography.Text>
</div>
<div>
<Typography.Text strong>浏览器标签页 Favicon 图标:</Typography.Text>
<Input
placeholder="https://.../favicon.ico"
value={faviconUrl}
onChange={(e) => setFaviconUrl(e.target.value)}
allowClear
style={{ marginTop: 6 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
浏览器选项卡显示的小图标,建议 32x32 像素
</Typography.Text>
</div>
</Space>
),
},
{
key: 'banners',
label: `首页轮播横幅 (${banners.length})`,
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography.Text type="secondary">
为分销商城首页配置吸睛的促销横幅(支持自动轮播展示)。
</Typography.Text>
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddBanner}>
添加轮播横幅
</Button>
</div>
{banners.length === 0 ? (
<div style={{ textAlign: 'center', padding: '32px 0', color: '#999' }}>
暂未添加轮播横幅,点击上方按钮新增
</div>
) : (
banners.map((item, idx) => (
<div
key={idx}
style={{
background: '#fbfbfb',
border: '1px solid #e8e8e8',
borderRadius: 8,
padding: '12px 16px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<strong style={{ color: '#1677ff' }}>横幅 #{idx + 1}</strong>
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDeleteBanner(idx)}
>
删除
</Button>
</div>
<Row gutter={12}>
<Col span={6}>
<div style={{ height: 80, background: '#eee', borderRadius: 4, overflow: 'hidden' }}>
{item.image_url ? (
<Image
src={item.image_url}
width="100%"
height={80}
style={{ objectFit: 'cover' }}
/>
) : (
<div style={{ lineHeight: '80px', textAlign: 'center', color: '#aaa', fontSize: 12 }}>无图片</div>
)}
</div>
</Col>
<Col span={18}>
<Space direction="vertical" style={{ width: '100%' }} size="small">
<Input
placeholder="输入图片 URL (https://...)"
value={item.image_url}
onChange={(e) => handleUpdateBanner(idx, 'image_url', e.target.value)}
/>
<Input
placeholder="横幅标题 (例如:特惠专区)"
value={item.title}
onChange={(e) => handleUpdateBanner(idx, 'title', e.target.value)}
/>
<Input
placeholder="跳转链接 (可选)"
value={item.link}
onChange={(e) => handleUpdateBanner(idx, 'link', e.target.value)}
/>
</Space>
</Col>
</Row>
</div>
))
)}
</Space>
),
},
{
key: 'announcements',
label: `跑马灯公告 (${announcements.length})`,
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography.Text type="secondary">
在商城顶部轮播滚动的最新活动或服务保障公告。
</Typography.Text>
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddAnnouncement}>
添加公告
</Button>
</div>
{announcements.length === 0 ? (
<div style={{ textAlign: 'center', padding: '32px 0', color: '#999' }}>
暂无公告,点击上方按钮新增
</div>
) : (
announcements.map((item, idx) => (
<div
key={idx}
style={{
background: '#fbfbfb',
border: '1px solid #e8e8e8',
borderRadius: 8,
padding: '12px 16px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<strong>公告 #{idx + 1}</strong>
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDeleteAnnouncement(idx)}
>
删除
</Button>
</div>
<Space direction="vertical" style={{ width: '100%' }} size="small">
<Input
placeholder="公告标题"
value={item.title}
onChange={(e) => handleUpdateAnnouncement(idx, 'title', e.target.value)}
/>
<Input.TextArea
placeholder="公告详细内容 (可选)"
rows={2}
value={item.content}
onChange={(e) => handleUpdateAnnouncement(idx, 'content', e.target.value)}
/>
</Space>
</div>
))
)}
</Space>
),
},
{
key: 'footer',
label: '页脚与客服备案',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<Typography.Text strong>商城版权文案:</Typography.Text>
<Input
placeholder="例如:© 2026 游戏账号分销商城 版权所有"
value={footerInfo.copyright}
onChange={(e) => setFooterInfo({ ...footerInfo, copyright: e.target.value })}
style={{ marginTop: 6 }}
/>
</div>
<div>
<Typography.Text strong>ICP 备案编号:</Typography.Text>
<Input
placeholder="例如:京ICP备20260001号-1"
value={footerInfo.icp}
onChange={(e) => setFooterInfo({ ...footerInfo, icp: e.target.value })}
style={{ marginTop: 6 }}
/>
</div>
<div>
<Typography.Text strong>官方客服微信 / QQ:</Typography.Text>
<Input
placeholder="例如:客服微信: hfb_kf01 / QQ群: 987654321"
value={footerInfo.contact}
onChange={(e) => setFooterInfo({ ...footerInfo, contact: e.target.value })}
style={{ marginTop: 6 }}
/>
</div>
</Space>
),
},
]}
/>
<Divider style={{ margin: '16px 0' }} />
{/* Advanced Raw JSON Inspector (Collapsed) */}
<Collapse
size="small"
items={[
{
key: 'raw',
label: '开发者底包 JSON 数据(自动同步生成,无需手写)',
children: (
<pre style={{ fontSize: 11, background: '#f5f5f5', padding: 8, borderRadius: 4, maxHeight: 120, overflow: 'auto' }}>
{JSON.stringify(
{
banners,
announcements,
footer_info: footerInfo,
},
null,
2
)}
</pre>
),
},
]}
/>
</Card>
</Col>
{/* Right Live Visual Simulation Card */}
<Col xs={24} lg={10}>
<Card title="分销商城前台实时模拟效果" style={{ background: '#f8fafc' }}>
<div
style={{
background: '#fff',
border: '1px solid #d9d9d9',
borderRadius: 8,
overflow: 'hidden',
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
}}
>
{/* Simulated Browser Bar */}
<div
style={{
background: '#f0f2f5',
padding: '6px 12px',
borderBottom: '1px solid #e8e8e8',
display: 'flex',
alignItems: 'center',
gap: 8,
fontSize: 12,
color: '#666',
}}
>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#ff4d4f' }} />
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#faad14' }} />
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#52c41a' }} />
</div>
<div
style={{
flex: 1,
background: '#fff',
borderRadius: 4,
padding: '2px 8px',
fontSize: 11,
textAlign: 'center',
}}
>
https://{currentSite?.domain || `${currentSite?.site_code || 'demo'}.hfb.com`}
</div>
</div>
{/* Simulated Header Nav */}
<div
style={{
padding: '10px 14px',
borderBottom: '1px solid #f0f0f0',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{logoUrl ? (
<img src={logoUrl} alt="Logo" style={{ height: 26, objectFit: 'contain' }} />
) : (
<strong style={{ fontSize: 14, color: '#1677ff' }}>
{currentSite?.site_name || 'HFB 分销商城'}
</strong>
)}
<Tag color="blue">分销前台</Tag>
</div>
{/* Simulated Announcements Marquee */}
{announcements.length > 0 && (
<div
style={{
background: '#fffbe6',
padding: '4px 12px',
fontSize: 12,
color: '#d46b08',
borderBottom: '1px solid #ffe58f',
display: 'flex',
alignItems: 'center',
gap: 6,
}}
>
<NotificationOutlined />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{announcements[0]?.title}
</span>
</div>
)}
{/* Simulated Banner Carousel */}
<div style={{ height: 140, background: '#eee', position: 'relative', overflow: 'hidden' }}>
{banners.length > 0 && banners[0]?.image_url ? (
<img
src={banners[0].image_url}
alt="Banner"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<div
style={{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#999',
fontSize: 12,
}}
>
暂无轮播横幅,请在左侧添加
</div>
)}
{banners.length > 0 && banners[0]?.title && (
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
background: 'rgba(0,0,0,0.5)',
color: '#fff',
padding: '4px 10px',
fontSize: 12,
}}
>
{banners[0].title}
</div>
)}
</div>
{/* Simulated Account Listings Grid Preview */}
<div style={{ padding: 12 }}>
<Typography.Text strong style={{ fontSize: 13, display: 'block', marginBottom: 8 }}>
精选租号货架
</Typography.Text>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<div style={{ border: '1px solid #f0f0f0', borderRadius: 4, padding: 8, fontSize: 11 }}>
<strong>三角洲全金典藏号</strong>
<div style={{ color: '#cf1322', marginTop: 4, fontWeight: 'bold' }}>¥35.00 /小时</div>
</div>
<div style={{ border: '1px solid #f0f0f0', borderRadius: 4, padding: 8, fontSize: 11 }}>
<strong>无畏契约全套幻化</strong>
<div style={{ color: '#cf1322', marginTop: 4, fontWeight: 'bold' }}>¥28.00 /小时</div>
</div>
</div>
</div>
{/* Simulated Footer */}
<div
style={{
background: '#fafafa',
borderTop: '1px solid #f0f0f0',
padding: '10px 12px',
fontSize: 11,
color: '#8c8c8c',
textAlign: 'center',
}}
>
<div>{footerInfo.copyright || `© 2026 ${currentSite?.site_name || '分销网'}`}</div>
{footerInfo.icp && <div>{footerInfo.icp}</div>}
{footerInfo.contact && <div style={{ color: '#1677ff' }}>{footerInfo.contact}</div>}
</div>
</div>
</Card>
</Col>
</Row>
</Space>
)
}