feat(distribution): 重构分销后台为SaaS商户工作台并全面商业化优化

- 商品货架 (Accounts.tsx): 支持期望售价/固定金额/百分比多模式实时联动加价与批量调价,高亮100%独享溢价
- 订单中心 (Orders.tsx): 透传账号游戏租客信息,新增收益对账抽屉透明拆解底价、手续费、分成与自主溢价
- 结算与规则 (Settlements.tsx & Rules.tsx): 纠正角色倒错,商户视角展现周期对账单与分成合作政策
- 店铺装修 (SiteSettings.tsx): 废除手写JSON,提供横幅/公告/页脚可视化列表编辑器与前台实时模拟器
- 数据大盘 (Dashboard.tsx): 对齐最新订单字段,大字展示分销商净收益
- 后端数据层 (dto.go, repository.go, handler.go): 聚合计算分销商订单收益,支持商品与租客搜索
This commit is contained in:
yml2213
2026-09-09 14:44:02 +08:00
parent aa466051ac
commit f6ad7c34ee
12 changed files with 2257 additions and 465 deletions
+504 -155
View File
@@ -12,14 +12,20 @@ import {
Col,
Divider,
Image,
Alert,
Tag,
Tabs,
Collapse,
} from 'antd'
import {
SaveOutlined,
PictureOutlined,
NotificationOutlined,
EyeOutlined,
PlusOutlined,
DeleteOutlined,
InfoCircleOutlined,
SkinOutlined,
GlobalOutlined,
} from '@ant-design/icons'
import {
listSites,
@@ -30,6 +36,23 @@ import {
} 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[]>([])
@@ -38,7 +61,16 @@ export default function SiteSettings() {
const [saving, setSaving] = useState(false)
const [currentSite, setCurrentSite] = useState<Site | null>(null)
const [form] = Form.useForm()
// 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 loadSite = useCallback(async (id: number) => {
setLoading(true)
@@ -48,20 +80,33 @@ export default function SiteSettings() {
const found = rows.find((s) => s.id === id)
if (found) {
setCurrentSite(found)
form.setFieldsValue({
logo_url: found.logo_url || '',
favicon_url: found.favicon_url || '',
banners_json: found.banners_json || '[]',
announcements_json: found.announcements_json || '[]',
footer_info_json: found.footer_info_json || '{}',
})
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({})
}
}
} catch {
message.error('加载站点设置失败')
} finally {
setLoading(false)
}
}, [form])
}, [])
useEffect(() => {
async function init() {
@@ -95,21 +140,59 @@ export default function SiteSettings() {
void init()
}, [member, loadSite])
async function handleSave(values: SiteBrandingPayload) {
if (!siteID) return
// Validate JSON formatting
try {
if (values.banners_json) JSON.parse(values.banners_json)
if (values.announcements_json) JSON.parse(values.announcements_json)
if (values.footer_info_json) JSON.parse(values.footer_info_json)
} catch {
message.error('轮播图、公告或页脚配置的 JSON 格式不正确,请检查!')
return
}
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 handleSave() {
if (!siteID) return
setSaving(true)
try {
const updated = await updateSiteBranding(siteID, values)
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) {
@@ -120,39 +203,21 @@ export default function SiteSettings() {
}
}
const logoUrl = Form.useWatch('logo_url', form)
const bannersJsonStr = Form.useWatch('banners_json', form)
const announcementsJsonStr = Form.useWatch('announcements_json', form)
let banners: { image_url: string; title?: string }[] = []
try {
if (bannersJsonStr) banners = JSON.parse(bannersJsonStr)
} catch {
// ignore json error in preview
}
let announcements: { title: string; content?: string }[] = []
try {
if (announcementsJsonStr) announcements = JSON.parse(announcementsJsonStr)
} catch {
// ignore
}
return (
<Space direction="vertical" className="page" style={{ width: '100%' }}>
<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、浏览器图标、轮播横幅、站点公告及页脚备案信息。
可视化配置当前分销独立站的 Logo、首页轮播海报、跑马灯公告及页脚客服信息。
</Typography.Text>
</div>
<Space>
{member?.is_superadmin && sites.length > 1 && (
<Select
placeholder="分销网站"
placeholder="切换分销网站"
style={{ width: 220 }}
value={siteID}
options={sites.map((site) => ({ label: `${site.site_name} (${site.site_code})`, value: site.id }))}
@@ -166,148 +231,432 @@ export default function SiteSettings() {
<Button
icon={<EyeOutlined />}
onClick={() => {
window.open(`http://${window.location.hostname}:5189/?site=${currentSite.site_code}`, '_blank')
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>
<Row gutter={24}>
<Col span={14}>
<Card title="装修配置表单" loading={loading}>
<Form form={form} layout="vertical" onFinish={handleSave}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="站点 Logo 图片地址"
name="logo_url"
extra="支持 PNG / SVG / JPG 格式的公开图片链接"
>
<Input placeholder="https://.../logo.png" allowClear />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="浏览器 Favicon 图标地址"
name="favicon_url"
extra="浏览器标签页小图标 (ICO / PNG)"
>
<Input placeholder="https://.../favicon.ico" allowClear />
</Form.Item>
</Col>
</Row>
{/* 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>
<Divider style={{ margin: '12px 0' }} />
<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>
<Form.Item
label={
<Space>
<PictureOutlined />
<span>首页轮播图配置 (Banners JSON)</span>
</Space>
}
name="banners_json"
extra='格式:[{"image_url": "图片URL", "title": "标题", "link": "点击跳转链接"}]'
>
<Input.TextArea rows={4} placeholder='[{"image_url":"https://.../banner1.jpg","title":"暑期特惠"}]' />
</Form.Item>
{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 orientation="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>
<Form.Item
label={
<Space>
<NotificationOutlined />
<span>站点跑马灯公告 (Announcements JSON)</span>
</Space>
}
name="announcements_json"
extra='格式:[{"title": "公告标题", "content": "详细内容"}]'
>
<Input.TextArea rows={3} placeholder='[{"title":"本站三角洲账号货源充足,上号快!"}]' />
</Form.Item>
{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>
<Form.Item
label="页脚信息配置 (Footer Info JSON)"
name="footer_info_json"
extra='格式:{"copyright": "© 2026 分销站点", "icp": "京ICP备XXXXX号", "contact": "客服QQ: 123456"}'
>
<Input.TextArea rows={3} placeholder='{"copyright":"© 2026 演示分销网","icp":"浙ICP备000000号"}' />
</Form.Item>
<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>
<Form.Item style={{ marginBottom: 0 }}>
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving} size="large">
保存装修配置
</Button>
</Form.Item>
</Form>
<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>
<Col span={10}>
<Card title="效果实时预览" style={{ background: '#f5f7fa' }}>
<div style={{ background: '#fff', borderRadius: 8, padding: 16, border: '1px solid #e8e8e8' }}>
{/* Header Preview */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: '1px solid #eee', paddingBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{logoUrl ? (
<Image src={logoUrl} height={28} preview={false} style={{ objectFit: 'contain' }} fallback="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='28'%3E%3Ctext x='10' y='20' fill='%23999'%3ELogo 加载中%3C/text%3E%3C/svg%3E" />
) : (
<Typography.Text strong style={{ fontSize: 16, color: '#1677ff' }}>
{currentSite?.site_name || '分销站点名称'}
</Typography.Text>
)}
{/* 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>
<Tag color="cyan" style={{ margin: 0 }}>分销前台顶栏</Tag>
</div>
{/* Announcement Preview */}
{/* 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={{ marginTop: 12 }}>
<Alert
message={
<Space>
<NotificationOutlined style={{ color: '#fa8c16' }} />
<span>{announcements[0].title}</span>
</Space>
}
type="warning"
banner
style={{ borderRadius: 4, padding: '4px 12px' }}
/>
<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>
)}
{/* Banner Preview */}
<div style={{ marginTop: 12 }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>轮播图预览:</div>
{banners.length > 0 && banners[0].image_url ? (
<div style={{ width: '100%', height: 120, borderRadius: 6, overflow: 'hidden', background: '#000' }}>
<img
src={banners[0].image_url}
alt="Banner"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={(e) => {
(e.target as HTMLImageElement).src = 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=600&auto=format&fit=crop&q=60'
}}
/>
</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={{ width: '100%', height: 90, background: '#f0f2f5', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999', fontSize: 12 }}>
暂无轮播图配置
<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>
{/* Content Placeholder */}
<div style={{ marginTop: 12, padding: 12, background: '#fafafa', borderRadius: 4, textAlign: 'center', color: '#999', fontSize: 12 }}>
[ 游戏商品列表展示区域 ]
{/* 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>
{/* Footer Preview */}
<div style={{ marginTop: 16, borderTop: '1px solid #eee', paddingTop: 12, textAlign: 'center', fontSize: 11, color: '#999' }}>
<div>{currentSite?.site_name || '分销站点'} · 技术支持由 HFB 平台提供</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>