@@ -0,0 +1,369 @@
import { useState , useEffect } from 'react'
import { Tabs , Table , Button , Modal , Form , Input , InputNumber , DatePicker , message , Space , Upload , Popconfirm } from 'antd'
import { PlusOutlined , EditOutlined , DeleteOutlined , DownloadOutlined , UploadOutlined } from '@ant-design/icons'
import { listEpisodes , createEpisode , updateEpisode , deleteEpisode } from '../../services/episodeService'
import { listTargets , createTarget , updateTarget , deleteTarget } from '../../services/yearlyTargetService'
import http from '../../services/http'
const { TabPane } = Tabs
// ============================================================
// Tab1: 节目期次
// ============================================================
function EpisodesTab ( ) {
const [ episodes , setEpisodes ] = useState ( [ ] )
const [ loading , setLoading ] = useState ( false )
const [ modalVisible , setModalVisible ] = useState ( false )
const [ editing , setEditing ] = useState ( null )
const [ form ] = Form . useForm ( )
useEffect ( ( ) => {
loadEpisodes ( )
} , [ ] )
const loadEpisodes = ( ) => {
setLoading ( true )
listEpisodes ( ) . then ( data => {
setEpisodes ( ( data || [ ] ) . sort ( ( a , b ) => new Date ( b . air _date ) - new Date ( a . air _date ) ) )
setLoading ( false )
} ) . catch ( ( ) => {
message . error ( '加载节目列表失败' )
setLoading ( false )
} )
}
const handleCreate = ( ) => {
setEditing ( null )
form . resetFields ( )
setModalVisible ( true )
}
const handleEdit = ( record ) => {
setEditing ( record )
form . setFieldsValue ( {
episode _number : record . episode _number ,
program _name : record . program _name ,
air _date : record . air _date ? record . air _date : null ,
editor _name _snapshot : record . editor _name _snapshot || '' ,
audience _share : record . audience _share ,
audience _rating : record . audience _rating ,
is _rerun : record . is _rerun ,
notes : record . notes ,
} )
setModalVisible ( true )
}
const handleDelete = ( id ) => {
deleteEpisode ( id ) . then ( ( ) => {
message . success ( '删除成功' )
loadEpisodes ( )
} ) . catch ( err => {
message . error ( err . response ? . data ? . detail || '删除失败' )
} )
}
const handleSubmit = ( ) => {
form . validateFields ( ) . then ( values => {
const payload = {
... values ,
air _date : values . air _date ? values . air _date . format ( 'YYYY-MM-DD' ) : null ,
}
const promise = editing
? updateEpisode ( editing . id , payload )
: createEpisode ( payload )
promise . then ( ( ) => {
message . success ( editing ? '更新成功' : '创建成功' )
setModalVisible ( false )
loadEpisodes ( )
} ) . catch ( err => {
message . error ( err . response ? . data ? . detail || '操作失败' )
} )
} )
}
const columns = [
{ title : '期次号' , dataIndex : 'episode_number' , key : 'episode_number' , width : 80 } ,
{ title : '节目名' , dataIndex : 'program_name' , key : 'program_name' } ,
{ title : '播出日期' , dataIndex : 'air_date' , key : 'air_date' , render : d => d || '-' } ,
{ title : '编导' , dataIndex : 'editor_name_snapshot' , key : 'editor_name_snapshot' , render : v => v || '-' } ,
{ title : '收视份额' , dataIndex : 'audience_share' , key : 'audience_share' , render : v => v != null ? v . toFixed ( 4 ) : '-' } ,
{ title : '收视率' , dataIndex : 'audience_rating' , key : 'audience_rating' , render : v => v != null ? v . toFixed ( 2 ) : '-' } ,
{
title : '操作' ,
key : 'action' ,
render : ( _ , record ) => (
< Space >
< Button size = "small" icon = { < EditOutlined / > } onClick = { ( ) => handleEdit ( record ) } / >
< Popconfirm title = "确认删除?" onConfirm = { ( ) => handleDelete ( record . id ) } >
< Button size = "small" danger icon = { < DeleteOutlined / > } / >
< / Popconfirm >
< / Space >
) ,
} ,
]
return (
< div >
< div style = { { marginBottom : 16 , display : 'flex' , justifyContent : 'flex-end' } } >
< Button type = "primary" icon = { < PlusOutlined / > } onClick = { handleCreate } > 新增期次 < / Button >
< / div >
< Table columns = { columns } dataSource = { episodes } rowKey = "id" loading = { loading } pagination = { { pageSize : 20 } } / >
< Modal title = { editing ? '编辑期次' : '新增期次' } open = { modalVisible } onOk = { handleSubmit } onCancel = { ( ) => setModalVisible ( false ) } okText = "保存" width = { 520 } >
< Form form = { form } layout = "vertical" style = { { marginTop : 16 } } >
< Form.Item name = "episode_number" label = "期次号" rules = { [ { required : true , message : '请输入期次号' } ] } >
< InputNumber style = { { width : '100%' } } placeholder = "如 1248" disabled = { ! ! editing } / >
< / Form.Item >
< Form.Item name = "program_name" label = "节目名称" rules = { [ { required : true , message : '请输入节目名称' } ] } >
< Input placeholder = "如《军事科技》武器解析系列" / >
< / Form.Item >
< Form.Item name = "air_date" label = "播出日期" rules = { [ { required : true , message : '请选择播出日期' } ] } >
< DatePicker style = { { width : '100%' } } format = "YYYY-MM-DD" disabled = { ! ! editing } / >
< / Form.Item >
< Form.Item name = "editor_name_snapshot" label = "编导姓名(快照)" >
< Input placeholder = "可留空,离职编导只存姓名" / >
< / Form.Item >
< Form.Item name = "audience_share" label = "收视份额(0-1) " >
< InputNumber style = { { width : '100%' } } min = { 0 } max = { 1 } precision = { 4 } placeholder = "0.72" / >
< / Form.Item >
< Form.Item name = "audience_rating" label = "收视率" >
< InputNumber style = { { width : '100%' } } min = { 0 } precision = { 2 } placeholder = "3.5" / >
< / Form.Item >
< Form.Item name = "notes" label = "备注" >
< Input.TextArea rows = { 2 } placeholder = "可选" / >
< / Form.Item >
< / Form >
< / Modal >
< / div >
)
}
// ============================================================
// Tab2: 年度目标
// ============================================================
function YearlyTargetsTab ( ) {
const [ targets , setTargets ] = useState ( [ ] )
const [ loading , setLoading ] = useState ( false )
const [ modalVisible , setModalVisible ] = useState ( false )
const [ editing , setEditing ] = useState ( null )
const [ form ] = Form . useForm ( )
useEffect ( ( ) => {
loadTargets ( )
} , [ ] )
const loadTargets = ( ) => {
setLoading ( true )
listTargets ( ) . then ( data => {
setTargets ( ( data || [ ] ) . sort ( ( a , b ) => b . year - a . year ) )
setLoading ( false )
} ) . catch ( ( ) => {
message . error ( '加载年度目标失败' )
setLoading ( false )
} )
}
const handleCreate = ( ) => {
setEditing ( null )
form . resetFields ( )
setModalVisible ( true )
}
const handleEdit = ( record ) => {
setEditing ( record )
form . setFieldsValue ( {
year : record . year ,
base _target : record . base _target ,
stretch _target : record . stretch _target ,
notes : record . notes ,
} )
setModalVisible ( true )
}
const handleDelete = ( id ) => {
deleteTarget ( id ) . then ( ( ) => {
message . success ( '删除成功' )
loadTargets ( )
} ) . catch ( err => {
message . error ( err . response ? . data ? . detail || '删除失败' )
} )
}
const handleSubmit = ( ) => {
form . validateFields ( ) . then ( values => {
const promise = editing
? updateTarget ( editing . id , values )
: createTarget ( values )
promise . then ( ( ) => {
message . success ( editing ? '更新成功' : '创建成功' )
setModalVisible ( false )
loadTargets ( )
} ) . catch ( err => {
const detail = err . response ? . data ? . detail || ''
if ( detail . includes ( '已存在' ) || detail . includes ( '历史年份' ) ) {
message . error ( '该年目标已存在,历史年份不可覆盖' )
} else {
message . error ( detail || '操作失败' )
}
} )
} )
}
const columns = [
{ title : '年份' , dataIndex : 'year' , key : 'year' , width : 80 } ,
{ title : '基础目标' , dataIndex : 'base_target' , key : 'base_target' , render : v => v ? . toFixed ( 4 ) || '-' } ,
{ title : '摸高目标' , dataIndex : 'stretch_target' , key : 'stretch_target' , render : v => v ? . toFixed ( 4 ) || '-' } ,
{ title : '备注' , dataIndex : 'notes' , key : 'notes' , render : v => v || '-' } ,
{
title : '操作' ,
key : 'action' ,
render : ( _ , record ) => (
< Space >
< Button size = "small" icon = { < EditOutlined / > } onClick = { ( ) => handleEdit ( record ) } / >
< Popconfirm title = "确认删除?" onConfirm = { ( ) => handleDelete ( record . id ) } >
< Button size = "small" danger icon = { < DeleteOutlined / > } / >
< / Popconfirm >
< / Space >
) ,
} ,
]
return (
< div >
< div style = { { marginBottom : 16 , display : 'flex' , justifyContent : 'flex-end' } } >
< Button type = "primary" icon = { < PlusOutlined / > } onClick = { handleCreate } > 新增年度目标 < / Button >
< / div >
< Table columns = { columns } dataSource = { targets } rowKey = "id" loading = { loading } pagination = { false } / >
< Modal title = { editing ? '编辑年度目标' : '新增年度目标' } open = { modalVisible } onOk = { handleSubmit } onCancel = { ( ) => setModalVisible ( false ) } okText = "保存" >
< Form form = { form } layout = "vertical" style = { { marginTop : 16 } } >
< Form.Item
name = "year"
label = "年份"
rules = { [ { required : true , message : '请输入年份' } ] }
>
< InputNumber
style = { { width : '100%' } }
placeholder = "如 2024"
min = { 2000 }
max = { 2100 }
disabled = { ! ! editing }
/ >
< / Form.Item >
< Form.Item name = "base_target" label = "基础目标" rules = { [ { required : true , message : '请输入基础目标' } ] } >
< InputNumber style = { { width : '100%' } } min = { 0 } max = { 1 } precision = { 4 } placeholder = "0.6448" / >
< / Form.Item >
< Form.Item name = "stretch_target" label = "摸高目标" rules = { [ { required : true , message : '请输入摸高目标' } ] } >
< InputNumber style = { { width : '100%' } } min = { 0 } max = { 1 } precision = { 4 } placeholder = "0.8989" / >
< / Form.Item >
< Form.Item name = "notes" label = "备注" >
< Input.TextArea rows = { 2 } placeholder = "可选" / >
< / Form.Item >
< / Form >
< / Modal >
< / div >
)
}
// ============================================================
// Tab3: Excel 批量导入
// ============================================================
function ImportTab ( ) {
const [ uploading , setUploading ] = useState ( false )
const [ result , setResult ] = useState ( null )
const downloadTemplate = ( ) => {
window . open ( '/api/imports/template' , '_blank' )
}
const uploadProps = {
name : 'file' ,
accept : '.xlsx' ,
showUploadList : false ,
beforeUpload : ( file ) => {
if ( ! file . filename . endsWith ( '.xlsx' ) && ! file . name . endsWith ( '.xlsx' ) ) {
message . error ( '仅支持 .xlsx 格式' )
return false
}
setUploading ( true )
const formData = new FormData ( )
formData . append ( 'file' , file )
http . post ( '/imports/episodes' , formData , {
headers : { 'Content-Type' : 'multipart/form-data' } ,
} ) . then ( res => {
setResult ( res . data )
setUploading ( false )
if ( res . data . errors ? . length ) {
message . warning ( ` 导入完成:成功 ${ res . data . imported } 条,失败 ${ res . data . errors . length } 条 ` )
} else {
message . success ( ` 导入成功: ${ res . data . imported } 条 ` )
}
} ) . catch ( err => {
setUploading ( false )
message . error ( err . response ? . data ? . detail || '上传失败' )
} )
return false
} ,
}
const downloadErrors = ( ) => {
if ( ! result ? . batch _id ) return
window . open ( ` /api/imports/errors/ ${ result . batch _id } ` , '_blank' )
}
return (
< div >
< div style = { { display : 'flex' , gap : 16 , marginBottom : 24 } } >
< Button icon = { < DownloadOutlined / > } onClick = { downloadTemplate } > 下载导入模板 < / Button >
< Upload { ...uploadProps } >
< Button type = "primary" icon = { < UploadOutlined / > } loading = { uploading } >
{ uploading ? '上传中...' : '选择 Excel 上传' }
< / Button >
< / Upload >
< / div >
{ result && (
< div style = { { background : '#f6f8fa' , padding : 16 , borderRadius : 8 } } >
< p style = { { margin : '0 0 8px' , fontWeight : 600 } } > 导入结果 < / p >
< p style = { { margin : 0 } } > 总计 : { result . total } 条 < / p >
< p style = { { margin : '4px 0' , color : '#52c41a' } } > 成功 : { result . imported } 条 < / p >
{ result . errors ? . length > 0 && (
< >
< p style = { { margin : '4px 0' , color : '#cf1322' } } > 失败 : { result . errors . length } 条 < / p >
< Button size = "small" onClick = { downloadErrors } style = { { marginTop : 8 } } >
下载失败行
< / Button >
< / >
) }
< / div >
) }
< div style = { { marginTop : 24 , color : '#999' , fontSize : 12 } } >
< p > 支持批量导入节目期次 , 导入失败时可根据提示修正后重新上传 。 < / p >
< p > 注意 : Phase 2 不支持重播行导入 , 有重播行请留空 。 < / p >
< / div >
< / div >
)
}
// ============================================================
// 主组件
// ============================================================
function EditorDesk ( ) {
return (
< div style = { { padding : '0 0 24px' } } >
< h2 style = { { margin : '0 0 16px' , fontSize : 20 , fontWeight : 600 } } > 责编录入 < / h2 >
< Tabs defaultActiveKey = "episodes" >
< TabPane tab = "节目期次" key = "episodes" >
< EpisodesTab / >
< / TabPane >
< TabPane tab = "年度目标" key = "targets" >
< YearlyTargetsTab / >
< / TabPane >
< TabPane tab = "Excel 批量导入" key = "import" >
< ImportTab / >
< / TabPane >
< / Tabs >
< / div >
)
}
export default EditorDesk