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 (
+
+ )
+}
+
+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 }),
}