forked from Gitlink/forgeplus-react
480 lines
21 KiB
TypeScript
480 lines
21 KiB
TypeScript
|
||
import React, { useEffect, useRef, useState } from 'react'
|
||
import type { FC } from 'react';
|
||
import {
|
||
connect,
|
||
ConnectProps,
|
||
Dispatch,
|
||
useParams
|
||
} from 'umi';
|
||
import {
|
||
Tabs,
|
||
Row,
|
||
Pagination,
|
||
Table,
|
||
Button,
|
||
Modal,
|
||
Form,
|
||
message,
|
||
Upload,
|
||
Tooltip,
|
||
Input
|
||
} from 'antd';
|
||
import {
|
||
UploadOutlined
|
||
} from '@ant-design/icons';
|
||
import styles from './index.less';
|
||
import NoData from '@/components/NoData';
|
||
import { AsyncButton } from '@/components/AsyncButton';
|
||
import MarkdownEditor from '@/components/markdown-editor';
|
||
import RenderHtml from '@/components/RenderHtml';
|
||
import moment from 'moment';
|
||
import { uploadFile } from '@/components/UploadFile';
|
||
import JSZip from 'jszip';
|
||
import {downLoadLink} from "@/utils/util";
|
||
import Fetch from '@/utils/fetch';
|
||
|
||
interface PageProps extends Partial<ConnectProps> {
|
||
dispatch?: Dispatch,
|
||
userinfo?:any,
|
||
Editable?:boolean,
|
||
ItemData?:any,
|
||
TabResults?:any,
|
||
HeaderDetail?:any,
|
||
StaffDetail?:any,
|
||
getTabResults?:Function,//获取Tab和md数据
|
||
}
|
||
|
||
const WorkSubmit: FC<PageProps> = ({
|
||
dispatch,
|
||
userinfo,
|
||
Editable,
|
||
ItemData,
|
||
TabResults,
|
||
HeaderDetail,
|
||
StaffDetail,
|
||
getTabResults=()=>{}
|
||
}) => {
|
||
// 开启/关闭 编辑
|
||
const [isEdit,setIsEdit] = useState<boolean>(false)
|
||
const { identifier } = useParams();
|
||
// 提交作品:1, 仅提交文件:2
|
||
const [modelType,setModelType] = useState<number>(1);
|
||
// 竞赛管理员、超管、运营
|
||
const [identity,setIdentity] = useState<boolean>(false);
|
||
const [isSubmitModel,setIsSubmitModel] = useState<boolean>(false);
|
||
// 上传列表数据
|
||
const [fileList,setFileList] = useState<any[]>([]);
|
||
// 表格数据
|
||
const [tableLoading,setTableLoading] = useState<boolean>(false);
|
||
const [rowTable,setRowTable] = useState<any>({
|
||
name:"",
|
||
url:"",
|
||
id:null,
|
||
fileList:[],
|
||
});
|
||
const [tableList,setTableList] = useState<any>({
|
||
total_count:0,
|
||
results:[],
|
||
});
|
||
// 当前赛事
|
||
const [gameItem,setGameItem] = useState<any>(null);
|
||
// 分页,搜索
|
||
const [urlData,setUrlData] = useState<any>({
|
||
page:1,
|
||
per_page:20,
|
||
keyword:"",
|
||
});
|
||
// MD内容
|
||
const [mdContent,setMdContent] = useState<any>("");
|
||
// 下载所有文件时打包成zip文件
|
||
const [zipAll, setZipAll] = useState(new JSZip());
|
||
useEffect(()=>{
|
||
setIdentity(userinfo?.admin||userinfo?.business||Editable)
|
||
},[userinfo?.admin,userinfo?.business,Editable])
|
||
useEffect(()=>{
|
||
if(ItemData?.only_file)setModelType(2);
|
||
},[ItemData])
|
||
useEffect(()=>{
|
||
if(TabResults.stages&&TabResults.stages?.length>0){
|
||
TabResults.stages?.[0]?.children?.length>0 ?
|
||
setGameItem(TabResults.stages?.[0]?.children?.[0]||null) :
|
||
setGameItem(TabResults.stages?.[0]||null);
|
||
}
|
||
},[TabResults.stages])
|
||
useEffect(()=>{
|
||
getResults();
|
||
},[gameItem?.id,urlData.page])
|
||
// 表格头部数据
|
||
const columns:any[] = [
|
||
{
|
||
title: <span style={{color:"#5F6368"}}>队伍名称</span>,
|
||
dataIndex: 'team_name',
|
||
width:"120px",
|
||
ellipsis:true,
|
||
render: (text: any) => <Tooltip placement="topLeft" title={text}>{text||"- -"}</Tooltip>,
|
||
isShow:[1,2],
|
||
},
|
||
{
|
||
title: <span style={{color:"#5F6368"}}>提交人</span>,
|
||
dataIndex: 'user_name',
|
||
width:"120px",
|
||
ellipsis:true,
|
||
render: (text: any) => <Tooltip placement="topLeft" title={text}>{text||"- -"}</Tooltip>,
|
||
isShow:[1,2],
|
||
},
|
||
{
|
||
title: <span style={{color:"#5F6368"}}>作品名称</span>,
|
||
dataIndex: 'name',
|
||
ellipsis:true,
|
||
render: (text: any,record:any) => text?<a href={record.url||'#'} target="_blank"><Tooltip placement="topLeft" title={text}>{text}</Tooltip></a>:"--",
|
||
isShow:[1],
|
||
},
|
||
{
|
||
title: <span style={{color:"#5F6368"}}>作品文件</span>,
|
||
dataIndex: 'file_name',
|
||
width:modelType==1?"180px":"",
|
||
ellipsis:true,
|
||
render: (text: any) => <Tooltip placement="topLeft" title={text}>{text||"- -"}</Tooltip>,
|
||
isShow:[1,2],
|
||
},
|
||
{
|
||
title: <span style={{color:"#5F6368"}}>更新时间</span>,
|
||
dataIndex: 'updated_at',
|
||
width:"180px",
|
||
render: (text: any) => text || '- -',
|
||
isShow:[1,2],
|
||
},
|
||
{
|
||
title: <span style={{color:"#5F6368"}}>操作</span>,
|
||
dataIndex: 'result_url',
|
||
ellipsis:true,
|
||
width:"140px",
|
||
align: 'center',
|
||
render: (text: any,record:any) => <Row justify={identity?"center":"space-between"}>
|
||
{!identity&&<Button type="link" onClick={()=>{
|
||
setIsSubmitModel(true)
|
||
setRowTable({
|
||
name:record.name,
|
||
url:record.url,
|
||
id:record.id,
|
||
})
|
||
if(record.file_name){
|
||
setFileList([{
|
||
uid:"-1", // TODO : 这里目前无法获取到id或者唯一标识,暂时只能用-1
|
||
name:record.file_name,
|
||
status: 'done',
|
||
url: record.result_url||"",
|
||
}])
|
||
}
|
||
}}>编辑</Button>}
|
||
<AsyncButton type="link" disabled={!text} onClick={async ()=>downLoadLink(record.file_name,text)}>{identity?"下载文件":"下载"}</AsyncButton>
|
||
</Row>,
|
||
isShow:[1,2],
|
||
},
|
||
].filter((item:any)=>item.isShow.includes(modelType))
|
||
// 获取列表数据
|
||
const getResults = async ()=> {
|
||
setTableLoading(true)
|
||
const data:any = await dispatch({
|
||
type: 'competitions/Results',
|
||
payload: {
|
||
identifier: identifier,
|
||
stage_id: gameItem?.id || null,
|
||
module_type:'worksubmit',
|
||
...urlData,
|
||
}
|
||
})
|
||
setTableList(data)
|
||
setTableLoading(false)
|
||
}
|
||
// 更新MD内容
|
||
const getUpMDContent = async (data:any)=>{
|
||
const res:any = await dispatch({
|
||
type: 'competitions/updateMdContent',
|
||
payload: {
|
||
identifier: identifier,
|
||
stage_id: gameItem?.id || null,
|
||
competition_module_id:ItemData.id,
|
||
content:mdContent,
|
||
md_content_id:data?.id,
|
||
}
|
||
})
|
||
res.status==0&&message.success(res.message);
|
||
setIsEdit(false);
|
||
await getTabResults();
|
||
}
|
||
// 作品提交或者编辑
|
||
const handleFormFinish = async (values:any)=>{
|
||
console.log(values,fileList);
|
||
let res:any;
|
||
if(modelType==2&&fileList.length==0){
|
||
message.error("请选择文件");
|
||
return;
|
||
}
|
||
const Data:any = {
|
||
login:userinfo?.login,
|
||
container_type:"Competition",
|
||
file_name:fileList?.[0]?.name,
|
||
stage_type:gameItem?.id || null,
|
||
container_id:HeaderDetail?.id,
|
||
result_id:rowTable?.id,
|
||
module_type:'worksubmit',
|
||
}
|
||
if(modelType==1){
|
||
const res:any = await Fetch(`https://data.educoder.net/api/competitions/${identifier}/check_result_url.json`,
|
||
{
|
||
method:"GET",
|
||
params:{
|
||
url:values.url,
|
||
stage_id:gameItem?.id || null,
|
||
result_id:rowTable?.id,
|
||
}
|
||
}
|
||
)
|
||
if(res?.status!=0)return
|
||
Data["name"]=values.name;
|
||
Data["url"]=values.url;
|
||
}
|
||
if(modelType==1&&fileList.length==0){
|
||
res=await dispatch({
|
||
type: 'competitions/getWorkSubmitUpdateRes',
|
||
payload: {
|
||
identifier:identifier,
|
||
result_id:rowTable?.id,
|
||
name:values.name,
|
||
url:values.url,
|
||
stage_id:gameItem?.id || null,
|
||
result_url:rowTable?.result_url,
|
||
module_type:'worksubmit',
|
||
}
|
||
})
|
||
}else{
|
||
res=await uploadFile(fileList[0],Data)
|
||
}
|
||
if(res?.status == 0){
|
||
message.success("提交成功");
|
||
}else{
|
||
message.info(res?.message||"提交失败");
|
||
}
|
||
setIsSubmitModel(false);
|
||
setFileList([]);
|
||
getResults();
|
||
}
|
||
// 下载所有作品
|
||
const handleAllDownload = async ()=>{
|
||
const res:any = await dispatch({
|
||
type: 'competitions/Results',
|
||
payload: {
|
||
identifier: identifier,
|
||
stage_id: gameItem?.id || null,
|
||
page:1,
|
||
per_page:9999999, // TODO :查询所有列表数据
|
||
keyword:"",
|
||
module_type:'worksubmit',
|
||
}
|
||
})
|
||
const DownloadList = res?.results?.filter((item:any)=>item?.result_url&&item.file_name)
|
||
const modal = Modal.info({
|
||
width: 460,
|
||
title: <div>正在导出参赛文件,<span className="c-red">请勿关闭或操作当前浏览器窗口</span></div>,
|
||
content:<div>当前进度: <span className="c-blue">{0}</span>/{DownloadList.length}</div>,
|
||
maskClosable: false,
|
||
className: styles.modal,
|
||
onOk: () => {
|
||
}
|
||
});
|
||
const nameLsit:string[] = [];
|
||
const addzip = async (data:any,progress:number)=>{
|
||
try{
|
||
const zip = new JSZip();
|
||
const response = await fetch(data.result_url, {method:"get"});
|
||
const blob = await response.blob();
|
||
zip.file(data.file_name,blob);
|
||
await zip.generateAsync({type:"blob"}).then((blob)=>{
|
||
const getNameFile = (name:string,index:number=0)=>{
|
||
if((index==0&&nameLsit.includes(name))||nameLsit.includes(`${name}(${index})`)){
|
||
getNameFile(name,index+1);
|
||
}else{
|
||
nameLsit.push(index==0 ? name:`${name}(${index})`)
|
||
zipAll.file(index==0 ? name+".zip" : `${name}(${index})`+".zip",blob);
|
||
}
|
||
}
|
||
getNameFile(`${data?.team_name}-${data?.user_name}`);
|
||
})
|
||
modal.update({
|
||
content: <div>当前进度: <span className="c-blue">{progress}</span>/{DownloadList.length}</div>,
|
||
})
|
||
}catch(error){
|
||
message.error(`${data.file_name}下载失败`);
|
||
}
|
||
}
|
||
if(DownloadList.length>0){
|
||
let i = 0;
|
||
for (let item of DownloadList) {
|
||
await addzip(item,++i);
|
||
}
|
||
zipAll.generateAsync({type:"blob"}).then((blob)=>{
|
||
downLoadLink(HeaderDetail.name,window.URL.createObjectURL(blob));
|
||
})
|
||
}
|
||
modal.destroy()
|
||
}
|
||
const uploadProps = {
|
||
maxCount: 1,
|
||
withCredentials: true,
|
||
fileList:fileList,
|
||
onRemove: () => {
|
||
setFileList([]);
|
||
},
|
||
beforeUpload: (file: any) => {
|
||
const fileSize = file.size / 1024 / 1024;
|
||
const fileType = file.name.split(".").slice(-1)[0].toLowerCase();
|
||
if ((modelType==1&&fileSize > 150||modelType==2&&fileSize/1024 > 1)||fileSize==0) {
|
||
message.error(`${file.name} 文件无法上传。${fileSize == 0 ? "文件内容不能为空" : `超过文件大小限制(${modelType==1? '150MB':'1G'})`}`)
|
||
return Promise.reject()
|
||
}
|
||
setFileList([file])
|
||
return false
|
||
}
|
||
};
|
||
|
||
|
||
return (<div className={styles.WorkSubmit}>
|
||
{gameItem?<Tabs defaultActiveKey={TabResults.stages?.[0]?.id||1}
|
||
destroyInactiveTabPane
|
||
tabBarExtraContent={<Row style={{marginBottom:"10px"}}>
|
||
{!identity&&StaffDetail.enrolled&&<Button type="primary" onClick={()=>setIsSubmitModel(true)}>作品提交</Button>}
|
||
{identity&&!isEdit&&<Button style={{marginLeft:"10px"} } type="primary" onClick={()=>setIsEdit(true)}>编辑</Button>}
|
||
</Row>}
|
||
onChange={(activeKey)=>{
|
||
const item = TabResults.stages.find((item:any)=>item.id==activeKey)
|
||
item.children.length>0?setGameItem(item.children[0]):setGameItem(item)
|
||
}}>
|
||
{TabResults.stages?.map((item:any,index:number)=><Tabs.TabPane tab={<div style={{marginBottom:"20px"}}>{item.name}</div>} key={item.id}>
|
||
{
|
||
isEdit ?
|
||
<>
|
||
<MarkdownEditor defaultValue={TabResults.rule_contents[index]?.content||""} onChange={(e:any)=>setMdContent(e)}/>
|
||
<Row>
|
||
<AsyncButton type="primary" onClick={()=> getUpMDContent(TabResults.rule_contents[index])}>提交</AsyncButton>
|
||
<Button style={{marginLeft:"10px"} } onClick={async ()=>setIsEdit(false)}>取消</Button>
|
||
</Row>
|
||
</>:<RenderHtml style={{marginTop:"10px"}} value={TabResults.rule_contents[index]?.content||""} />
|
||
}
|
||
{item.children.length>0&&<Tabs defaultActiveKey={item.children[0]?.id} destroyInactiveTabPane onChange={(activeKey)=>{setGameItem(item.children.find((item:any)=>item.id==activeKey))}}>
|
||
{item.children?.map((ChildItem:any)=><Tabs.TabPane tab={ChildItem.name} key={ChildItem.id} ></Tabs.TabPane>)}
|
||
</Tabs>}
|
||
</Tabs.TabPane>)}
|
||
</Tabs>:<div>
|
||
{(StaffDetail.enrolled||identity)&&<Row style={{marginBottom:"10px",paddingBottom:"10px",borderBottom:"1px solid #eee"}}>
|
||
{!identity&&StaffDetail.enrolled&&<Button style={{marginLeft:"auto"}} type="primary" onClick={()=>setIsSubmitModel(true)}>作品提交</Button>}
|
||
{identity&&!isEdit&&<Button style={{marginLeft:!identity&&StaffDetail.enrolled?"10px":"auto"} } type="primary" onClick={()=>setIsEdit(true)}>编辑</Button>}
|
||
</Row>}
|
||
{
|
||
isEdit ?
|
||
<>
|
||
<MarkdownEditor defaultValue={TabResults.rule_contents?.[0]?.content||""} onChange={(e:any)=>setMdContent(e)}/>
|
||
<Row>
|
||
<AsyncButton type="primary" onClick={()=> getUpMDContent(TabResults.rule_contents?.[0])}>提交</AsyncButton>
|
||
<Button style={{marginLeft:"10px"} } onClick={async ()=>setIsEdit(false)}>取消</Button>
|
||
</Row>
|
||
</>:<RenderHtml style={{marginTop:"10px"}} value={TabResults.rule_contents?.[0]?.content||""} />
|
||
}
|
||
</div>}
|
||
{identity&&<Row align="middle" justify="space-between" style={{marginTop:"21px"}}>
|
||
<Row align="middle">
|
||
<Input className={styles.search} placeholder="请输入队伍名称或提交人姓名搜索"
|
||
suffix={<i className='iconfont icon-sousuo9' onClick={getResults} style={{color:"#000",cursor:"pointer",fontSize:"14px"}}/>}
|
||
onChange={(e:any)=>{
|
||
urlData.keyword = e.target.value
|
||
setUrlData(urlData)
|
||
}}
|
||
style={{background:"#F6F7F9"}}
|
||
bordered={false}
|
||
onPressEnter={getResults}/>
|
||
<div style={{color: "#9B9B9B",fontSize:"14px",marginLeft:"20px"}}>当前共计作品<span style={{color:"#165DFF"}}>{tableList.total_count}</span>个</div>
|
||
</Row>
|
||
<Row align="middle">
|
||
{modelType==1&&<Button style={{lineHeight:"32px"}} target="_blank"
|
||
href={`/api/competitions/${identifier}/results.xlsx?identifier=${identifier}&stage_id=${gameItem?.id||""}&module_type=worksubmit`}
|
||
icon={<i className='iconfont icon-lianjie3' style={{fontSize:"16px", color:"#44D7B6"}}/>}
|
||
className={styles.downBut}>下载作品链接</Button>}
|
||
<AsyncButton icon={<i className='iconfont icon-wenjian4' style={{fontSize:"16px", color:"#F6C555"}}/>} className={styles.downBut} style={{marginLeft:"20px"}} onClick={handleAllDownload}>下载所有文件</AsyncButton>
|
||
</Row>
|
||
</Row>}
|
||
<Table style={{marginTop:"17px"}} loading={tableLoading} columns={columns} dataSource={tableList.results} locale={{emptyText:<NoData />}} pagination={false}/>
|
||
<Row style={{marginTop:"20px"}} align="middle" justify='space-between'>
|
||
{
|
||
(gameItem?.end_time&&gameItem?.start_time||HeaderDetail.start_time&&HeaderDetail.end_time)&&
|
||
<span className={styles.span}>提交时间:<span>{moment(gameItem?.start_time||HeaderDetail.start_time).format("YYYY-MM-DD HH:mm:ss")}</span> 至 <span>{moment(gameItem?.end_time||HeaderDetail.end_time).format("YYYY-MM-DD HH:mm:ss")}</span></span>
|
||
}
|
||
<Pagination showQuickJumper
|
||
hideOnSinglePage
|
||
total={tableList.total_count}
|
||
pageSize={urlData.per_page}
|
||
onChange={(page:number)=>{
|
||
urlData.page = page
|
||
setUrlData(urlData)
|
||
}}/>
|
||
</Row>
|
||
|
||
<Modal centered destroyOnClose
|
||
title={<div style={{
|
||
fontWeight: "500",
|
||
color: "#000000",
|
||
marginTop:"10px"
|
||
}}>参数作品提交</div>}
|
||
visible={isSubmitModel}
|
||
bodyStyle={{padding:" 0px 25px 10px 25px"}}
|
||
footer={null}
|
||
onCancel={()=>{setIsSubmitModel(false);setFileList([]);setRowTable(null);}}>
|
||
<div className={styles.WorkSubmitDesc}>说明:参赛作品不支持删除,大赛进行中上传后的作品如需修改,可在【作品提交】列表点击“编辑”进行修改。</div>
|
||
<Form initialValues={rowTable} colon={false} onFinish={handleFormFinish}>
|
||
{modelType==1&&<><Form.Item label="作品名称" name="name" rules={[{ required: true, message: "请填写作品名称" }]} >
|
||
<Input maxLength={60} placeholder="请输入作品名称"/>
|
||
</Form.Item>
|
||
<Form.Item label="作品链接" name="url" rules={[{ required: true, message: "请填写作品链接" }]} >
|
||
<Input placeholder="请输入作品链接"/>
|
||
</Form.Item></>}
|
||
<Form.Item name="fileList" style={{paddingLeft: modelType==1 ?"76px":"0"}} valuePropName="fileList"
|
||
getValueFromEvent={(e: any) => {
|
||
if (Array.isArray(e))return e;
|
||
return e?.fileList;
|
||
}}>
|
||
{modelType==1&&<Upload {...uploadProps}>
|
||
<Row align="middle">
|
||
<Button type="primary" ghost icon={<UploadOutlined />}>{fileList.length>0?"更换文件":"文件上传"}</Button>
|
||
<Tooltip placement="right" overlayStyle={{ maxWidth: 600 }}
|
||
title={<div onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
|
||
<p>提示:</p>
|
||
<p>1. 一次最多上传1个文件。</p>
|
||
<p>2. 单个文件大小请勿超过150M。</p>
|
||
</div>}>
|
||
<i onClick={(e) => { e.preventDefault(); e.stopPropagation(); }} className='iconfont icon-tishixiaowenhao ml5' style={{ cursor: 'pointer', color: '#C5C5C5' }} />
|
||
</Tooltip>
|
||
</Row>
|
||
</Upload>}
|
||
{modelType==2&&<Upload.Dragger className={styles.fileList} style={{background:"#fff",padding:"31px 0px"}} {...uploadProps}>
|
||
<p className="ant-upload-drag-icon"><i className='iconfont icon-shangchuan4' style={{fontSize:"32px", color:"#165DFF"}}/></p>
|
||
<p className="ant-upload-text">拖拽文件或者点击上传</p>
|
||
</Upload.Dragger>}
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Row align="middle">
|
||
<Button style={{marginLeft:"auto"}} onClick={()=>{setIsSubmitModel(false);setFileList([]);setRowTable(null);}}>取消</Button>
|
||
<Button type='primary' style={{marginLeft:"10px"}} htmlType="submit">确定</Button>
|
||
</Row>
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>)
|
||
}
|
||
|
||
|
||
export default connect((
|
||
{
|
||
}: {
|
||
}) => ({
|
||
}))(WorkSubmit)
|