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, Upload, } 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, uploadSiteImage, type Site, type SiteBrandingPayload, type SiteSMSConfig, type UpdateSiteSMSConfigPayload, type TestSiteSMSConfigPayload, } from '../api' import { useMember } from '../MemberContext' import BannerVisualEditor from '../components/BannerVisualEditor' import { HOME_BANNER_MOBILE_RATIO, HOME_BANNER_PC_RATIO, createEmptyDistributionBanner, parseDistributionBanners, serializeDistributionBanners, type DistributionBannerConfig, } from '../homeBanner' interface AnnouncementItem { title: string 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 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 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() { const member = useMember() const [sites, setSites] = useState([]) const [siteID, setSiteID] = useState(member?.site_id) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [currentSite, setCurrentSite] = useState(null) // Visual state for customizer const [logoUrl, setLogoUrl] = useState('') const [faviconUrl, setFaviconUrl] = useState('') const [banners, setBanners] = useState([]) const [announcements, setAnnouncements] = useState([]) const [storefrontText, setStorefrontText] = useState({ ...DEFAULT_STOREFRONT_TEXT, }) const [footerInfo, setFooterInfo] = useState({ copyright: '', icp: '', contact: '', }) const [receiveCostDelta, setReceiveCostDelta] = useState(0) const [awUseMain, setAwUseMain] = useState(true) const [awEntry, setAwEntry] = useState(defaultAWEntry) const [coopUseMain, setCoopUseMain] = useState(true) const [coopEntry, setCoopEntry] = useState(defaultCoopEntry) const [savingRatio, setSavingRatio] = useState(false) // SMS config state const [smsConfig, setSmsConfig] = useState({ 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 || '') setBanners(parseDistributionBanners(found.banners_json, found.site_name)) 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({}) } try { const parsed = found.storefront_text_json ? (JSON.parse(found.storefront_text_json) as Partial) : {} 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) setAwEntry(aw.value) const coop = parseHomeEntry(found.cooperation_feedback_json, defaultCoopEntry) setCoopUseMain(coop.useMain) setCoopEntry(coop.value) 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, { ...createEmptyDistributionBanner(), eyebrow: '首页推荐', }, ]) } function handleUpdateBanner(index: number, patch: Partial) { setBanners((current) => current.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)), ) } function handleMoveBanner(index: number, step: number) { const target = index + step if (target < 0 || target >= banners.length) return const next = [...banners] const [item] = next.splice(index, 1) if (!item) return next.splice(target, 0, item) 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 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 (
继承主站配置
{useMain ? ( 当前展示主站的标题、说明和图片。 ) : ( <> setEntry({ ...entry, title: e.target.value })} /> setEntry({ ...entry, subtitle: e.target.value })} />
前台显示该入口 setEntry({ ...entry, enabled: checked })} />
{entry.image_url ? ( ) : ( 尚未上传图片 )} uploadEntryImage(file, scene, (url) => setEntry({ ...entry, image_url: url }))} > )}
) } /** 全部留空时清除该配置(前台回落默认文案),否则按 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) try { const payload: SiteBrandingPayload = { logo_url: logoUrl.trim(), favicon_url: faviconUrl.trim(), banners_json: serializeDistributionBanners(banners), announcements_json: JSON.stringify(announcements), 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), } 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) } } const previewBanner = banners.find((item) => item.enabled) ?? banners[0] const previewBannerImage = previewBanner?.mobile.image_url || previewBanner?.pc.image_url || '' return (
品牌与站点可视化装修 可视化配置当前分销独立站的 Logo、首页入口、客服二维码、轮播海报和页脚信息。未单独配置的回收与合作入口继续使用主站。
{member?.is_superadmin && sites.length > 1 && ( setSmsConfig({ ...smsConfig, provider: val })} options={[{ label: '阿里云短信服务 (Aliyun Dysms)', value: 'aliyun' }]} /> setSmsConfig({ ...smsConfig, sign_name: e.target.value })} /> setSmsConfig({ ...smsConfig, login_template_code: e.target.value })} /> setSmsConfig({ ...smsConfig, access_key_id: e.target.value })} /> setSmsSecretInput(e.target.value)} /> setSmsConfig({ ...smsConfig, endpoint: e.target.value })} /> setTestPhone(e.target.value)} /> 0} onConfirm={() => void handleTestSMS()} > {/* Left Visual Form Customizer */}
站点 Logo: {logoUrl ? ( ) : ( 尚未上传 Logo )} uploadEntryImage(file, 'avatar', setLogoUrl)} > setLogoUrl(e.target.value)} allowClear /> 建议尺寸 240x60 像素,透明底色 PNG
浏览器标签页 Favicon: {faviconUrl ? ( ) : ( 尚未上传图标 )} uploadEntryImage(file, 'avatar', setFaviconUrl)} > setFaviconUrl(e.target.value)} allowClear /> 浏览器选项卡显示的小图标,建议 32x32 像素
), }, { key: 'banners', label: `首页轮播横幅 (${banners.length})`, children: (
为分销商城首页配置轮播海报,后台坐标为百分比,换分辨率也不会跑位。
{banners.length === 0 ? (
暂未添加轮播横幅,点击上方按钮新增
) : ( banners.map((item, idx) => (
轮播 #{idx + 1} handleUpdateBanner(idx, { enabled })} />
handleUpdateBanner(idx, { eyebrow: e.target.value })} /> handleUpdateBanner(idx, { title: e.target.value })} /> handleUpdateBanner(idx, { badge: e.target.value })} /> handleUpdateBanner(idx, { pill: e.target.value })} /> handleUpdateAnnouncement(idx, 'title', e.target.value)} /> handleUpdateAnnouncement(idx, 'content', e.target.value)} />
)) )}
), }, { key: 'entries', label: '首页入口', children: (
AW炫彩回收
{renderEntryEditor(awUseMain, setAwUseMain, awEntry, setAwEntry, 'aw-recycle')}
合作与反馈
{renderEntryEditor( coopUseMain, setCoopUseMain, coopEntry, setCoopEntry, 'cooperation-feedback', )}
), }, { key: 'storefront_text', label: '首页文案', children: ( 留空则使用默认文案(占位提示即默认值)。这些文案出现在商城头部与首页主视觉,可按本站品牌口径自由填写。
头部品牌区副标题 setStorefrontText({ ...storefrontText, tagline: e.target.value }) } />
首页主视觉徽标 setStorefrontText({ ...storefrontText, hero_badge: e.target.value }) } />
首页主视觉描述 setStorefrontText({ ...storefrontText, hero_desc: e.target.value }) } />
移动端认证卡片副标题 setStorefrontText({ ...storefrontText, card_sub: e.target.value }) } />
), }, { key: 'footer', label: '页脚与客服备案', children: (
商城版权文案: setFooterInfo({ ...footerInfo, copyright: e.target.value })} style={{ marginTop: 6 }} />
ICP 备案编号: setFooterInfo({ ...footerInfo, icp: e.target.value })} style={{ marginTop: 6 }} />
官方客服微信 / QQ: setFooterInfo({ ...footerInfo, contact: e.target.value })} style={{ marginTop: 6 }} />
), }, ]} /> {/* Advanced Raw JSON Inspector (Collapsed) */} {JSON.stringify( { banners, announcements, footer_info: footerInfo, }, null, 2 )} ), }, ]} />
{/* Right Live Visual Simulation Card */}
{/* Simulated Browser Bar */}
https://{currentSite?.domain || `${currentSite?.site_code || 'demo'}.hfb.com`}
{/* Simulated Header Nav */}
{logoUrl ? ( Logo ) : ( {currentSite?.site_name || '解忧分销商城'} {storefrontText.tagline.trim() || DEFAULT_STOREFRONT_TEXT.tagline} )} {storefrontText.hero_badge.trim() || DEFAULT_STOREFRONT_TEXT.hero_badge}
{/* Simulated Announcements Marquee */} {announcements.length > 0 && (
{announcements[0]?.title || announcements[0]?.content}
)} {/* Simulated Banner Carousel */}
{previewBannerImage ? ( Banner ) : previewBanner ? (
{previewBanner.eyebrow && ( {previewBanner.eyebrow} )} {previewBanner.title || '精选游戏账号 · 随租随玩'} {previewBanner.pill && ( {previewBanner.pill} )}
) : (
暂无轮播横幅,请在左侧添加
)} {previewBanner?.badge && ( {previewBanner.badge} )}
{/* Simulated Account Listings Grid Preview */}
精选租号货架
三角洲全金典藏号
¥35.00 /小时
无畏契约全套幻化
¥28.00 /小时
{/* Simulated Footer */}
{footerInfo.copyright || `© 2026 ${currentSite?.site_name || '分销网'}`}
{footerInfo.icp &&
{footerInfo.icp}
} {footerInfo.contact &&
{footerInfo.contact}
}
) }