feat(distribution): 分站品牌装修可配置首页入口并上传图片
分站可单独配置回收、合作反馈和客服二维码,未配置时继承主站;Logo、图标和轮播支持直接上传。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -127,6 +127,8 @@ export interface Site {
|
||||
banners_json?: string
|
||||
announcements_json?: string
|
||||
footer_info_json?: string
|
||||
aw_recycle_json?: string
|
||||
cooperation_feedback_json?: string
|
||||
listing_qrcode_url?: string
|
||||
receive_ratio_delta?: number
|
||||
receive_cost_delta?: number
|
||||
@@ -646,6 +648,19 @@ export interface SiteBrandingPayload {
|
||||
banners_json?: string
|
||||
announcements_json?: string
|
||||
footer_info_json?: string
|
||||
aw_recycle_json?: string
|
||||
cooperation_feedback_json?: string
|
||||
listing_qrcode_url?: string
|
||||
}
|
||||
|
||||
export async function uploadSiteImage(file: File, scene: string): Promise<string> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
form.append('scene', scene)
|
||||
const resp = await api.post<ApiResponse<{ url: string }>>('/admin/files/upload', form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return extract(resp).url
|
||||
}
|
||||
|
||||
export async function updateSiteBranding(siteID: number, payload: SiteBrandingPayload): Promise<Site> {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Switch,
|
||||
Alert,
|
||||
Popconfirm,
|
||||
Upload,
|
||||
} from 'antd'
|
||||
import {
|
||||
SaveOutlined,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
fetchSiteSMSConfig,
|
||||
updateSiteSMSConfig,
|
||||
testSiteSMSConfig,
|
||||
uploadSiteImage,
|
||||
type Site,
|
||||
type SiteBrandingPayload,
|
||||
type SiteSMSConfig,
|
||||
@@ -66,6 +68,46 @@ interface FooterInfo {
|
||||
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[]>([])
|
||||
@@ -85,6 +127,11 @@ export default function SiteSettings() {
|
||||
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 [listingQrcode, setListingQrcode] = useState('')
|
||||
const [savingRatio, setSavingRatio] = useState(false)
|
||||
|
||||
// SMS config state
|
||||
@@ -141,6 +188,13 @@ export default function SiteSettings() {
|
||||
setFooterInfo({})
|
||||
}
|
||||
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)
|
||||
setListingQrcode(found.listing_qrcode_url || '')
|
||||
|
||||
try {
|
||||
const sms = await fetchSiteSMSConfig(id)
|
||||
@@ -193,7 +247,7 @@ export default function SiteSettings() {
|
||||
setBanners([
|
||||
...banners,
|
||||
{
|
||||
image_url: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=1200&q=80',
|
||||
image_url: '',
|
||||
title: '热门推荐活动',
|
||||
link: '',
|
||||
},
|
||||
@@ -349,6 +403,71 @@ export default function SiteSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!siteID) return
|
||||
setSaving(true)
|
||||
@@ -359,6 +478,9 @@ export default function SiteSettings() {
|
||||
banners_json: JSON.stringify(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),
|
||||
listing_qrcode_url: listingQrcode.trim(),
|
||||
}
|
||||
const updated = await updateSiteBranding(siteID, payload)
|
||||
setCurrentSite(updated)
|
||||
@@ -379,7 +501,7 @@ export default function SiteSettings() {
|
||||
品牌与站点可视化装修
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
可视化配置当前分销独立站的 Logo、首页轮播海报、跑马灯公告及页脚客服信息。
|
||||
可视化配置当前分销独立站的 Logo、首页入口、客服二维码、轮播海报和页脚信息。未单独配置的回收与合作入口继续使用主站。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
@@ -606,28 +728,64 @@ export default function SiteSettings() {
|
||||
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 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 或 SVG 格式
|
||||
建议尺寸 240x60 像素,透明底色 PNG
|
||||
</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 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>
|
||||
@@ -693,8 +851,21 @@ export default function SiteSettings() {
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
<Space>
|
||||
<Upload
|
||||
accept="image/*"
|
||||
showUploadList={false}
|
||||
beforeUpload={(file) =>
|
||||
uploadEntryImage(file, 'home-banner', (url) =>
|
||||
handleUpdateBanner(idx, 'image_url', url),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button icon={<PictureOutlined />}>上传横幅</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
<Input
|
||||
placeholder="输入图片 URL (https://...)"
|
||||
placeholder="或粘贴图片链接 https://..."
|
||||
value={item.image_url}
|
||||
onChange={(e) => handleUpdateBanner(idx, 'image_url', e.target.value)}
|
||||
/>
|
||||
@@ -777,6 +948,59 @@ export default function SiteSettings() {
|
||||
</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>
|
||||
<div>
|
||||
<Typography.Text strong>客服二维码</Typography.Text>
|
||||
<Space direction="vertical" style={{ width: '100%', marginTop: 8 }} size="small">
|
||||
{listingQrcode ? (
|
||||
<Image src={listingQrcode} width={160} style={{ objectFit: 'contain' }} />
|
||||
) : (
|
||||
<Typography.Text type="secondary">未配置,发布群使用主站二维码池。</Typography.Text>
|
||||
)}
|
||||
<Space>
|
||||
<Upload
|
||||
accept="image/*"
|
||||
showUploadList={false}
|
||||
beforeUpload={(file) => uploadEntryImage(file, 'support-qrcode', setListingQrcode)}
|
||||
>
|
||||
<Button icon={<PictureOutlined />}>上传二维码</Button>
|
||||
</Upload>
|
||||
<Button disabled={!listingQrcode} onClick={() => setListingQrcode('')}>
|
||||
恢复主站
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'footer',
|
||||
label: '页脚与客服备案',
|
||||
|
||||
Reference in New Issue
Block a user