品牌真正接入全站渲染 + document.title 同步 + 设置页实时刷新

- BrandContext 全局上下文: 挂载时拉取 GET /api/settings/brand
- HomePage: 品牌名称/标语/Logo 动态渲染, 封面卡片精调
- AdminLayout: 侧栏品牌名称/顶栏标语动态显示
- LoginPage: 登录页品牌 Logo + 名称
- WatchPage: 播放页接入品牌上下文
- Settings: 保存后调用 refresh() 全站即时刷新
- App.tsx: document.title 跟随 brand_name 变化
- 组件分层: AppRoutes + BrandProvider 避免 import 循环
This commit is contained in:
admin committed 2026-08-10 16:28:00 +08:00
1 parent efde5597bd
commit d4a3742136
7 files changed
+178 -61

No files matched your search

+5 -2
View File
@@ -6,6 +6,7 @@ import {
CloudServerOutlined, SettingOutlined, LogoutOutlined,
} from '@ant-design/icons'
import { clearToken } from '../api/client'
import { useBrand } from '../brand/BrandContext'
const { Sider, Content, Header } = Layout
const { Text } = Typography
@@ -19,6 +20,7 @@ const menuItems = [
]
export default function AdminLayout() {
const { brand } = useBrand()
const [collapsed, setCollapsed] = useState(false)
const navigate = useNavigate()
const location = useLocation()
@@ -48,7 +50,7 @@ export default function AdminLayout() {
fontFamily: '"Source Serif 4", "Noto Serif SC", Georgia, serif',
color: '#C2693A',
}}>
{collapsed ? 'SC' : 'StreamCore'}
{collapsed ? brand.brand_name.slice(0, 2) : brand.brand_name}
</Text>
</div>
<Menu
@@ -60,9 +62,10 @@ export default function AdminLayout() {
<Layout>
<Header style={{
background: '#FBF9F6', borderBottom: '1px solid #E8E3D8',
display: 'flex', justifyContent: 'flex-end', alignItems: 'center',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '0 40px',
}}>
<Text type="secondary" style={{ fontSize: 13 }}>{brand.brand_tagline}</Text>
<Button type="text" icon={<LogoutOutlined />} onClick={handleLogout}>
退出登录
</Button>
+8 -2
View File
@@ -3,10 +3,12 @@ import { useNavigate } from 'react-router-dom'
import { Card, Form, Input, Button, Typography, message, Space } from 'antd'
import { LockOutlined } from '@ant-design/icons'
import { login } from '../api/auth'
import { useBrand } from '../brand/BrandContext'
const { Title, Text } = Typography
export default function LoginPage() {
const { brand } = useBrand()
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
@@ -31,12 +33,16 @@ export default function LoginPage() {
justifyContent: 'center',
background: '#F6F3ED',
}}>
<Card style={{ width: 400, borderRadius: 12, boxShadow: '0 8px 32px rgba(20,20,16,0.06)' }}>
<Card style={{ width: 400, borderRadius: 12, boxShadow: '0 12px 40px rgba(20,20,16,0.06)' }}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ textAlign: 'center' }}>
{brand.brand_logo_url && (
<img src={brand.brand_logo_url} alt={brand.brand_name}
style={{ height: 36, marginBottom: 12 }} />
)}
<Title level={2} style={{ marginBottom: 4, color: '#C2693A', fontWeight: 600,
fontFamily: '"Source Serif 4", "Noto Serif SC", Georgia, serif' }}>
StreamCore
{brand.brand_name}
</Title>
<Text type="secondary">管理控制台</Text>
</div>
+34 -21
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { Card, Select, Switch, Form, Input, Button, Typography, message, Divider } from 'antd'
import { Card, Select, Switch, Form, Input, Button, Typography, message } from 'antd'
import api from '../../api/client'
import { useBrand } from '../../brand/BrandContext'
const { Title, Text } = Typography
@@ -12,17 +13,20 @@ const strategies = [
]
export default function SettingsPage() {
const { brand, refresh } = useBrand()
const [strategy, setStrategy] = useState('subnet')
const [multicastEnabled, setMulticastEnabled] = useState(true)
const [multicastAvailable, setMulticastAvailable] = useState(false)
const [saving, setSaving] = useState(false)
const [brandName, setBrandName] = useState('StreamCore')
const [brandTagline, setBrandTagline] = useState('企业级直播平台')
const [brandLogoUrl, setBrandLogoUrl] = useState('')
const [brandName, setBrandName] = useState(brand.brand_name)
const [brandTagline, setBrandTagline] = useState(brand.brand_tagline)
const [brandLogoUrl, setBrandLogoUrl] = useState(brand.brand_logo_url)
const [brandSaving, setBrandSaving] = useState(false)
useEffect(() => { loadSettings(); loadBrand() }, [])
useEffect(() => { loadSettings() }, [])
useEffect(() => { setBrandName(brand.brand_name); setBrandTagline(brand.brand_tagline); setBrandLogoUrl(brand.brand_logo_url) }, [brand])
async function loadSettings() {
try {
@@ -32,30 +36,34 @@ export default function SettingsPage() {
setMulticastAvailable(data.multicast_available)
} catch { /* */ }
}
async function loadBrand() {
try {
const { data } = await api.get('/settings/brand')
setBrandName(data.brand_name || 'StreamCore')
setBrandTagline(data.brand_tagline || '')
setBrandLogoUrl(data.brand_logo_url || '')
} catch { /* */ }
}
async function saveStrategy(val: string) {
setSaving(true)
try { await api.put('/nodes/strategy', { strategy: val, multicast_enabled: multicastEnabled }); setStrategy(val); message.success('策略已更新') }
catch { message.error('更新失败') }
try {
await api.put('/nodes/strategy', { strategy: val, multicast_enabled: multicastEnabled })
setStrategy(val)
message.success('策略已更新')
} catch { message.error('更新失败') }
setSaving(false)
}
async function toggleMulticast(val: boolean) {
setMulticastEnabled(val)
try { await api.put('/nodes/strategy', { strategy, multicast_enabled: val }); message.success('组播设置已更新') }
catch { message.error('更新失败') }
try {
await api.put('/nodes/strategy', { strategy, multicast_enabled: val })
message.success('组播设置已更新')
} catch { message.error('更新失败') }
}
async function saveBrand() {
setBrandSaving(true)
try {
await api.put('/settings/brand', { brand_name: brandName, brand_tagline: brandTagline, brand_logo_url: brandLogoUrl })
await api.put('/settings/brand', {
brand_name: brandName,
brand_tagline: brandTagline,
brand_logo_url: brandLogoUrl,
})
await refresh()
message.success('品牌设置已保存')
} catch { message.error('保存失败') }
setBrandSaving(false)
@@ -82,16 +90,21 @@ export default function SettingsPage() {
<Card title="品牌设置" style={{ borderRadius: 10 }}>
<Form layout="vertical">
<Form.Item label="平台名称">
<Input value={brandName} onChange={e => setBrandName(e.target.value)} placeholder="StreamCore" />
<Input value={brandName} onChange={e => setBrandName(e.target.value)} placeholder="StreamCore" size="large" />
</Form.Item>
<Form.Item label="标语">
<Input value={brandTagline} onChange={e => setBrandTagline(e.target.value)} placeholder="企业级直播平台" />
<Input value={brandTagline} onChange={e => setBrandTagline(e.target.value)} placeholder="企业级直播平台" size="large" />
</Form.Item>
<Form.Item label="Logo 图片地址">
<Input value={brandLogoUrl} onChange={e => setBrandLogoUrl(e.target.value)} placeholder="https://..." />
</Form.Item>
<Form.Item>
<Button type="primary" loading={brandSaving} onClick={saveBrand}>保存品牌设置</Button>
<Button type="primary" loading={brandSaving} onClick={saveBrand} size="large">
保存品牌设置
</Button>
<Text type="secondary" style={{ marginLeft: 12, fontSize: 13 }}>
保存后全站立即生效
</Text>
</Form.Item>
</Form>
</Card>