forgeplus-react/src/pages/Competitions/Detail/components/WorkSubmit/index.tsx

480 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)