merge: 二期前端可视化(分账/追责/确权/授权)

This commit is contained in:
selfrelease
2026-06-14 17:32:12 +08:00
3 changed files with 173 additions and 1 deletions
@@ -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 (
<div>
<Row gutter={16} style={{ marginBottom: 12 }}>
<Col span={6}><Card size="small"><Statistic title="总播放" value={sum.total_plays} /></Card></Col>
<Col span={6}><Card size="small"><Statistic title="完播" value={sum.total_complete} /></Card></Col>
<Col span={6}><Card size="small"><Statistic title="总收益" value={yuan(sum.total_revenue_cent || 0)} /></Card></Col>
</Row>
{st ? (
<Card size="small" title="分账结算(依据:链上可信播放数据)">
<Descriptions bordered size="small" column={2}>
<Descriptions.Item label="总收益">{yuan(st.total_revenue_cent)}</Descriptions.Item>
<Descriptions.Item label="结算周期">{st.period}</Descriptions.Item>
<Descriptions.Item label="内容方(CP) 60%"><Text strong>{yuan(st.cp_share_cent)}</Text></Descriptions.Item>
<Descriptions.Item label="平台/运营商 34%">{yuan(st.platform_share_cent)}</Descriptions.Item>
<Descriptions.Item label="运营服务费 6%">{yuan(st.hub_fee_cent)}</Descriptions.Item>
<Descriptions.Item label="数据来源"><Tag color="blue">{st.data_source}</Tag></Descriptions.Item>
</Descriptions>
</Card>
) : <Alert type="info" message="暂无收益数据,可在运营商工作台回传播放/购买事件" />}
</div>
)
}
// 追责取证面板
function AccountabilityPanel({ maCode }) {
const [rep, setRep] = useState(null)
useEffect(() => { api.accountability(maCode).then((r) => setRep(r.data?.data)) }, [maCode])
if (!rep) return null
return (
<div>
{rep.consistent
? <Result status="success" title="审播一致" subTitle={rep.conclusion} style={{ padding: 12 }} />
: <Result status="error" title="检出哈希不一致" subTitle={rep.conclusion} style={{ padding: 12 }} />}
<Card size="small" title={'全链路存证(基准哈希 ' + (rep.baseline_hash || '-') + ''}>
<Timeline items={(rep.trail || []).map((e) => ({
color: rep.first_change && e.node === rep.first_change.node && e.hash_value === rep.first_change.hash_value ? 'red' : 'green',
children: (
<Space direction="vertical" size={0}>
<Space><Tag>{nodeLabel[e.node] || e.node}</Tag><Text strong>{e.operator}</Text></Space>
<Text type="secondary" style={{ fontSize: 12 }}>{e.detail} {e.hash_value ? '· hash=' + e.hash_value : ''}</Text>
</Space>
),
}))} />
</Card>
</div>
)
}
// 确权举证面板
function EvidencePanel({ maCode }) {
const [ev, setEv] = useState(null)
useEffect(() => { api.evidence(maCode).then((r) => setEv(r.data?.data)) }, [maCode])
if (!ev) return null
return (
<Card size="small" title="版权确权证据链">
<Descriptions bordered size="small" column={1}>
<Descriptions.Item label="作品">{ev.title}</Descriptions.Item>
<Descriptions.Item label="MA 码">{ev.ma_code}</Descriptions.Item>
<Descriptions.Item label="内容哈希">{ev.content_hash || '-'}</Descriptions.Item>
<Descriptions.Item label="签发方">{ev.issuer}</Descriptions.Item>
<Descriptions.Item label="链上锚定">{ev.chain_anchor}</Descriptions.Item>
<Descriptions.Item label="最早登记时间">{ev.first_seen_at}</Descriptions.Item>
</Descriptions>
<Alert style={{ marginTop: 12 }} type="success" showIcon message="确权声明" description={ev.statement} />
</Card>
)
}
// 授权管理面板
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 (
<Form form={form} layout="vertical" size="small">
<Card size="small" title="登记授权范围" style={{ marginBottom: 12 }}>
<Form.Item label="授权地域(省码,逗号分隔,空=全国)" name="regions"><Input placeholder="610000,440000" /></Form.Item>
<Form.Item label="授权平台(逗号分隔,空=不限)" name="platforms"><Input placeholder="CT-SX-IPTV,CM-SX-IPTV" /></Form.Item>
<Form.Item label="到期时间(空=长期)" name="expiry"><DatePicker showTime style={{ width: '100%' }} /></Form.Item>
<Button type="primary" onClick={grant}>登记授权</Button>
</Card>
<Card size="small" title="授权核验">
<Space>
<Form.Item name="checkRegion" noStyle><Input placeholder="地域省码" style={{ width: 130 }} /></Form.Item>
<Form.Item name="checkPlatform" noStyle><Input placeholder="平台编码" style={{ width: 150 }} /></Form.Item>
<Button onClick={check}>核验</Button>
</Space>
{checkResult && (
<Alert style={{ marginTop: 12 }} type={checkResult.allowed ? 'success' : 'error'}
message={checkResult.allowed ? '在授权范围内' : '拦截:' + checkResult.reason} />
)}
</Card>
</Form>
)
}
export default function GovernancePanel({ maCode }) {
return (
<Tabs size="small" items={[
{ key: 'settle', label: '💰 分账', children: <SettlementPanel maCode={maCode} /> },
{ key: 'account', label: '⚖️ 追责取证', children: <AccountabilityPanel maCode={maCode} /> },
{ key: 'evidence', label: '📜 确权举证', children: <EvidencePanel maCode={maCode} /> },
{ key: 'auth', label: '🔑 授权管理', children: <AuthPanel maCode={maCode} /> },
]} />
)
}
+18 -1
View File
@@ -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) => <Text code>{v}</Text> },
{ title: '作品', dataIndex: 'title' },
@@ -196,6 +207,7 @@ function OperatorDesk({ tick, onChanged }) {
<Space>
<Button size="small" type="primary" onClick={() => inject(r, false)}>CDN 注入正确</Button>
<Button size="small" danger onClick={() => inject(r, true)}>模拟篡改注入</Button>
<Button size="small" onClick={() => reportPlay(r)}>回传播放(含购买)</Button>
</Space>
) },
]
@@ -366,9 +378,11 @@ function LibraryDesk({ tick, onChanged }) {
pagination={{ pageSize: 8 }}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="片库暂无内容" /> }} />
<Modal open={!!detail} onCancel={() => setDetail(null)} footer={null} width={760}
<Modal open={!!detail} onCancel={() => setDetail(null)} footer={null} width={820}
title={detail ? `片库详情 · ${detail.content.title}` : ''}>
{detail && (
<Tabs size="small" items={[
{ key: 'overview', label: '概览', children: (
<>
<Descriptions bordered size="small" column={1} style={{ marginBottom: 12 }}>
<Descriptions.Item label="MA 码">{detail.content.ma_code}</Descriptions.Item>
@@ -405,6 +419,9 @@ function LibraryDesk({ tick, onChanged }) {
]} />
</Card>
</>
) },
{ key: 'gov', label: '权益与治理', children: <GovernancePanel maCode={detail.content.ma_code} /> },
]} />
)}
</Modal>
</Card>
+9
View File
@@ -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 }),
}