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:
selfrelease
2026-06-14 19:42:29 +08:00
parent 9db0a8a4d4
commit a287c52000
4 changed files with 235 additions and 128 deletions
+224
View File
@@ -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>
)
}