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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user