feat(web): 删除冗余「监管大屏」tab,新增「大小屏融合」tab
- 监管大屏功能(查映射/验真/下架)已被角色工作台·监管片库覆盖,移除该tab及RegulatorConsole - 新增 ScreenFusion.jsx「大小屏融合」tab:四期能力可视化 - 跨域解析网关(C.1/C.2):六段式+集级子标识解析、流通状态、三屏可用 - 扫码验真(B.2):真伪/合规结果卡片,防盗版 - 跨屏权益通兑(D.1):一屏购买→换屏核验通看不重复付费 - api.js: 新增 resolve/scanVerify/purchase/verifyRights - seed_demo.sh: 更新查看入口提示 - 前端 build 通过
This commit is contained in:
@@ -79,4 +79,5 @@ echo ""
|
||||
echo "=== 已生成 MA 码(可复制到监管大屏查询)==="
|
||||
cat /tmp/tcs_demo_macodes.txt
|
||||
echo ""
|
||||
echo "提示:在 http://localhost:5174 输入上述任一 MA 码查询全链路三方映射。"
|
||||
echo "提示:在 http://localhost:5174「角色工作台 → 监管片库」点详情查看全链路三方映射与集级哈希;"
|
||||
echo " 或在「大小屏融合」tab 用上述 MA 码体验跨域解析 / 扫码验真 / 跨屏权益。"
|
||||
|
||||
@@ -1,135 +1,12 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Layout, Typography, Card, Input, Button, Space, Table, Tag,
|
||||
message, Modal, Descriptions, Alert, Row, Col, Statistic, Tabs,
|
||||
} from 'antd'
|
||||
import { SearchOutlined, StopOutlined, CheckCircleOutlined } from '@ant-design/icons'
|
||||
import { api } from './api.js'
|
||||
import React from 'react'
|
||||
import { Layout, Typography, Alert, Tabs } from 'antd'
|
||||
import FlowDemo from './FlowDemo.jsx'
|
||||
import RoleDesk from './RoleDesk.jsx'
|
||||
import ScreenFusion from './ScreenFusion.jsx'
|
||||
|
||||
const { Header, Content } = Layout
|
||||
const { Title, Text } = Typography
|
||||
|
||||
const partyLabel = { cp: '内容提供商', reviewer: '审核和监管部门', operator: '运营商' }
|
||||
const partyColor = { cp: 'green', reviewer: 'blue', operator: 'orange' }
|
||||
|
||||
function RegulatorConsole() {
|
||||
const [maCode, setMaCode] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [mappings, setMappings] = useState(null)
|
||||
const [cdnEndpoints, setCdnEndpoints] = useState([])
|
||||
const [verifyHash, setVerifyHash] = useState('')
|
||||
const [verifyResult, setVerifyResult] = useState(null)
|
||||
|
||||
async function doQuery() {
|
||||
if (!maCode) return message.warning('请输入 MA 码')
|
||||
setLoading(true)
|
||||
try {
|
||||
const { status, data } = await api.mappings(maCode)
|
||||
if (status === 200) {
|
||||
setMappings(data.data.mappings || [])
|
||||
setCdnEndpoints(data.data.cdn_endpoints || [])
|
||||
message.success('查询成功')
|
||||
} else {
|
||||
setMappings(null); setCdnEndpoints([])
|
||||
message.error(data.message || '查询失败')
|
||||
}
|
||||
} catch (e) { message.error('请求失败:' + e.message) }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
async function doVerify() {
|
||||
if (!maCode || !verifyHash) return message.warning('请输入 MA 码与文件哈希')
|
||||
try {
|
||||
const { status, data } = await api.verify(maCode, verifyHash)
|
||||
setVerifyResult({ ok: status === 200, ...data })
|
||||
} catch (e) { message.error('请求失败:' + e.message) }
|
||||
}
|
||||
|
||||
function confirmTakedown() {
|
||||
if (!maCode) return message.warning('请先输入 MA 码')
|
||||
Modal.confirm({
|
||||
title: '违规应急下架',
|
||||
content: `确认对 ${maCode} 执行全网下架?该操作将解析三方编码并秒级同步。`,
|
||||
okText: '确认下架', okType: 'danger', cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const { status, data } = await api.takedown(maCode, '监管大屏手动下架')
|
||||
if (status === 200) {
|
||||
message.success('已下架,受影响 CDN: ' + (data.data.cdn_endpoints || []).join(', '))
|
||||
doQuery()
|
||||
} else {
|
||||
message.error(data.message || '下架失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '角色', dataIndex: 'party', render: (p) => <Tag color={partyColor[p]}>{partyLabel[p] || p}</Tag> },
|
||||
{ title: '本方编码', dataIndex: 'party_id' },
|
||||
{ title: '名称', dataIndex: 'party_name', render: (v) => v || '-' },
|
||||
{ title: 'CDN 端点', dataIndex: 'cdn_endpoint', render: (v) => v || '-' },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="按 MA 码查询全链路" style={{ marginBottom: 16 }}>
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 720 }}>
|
||||
<Input
|
||||
placeholder="如 MA.156.8531.6101/WD/20260000001"
|
||||
value={maCode} onChange={(e) => setMaCode(e.target.value)}
|
||||
onPressEnter={doQuery}
|
||||
/>
|
||||
<Button type="primary" icon={<SearchOutlined />} loading={loading} onClick={doQuery}>
|
||||
查询
|
||||
</Button>
|
||||
<Button danger icon={<StopOutlined />} onClick={confirmTakedown}>
|
||||
应急下架
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Card>
|
||||
|
||||
{mappings && (
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}><Card><Statistic title="三方映射数" value={mappings.length} /></Card></Col>
|
||||
<Col span={6}><Card><Statistic title="CDN 端点数" value={cdnEndpoints.length} /></Card></Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{mappings && (
|
||||
<Card title="三方编码映射" style={{ marginBottom: 16 }}>
|
||||
<Table rowKey={(r, i) => i} columns={columns} dataSource={mappings} pagination={false} size="middle" />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card title="哈希验真">
|
||||
<Space direction="vertical" style={{ width: '100%', maxWidth: 720 }}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="待校验文件哈希(file_sha256)"
|
||||
value={verifyHash} onChange={(e) => setVerifyHash(e.target.value)}
|
||||
/>
|
||||
<Button icon={<CheckCircleOutlined />} onClick={doVerify}>验真</Button>
|
||||
</Space.Compact>
|
||||
{verifyResult && (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="结果">
|
||||
{verifyResult.ok && verifyResult.data?.match
|
||||
? <Tag color="green">匹配(正版过审内容)</Tag>
|
||||
: <Tag color="red">不匹配(疑似版本替换)</Tag>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="绑定哈希">{verifyResult.data?.bound_hash || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="提交哈希">{verifyResult.data?.submitted_hash || verifyHash}</Descriptions.Item>
|
||||
<Descriptions.Item label="消息">{verifyResult.message || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
@@ -149,7 +26,7 @@ export default function App() {
|
||||
items={[
|
||||
{ key: 'desk', label: '角色工作台(多方协作)', children: <RoleDesk /> },
|
||||
{ key: 'flow', label: '全流程演示(一键)', children: <FlowDemo /> },
|
||||
{ key: 'console', label: '监管大屏', children: <RegulatorConsole /> },
|
||||
{ key: 'fusion', label: '大小屏融合(OTT/手机)', children: <ScreenFusion /> },
|
||||
]}
|
||||
/>
|
||||
</Content>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Card, Input, Button, Space, Tag, message, Descriptions, Row, Col,
|
||||
Segmented, Typography, Result, Divider,
|
||||
} from 'antd'
|
||||
import {
|
||||
ScanOutlined, GlobalOutlined, MobileOutlined, DesktopOutlined,
|
||||
ShoppingOutlined, SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { api } from './api.js'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
const screenMeta = {
|
||||
iptv: { label: 'IPTV 大屏', color: 'blue', icon: <DesktopOutlined /> },
|
||||
ott: { label: 'OTT/智能电视', color: 'geekblue', icon: <DesktopOutlined /> },
|
||||
app: { label: '手机 APP', color: 'green', icon: <MobileOutlined /> },
|
||||
}
|
||||
|
||||
function ScreenTags({ screens }) {
|
||||
if (!screens || screens.length === 0) return <Tag>暂不可用</Tag>
|
||||
return (
|
||||
<Space>
|
||||
{screens.map((s) => (
|
||||
<Tag key={s} color={screenMeta[s]?.color} icon={screenMeta[s]?.icon}>
|
||||
{screenMeta[s]?.label || s}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 跨域解析网关(C.1/C.2)============
|
||||
function ResolvePanel() {
|
||||
const [maCode, setMaCode] = useState('')
|
||||
const [res, setRes] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function doResolve() {
|
||||
if (!maCode) return message.warning('请输入 MA 码(支持集级子标识 #E03)')
|
||||
setLoading(true)
|
||||
const r = await api.resolve(maCode.trim())
|
||||
setLoading(false)
|
||||
if (r.ok) setRes(r.data.data)
|
||||
else { setRes(null); message.error(r.data.message || '解析失败') }
|
||||
}
|
||||
|
||||
const p = res?.parsed
|
||||
return (
|
||||
<Card size="small" title={<Space><GlobalOutlined />MA 跨域解析网关 · 同一码三屏统一解析</Space>}>
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 640 }}>
|
||||
<Input placeholder="如 MA.156.8531.6101/WD/20260000021 或 ...#E03"
|
||||
value={maCode} onChange={(e) => setMaCode(e.target.value)} onPressEnter={doResolve} />
|
||||
<Button type="primary" loading={loading} onClick={doResolve}>解析</Button>
|
||||
</Space.Compact>
|
||||
|
||||
{res && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Descriptions bordered size="small" column={2}>
|
||||
<Descriptions.Item label="解析结果">
|
||||
{res.resolved ? <Tag color="green">解析成功</Tag> : <Tag color="red">未解析/未登记</Tag>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="流通状态">
|
||||
{res.in_circulation ? <Tag color="blue">流通中</Tag> : <Tag color="orange">{res.status || '不可用'}</Tag>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="作品">{res.title || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="发证主体">{res.issuer || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="跨屏可用" span={2}><ScreenTags screens={res.screens} /></Descriptions.Item>
|
||||
<Descriptions.Item label="结构解析" span={2}>
|
||||
{p?.valid ? (
|
||||
<Space wrap>
|
||||
<Tag>国家码 {p.country_code}</Tag>
|
||||
<Tag>行业 {p.industry_node}</Tag>
|
||||
<Tag>机构 {p.org_node}</Tag>
|
||||
<Tag color="purple">类目 {p.category}</Tag>
|
||||
<Tag>{p.year} 年</Tag>
|
||||
<Tag>序列 {p.sequence}</Tag>
|
||||
{p.episode > 0 && <Tag color="magenta">第 {p.episode} 集</Tag>}
|
||||
</Space>
|
||||
) : <Tag color="red">结构非法</Tag>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="说明" span={2}><Text type="secondary">{res.message}</Text></Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 扫码验真(B.2)============
|
||||
function ScanVerifyPanel() {
|
||||
const [maCode, setMaCode] = useState('')
|
||||
const [res, setRes] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function doScan() {
|
||||
if (!maCode) return message.warning('请输入/扫描 MA 码')
|
||||
setLoading(true)
|
||||
const r = await api.scanVerify(maCode.trim())
|
||||
setLoading(false)
|
||||
if (r.ok) setRes(r.data.data)
|
||||
else { setRes(null); message.error(r.data.message || '验真失败') }
|
||||
}
|
||||
|
||||
let status = 'info', title = '请扫码验真'
|
||||
if (res) {
|
||||
if (res.authentic && res.compliant) { status = 'success'; title = '正版内容 · 合规流通' }
|
||||
else if (res.authentic && !res.compliant) { status = 'warning'; title = '真码 · 但不合规(已下架/未流通)' }
|
||||
else { status = 'error'; title = '验真失败 · 疑似盗版/伪造' }
|
||||
}
|
||||
|
||||
return (
|
||||
<Card size="small" title={<Space><ScanOutlined />用户扫码验真 · 防盗版</Space>}>
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 640 }}>
|
||||
<Input placeholder="模拟扫码:粘贴 MA 码" value={maCode}
|
||||
onChange={(e) => setMaCode(e.target.value)} onPressEnter={doScan} />
|
||||
<Button type="primary" icon={<ScanOutlined />} loading={loading} onClick={doScan}>扫码验真</Button>
|
||||
</Space.Compact>
|
||||
|
||||
{res && (
|
||||
<Result style={{ paddingTop: 16, paddingBottom: 8 }}
|
||||
status={status} title={title}
|
||||
subTitle={
|
||||
<Space direction="vertical">
|
||||
<Space>
|
||||
<Tag color={res.authentic ? 'green' : 'red'} icon={<SafetyCertificateOutlined />}>
|
||||
{res.authentic ? '真码' : '假码/未登记'}
|
||||
</Tag>
|
||||
<Tag color={res.compliant ? 'blue' : 'orange'}>{res.compliant ? '合规流通' : '不合规'}</Tag>
|
||||
{res.title && <Text>《{res.title}》</Text>}
|
||||
</Space>
|
||||
<ScreenTags screens={res.screens} />
|
||||
<Text type="secondary">{res.message}</Text>
|
||||
</Space>
|
||||
} />
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 跨屏权益通兑(D.1)============
|
||||
function RightsPanel() {
|
||||
const [maCode, setMaCode] = useState('')
|
||||
const [userHash, setUserHash] = useState('user-demo-001')
|
||||
const [buyScreen, setBuyScreen] = useState('iptv')
|
||||
const [verifyScreen, setVerifyScreen] = useState('app')
|
||||
const [buyRes, setBuyRes] = useState(null)
|
||||
const [verifyRes, setVerifyRes] = useState(null)
|
||||
|
||||
async function doBuy() {
|
||||
if (!maCode) return message.warning('请输入 MA 码')
|
||||
const r = await api.purchase(maCode.trim(), userHash, buyScreen)
|
||||
if (r.ok) { setBuyRes(r.data.data); message.success(`已在「${screenMeta[buyScreen].label}」购买`) }
|
||||
else message.error(r.data.message || '购买失败')
|
||||
}
|
||||
async function doVerify() {
|
||||
if (!maCode) return message.warning('请输入 MA 码')
|
||||
const r = await api.verifyRights(maCode.trim(), userHash, verifyScreen)
|
||||
if (r.ok) setVerifyRes(r.data.data)
|
||||
else message.error(r.data.message || '核验失败')
|
||||
}
|
||||
|
||||
const opts = Object.entries(screenMeta).map(([v, m]) => ({ label: m.label, value: v }))
|
||||
|
||||
return (
|
||||
<Card size="small" title={<Space><ShoppingOutlined />跨屏权益通兑 · 一次购买全屏通看</Space>}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space wrap>
|
||||
<Input addonBefore="MA 码" style={{ width: 380 }} value={maCode}
|
||||
onChange={(e) => setMaCode(e.target.value)} placeholder="已发布的 MA 码" />
|
||||
<Input addonBefore="用户" style={{ width: 220 }} value={userHash}
|
||||
onChange={(e) => setUserHash(e.target.value)} />
|
||||
</Space>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="① 购买(任一屏)" type="inner">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Segmented value={buyScreen} onChange={setBuyScreen} options={opts} />
|
||||
<Button type="primary" icon={<ShoppingOutlined />} onClick={doBuy}>购买</Button>
|
||||
{buyRes && (
|
||||
<Text type="success">
|
||||
已购买:{screenMeta[buyRes.screen]?.label}({new Date(buyRes.purchased_at).toLocaleString()})
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="② 换一屏核验权益" type="inner">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Segmented value={verifyScreen} onChange={setVerifyScreen} options={opts} />
|
||||
<Button icon={<SafetyCertificateOutlined />} onClick={doVerify}>核验权益</Button>
|
||||
{verifyRes && (
|
||||
verifyRes.entitled
|
||||
? <Tag color="green" style={{ whiteSpace: 'normal' }}>✓ 有权益(通兑):{verifyRes.message}</Tag>
|
||||
: <Tag color="red" style={{ whiteSpace: 'normal' }}>✗ 无权益:{verifyRes.message}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
演示:在「IPTV 大屏」购买后,切到「手机 APP」核验,应通兑通看且不重复付费(权益归一到整剧 MA 码)。
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ScreenFusion() {
|
||||
return (
|
||||
<div>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 16 }}>
|
||||
四期·大小屏融合:同一 MA 码贯通 IPTV / OTT / 手机 APP,统一解析、扫码验真、一次购买全屏通看。
|
||||
</Paragraph>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<ResolvePanel />
|
||||
<ScanVerifyPanel />
|
||||
<RightsPanel />
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -74,4 +74,9 @@ export const api = {
|
||||
authorize: (maCode, regions, platforms, expiryAt) => request('regulator', 'POST', '/content/authorize', { ma_code: maCode, regions, platforms, expiry_at: expiryAt }),
|
||||
authCheck: (maCode, region, platform) => request('regulator', 'POST', '/content/auth-check', { ma_code: maCode, region, platform }),
|
||||
crossProvince: (maCode, fileHash, province) => request('regulator', 'POST', '/content/cross-province', { ma_code: maCode, file_sha256: fileHash, province }),
|
||||
// 四期:大小屏融合(跨域解析/扫码验真/跨屏权益)
|
||||
resolve: (maCode) => request('regulator', 'GET', '/content/resolve?ma_code=' + encodeURIComponent(maCode)),
|
||||
scanVerify: (maCode) => request('operator', 'POST', '/content/scan-verify', { ma_code: maCode }),
|
||||
purchase: (maCode, userHash, screen) => request('operator', 'POST', '/rights/purchase', { ma_code: maCode, user_hash: userHash, screen }),
|
||||
verifyRights: (maCode, userHash, screen) => request('operator', 'POST', '/rights/verify', { ma_code: maCode, user_hash: userHash, screen }),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user