品牌真正接入全站渲染 + 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:
1 parent
efde5597bd
commit
d4a3742136
7 files changed
+178
-61
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user