1417 lines
56 KiB
TypeScript
1417 lines
56 KiB
TypeScript
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<HomeEntryConfig>
|
||
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<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<DistributionBannerConfig[]>([])
|
||
const [announcements, setAnnouncements] = useState<AnnouncementItem[]>([])
|
||
const [storefrontText, setStorefrontText] = useState<StorefrontTextForm>({
|
||
...DEFAULT_STOREFRONT_TEXT,
|
||
})
|
||
const [footerInfo, setFooterInfo] = useState<FooterInfo>({
|
||
copyright: '',
|
||
icp: '',
|
||
contact: '',
|
||
})
|
||
const [receiveCostDelta, setReceiveCostDelta] = useState(0)
|
||
const [awUseMain, setAwUseMain] = useState(true)
|
||
const [awEntry, setAwEntry] = useState<HomeEntryConfig>(defaultAWEntry)
|
||
const [coopUseMain, setCoopUseMain] = useState(true)
|
||
const [coopEntry, setCoopEntry] = useState<HomeEntryConfig>(defaultCoopEntry)
|
||
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 || '')
|
||
|
||
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<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)
|
||
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<DistributionBannerConfig>) {
|
||
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 (
|
||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<Typography.Text>继承主站配置</Typography.Text>
|
||
<Switch checked={useMain} onChange={setUseMain} />
|
||
</div>
|
||
{useMain ? (
|
||
<Typography.Text type="secondary">当前展示主站的标题、说明和图片。</Typography.Text>
|
||
) : (
|
||
<>
|
||
<Input
|
||
placeholder="标题"
|
||
value={entry.title}
|
||
onChange={(e) => setEntry({ ...entry, title: e.target.value })}
|
||
/>
|
||
<Input
|
||
placeholder="副标题"
|
||
value={entry.subtitle}
|
||
onChange={(e) => setEntry({ ...entry, subtitle: e.target.value })}
|
||
/>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<Typography.Text>前台显示该入口</Typography.Text>
|
||
<Switch checked={entry.enabled} onChange={(checked) => setEntry({ ...entry, enabled: checked })} />
|
||
</div>
|
||
{entry.image_url ? (
|
||
<Image src={entry.image_url} width={160} style={{ objectFit: 'contain' }} />
|
||
) : (
|
||
<Typography.Text type="secondary">尚未上传图片</Typography.Text>
|
||
)}
|
||
<Space>
|
||
<Upload
|
||
accept="image/*"
|
||
showUploadList={false}
|
||
beforeUpload={(file) => uploadEntryImage(file, scene, (url) => setEntry({ ...entry, image_url: url }))}
|
||
>
|
||
<Button icon={<PictureOutlined />}>上传图片</Button>
|
||
</Upload>
|
||
<Button disabled={!entry.image_url} onClick={() => setEntry({ ...entry, image_url: '' })}>
|
||
清除图片
|
||
</Button>
|
||
</Space>
|
||
</>
|
||
)}
|
||
</Space>
|
||
)
|
||
}
|
||
|
||
/** 全部留空时清除该配置(前台回落默认文案),否则按 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 (
|
||
<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: '#1e4d42' }} />
|
||
<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>
|
||
<Space direction="vertical" style={{ width: '100%', marginTop: 6 }} size="small">
|
||
{logoUrl ? (
|
||
<Image src={logoUrl} height={48} style={{ objectFit: 'contain' }} />
|
||
) : (
|
||
<Typography.Text type="secondary">尚未上传 Logo</Typography.Text>
|
||
)}
|
||
<Space>
|
||
<Upload
|
||
accept="image/*"
|
||
showUploadList={false}
|
||
beforeUpload={(file) => uploadEntryImage(file, 'avatar', setLogoUrl)}
|
||
>
|
||
<Button icon={<PictureOutlined />}>上传 Logo</Button>
|
||
</Upload>
|
||
<Button disabled={!logoUrl} onClick={() => setLogoUrl('')}>
|
||
清除
|
||
</Button>
|
||
</Space>
|
||
<Input
|
||
placeholder="或粘贴图片链接 https://.../logo.png"
|
||
value={logoUrl}
|
||
onChange={(e) => setLogoUrl(e.target.value)}
|
||
allowClear
|
||
/>
|
||
</Space>
|
||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
|
||
建议尺寸 240x60 像素,透明底色 PNG
|
||
</Typography.Text>
|
||
</div>
|
||
|
||
<div>
|
||
<Typography.Text strong>浏览器标签页 Favicon:</Typography.Text>
|
||
<Space direction="vertical" style={{ width: '100%', marginTop: 6 }} size="small">
|
||
{faviconUrl ? (
|
||
<Image src={faviconUrl} width={32} height={32} style={{ objectFit: 'contain' }} />
|
||
) : (
|
||
<Typography.Text type="secondary">尚未上传图标</Typography.Text>
|
||
)}
|
||
<Space>
|
||
<Upload
|
||
accept="image/*"
|
||
showUploadList={false}
|
||
beforeUpload={(file) => uploadEntryImage(file, 'avatar', setFaviconUrl)}
|
||
>
|
||
<Button icon={<PictureOutlined />}>上传图标</Button>
|
||
</Upload>
|
||
<Button disabled={!faviconUrl} onClick={() => setFaviconUrl('')}>
|
||
清除
|
||
</Button>
|
||
</Space>
|
||
<Input
|
||
placeholder="或粘贴图片链接 https://.../favicon.png"
|
||
value={faviconUrl}
|
||
onChange={(e) => setFaviconUrl(e.target.value)}
|
||
allowClear
|
||
/>
|
||
</Space>
|
||
<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">
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="PC 与移动端分开配置"
|
||
description="出图规范:PC 端 1600×400(4:1)、移动端 750×375(2:1),两端容器严格等比。每端可单独上传图片、配置整图跳转和热区;某一端留空时,该端前台回退为文字卡片。"
|
||
/>
|
||
<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: '#ffffff',
|
||
border: '1px solid #eef1f5',
|
||
borderRadius: 10,
|
||
padding: '12px 14px',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
marginBottom: 10,
|
||
}}
|
||
>
|
||
<Space>
|
||
<strong style={{ color: '#ff6a00' }}>轮播 #{idx + 1}</strong>
|
||
<Switch
|
||
size="small"
|
||
checked={item.enabled}
|
||
checkedChildren="启用"
|
||
unCheckedChildren="停用"
|
||
onChange={(enabled) => handleUpdateBanner(idx, { enabled })}
|
||
/>
|
||
</Space>
|
||
<Space size={4}>
|
||
<Button
|
||
size="small"
|
||
disabled={idx === 0}
|
||
onClick={() => handleMoveBanner(idx, -1)}
|
||
>
|
||
上移
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
disabled={idx === banners.length - 1}
|
||
onClick={() => handleMoveBanner(idx, 1)}
|
||
>
|
||
下移
|
||
</Button>
|
||
<Button
|
||
type="text"
|
||
danger
|
||
size="small"
|
||
icon={<DeleteOutlined />}
|
||
onClick={() => handleDeleteBanner(idx)}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
|
||
<Row gutter={[8, 8]} style={{ marginBottom: 12 }}>
|
||
<Col xs={24} sm={12}>
|
||
<Input
|
||
placeholder="眉标,如 三角洲行动账号专区"
|
||
value={item.eyebrow}
|
||
onChange={(e) => handleUpdateBanner(idx, { eyebrow: e.target.value })}
|
||
/>
|
||
</Col>
|
||
<Col xs={24} sm={12}>
|
||
<Input
|
||
placeholder="主标题(无图时展示)"
|
||
value={item.title}
|
||
onChange={(e) => handleUpdateBanner(idx, { title: e.target.value })}
|
||
/>
|
||
</Col>
|
||
<Col xs={12} sm={6}>
|
||
<Input
|
||
placeholder="角标,如 HOT"
|
||
value={item.badge}
|
||
onChange={(e) => handleUpdateBanner(idx, { badge: e.target.value })}
|
||
/>
|
||
</Col>
|
||
<Col xs={12} sm={10}>
|
||
<Input
|
||
placeholder="胶囊文案(可选)"
|
||
value={item.pill}
|
||
onChange={(e) => handleUpdateBanner(idx, { pill: e.target.value })}
|
||
/>
|
||
</Col>
|
||
<Col xs={24} sm={8}>
|
||
<Select
|
||
style={{ width: '100%' }}
|
||
value={item.tone}
|
||
onChange={(tone) => handleUpdateBanner(idx, { tone })}
|
||
options={[
|
||
{ label: '金色', value: 'gold' },
|
||
{ label: '蓝色', value: 'blue' },
|
||
{ label: '绿色', value: 'green' },
|
||
{ label: '橙色', value: 'orange' },
|
||
]}
|
||
/>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Tabs
|
||
size="small"
|
||
items={[
|
||
{
|
||
key: 'pc',
|
||
label: 'PC 端 1600×400(4:1)',
|
||
children: (
|
||
<BannerVisualEditor
|
||
value={item.pc}
|
||
onChange={(pc) => handleUpdateBanner(idx, { pc })}
|
||
ratio={HOME_BANNER_PC_RATIO}
|
||
label="PC 端展示"
|
||
standard="1600×400(4:1)"
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
key: 'mobile',
|
||
label: '移动端 750×375(2:1)',
|
||
children: (
|
||
<BannerVisualEditor
|
||
value={item.mobile}
|
||
onChange={(mobile) => handleUpdateBanner(idx, { mobile })}
|
||
ratio={HOME_BANNER_MOBILE_RATIO}
|
||
label="移动端展示"
|
||
standard="750×375(2:1)"
|
||
/>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</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: 'entries',
|
||
label: '首页入口',
|
||
children: (
|
||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="回收和合作入口默认继承主站。关掉继承后,只影响当前分销站首页。发布群进群码由主站在分站二维码池维护。"
|
||
/>
|
||
<div>
|
||
<Typography.Text strong>AW炫彩回收</Typography.Text>
|
||
<div style={{ marginTop: 8 }}>
|
||
{renderEntryEditor(awUseMain, setAwUseMain, awEntry, setAwEntry, 'aw-recycle')}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Typography.Text strong>合作与反馈</Typography.Text>
|
||
<div style={{ marginTop: 8 }}>
|
||
{renderEntryEditor(
|
||
coopUseMain,
|
||
setCoopUseMain,
|
||
coopEntry,
|
||
setCoopEntry,
|
||
'cooperation-feedback',
|
||
)}
|
||
</div>
|
||
</div>
|
||
</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: '页脚与客服备案',
|
||
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' }} />
|
||
) : (
|
||
<span style={{ display: 'flex', flexDirection: 'column' }}>
|
||
<strong style={{ fontSize: 14, color: '#1e4d42' }}>
|
||
{currentSite?.site_name || '解忧分销商城'}
|
||
</strong>
|
||
<small style={{ fontSize: 10, color: '#94a3b8' }}>
|
||
{storefrontText.tagline.trim() || DEFAULT_STOREFRONT_TEXT.tagline}
|
||
</small>
|
||
</span>
|
||
)}
|
||
<Tag color="blue">
|
||
{storefrontText.hero_badge.trim() || DEFAULT_STOREFRONT_TEXT.hero_badge}
|
||
</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 || announcements[0]?.content}
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Simulated Banner Carousel */}
|
||
<div
|
||
style={{
|
||
aspectRatio: '2 / 1',
|
||
background: 'repeating-conic-gradient(#f1f5f9 0% 25%, #ffffff 0% 50%) 50% / 16px 16px',
|
||
position: 'relative',
|
||
overflow: 'hidden',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
{previewBannerImage ? (
|
||
<img
|
||
src={previewBannerImage}
|
||
alt="Banner"
|
||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||
/>
|
||
) : previewBanner ? (
|
||
<div
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
padding: 16,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'center',
|
||
background: 'linear-gradient(135deg, #1e293b, #334155)',
|
||
color: '#fff',
|
||
}}
|
||
>
|
||
{previewBanner.eyebrow && (
|
||
<span style={{ fontSize: 11, opacity: 0.8 }}>{previewBanner.eyebrow}</span>
|
||
)}
|
||
<strong style={{ fontSize: 16, marginTop: 4 }}>
|
||
{previewBanner.title || '精选游戏账号 · 随租随玩'}
|
||
</strong>
|
||
{previewBanner.pill && (
|
||
<span style={{ fontSize: 11, marginTop: 6, color: '#fbbf24' }}>
|
||
{previewBanner.pill}
|
||
</span>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div style={{ color: '#999', fontSize: 12 }}>暂无轮播横幅,请在左侧添加</div>
|
||
)}
|
||
{previewBanner?.badge && (
|
||
<span
|
||
style={{
|
||
position: 'absolute',
|
||
top: 8,
|
||
right: 8,
|
||
padding: '1px 6px',
|
||
borderRadius: 4,
|
||
background: 'rgba(15, 23, 42, 0.72)',
|
||
color: '#fff',
|
||
fontSize: 10,
|
||
}}
|
||
>
|
||
{previewBanner.badge}
|
||
</span>
|
||
)}
|
||
</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: '#1e4d42' }}>{footerInfo.contact}</div>}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</Space>
|
||
)
|
||
}
|