import React, { useState, useEffect } from 'react' import { Card, Tabs, Table, Button, Space, Tag, Form, Input, Select, message, Typography, Empty, Badge, Modal, Descriptions, Segmented, } from 'antd' import { ReloadOutlined, SendOutlined, StopOutlined } from '@ant-design/icons' import { call, api } from './api.js' import GovernancePanel from './GovernancePanel.jsx' const { Text } = Typography const statusMeta = { approved: { label: '待入库', color: 'green' }, in_library: { label: '在库待发布', color: 'cyan' }, published: { label: '流通中', color: 'blue' }, revoked: { label: '已下架', color: 'red' }, } const catLabel = { WD: '微短剧', WJ: '网络剧', DY: '网络电影', DH: '网络动画' } // 各角色工作台共享的刷新钩子:通过 bump 触发全局重拉 function useTick() { const [tick, setTick] = useState(0) return [tick, () => setTick((t) => t + 1)] } // ============ CP 工作台 ============ function CPDesk({ onChanged }) { const [form] = Form.useForm() const [submitting, setSubmitting] = useState(false) async function submit() { const v = await form.validateFields() setSubmitting(true) const fh = 'fh-' + Date.now().toString(36) const epCount = Number(v.episodes) || 1 const episodes = [] for (let i = 1; i <= epCount; i++) { episodes.push({ episode: i, file_sha256: `${fh}-E${i}`, merkle_root: `mr-${fh}-E${i}` }) } const res = await call('cp', 'POST', '/content/register', { title: v.title, episode_count: epCount, category: v.category, file_sha256: fh, merkle_root: 'mr-' + fh, perceptual_hash: 'ph-' + fh, episodes, cp_media_id: v.cpId, cp_name: v.cpName, }) setSubmitting(false) if (res.ok) { message.success(`送审成功,流水号 ${res.data.data.review_id}(原片走审核渠道,哈希上链)`) onChanged() } else message.error(res.data.message || '送审失败') } return (
送审后到「审核监管工作台」的"待审队列"领取审核。
) } // ============ 审核监管工作台 ============ function ReviewerDesk({ tick, onChanged }) { const [pending, setPending] = useState([]) const [toIssue, setToIssue] = useState([]) const [toIngest, setToIngest] = useState([]) const [toPublish, setToPublish] = useState([]) async function load() { const [p, i, ing, pub] = await Promise.all([ call('reviewer', 'GET', '/content/reviews?status=pending'), call('regulator', 'GET', '/content/reviews?status=approved'), call('reviewer', 'GET', '/content/list?status=approved'), call('reviewer', 'GET', '/content/list?status=in_library'), ]) setPending(p.data?.data?.reviews || []) setToIssue(i.data?.data?.reviews || []) setToIngest(ing.data?.data?.contents || []) setToPublish(pub.data?.data?.contents || []) } useEffect(() => { load() }, [tick]) async function review(reviewID, approved) { const res = await call('reviewer', 'POST', '/content/csps-result', { review_id: reviewID, approved, reviewer_id: 'sxiptv-审核01' }) if (res.ok) { message.success(approved ? 'CSPS 审核通过' : '已驳回'); onChanged() } else message.error(res.data.message) } async function issue(reviewID) { const res = await call('regulator', 'POST', '/content/issue', { review_id: reviewID, issuer: '陕西IPTV运营公司' }) if (res.ok) { message.success(`发码成功:${res.data.data.ma_code}`); onChanged() } else message.error(res.data.message) } async function ingest(r) { const res = await call('reviewer', 'POST', '/content/ingest', { ma_code: r.ma_code, content_twin_id: r.content_twin_id, media_asset_id: 'SXMEDIA-' + r.ma_code.slice(-4), lib_name: '陕西IPTV媒体资源库' }) if (res.ok) { message.success('已入媒资库'); onChanged() } else message.error(res.data.message) } async function publish(r) { const cert = `CERT|${r.ma_code}|x|x` // 演示证书;真实证书在发码时返回 const res = await call('reviewer', 'POST', '/content/publish', { ma_code: r.ma_code, certificate: cert }) if (res.ok) { message.success('已发布给运营商'); onChanged() } else message.error(res.data.message + '(提示:发布需发码时的原始证书,演示可用全流程页)') } const reviewCols = [ { title: '流水号', dataIndex: 'review_id' }, { title: '作品', dataIndex: 'title' }, { title: '类目', dataIndex: 'category', render: (c) => {catLabel[c] || c} }, { title: 'CP', dataIndex: 'cp_name', ellipsis: true }, { title: '操作', render: (_, r) => ( ) }, ] const issueCols = [ { title: '流水号', dataIndex: 'review_id' }, { title: '作品', dataIndex: 'title' }, { title: '类目', dataIndex: 'category', render: (c) => {catLabel[c] || c} }, { title: '操作', render: (_, r) => }, ] const contentCols = (action, label, color) => [ { title: 'MA 码', dataIndex: 'ma_code', render: (v) => {v} }, { title: '作品', dataIndex: 'title' }, { title: '状态', dataIndex: 'status', render: (s) => {s} }, { title: '操作', render: (_, r) => }, ] const queue = (title, count, table) => ( {title}} style={{ marginBottom: 12 }}> {table} ) return (
{queue('① 待审队列(CSPS 合规审核)', pending.length, }} />)} {queue('② 待发码队列(审核通过 → 发码)', toIssue.length,
}} />)} {queue('③ 待入库队列(已发码 → 入媒资库)', toIngest.length,
}} />)} {queue('④ 待发布队列(已入库 → 发布运营商)', toPublish.length,
}} />)} ) } // ============ 运营商工作台 ============ function OperatorDesk({ tick, onChanged }) { const [toInject, setToInject] = useState([]) async function load() { const res = await call('operator', 'GET', '/content/list?status=published') setToInject(res.data?.data?.contents || []) } useEffect(() => { load() }, [tick]) async function inject(r, tamper) { const fh = tamper ? 'TAMPERED-' + r.file_hash : r.file_hash const res = await call('operator', 'POST', '/content/inject', { content_twin_id: r.content_twin_id, ma_code: r.ma_code, file_sha256: fh, operator_id: 'CT-SX-IPTV', cdn_endpoint: 'cdn://ct-sx/vod/' + r.ma_code.slice(-4), }) if (res.ok) { message.success(`注入成功 ${res.data.data.distribution_id}`); 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' }, { title: '操作', render: (_, r) => ( ) }, ] return ( 运营商工作台 · 待注入队列} extra={}>
}} /> ) } // ============ 总览看板 ============ function Overview({ tick }) { const [counts, setCounts] = useState({}) async function load() { const statuses = ['approved', 'in_library', 'published', 'revoked'] const results = await Promise.all([ call('regulator', 'GET', '/content/reviews?status=pending'), call('regulator', 'GET', '/content/reviews?status=approved'), ...statuses.map((s) => call('regulator', 'GET', '/content/list?status=' + s)), ]) setCounts({ pending: results[0].data?.data?.reviews?.length || 0, toIssue: results[1].data?.data?.reviews?.length || 0, toIngest: results[2].data?.data?.contents?.length || 0, toPublish: results[3].data?.data?.contents?.length || 0, published: results[4].data?.data?.contents?.length || 0, revoked: results[5].data?.data?.contents?.length || 0, }) } useEffect(() => { load() }, [tick]) const items = [ { label: '待审', v: counts.pending, c: '#faad14' }, { label: '待发码', v: counts.toIssue, c: '#1677ff' }, { label: '待入库', v: counts.toIngest, c: '#52c41a' }, { label: '待发布', v: counts.toPublish, c: '#fa8c16' }, { label: '已发布(流通中)', v: counts.published, c: '#13c2c2' }, { label: '已下架', v: counts.revoked, c: '#f5222d' }, ] return ( } size="small" onClick={load}>刷新}> {items.map((it) => (
{it.v ?? 0}
{it.label}
))}
) } // ============ 监管片库 ============ function LibraryDesk({ tick, onChanged }) { const [filter, setFilter] = useState('all') const [rows, setRows] = useState([]) const [loading, setLoading] = useState(false) const [detail, setDetail] = useState(null) // {maCode, mappings, episodes} async function load() { setLoading(true) const status = filter === 'all' ? '' : filter const res = await call('regulator', 'GET', '/content/list?status=' + status) setRows(res.data?.data?.contents || []) setLoading(false) } useEffect(() => { load() }, [tick, filter]) function confirmTakedown(r) { Modal.confirm({ title: '违规应急下架', content: `确认对《${r.title}》(${r.ma_code}) 执行全网下架?将解析三方编码秒级同步。`, okText: '确认下架', okType: 'danger', cancelText: '取消', onOk: async () => { const res = await api.takedown(r.ma_code, '监管片库应急下架') if (res.ok) { message.success('已全网下架,受影响 CDN: ' + (res.data.data.cdn_endpoints || []).join(', ')) onChanged() } else message.error(res.data.message || '下架失败') }, }) } async function viewDetail(r) { const [m, e] = await Promise.all([api.mappings(r.ma_code), api.episodes(r.ma_code)]) setDetail({ content: r, mappings: m.data?.data?.mappings || [], cdn: m.data?.data?.cdn_endpoints || [], episodes: e.data?.data?.episodes || [], }) } function takedownEpisode(maCode, episode) { Modal.confirm({ title: `集级下架 · 第 ${episode} 集`, content: `只下架《${maCode}#E${String(episode).padStart(2, '0')}》本集,整剧其他集继续流通。确认?`, okText: '下架本集', okType: 'danger', cancelText: '取消', onOk: async () => { const res = await api.takedownEpisode(maCode, episode, '监管片库集级下架') if (res.ok) { message.success(`第 ${episode} 集已下架`) await viewDetail(detail.content) // 刷新弹窗 onChanged() } else message.error(res.data.message || '下架失败') }, }) } async function doRestore(r) { const res = await api.restore(r.ma_code) if (res.ok) { message.success('《' + r.title + '》已恢复上架'); onChanged() } else message.error(res.data.message || '恢复失败') } async function restoreEpisode(maCode, episode) { const res = await api.restoreEpisode(maCode, episode) if (res.ok) { message.success('第 ' + episode + ' 集已恢复上架') await viewDetail(detail.content) onChanged() } else message.error(res.data.message || '恢复失败') } const cols = [ { title: 'MA 码', dataIndex: 'ma_code', render: (v) => {v} }, { title: '作品', dataIndex: 'title' }, { title: '集数', dataIndex: 'episode_count', width: 70 }, { title: '状态', dataIndex: 'status', width: 110, render: (s) => { const m = statusMeta[s] || { label: s, color: 'default' } return {m.label} } }, { title: '操作', width: 240, render: (_, r) => ( {r.status === 'revoked' ? ( ) : ( )} ) }, ] return ( 监管片库} extra={ } >
}} /> setDetail(null)} footer={null} width={820} title={detail ? `片库详情 · ${detail.content.title}` : ''}> {detail && ( {detail.content.ma_code} {(statusMeta[detail.content.status] || {}).label || detail.content.status} {detail.content.file_hash || '-'}
i} size="small" pagination={false} dataSource={detail.mappings} columns={[ { title: '角色', dataIndex: 'party' }, { title: '编码', dataIndex: 'party_id' }, { title: '名称', dataIndex: 'party_name', render: (v) => v || '-' }, { title: 'CDN', dataIndex: 'cdn_endpoint', render: (v) => v || '-' }, ]} />
{`${detail.content.ma_code}#E${String(r.episode).padStart(2, '0')}`} }, { title: '哈希', dataIndex: 'hash_value', ellipsis: true }, { title: '状态', width: 80, render: (_, r) => r.revoked ? 已下架 : 流通中 }, { title: '操作', width: 110, render: (_, r) => ( r.revoked ? : ) }, ]} /> ) }, { key: 'gov', label: '权益与治理', children: }, ]} /> )} ) } export default function RoleDesk() { const [tick, bump] = useTick() return (
}, { key: 'reviewer', label: '🔵 审核监管(CSPS/媒资)', children: }, { key: 'operator', label: '🟠 运营商', children: }, { key: 'library', label: '🔴 监管片库(下架处置)', children: }, ]} />
) }