From a287c5200031429cecb40a8282651878eb4a5b18 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sun, 14 Jun 2026 19:42:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=88=A0=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E3=80=8C=E7=9B=91=E7=AE=A1=E5=A4=A7=E5=B1=8F=E3=80=8D?= =?UTF-8?q?tab=EF=BC=8C=E6=96=B0=E5=A2=9E=E3=80=8C=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E5=B1=8F=E8=9E=8D=E5=90=88=E3=80=8Dtab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 监管大屏功能(查映射/验真/下架)已被角色工作台·监管片库覆盖,移除该tab及RegulatorConsole - 新增 ScreenFusion.jsx「大小屏融合」tab:四期能力可视化 - 跨域解析网关(C.1/C.2):六段式+集级子标识解析、流通状态、三屏可用 - 扫码验真(B.2):真伪/合规结果卡片,防盗版 - 跨屏权益通兑(D.1):一屏购买→换屏核验通看不重复付费 - api.js: 新增 resolve/scanVerify/purchase/verifyRights - seed_demo.sh: 更新查看入口提示 - 前端 build 通过 --- tcs-iptv/scripts/seed_demo.sh | 3 +- tcs-iptv/web-console/src/App.jsx | 131 +------------ tcs-iptv/web-console/src/ScreenFusion.jsx | 224 ++++++++++++++++++++++ tcs-iptv/web-console/src/api.js | 5 + 4 files changed, 235 insertions(+), 128 deletions(-) create mode 100644 tcs-iptv/web-console/src/ScreenFusion.jsx diff --git a/tcs-iptv/scripts/seed_demo.sh b/tcs-iptv/scripts/seed_demo.sh index d7337fb..52dfbb4 100644 --- a/tcs-iptv/scripts/seed_demo.sh +++ b/tcs-iptv/scripts/seed_demo.sh @@ -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 码体验跨域解析 / 扫码验真 / 跨屏权益。" diff --git a/tcs-iptv/web-console/src/App.jsx b/tcs-iptv/web-console/src/App.jsx index 159b54a..1249cb0 100644 --- a/tcs-iptv/web-console/src/App.jsx +++ b/tcs-iptv/web-console/src/App.jsx @@ -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) => {partyLabel[p] || p} }, - { title: '本方编码', dataIndex: 'party_id' }, - { title: '名称', dataIndex: 'party_name', render: (v) => v || '-' }, - { title: 'CDN 端点', dataIndex: 'cdn_endpoint', render: (v) => v || '-' }, - ] - - return ( - <> - - - setMaCode(e.target.value)} - onPressEnter={doQuery} - /> - - - - - - {mappings && ( - - - - - )} - - {mappings && ( - - i} columns={columns} dataSource={mappings} pagination={false} size="middle" /> - - )} - - - - - setVerifyHash(e.target.value)} - /> - - - {verifyResult && ( - - - {verifyResult.ok && verifyResult.data?.match - ? 匹配(正版过审内容) - : 不匹配(疑似版本替换)} - - {verifyResult.data?.bound_hash || '-'} - {verifyResult.data?.submitted_hash || verifyHash} - {verifyResult.message || '-'} - - )} - - - - ) -} - export default function App() { return ( @@ -149,7 +26,7 @@ export default function App() { items={[ { key: 'desk', label: '角色工作台(多方协作)', children: }, { key: 'flow', label: '全流程演示(一键)', children: }, - { key: 'console', label: '监管大屏', children: }, + { key: 'fusion', label: '大小屏融合(OTT/手机)', children: }, ]} /> diff --git a/tcs-iptv/web-console/src/ScreenFusion.jsx b/tcs-iptv/web-console/src/ScreenFusion.jsx new file mode 100644 index 0000000..00dff12 --- /dev/null +++ b/tcs-iptv/web-console/src/ScreenFusion.jsx @@ -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: }, + ott: { label: 'OTT/智能电视', color: 'geekblue', icon: }, + app: { label: '手机 APP', color: 'green', icon: }, +} + +function ScreenTags({ screens }) { + if (!screens || screens.length === 0) return 暂不可用 + return ( + + {screens.map((s) => ( + + {screenMeta[s]?.label || s} + + ))} + + ) +} + +// ============ 跨域解析网关(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 ( + MA 跨域解析网关 · 同一码三屏统一解析}> + + setMaCode(e.target.value)} onPressEnter={doResolve} /> + + + + {res && ( +
+ + + {res.resolved ? 解析成功 : 未解析/未登记} + + + {res.in_circulation ? 流通中 : {res.status || '不可用'}} + + {res.title || '-'} + {res.issuer || '-'} + + + {p?.valid ? ( + + 国家码 {p.country_code} + 行业 {p.industry_node} + 机构 {p.org_node} + 类目 {p.category} + {p.year} 年 + 序列 {p.sequence} + {p.episode > 0 && 第 {p.episode} 集} + + ) : 结构非法} + + {res.message} + +
+ )} +
+ ) +} + +// ============ 扫码验真(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 ( + 用户扫码验真 · 防盗版}> + + setMaCode(e.target.value)} onPressEnter={doScan} /> + + + + {res && ( + + + }> + {res.authentic ? '真码' : '假码/未登记'} + + {res.compliant ? '合规流通' : '不合规'} + {res.title && 《{res.title}》} + + + {res.message} + + } /> + )} + + ) +} + +// ============ 跨屏权益通兑(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 ( + 跨屏权益通兑 · 一次购买全屏通看}> + + + setMaCode(e.target.value)} placeholder="已发布的 MA 码" /> + setUserHash(e.target.value)} /> + + + +
+ + + + + {buyRes && ( + + 已购买:{screenMeta[buyRes.screen]?.label}({new Date(buyRes.purchased_at).toLocaleString()}) + + )} + + + + + + + + + {verifyRes && ( + verifyRes.entitled + ? ✓ 有权益(通兑):{verifyRes.message} + : ✗ 无权益:{verifyRes.message} + )} + + + + + + 演示:在「IPTV 大屏」购买后,切到「手机 APP」核验,应通兑通看且不重复付费(权益归一到整剧 MA 码)。 + + + + ) +} + +export default function ScreenFusion() { + return ( +
+ + 四期·大小屏融合:同一 MA 码贯通 IPTV / OTT / 手机 APP,统一解析、扫码验真、一次购买全屏通看。 + + + + + + +
+ ) +} diff --git a/tcs-iptv/web-console/src/api.js b/tcs-iptv/web-console/src/api.js index 49f54dc..1735b0e 100644 --- a/tcs-iptv/web-console/src/api.js +++ b/tcs-iptv/web-console/src/api.js @@ -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 }), }