From 73e22f79d21d2def36ddd01deb9fd380efce618d Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sun, 14 Jun 2026 17:31:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(phase2-fe):=20=E4=BA=8C=E6=9C=9F=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=96(=E5=88=86=E8=B4=A6/=E8=BF=BD=E8=B4=A3/?= =?UTF-8?q?=E7=A1=AE=E6=9D=83/=E6=8E=88=E6=9D=83/=E5=9B=9E=E4=BC=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GovernancePanel: 监管片库详情新增'权益与治理'标签(分账/追责取证/确权举证/授权管理) - 分账面板: 播放聚合统计+CP60/平台34/服务费6分账展示 - 追责面板: 全链路存证Timeline+审播一致/篡改定位结果 - 确权面板: 证据链+谁先锁定谁有权声明 - 授权面板: 登记授权范围(地域/平台/期限)+核验 - 运营商台: 回传播放(含购买)按钮喂分账数据 - 前端build通过, HMR生效 --- tcs-iptv/web-console/src/GovernancePanel.jsx | 146 +++++++++++++++++++ tcs-iptv/web-console/src/RoleDesk.jsx | 19 ++- tcs-iptv/web-console/src/api.js | 9 ++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 tcs-iptv/web-console/src/GovernancePanel.jsx diff --git a/tcs-iptv/web-console/src/GovernancePanel.jsx b/tcs-iptv/web-console/src/GovernancePanel.jsx new file mode 100644 index 0000000..1a938e1 --- /dev/null +++ b/tcs-iptv/web-console/src/GovernancePanel.jsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect } from 'react' +import { + Tabs, Card, Button, Space, Tag, Table, Descriptions, Statistic, Row, Col, + Form, Input, DatePicker, message, Timeline, Typography, Alert, Result, +} from 'antd' +import { api } from './api.js' + +const { Text, Paragraph } = Typography +const yuan = (cent) => '¥' + (cent / 100).toFixed(2) + +const nodeLabel = { + cp_submit: 'CP送审', csps_review: 'CSPS审核', ma_issue: '发码签发', + transcode: '转码', media_ingest: '媒资入库', cdn_inject: 'CDN注入', +} + +// 分账面板 +function SettlementPanel({ maCode }) { + const [sum, setSum] = useState(null) + const [st, setSt] = useState(null) + async function load() { + const s = await api.playbackSummary(maCode) + setSum(s.data?.data) + const r = await api.settlement(maCode, '2026-06') + if (r.ok) setSt(r.data.data) + } + useEffect(() => { load() }, [maCode]) + if (!sum) return null + return ( +
+ + + + + + {st ? ( + + + {yuan(st.total_revenue_cent)} + {st.period} + {yuan(st.cp_share_cent)} + {yuan(st.platform_share_cent)} + {yuan(st.hub_fee_cent)} + {st.data_source} + + + ) : } +
+ ) +} + +// 追责取证面板 +function AccountabilityPanel({ maCode }) { + const [rep, setRep] = useState(null) + useEffect(() => { api.accountability(maCode).then((r) => setRep(r.data?.data)) }, [maCode]) + if (!rep) return null + return ( +
+ {rep.consistent + ? + : } + + ({ + color: rep.first_change && e.node === rep.first_change.node && e.hash_value === rep.first_change.hash_value ? 'red' : 'green', + children: ( + + {nodeLabel[e.node] || e.node}{e.operator} + {e.detail} {e.hash_value ? '· hash=' + e.hash_value : ''} + + ), + }))} /> + +
+ ) +} + +// 确权举证面板 +function EvidencePanel({ maCode }) { + const [ev, setEv] = useState(null) + useEffect(() => { api.evidence(maCode).then((r) => setEv(r.data?.data)) }, [maCode]) + if (!ev) return null + return ( + + + {ev.title} + {ev.ma_code} + {ev.content_hash || '-'} + {ev.issuer} + {ev.chain_anchor} + {ev.first_seen_at} + + + + ) +} + +// 授权管理面板 +function AuthPanel({ maCode }) { + const [form] = Form.useForm() + const [checkResult, setCheckResult] = useState(null) + async function grant() { + const v = await form.validateFields() + const regions = v.regions ? v.regions.split(',').map((s) => s.trim()).filter(Boolean) : [] + const platforms = v.platforms ? v.platforms.split(',').map((s) => s.trim()).filter(Boolean) : [] + const expiry = v.expiry ? v.expiry.toISOString() : '' + const res = await api.authorize(maCode, regions, platforms, expiry) + if (res.ok) message.success('授权已登记') + else message.error(res.data.message) + } + async function check() { + const v = form.getFieldsValue() + const res = await api.authCheck(maCode, v.checkRegion || '', v.checkPlatform || '') + setCheckResult(res.data?.data) + } + return ( +
+ + + + + + + + + + + + + {checkResult && ( + + )} + +
+ ) +} + +export default function GovernancePanel({ maCode }) { + return ( + }, + { key: 'account', label: '⚖️ 追责取证', children: }, + { key: 'evidence', label: '📜 确权举证', children: }, + { key: 'auth', label: '🔑 授权管理', children: }, + ]} /> + ) +} diff --git a/tcs-iptv/web-console/src/RoleDesk.jsx b/tcs-iptv/web-console/src/RoleDesk.jsx index 2ed494e..564aa27 100644 --- a/tcs-iptv/web-console/src/RoleDesk.jsx +++ b/tcs-iptv/web-console/src/RoleDesk.jsx @@ -5,6 +5,7 @@ import { } from 'antd' import { ReloadOutlined, SendOutlined, StopOutlined } from '@ant-design/icons' import { call, api } from './api.js' +import GovernancePanel from './GovernancePanel.jsx' const { Text } = Typography @@ -189,6 +190,16 @@ function OperatorDesk({ tick, onChanged }) { else message.warning('注入校验:' + (res.data.message || '哈希不匹配被拒')) } + async function reportPlay(r) { + // 演示:回传 1 次播放 + 1 次购买(15元) + const res = await api.playback(r.operator_id || 'CT-SX-IPTV', [ + { ma_code: r.ma_code, event_type: 'play' }, + { ma_code: r.ma_code, event_type: 'purchase', revenue_cent: 1500 }, + ]) + if (res.ok) message.success(`已回传播放数据(接收 ${res.data.data.accepted} 条),可在监管片库查看分账`) + else message.error(res.data.message) + } + const cols = [ { title: 'MA 码', dataIndex: 'ma_code', render: (v) => {v} }, { title: '作品', dataIndex: 'title' }, @@ -196,6 +207,7 @@ function OperatorDesk({ tick, onChanged }) { + ) }, ] @@ -366,9 +378,11 @@ function LibraryDesk({ tick, onChanged }) { pagination={{ pageSize: 8 }} locale={{ emptyText: }} /> - setDetail(null)} footer={null} width={760} + setDetail(null)} footer={null} width={820} title={detail ? `片库详情 · ${detail.content.title}` : ''}> {detail && ( + {detail.content.ma_code} @@ -405,6 +419,9 @@ function LibraryDesk({ tick, onChanged }) { ]} /> + ) }, + { key: 'gov', label: '权益与治理', children: }, + ]} /> )} diff --git a/tcs-iptv/web-console/src/api.js b/tcs-iptv/web-console/src/api.js index 481b134..49f54dc 100644 --- a/tcs-iptv/web-console/src/api.js +++ b/tcs-iptv/web-console/src/api.js @@ -65,4 +65,13 @@ export const api = { // 工作队列(多角色工作台) reviews: (role, status) => request(role, 'GET', '/content/reviews?status=' + status), list: (role, status) => request(role, 'GET', '/content/list?status=' + status), + // 二期:分账/追责/确权/授权/跨省/追更/回传 + playback: (platformId, batch) => request('operator', 'POST', '/data/playback', { platform_id: platformId, batch }), + playbackSummary: (maCode) => request('regulator', 'GET', '/data/playback-summary?ma_code=' + encodeURIComponent(maCode)), + settlement: (maCode, period) => request('regulator', 'POST', '/settlement/compute', { ma_code: maCode, period }), + accountability: (maCode) => request('regulator', 'GET', '/content/accountability?ma_code=' + encodeURIComponent(maCode)), + evidence: (maCode) => request('regulator', 'GET', '/content/evidence?ma_code=' + encodeURIComponent(maCode)), + 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 }), }