ADD 成果库模块功

This commit is contained in:
jasder 2021-05-25 14:15:20 +08:00
commit d9e3e0d96b
30 changed files with 2611 additions and 1 deletions

View File

@ -87,6 +87,7 @@
"react-infinite-scroller": "^1.2.4",
"react-loadable": "^5.3.1",
"react-monaco-editor": "0.37",
"react-pdf": "^4.2.0",
"react-player": "^1.15.3",
"react-redux": "5.0.7",
"react-resizable": "^1.10.1",

View File

@ -75,6 +75,17 @@ const EducoderLogin = Loadable({
loading: Loading,
})
// 成果库
const Achieve = Loadable({
loader: () => import('./achieveRequirement/achieve'),
loading: Loading,
})
// 需求管理
const Requirement = Loadable({
loader: () => import('./achieveRequirement/requirement'),
loading: Loading,
})
class App extends Component {
constructor(props) {
super(props);
@ -259,7 +270,28 @@ class App extends Component {
(props) => {
return (<InfosIndex {...this.props} {...this.state} />)
}
}></Route>
}></Route>
{/*成果库*/}
<Route
path={"/achieve"}
render={
(props) => {
return (<Achieve {...this.props} {...props} {...this.state} />)
}
}>
</Route>
{/*需求管理/创客行动*/}
<Route
path={"/requirement"}
render={
(props) => {
return (<Requirement {...this.props} {...props} {...this.state} />)
}
}>
</Route>
<Route exact path="/"
render={
(props) => (
@ -267,7 +299,12 @@ class App extends Component {
)
}
/>
<Route component={Shixunnopage} />
</Switch>
</Router>
</MuiThemeProvider>

View File

@ -0,0 +1,97 @@
import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
import { withRouter } from "react-router";
import { SnackbarHOC } from "educoder";
import { CNotificationHOC } from "../modules/courses/common/CNotificationHOC";
import { TPMIndexHOC } from "../modules/tpm/TPMIndexHOC";
import Loadable from "react-loadable";
import Loading from "../Loading";
import { ImageLayerOfCommentHOC } from "../modules/page/layers/ImageLayerOfCommentHOC";
import './index.scss';
const AchieveIndex = Loadable({
loader: () => import("./achieve/achieveList"),
loading: Loading,
});
const AchieveDetail = Loadable({
loader: () => import("./achieve/achieveDetail"),
loading: Loading,
});
const AchieveMine = Loadable({
loader: () => import("./achieve/achieveMine"),
loading: Loading,
});
const AchieveAdd = Loadable({
loader: () => import("./achieve/achieveEdit"),
loading: Loading,
});
const AchieveEdit = Loadable({
loader: () => import("./achieve/achieveEdit"),
loading: Loading,
});
class Index extends Component {
render() {
return (
<div className="newMain clearfix">
<Switch {...this.props}>
<Route
path="/achieve/achieveDetail/:achieveId"
render={(props) => (
<AchieveDetail {...this.props} {...props} />
)}
></Route>
<Route
path="/achieve/achieveMine"
render={(props) => (
<AchieveMine {...this.props} {...props} />
)}
></Route>
<Route
path="/achieve/achieveAdd"
render={(props) => (
<AchieveAdd {...this.props} {...props} />
)}
></Route>
<Route
path="/achieve/achieveEdit/:achieveId"
render={(props) => (
<AchieveEdit {...this.props} {...props} />
)}
></Route>
<Route
path="/achieve"
render={(props) => (
<AchieveIndex {...this.props} {...props} />
)}
></Route>
<Route
path="/"
render={(props) => (
<AchieveIndex {...this.props} {...props} />
)}
></Route>
{/* <Route
exact
path="/"
render={(props) => (
this.props.current_user && this.props.current_user.login ?
<Infos {...this.props} {...props} />
:
<AchieveIndex {...this.props} {...props} />
)}
></Route> */}
</Switch>
</div>
);
}
}
export default withRouter(
ImageLayerOfCommentHOC({
imgSelector: ".imageLayerParent img, .imageLayerParent .imageTarget",
parentSelector: ".newMain",
})(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(Index))))
);

View File

@ -0,0 +1,142 @@
import React, { useEffect, useState } from 'react';
import { Descriptions, Modal } from 'antd';
import { Document, Page, pdfjs } from 'react-pdf';
import DelModal from '../../components/DelModal';
import { httpUrl } from '../../../fetch';
import { deleteAchieve, getAchieveDetail } from '../api';
import './index.scss';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
export default (props) => {
const { match, history, current_user } = props;
const id = match.params.achieveId;
const [achiveForm, setAchiveForm] = useState({});
const [createUserId, setCreateUserId] = useState("");
const [visible, setVisible] = useState(false);
const [fileType, setFileType] = useState(4);
const [fileUrl, setFileUrl] = useState(4);
const [numPages, setNumPages] = useState(null);
useEffect(() => {
getAchieveDetail(id).then(data => {
setAchiveForm(data);
setCreateUserId(data.user.id);
})
}, [id]);
function editItem() {
props.history.push(`/achieve/achieveEdit/${id}`);
}
function deletItem() {
DelModal(() => {
deleteAchieve(id).then(res => {
if (res.code === '1') {
props.showNotification("删除成功");
history.go(-1);
} else {
props.showNotification("删除失败");
}
})
});
}
function downFile(item) {
let url = httpUrl + '/busiAttachments/download/' + item.id;
window.open(url);
}
function pdfShow(e) {
setFileType(e.fileType);
if (e.fileType === 1 || e.fileType === 2 || e.fileType === 3) {
// pdf||video||img
setVisible(true);
setFileUrl(httpUrl + '/busiAttachments/view/' + e.id);
}
}
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
}
return (
<div className="centerbox">
<div className="center-content">
<div className="centerScreen">
<div className="center-left-but">{achiveForm.achievementName}</div>
{
createUserId === current_user.user_id &&
<div className="center-right-but" ><span onClick={editItem}>编辑</span> <span onClick={deletItem}>删除</span></div>
}
</div>
<Descriptions className="item-content" column={1}>
<Descriptions.Item label="成果类别">{achiveForm.achievementType && achiveForm.achievementType.dicItemName}</Descriptions.Item>
<Descriptions.Item label="标签">
{
achiveForm.achievementLabel && achiveForm.achievementLabel.map(itemx => {
return <span className="list-tag" key={itemx + 'ax'}>{itemx}</span>
})
}
</Descriptions.Item>
<Descriptions.Item label="单位名称">{achiveForm.achievementBelong}</Descriptions.Item>
<Descriptions.Item label="发布时间">{achiveForm.updatedAt}</Descriptions.Item>
<Descriptions.Item label="成果描述">{achiveForm.achievementRemark}</Descriptions.Item>
<Descriptions.Item label="成果文件" className="file-list-item">
{
achiveForm.achievementAttachments && achiveForm.achievementAttachments.map(item => {
return <div className="file-list-box" key={item.id + 'down'}>
<p onClick={() => { pdfShow(item) }}>{item.fileName}</p>
<span className="downP" onClick={() => { downFile(item) }}>下载</span>
</div>
})
}
</Descriptions.Item>
</Descriptions>
</div>
<Modal
visible={visible}
maskClosable={true}
closable={true}
footer={null}
onCancel={() => { setVisible(false) }}
className="file-modal"
>
{
fileType === 3 && <div >
<img className="show-img" src={fileUrl} alt="" />
</div>
}
{
fileType === 2 && <div>
<video src={fileUrl} style={{ width: '100%' }} controls="controls">您的浏览器不支持 video 标签</video>
</div>
}
{
fileType === 1 && <div className="pdf-box">
{
<Document
file={fileUrl}
onLoadSuccess={onDocumentLoadSuccess}
>
{new Array(numPages).fill("").map((item, index) => {
return (
<Page
loading=""
noData=""
key={index}
pageNumber={index + 1}
renderTextLayer={true}
/>
);
})}
</Document>
}
</div>
}
</Modal>
</div>
)
}

View File

@ -0,0 +1,33 @@
// 内容详情
.item-content {
padding: 10px 10px 0 30px;
}
.ant-descriptions-item-label {
width: 5em;
margin-right: 5px;
color: rgba(0, 0, 0, 0.65);
text-align: right;
}
.ant-descriptions-item-content {
color: rgba(0, 0, 0, 1);
}
.file-list-item > span {
vertical-align: top;
}
.file-list-box {
width: 600px;
height: 60px;
display: flex;
justify-content: space-between;
p:hover {
cursor: pointer;
color: #0089ff;
}
.downP {
color: #0089ff;
cursor: pointer;
text-decoration: none;
}
}

View File

@ -0,0 +1,236 @@
import React, { forwardRef, useCallback, useEffect, useState } from 'react';
import { Form, Input, Select, Button, } from 'antd';
import Upload from '../../components/Upload';
import { httpUrl } from '../../../fetch';
import { getDictionary, getAchieveDetail, addAchieve, editAchieve } from '../api';
import './index.scss';
const Option = Select.Option;
const { TextArea } = Input;
export default Form.create()(
forwardRef(({ form, showNotification, match, history }, ref) => {
const [shareType, setShareType] = useState([]);
const [tagType, setTagType] = useState([]);
const [fileList, setFileList] = useState(null);
const [achievementFileTypes, setAchievementFileTypes] = useState('');
const id = match.params.achieveId;
const { getFieldDecorator, validateFields, setFieldsValue } = form;
useEffect(() => {
id && getAchieveDetail(id).then(data => {
const formValue = {
achievementName: data.achievementName,
achievementTypeId: data.achievementTypeId,
achievementBelong: data.achievementBelong,
achievementLabel: data.achievementLabel,
achievementRemark: data.achievementRemark,
achievementFiles: data.achievementFiles
};
setFieldsValue(formValue);
if (data.achievementAttachments.length) {
for (const item of data.achievementAttachments) {
item.name = item.fileName;
item.size = item.fileSize;
item.uid = "rc-upload" + item.id
}
setFileList(data.achievementAttachments);
}
})
}, [id]);
//
useEffect(() => {
getDictionary('share_type').then((res) => {
setShareType(res.data);
});
getDictionary("tag_type").then((res) => {
setTagType(res.data);
});
}, []);
//
function UploadFunc(fileList) {
setFileList(fileList);
let files = [];
let fileTypes = [];
for (const item of fileList) {
files.push(item.id || item.response.data.id);
fileTypes.push(item.fileType || item.response.data.fileType);
}
setFieldsValue({
achievementFiles: files.join()
});
setAchievementFileTypes(fileTypes.join());
}
const helper = useCallback(
(label, name, rules, widget) => (
<Form.Item label={label}>
{getFieldDecorator(name, { rules, validateFirst: true })(widget)}
</Form.Item>
),
[]
);
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 12 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 20,
offset: 4,
},
sm: {
span: 20,
offset: 4,
},
},
};
//
function saveItem() {
validateFields((error, values) => {
if (!error) {
let params = {
...values,
achievementFileTypes,
};
params.achievementLabel = params.achievementLabel.join();
if (id) {
//
params.id = id;
editAchieve(params).then(res => {
if (res.code === '1') {
showNotification("成果更新成功!");
history.go(-1);
} else {
showNotification(res.message);
}
});
} else {
//
params.uploadUserId = 1;
addAchieve(params).then(res => {
if (res.code === '1') {
showNotification("成果新增成功!");
history.go(-1);
} else {
showNotification(res.message);
}
});
}
}
})
}
return (
<div className="centerbox">
<div className="center-content">
<div className="centerScreen">
<div className="center-left-but">{id ? '修改' : '新增'}成果</div>
</div>
<Form className="achieve-form" {...formItemLayout}>
{helper(
"成果名称:",
"achievementName",
[{ required: true, message: "请输入成果名称" }],
<Input
className="edit-input"
placeholder="请输入成果名称"
/>
)}
{helper(
"成果类别:",
"achievementTypeId",
[{ required: true, message: "请选择成果类别" }],
<Select placeholder="请选择成果类别"
className="edit-input"
>
{
shareType.map(item => {
return <Option key={item.dicItemName + item.id} value={item.id}>{item.dicItemName}</Option>
})
}
</Select>
)}
{helper(
"单位名称:",
"achievementBelong",
[{ required: true, message: "请输入单位名称" }],
<Input
className="edit-input"
placeholder="请输入单位名称"
/>
)}
<Form.Item label={"成果文件:"} required={true}>
<Upload
className="commentStyle"
load={UploadFunc}
size={100}
showNotification={showNotification}
actionUrl={httpUrl}
fileList={fileList}
/>
{/* 用一个隐藏的input实现上传文件的必填校验 */}
{getFieldDecorator('achievementFiles', {
rules: [{ required: true, message: "请上传成果文件" }], validateFirst: true
})(<Input style={{ display: 'none' }} />)}
</Form.Item>
{helper(
"标签:",
"achievementLabel",
[{ required: true, message: "请选择成果标签" }],
<Select
className="edit-input"
placeholder="请选择成果标签"
mode="tags"
tokenSeparators={[',']}
>
{
tagType.map(item => {
return <Option key={item.dicItemName}>{item.dicItemName}</Option>
})
}
</Select>
)}
{helper(
"成果描述:",
"achievementRemark",
[{ required: true, message: "请输入成果描述" }],
<TextArea
placeholder="请输入成果描述"
autoSize={{ minRows: 3, maxRows: 5 }}
/>
)}
<Form.Item {...tailFormItemLayout}>
<Button className="mr20" type={"primary"} onClick={saveItem}>保存</Button>
<Button onClick={() => { history.go(-1) }}>取消</Button>
</Form.Item>
</Form>
</div>
</div>
)
})
)

View File

@ -0,0 +1,4 @@
.achieve-form{
padding:24px 40px 0px 40px;
}

View File

@ -0,0 +1,148 @@
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { Pagination, Button, Icon } from 'antd';
import ChooseNav from '../../components/chooseNav';
import ItemList from '../../components/itemList';
import { getDictionary, getAchieveList } from '../api';
export default (props) => {
const { current_user } = props;
// console.log(current_user);
const [achievementTypeId, setAchievementTypeId] = useState(''); //
const [achievementFileTypes, setAchievementFileTypes] = useState(''); //
const [orderBy, setOrderBy] = useState('-1');
const [curPage, setCurPage] = useState(1);
const [total, setTotal] = useState(0);
const [industryOption, setIndustryOption] = useState([]);
const [achiveList, setAchiveList] = useState([]);
useEffect(() => {
getDictionary('share_type').then(res => {
if (res && res.data) setIndustryOption(res.data);
});
}, []);
useEffect(() => {
const params = {
achievementTypeId,
achievementFileTypes,
achievementLabel: '',
isOwned: '',
orderBy,
curPage,
pageSize: 10
};
getAchieveList(params).then(data => {
setAchiveList(data.rows);
setTotal(data.total);
})
}, [achievementTypeId, achievementFileTypes, orderBy, curPage]);
function changeOptionId(option, type) {
if (type === 'share_type') {
setAchievementTypeId(option.id);
setCurPage(1);
} else if (type === 'file_type') {
setAchievementFileTypes(option.id);
setCurPage(1);
}
}
function changeSort(sortType) {
setOrderBy(sortType);
setCurPage(1);
}
function goAchieveMine() {
props.history.push('/achieve/achieveMine');
}
function goAdd() {
props.history.push("/achieve/achieveAdd");
}
function achiveClick(id) {
props.history.push(`/achieve/achieveDetail/${id}`);
}
const filetypeOption = [
{
dicItemName: '文件',
id: 1
},
{
dicItemName: '视频',
id: 2
},
{
dicItemName: '图片',
id: 3
},
{
dicItemName: '软件',
id: 4
},
{
dicItemName: '其它',
id: 5
},
];
return (
<div className="centerbox">
<div className="nav-content">
<ChooseNav
key={'share_type'}
type={'share_type'}
title={'成果类别'}
options={industryOption}
changeOptionId={changeOptionId}
/>
<ChooseNav
key={'file_type'}
type={'file_type'}
title={'文件类型'}
options={filetypeOption}
changeOptionId={changeOptionId}
/>
</div>
<div className="center-content">
<div className="centerScreen">
<div className="center-left-but">
<div className={classNames({ 'center-left-butD': true, 'center-left-butDACT': orderBy === '-1' })} onClick={() => { changeSort('-1') }}>默认</div>
<div className={classNames({ 'center-left-butD': true, 'center-left-butDACT': orderBy === 'created_at' })} onClick={() => { changeSort('created_at') }}><Icon type="arrow-down" />最新</div>
<div className={classNames({ 'center-left-butD': true, 'center-left-butDACT': orderBy === 'view_count' })} onClick={() => { changeSort('view_count') }}><Icon type="arrow-down" />最热</div>
</div>
{
// current_user.login &&
<div className="center-right-but">
<Button className="mr15 font-12" size="small" type="primary" onClick={goAchieveMine}>我的成果</Button>
<Button className="mr20 font-12" size="small" type="primary" onClick={goAdd}><i className="iconfont icon-zaibianji font-12 mr3"></i>发布成果</Button>
</div>
}
</div>
<ItemList
list={achiveList}
itemClick={achiveClick}
/>
</div>
<div className="edu-txt-center ">
<Pagination
showQuickJumper
onChange={(page) => { setCurPage(page) }}
current={curPage}
total={total}
showTotal={total => `${total}`}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,82 @@
import React, { useEffect, useState } from 'react';
import { Pagination } from 'antd';
import ItemList from '../../components/itemList';
import DelModal from '../../components/DelModal';
import { deleteAchieve, getAchieveList } from '../api';
export default (props) => {
const [achiveList, setAchiveList] = useState([]);
const [curPage, setCurPage] = useState(1);
const [total, setTotal] = useState(0);
useEffect(() => {
getAcheiveListOwn();
}, [curPage]);
function getAcheiveListOwn() {
const params = {
achievementTypeId: '',
achievementFileTypes: '',
achievementLabel: '',
// isOwned: '', //便
isOwned: true,
orderBy: -1,
curPage,
pageSize: 10
};
getAchieveList(params).then(data => {
setAchiveList(data.rows);
setTotal(data.total);
})
}
function achiveClick(id) {
props.history.push(`/achieve/achieveDetail/${id}`);
}
function editItem(id) {
props.history.push(`/achieve/achieveEdit/${id}`);
}
function deletItem(id) {
DelModal(() => {
deleteAchieve(id).then(res => {
if (res.code === '1') {
props.showNotification("删除成功");
getAcheiveListOwn();
} else {
props.showNotification("删除失败");
}
})
});
}
return (
<div className="centerbox">
<div className="center-content">
<div className="centerScreen">
<div className="center-left-but">
我的成果
</div>
</div>
<ItemList
list={achiveList}
itemClick={achiveClick}
deletItem={deletItem}
editItem={editItem}
/>
</div>
<div className="edu-txt-center ">
<Pagination
showQuickJumper
onChange={(page) => { setCurPage(page) }}
current={curPage}
total={total}
showTotal={total => `${total}`}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,62 @@
import fetch from '../../fetch';
// 获取字典分类列表
export function getDictionary(id) {
return fetch({
url: '/dicItem/getData?dicTypeCode=' + id,
method: 'get'
});
}
// 成果列表查询
export async function getAchieveList(params) {
let res = await fetch({
url: '/api/achievement/',
method: 'get',
params,
})
if (res && res.data) {
for (const i of res.data.rows) {
// 处理分解标签
i.tag = i.achievementLabel.split(',');
}
}
return res.data;
}
//新增成果
export function addAchieve(data) {
return fetch({
url: '/api/achievement/',
method: 'post',
data: data
});
}
//删除成果
export function deleteAchieve(id) {
return fetch({
url: '/api/achievement/' + id,
method: 'DELETE',
});
}
//更新成果
export function editAchieve(data) {
return fetch({
url: '/api/achievement/',
method: 'put',
data: data
});
}
// 成果详情查询
export async function getAchieveDetail(id) {
let res = await fetch({
url: '/api/achievement/' + id,
method: 'get',
});
res.data.achievementLabel = res.data.achievementLabel.split(',');
return res.data;
}

View File

@ -0,0 +1,20 @@
import { Modal } from 'antd';
export default (
handleOk,
title,
content,
handleCancel) => {
return Modal.confirm({
title: title || "警告",
content: content || "确认删除?",
okText: '确定',
cancelText: '取消',
onOk() {
handleOk && handleOk();
},
onCancel() {
handleCancel && handleCancel();
}
});
}

View File

@ -0,0 +1,97 @@
import React, { useEffect, useState } from "react";
import { Upload, Button } from 'antd';
import axios from 'axios';
import { appendFileSizeToUploadFileAll } from 'educoder';
axios.defaults.withCredentials = true;
function Uploads({ className, size, actionUrl, fileList, showNotification, load }) {
const [files, setFiles] = useState(undefined);
useEffect(() => {
if (fileList) {
init();
}
}, [fileList]);
function init() {
let f = appendFileSizeToUploadFileAll(fileList);
setFiles(f);
}
function onAttachmentRemove(file) {
if (!file.percent || file.percent === 100) {
deleteAttachment(file);
return false;
}
}
function deleteAttachment(file) {
let id = (file.response && file.response.data && file.response.data.id) || file.id;
//
let nf = files.filter(item => {
let itemId = (item.response && item.response.data && item.response.data.id) || item.id;
return itemId !== id;
});
setFiles(nf);
// fileIdList(nf);
load && load(nf);
//
// const url = actionUrl + `/busiAttachments/${id}`;
// axios.delete(url).then((response) => {
// if (response.data) {
// if (response.data.code === "1") {
// let nf = files.filter(item => (item.response.data.id !== id) && (item.id !== id));
// setFiles(nf);
// fileIdList(nf);
// } else {
// showNotification(response.data.message);
// }
// }
// }).catch(function (error) {
// console.log(error);
// });
}
function handleChange(info) {
if (info.file.status === 'uploading' || info.file.status === 'done' || info.file.status === 'removed') {
let fileList = info.fileList;
setFiles(appendFileSizeToUploadFileAll(fileList));
if (info.file.status === 'done' || info.file.status === 'removed') load && load(fileList);
}
}
// function fileIdList(fileList) {
// let id = [];
// let fileType = [];
// for (const item of fileList) {
// id.push(item.id || item.response.data.id);
// fileType.push(item.fileType || item.response.data.fileType);
// }
// load && load(id.join(), fileType.join());
// }
function beforeUpload(file) {
const isLt100M = file.size / 1024 / 1024 < size;
if (!isLt100M) {
showNotification(`文件大小必须小于${size}MB!`);
}
return isLt100M;
}
const upload = {
name: 'file',
fileList: files,
action: actionUrl + `/busiAttachments/upload`,
onChange: handleChange,
onRemove: onAttachmentRemove,
beforeUpload: beforeUpload,
};
return (
<Upload {...upload} className={className}>
<Button type="primary">点击上传</Button>
<span className="ml10 color-grey-9">(你可以上传小于<span className="color-red">{size}MB</span>的文件)</span>
</Upload>
)
}
export default Uploads;

View File

@ -0,0 +1,29 @@
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import './index.scss';
export default (props) => {
const { title, options, changeOptionId, type } = props;
const [option, setOption] = useState({ id: "", dicItemName: "" ,dicItemCode:""});
useEffect(() => {
changeOptionId(option, type);
}, [option])
return (
<div className="shop-box">
<div className="choose-title">{title}</div>
<div className="choose-list">
<div className={classNames({ "choose-item-checked": option.id === "", "choose-item": true })} key={"all"} onClick={() => { setOption({ id: "", dicItemName: "" }) }}>全部</div>
{
options.map((item) => {
return <div className={classNames({ "choose-item-checked": option.id === item.id, "choose-item": true })} key={item.dicItemName} onClick={() => { setOption(item) }} >{item.dicItemName}</div>
})
}
</div>
</div>
)
}

View File

@ -0,0 +1,41 @@
.nav-content {
margin-top:20px;
background: #fff;
border-bottom: 1px solid #eaeaea;
box-shadow: #ddd 0px 0px 5px;
}
.shop-box {
width: 1280px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: start;
}
.choose-title {
width: 110px;
background: #f3f3f3;
text-align: center;
padding: 16px 0;
font-size: 14px;
color: #666;
font-weight: 600;
}
.choose-list {
width: 1170px;
padding: 16px 0;
display: flex;
justify-content: start;
}
.choose-item {
font-size: 14px;
text-align: center;
padding: 0 15px ;
cursor: pointer;
&:hover{
color: #0072ff;
}
}
.choose-item-checked {
color: #0072ff;
}

View File

@ -0,0 +1,42 @@
import React from 'react';
import './index.scss';
const statusArr = ['待审核', '进行中', '成果验收中', '完成', '审核未通过', '延期', '其他', '验收未通过'];
const classArr = ['', 'list-done', 'list-done', 'list-done', 'list-error', '', '', 'list-error'];
export default (props) => {
const { list, itemClick, editItem, deletItem, activeByStatus } = props;
return (
list.map(item => {
return (
<div className="list-box" key={item.id}>
<div className="list-title" onClick={() => { itemClick(item.id) }}>
{item.achievementName || item.title}
{item.status && <span className={classArr[item.status]}>{statusArr[item.status]}</span>}
</div>
<div >
{
item.tag.map(i => {
return <span className="list-tag" key={i + "av"}>{i}</span>;
})
}
</div>
<div className="list-other">
{item.user && <p>发布者{item.user.nickname ? item.user.nickname : item.user.login}</p>}
{item.createUser && <p>发布者{item.createUser.nickname ? item.createUser.nickname : item.createUser.login}</p>}
<p>发布时间{item.achievementPublishTime || item.createTime}</p>
</div>
{
editItem && <div className="list-box-action">
{
!activeByStatus || (activeByStatus && item.status == '0' || item.status == '4' || item.status == '5') &&
<button onClick={() => { editItem(item.id) }}><i className="iconfont icon-zaibianji font-25"></i></button>
}
<button onClick={() => { deletItem(item.id) }}><i className="iconfont icon-shanchu font-30 "></i></button>
</div>
}
</div>
)
})
)
}

View File

@ -0,0 +1,70 @@
.list-box {
position: relative;
padding: 20px;
background: #fff;
border-bottom: 1px solid #dedede;
}
.list-title {
font-size: 17px;
color: #000;
cursor: pointer;
}
.list-title:hover {
color: #409eff;
}
.list-title span {
padding: 3px 5px;
margin-left:.5em;
background: #f8c753;
font-size: 13px;
color: #fff;
border-radius: 3px;
}
span.list-done {
background: #35d77e;
}
span.list-error {
background: #f56c6c;
}
.list-tag {
display: inline-block;
margin: 5px 10px 5px 0;
background: #cfe9ff;
color: #0089ff;
font-size: 14px;
border-radius: 3px;
padding: 2px 5px;
}
.list-other > p {
display: inline-block;
margin: 0 10px 0 0;
font-size: 12px;
color: #666;
}
.list-box-action {
position: absolute;
display: flex;
align-items: center;
top: 0;
bottom: 0;
right: 100px;
visibility: hidden;
}
.list-box:hover .list-box-action {
visibility: visible;
}
.list-box-action button {
display: inline-block;
margin: 0 25px;
width: 50px;
height: 50px;
color: #0089ff;
line-height: 50px;
text-align: center;
border: 0;
border-radius: 25px;
box-shadow: 2px 2px 5px 2px #eee;
cursor: pointer;
}

View File

@ -0,0 +1,100 @@
// 本模块公共样式
.centerbox {
width: 80vw;
max-width: 1280px;
margin: 40px auto;
}
.center-content {
margin: 20px 0;
background: #fff;
border: 1px solid #dedede;
box-shadow: #eee 0px 1px 1px 3px;
}
.centerScreen {
display: flex;
justify-content: space-between;
height: 46px;
background-color: #fff;
border-bottom: 1px solid #dedede;
}
// 内容标题左侧样式
.center-left-but {
display: flex;
justify-content: start;
align-items: center;
margin-left: 20px;
font-size: 16px;
font-weight: 600;
}
.center-left-butD {
height: 24px;
font-size: 14px;
font-weight: 500;
line-height: 24px;
color: #333;
padding: 0 10px;
background-color: #fff;
border: 1px solid #dedede;
}
.center-left-butD:hover {
background-color: #fff;
color: #409eff;
cursor: pointer;
}
.center-left-butDACT {
background-color: #409eff;
color: #fff;
}
// 内容标题右侧样式
.center-right-but {
display: flex;
align-items: center;
}
.center-right-but > span {
margin: 0 10px;
font-size: 16px;
color: #0089ff;
cursor: pointer;
}
// 通用标签样式
.list-tag {
display: inline-block;
margin-right: 10px;
background: #cfe9ff;
color: #0089ff;
font-size: 14px;
border-radius: 3px;
padding: 2px 5px;
line-height: 25px;
}
// 文件预览modal样式
.file-modal {
width: 800px !important;
.ant-modal-body {
padding-top: 40px;
.pdf-box {
height: 70vh;
overflow-y: scroll;
}
}
.ant-modal-close {
top: 0 !important;
}
.show-img {
width: 100%;
}
.react-pdf__Page__canvas,
.react-pdf__Page__textContent {
margin: 0 auto;
max-width: 100%;
}
}
.edit-input {
max-width: 500px;
}

View File

@ -0,0 +1,95 @@
import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
import { withRouter } from "react-router";
import { SnackbarHOC } from "educoder";
import { CNotificationHOC } from "../modules/courses/common/CNotificationHOC";
import { TPMIndexHOC } from "../modules/tpm/TPMIndexHOC";
import Loadable from "react-loadable";
import Loading from "../Loading";
import { ImageLayerOfCommentHOC } from "../modules/page/layers/ImageLayerOfCommentHOC";
import './index.scss';
const RequireList = Loadable({
loader: () => import("./requirement/requireList"),
loading: Loading,
});
const RequireDetail = Loadable({
loader: () => import("./requirement/requireDetail"),
loading: Loading,
});
const RequireMine = Loadable({
loader: () => import("./requirement/requireMine"),
loading: Loading,
});
const RequireAdd = Loadable({
loader: () => import("./requirement/requireEdit"),
loading: Loading,
});
const RequireEdit = Loadable({
loader: () => import("./requirement/requireEdit"),
loading: Loading,
});
class Index extends Component {
render() {
return (
<div className="newMain clearfix">
<Switch {...this.props}>
<Route
path="/requirement/requireDetail/:requireId"
render={(props) => (
<RequireDetail {...this.props} {...props} />
)}
></Route>
<Route
path="/requirement/requireMine"
render={(props) => (
<RequireMine {...this.props} {...props} />
)}
></Route>
<Route
path="/requirement/requireAdd"
render={(props) => (
<RequireAdd {...this.props} {...props} />
)}
></Route>
<Route
path="/requirement/requireEdit/:requireId"
render={(props) => (
<RequireEdit {...this.props} {...props} />
)}
></Route>
<Route
path="/requirement"
render={(props) => (
<RequireList {...this.props} {...props} />
)}
></Route>
{/* <Route
exact
path="/"
render={(props) => (
this.props.current_user && this.props.current_user.login ?
<Infos {...this.props} {...props} />
:
<RequireIndex {...this.props} {...props} />
)}
></Route> */}
</Switch>
</div>
);
}
}
export default withRouter(
ImageLayerOfCommentHOC({
imgSelector: ".imageLayerParent img, .imageLayerParent .imageTarget",
parentSelector: ".newMain",
})(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(Index))))
);

View File

@ -0,0 +1,71 @@
import fetch from '../../fetch';
// 获取字典分类列表
export function getDictionary(id) {
return fetch({
url: '/dicItem/getData?dicTypeCode=' + id,
method: 'get'
});
}
// 需求列表查询
export async function getRequirementList(data) {
let res = await fetch({
url: '/requirement/page',
method: 'post',
data,
});
if (res && res.data && res.data.rows) {
for (const i of res.data.rows) {
// 处理分解标签
i.tag = i.tag.split(',');
}
}
return res.data;
}
//新增需求
export function addRequirement(data) {
return fetch({
url: '/requirement/add',
method: 'post',
data: data
});
}
//删除需求
export function deleteRequirement(id) {
return fetch({
url: '/requirement/delete?id=' + id,
method: 'DELETE',
});
}
//更新需求
export function updateRequirement(data) {
return fetch({
url: '/requirement/update',
method: 'put',
data: data
});
}
// 需求详情查询
export async function getRequirementDetail(id) {
let res = await fetch({
url: '/requirement/getInfo?id=' + id,
method: 'get',
});
if (res && res.data) {
res.data.tag = res.data.tag.split(',');
return res.data;
}
}
//获取用户列表
export function getAllUsers() {
return fetch({
url: '/user/getUsers',
method: 'get'
});
}

View File

@ -0,0 +1,217 @@
import React, { useEffect, useState } from 'react';
import { Descriptions, Modal, Button } from 'antd';
import { Document, Page, pdfjs } from 'react-pdf';
import DelModal from '../../components/DelModal';
import Upload from '../../components/Upload';
import { httpUrl } from '../../../fetch';
import { deleteRequirement, getRequirementDetail, updateRequirement } from '../api';
import './index.scss';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
export default ({ match, history, current_user, showNotification }) => {
const id = match.params.requireId;
const [requireForm, setRequireForm] = useState({});
const [visible, setVisible] = useState(false);
const [visibleAchieve, setVisibleAchieve] = useState(false);
const [fileType, setFileType] = useState(4);
const [fileUrl, setFileUrl] = useState(4);
const [numPages, setNumPages] = useState(null);
const [fileList, setFileList] = useState(null);
const [edit, setEdit] = useState(false);
useEffect(() => {
getRequirementDetail(id).then(data => {
if (data) {
setRequireForm(data);
if (data.achievementList) {
for (const item of data.achievementList) {
item.name = item.fileName;
item.size = item.fileSize;
item.uid = "rc-upload" + item.id
}
setFileList(data.achievementList);
}
}
});
}, [id, edit]);
function editItem() {
history.push(`/requirement/requireEdit/${id}`);
}
function deletItem() {
DelModal(() => {
deleteRequirement(id).then(res => {
if (res.code === '1') {
showNotification("删除成功");
history.go(-1);
} else {
showNotification("删除失败");
}
})
});
}
function downFile(item) {
let url = httpUrl + '/busiAttachments/download/' + item.id;
window.open(url);
}
function pdfShow(e) {
setFileType(e.fileType);
if (e.fileType === 1 || e.fileType === 2 || e.fileType === 3) {
// pdf||video||img
setVisible(true);
setFileUrl(httpUrl + '/busiAttachments/view/' + e.id);
}
}
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
}
//
function UploadFunc(fileList) {
setFileList(fileList);
}
function uploadAchievement() {
let files = [];
let filestype = [];
for (const item of fileList) {
files.push(item.id || item.response.data.id);
filestype.push(item.fileType || item.response.data.fileType);
}
let data = {
id,
achievements: files.toString(),
// createUserAccount: sessionStorage.getItem('SET_Account'),//
// createUserName: sessionStorage.getItem('SET_NAME'),
status: "2"
}
updateRequirement(data).then(res => {
if (res.code === '1') {
showNotification('更新成果文件成功!');
setEdit(!edit);
setVisibleAchieve(false);
}
})
}
return (
<div className="centerbox">
<div className="center-content">
<div className="centerScreen">
<div className="center-left-but">{requireForm.title}</div>
{
<div className="center-right-but">
{
// (role||operator||needsForm.assignedUserLogin==userid||needsForm.createUserAccount==userid)&&(needsForm.status==1||needsForm.status==2||needsForm.status==7||needsForm.status==5)
((current_user.admin || current_user.operator || requireForm.assignedUserLogin == current_user.login || requireForm.createUserAccount == current_user.login)
&& (requireForm.status == 1 || requireForm.status == 2 || requireForm.status == 7 || requireForm.status == 5)) &&
<Button className="mr15 font-12" size="small" type="primary" onClick={() => { setVisibleAchieve(true) }}>上传成果</Button>
}
{
requireForm.createUserAccount === current_user.login &&
<React.Fragment>
<span onClick={editItem}>编辑</span>
<span onClick={deletItem}>删除</span>
</React.Fragment>
}
</div>
}
</div>
<Descriptions className="item-content" column={1}>
<Descriptions.Item label="需求类别">{requireForm.typeName}</Descriptions.Item>
<Descriptions.Item label="标签">
{
requireForm.tag && requireForm.tag.map(itemx => {
return <span className="list-tag" key={itemx + 'ax'}>{itemx}</span>
})
}
</Descriptions.Item>
<Descriptions.Item label="指派人员">{requireForm.assignedUserName}</Descriptions.Item>
<Descriptions.Item label="结束时间">{requireForm.endTime}</Descriptions.Item>
<Descriptions.Item label="需求描述">{requireForm.remark}</Descriptions.Item>
<Descriptions.Item label="需求文件" className="file-list-item">
{
requireForm.attachmentList && requireForm.attachmentList.map(item => {
return <div className="file-list-box" key={item.id + 'down'}>
<p onClick={() => { pdfShow(item) }}>{item.fileName}</p>
<span className="downP" onClick={() => { downFile(item) }}>下载</span>
</div>
})
}
</Descriptions.Item>
</Descriptions>
</div>
<Modal
visible={visible}
maskClosable={true}
closable={true}
footer={null}
onCancel={() => { setVisible(false) }}
className="file-modal"
>
{
fileType === 3 && <div >
<img className="show-img" src={fileUrl} alt="" />
</div>
}
{
fileType === 2 && <div>
<video src={fileUrl} style={{ width: '100%' }} controls="controls">您的浏览器不支持 video 标签</video>
</div>
}
{
fileType === 1 && <div className="pdf-box">
{
<Document
file={fileUrl}
onLoadSuccess={onDocumentLoadSuccess}
>
{new Array(numPages).fill("").map((item, index) => {
return (
<Page
loading=""
noData=""
key={index}
pageNumber={index + 1}
renderTextLayer={true}
width={750}
/>
);
})}
</Document>
}
</div>
}
</Modal>
<Modal
visible={visibleAchieve}
maskClosable={true}
closable={true}
onCancel={() => { setVisibleAchieve(false) }}
onOk={uploadAchievement}
className="upload-modal"
>
<Upload
className="commentStyle"
load={UploadFunc}
size={100}
showNotification={showNotification}
actionUrl={httpUrl}
fileList={fileList}
/>
</Modal>
</div>
)
}

View File

@ -0,0 +1,40 @@
// 内容详情
.item-content {
padding: 10px 10px 0 30px;
}
.ant-descriptions-item-label {
width: 5em;
margin-right: 5px;
color: rgba(0, 0, 0, 0.65);
text-align: right;
}
.ant-descriptions-item-content {
color: rgba(0, 0, 0, 1);
}
.file-list-item > span {
vertical-align: top;
}
.file-list-box {
width: 600px;
height: 60px;
display: flex;
justify-content: space-between;
p:hover {
cursor: pointer;
color: #0089ff;
}
.downP {
color: #0089ff;
cursor: pointer;
text-decoration: none;
}
}
.upload-modal{
width: 800px !important;
.commentStyle{
display: inline-block;
width: 100%;
}
}

View File

@ -0,0 +1,247 @@
import React, { forwardRef, useCallback, useEffect, useState } from 'react';
import { Form, Input, Select, Button, DatePicker } from 'antd';
import moment from 'moment';
import Upload from '../../components/Upload';
import { httpUrl } from '../../../fetch';
import { getDictionary, getRequirementDetail, addRequirement, updateRequirement } from '../api';
import './index.scss';
const Option = Select.Option;
const { TextArea } = Input;
const format = "YYYY-MM-DD HH:mm:ss";
export default Form.create()(forwardRef(({current_user, form, showNotification, match, history }, ref) => {
const [requireType, setRequireType] = useState([]);
const [tagType, setTagType] = useState([]);
const [fileList, setFileList] = useState(null);
const [typeCode, setTypeCode] = useState('');
const [typeName, setTypeName] = useState('');
const id = match.params.requireId;
const { getFieldDecorator, validateFields, setFieldsValue, } = form;
useEffect(() => {
id && getRequirementDetail(id).then(data => {
const formValue = {
title: data.title,
attachments: data.attachments,
// typeCode: data.typeCode, //使select labelInValue
endTime: moment(data.endTime),
tag: data.tag,
remark: data.remark
};
setFieldsValue(formValue);
setTypeName(data.typeName);
setTypeCode(data.typeCode)
if (data.attachmentList.length) {
for (const item of data.attachmentList) {
item.name = item.fileName;
item.size = item.fileSize;
item.uid = "rc-upload" + item.id
}
setFileList(data.attachmentList);
}
})
}, [id]);
//
useEffect(() => {
getDictionary('requirement_type').then((res) => {
setRequireType(res.data);
});
getDictionary("tag_type").then((res) => {
setTagType(res.data);
});
}, []);
//
function UploadFunc(fileList) {
setFileList(fileList);
let files = [];
for (const item of fileList) {
files.push(item.id || item.response.data.id);
}
setFieldsValue({
attachments: files.join()
});
}
const helper = useCallback(
(label, name, rules, widget, initialValue) => (
<Form.Item label={label}>
{getFieldDecorator(name, { rules, initialValue, validateFirst: true, })(widget)}
</Form.Item>
), []);
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 12 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 20,
offset: 4,
},
sm: {
span: 20,
offset: 4,
},
},
};
//
function saveItem() {
validateFields((error, values) => {
if (!error) {
let params = {
...values,
createUserAccount:current_user.login|| sessionStorage.getItem("SET_Account"), //
createUserName:current_user.username|| sessionStorage.getItem("SET_NAME"), //使
typeName,
typeCode,
status: 0, //
};
params.tag = params.tag.join();
params.endTime = params.endTime.format(format);
if (id) {
//
params.id = id;
updateRequirement(params).then(res => {
if (res.code === '1') {
showNotification("需求更新成功!");
history.go(-1);
} else {
showNotification(res.message);
}
});
} else {
//
addRequirement(params).then(res => {
if (res.code === '1') {
showNotification("需求新增成功!");
history.go(-1);
} else {
showNotification(res.message);
}
});
}
}
})
}
function changeTypeName(value) {
setTypeCode(value.key);
setTypeName(value.label);
}
return (
<div className="centerbox">
<div className="center-content">
<div className="centerScreen">
<div className="center-left-but">{id ? '修改' : '新增'}需求</div>
</div>
<Form className="achieve-form" {...formItemLayout}>
{helper(
"需求名称:",
"title",
[{ required: true, message: "请输入需求名称" }],
<Input
className="edit-input"
placeholder="请输入需求名称"
/>
)}
{helper(
"需求类型:",
"typeCode",
[{ required: true, message: "请选择需求类别" }],
<Select placeholder="请选择需求类别"
className="edit-input"
labelInValue
onChange={changeTypeName}
>
{
requireType.map(item => {
return <Option key={item.id + ''} value={item.id + ''}>{item.dicItemName}</Option>
})
}
</Select>
, { key: typeCode, label: typeName })}
{helper(
"结束时间:",
"endTime",
[{ required: true, message: "请输入结束时间" }],
<DatePicker
showTime
format={format}
placeholder="请输入结束时间"
/>
)}
<Form.Item label={"需求文件:"} required={true}>
<Upload
className="commentStyle"
load={UploadFunc}
size={100}
showNotification={showNotification}
actionUrl={httpUrl}
fileList={fileList}
/>
{/* 用一个隐藏的input实现上传文件的必填校验 */}
{getFieldDecorator('attachments', {
rules: [{ required: true, message: "请上传需求文件" }], validateFirst: true
})(<Input style={{ display: 'none' }} />)}
</Form.Item>
{helper(
"标签:",
"tag",
[{ required: true, message: "请选择需求标签" }],
<Select
placeholder="请选择需求标签"
className="edit-input"
mode="tags"
tokenSeparators={[',']}
>
{
tagType.map(item => {
return <Option key={item.dicItemName}>{item.dicItemName}</Option>
})
}
</Select>
)}
{helper(
"需求描述:",
"remark",
[{ required: true, message: "请输入需求描述" }],
<TextArea
placeholder="请输入需求描述"
autoSize={{ minRows: 3, maxRows: 5 }}
/>
)}
<Form.Item {...tailFormItemLayout}>
<Button className="mr20" type={"primary"} onClick={saveItem}>保存</Button>
<Button onClick={() => { history.go(-1) }}>取消</Button>
</Form.Item>
</Form>
</div>
</div>
)
})
)

View File

@ -0,0 +1,4 @@
.achieve-form{
padding:24px 40px 0px 40px;
}

View File

@ -0,0 +1,117 @@
import React, { useEffect, useState } from 'react';
import { Pagination, Button } from 'antd';
import ChooseNav from '../../components/chooseNav';
import ItemList from '../../components/itemList';
import { getDictionary, getRequirementList } from '../api';
export default (props) => {
const { current_user } = props;
// console.log(current_user);
const [requirementType, setrequirementType] = useState([]); //
const [requirementStatusArr, setrequirementStatusArr] = useState([]); //
const [requirementTypeName, setRequirementTypeName] = useState(''); //
const [requirementStatus, setRequirementStatus] = useState(''); //
const [curPage, setCurPage] = useState(1);
const [total, setTotal] = useState(0);
const [requirementList, setRequirementList] = useState([]);
useEffect(() => {
getDictionary('requirement_type').then(res => {
if (res && res.data) setrequirementType(res.data);
});
getDictionary('requirement_status').then(res => {
if (res && res.data) setrequirementStatusArr(res.data);
});
}, []);
useEffect(() => {
const params = {
tag: null,
status: requirementStatus || null,
typeName: requirementTypeName || null,
queryType: 'all',
curPage,
pageSize: 10
};
getRequirementList(params).then(data => {
if (data && data.rows) {
setRequirementList(data.rows);
setTotal(data.total);
}
})
}, [requirementTypeName, requirementStatus, curPage]);
function changeOptionId(option, type) {
if (type === 'requirement_type') {
setRequirementTypeName(option.dicItemName);
setCurPage(1);
} else if (type === 'requirement_status') {
setRequirementStatus(option.dicItemCode);
setCurPage(1);
}
}
function goRequirementMine() {
props.history.push('/requirement/requireMine');
}
function goAdd() {
props.history.push("/requirement/requireAdd");
}
function achiveClick(id) {
props.history.push(`/requirement/requireDetail/${id}`);
}
return (
<div className="centerbox">
<div className="nav-content">
<ChooseNav
key={'requirement_type'}
type={'requirement_type'}
title={'需求类别'}
options={requirementType}
changeOptionId={changeOptionId}
/>
<ChooseNav
key={'requirement_status'}
type={'requirement_status'}
title={'需求状态'}
options={requirementStatusArr}
changeOptionId={changeOptionId}
/>
</div>
<div className="center-content">
<div className="centerScreen">
<div className="center-left-but">需求大厅</div>
{
current_user.login &&
<div className="center-right-but">
<Button className="mr15 font-12" size="small" type="primary" onClick={goRequirementMine}>我的需求</Button>
<Button className="mr20 font-12" size="small" type="primary" onClick={goAdd}><i className="iconfont icon-zaibianji font-12 mr3"></i>发布需求</Button>
</div>
}
</div>
<ItemList
list={requirementList}
itemClick={achiveClick}
/>
</div>
<div className="edu-txt-center ">
<Pagination
showQuickJumper
onChange={(page) => { setCurPage(page) }}
current={curPage}
total={total}
showTotal={total => `${total}`}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,333 @@
import React, { useEffect, forwardRef, useCallback, useState } from 'react';
import classnames from 'classnames';
import { Form, Radio, Input, Select, Pagination, Table, Button, Modal } from 'antd';
import DelModal from '../../components/DelModal';
import { deleteRequirement, getRequirementList, getAllUsers, updateRequirement } from '../api';
const Option = Select.Option;
const { TextArea } = Input;
const statusArr = ['待审核', '进行中', '成果验收中', '完成', '审核未通过', '延期', '其他', '验收未通过'];
const chooseStatusArr = ['待审核', '进行中', '成果验收中', '完成', '审核未通过', '延期', '全部'];
const statusColor = ['#f8c753', '#67c23a', '#67c23a', '#67c23a', '#F56C6C', '#f8c753', '#f8c753', '#F56C6C'];
export default Form.create()(forwardRef(({ form, showNotification, match, history }, ref) => {
const [requirementList, setRequirementList] = useState([]);
const [curPage, setCurPage] = useState(1);
const [total, setTotal] = useState(0);
const [status, setStatus] = useState(6);
const [checkVisible, setCheckVisible] = useState(false);
const [checkStatus, setCheckStatus] = useState(1);
const [acceptVisible, setAcceptVisible] = useState(false);
const [currentId, setCurrentId] = useState('');
const [allUsers, setAllUsers] = useState([]);
const { getFieldDecorator, validateFields, } = form;
useEffect(() => {
getRequireList();
}, [curPage, status]);
useEffect(() => {
if (checkVisible) {
getAllUsers().then(res => {
if (res.code === '1') {
setAllUsers(res.data);
} else {
showNotification(res.message);
}
})
}
}, [checkVisible]);
function getRequireList() {
let searchStaus = "";
if (status !== 6) {
searchStaus = status;
}
const params = {
curPage,
pageSize: 10,
status: searchStaus,
tag: "",
title: "",
typeName: "",
currentUser: "",
queryType: "all" //all: my: tome:
};
getRequirementList(params).then(data => {
if (data) {
for (const item of data.rows) {
item.createUserName = item.createUser.nickname ? item.createUser.nickname : item.createUser.login;
}
setRequirementList(data.rows);
setTotal(data.total);
}
})
}
function detailItem(id) {
history.push(`/requirement/requireDetail/${id}`);
}
function editItem(id) {
history.push(`/requirement/requireEdit/${id}`);
}
function deletItem(id) {
DelModal(() => {
deleteRequirement(id).then(res => {
if (res.code === '1') {
showNotification("删除成功");
getRequireList();
} else {
showNotification("删除失败");
}
})
}, '此操作将删除该记录, 是否继续?');
}
//
function checkItem() {
validateFields((error, values) => {
if (!error) {
let params = {
id: currentId,
status: checkStatus, //
assignedUserLogin: values.vertifyUserAccount ? values.vertifyUserAccount.key : '',
assignedUserName: values.vertifyUserAccount ? values.vertifyUserAccount.label : '',
vertifyUserAccount: values.vertifyUserAccount ? values.vertifyUserAccount.key : '',
vertifyUserName: values.vertifyUserAccount ? values.vertifyUserAccount.label : '',
vertifyComments: values.vertifyComments,
};
console.log(params);
updateRequirement(params).then(res => {
if (res.code === '1') {
showNotification("操作成功!");
setCheckVisible(false);
getRequireList();
} else {
showNotification(res.message);
}
});
}
})
}
//
function acceptItem() {
validateFields((error, values) => {
if (!error) {
let params = {
status: values.status,
id: currentId,
checkComments: values.checkComments,
};
console.log(params);
updateRequirement(params).then(res => {
if (res.code === '1') {
showNotification("操作成功!");
setAcceptVisible(false);
getRequireList();
} else {
showNotification(res.message);
}
});
}
})
}
const columns = [
{
title: '需求标题',
dataIndex: 'title',
key: 'title',
width: '20%',
},
{
title: '类型',
dataIndex: 'typeName',
key: 'typeName',
},
{
title: '创建人',
dataIndex: 'createUserName',
key: 'createUserName',
},
{
title: '指派人',
key: 'assignedUserName',
dataIndex: 'assignedUserName',
},
{
title: '创建时间',
key: 'createTime',
dataIndex: 'createTime',
},
{
title: '结束时间',
key: 'endTime',
dataIndex: 'endTime',
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
render: (text, record) => {
return <span style={{ color: statusColor[text] }}>{statusArr[text]}</span>
}
},
{
title: '操作',
key: 'action',
render: (text, record) => (
<span>
<Button className="mr5 font-12" type="primary" size="small" onClick={() => { editItem(record.id) }}><i className="iconfont icon-zaibianji font-15 mr3"></i></Button>
<Button className="mr5 font-12" type="danger" size="small" onClick={() => { deletItem(record.id) }}><i className="iconfont icon-shanchu font-15 "></i></Button>
<Button className="mr5 font-12" type="info" size="small" onClick={() => { detailItem(record.id) }}>详情</Button>
{record.status === '0' && <Button className="mr5 font-12" type="primary" size="small" onClick={() => { setCurrentId(record.id); setCheckVisible(true); }}>审核</Button>}
{record.status === '2' && <Button className="mr5 font-12" type="success" size="small" onClick={() => { setCurrentId(record.id); setAcceptVisible(true) }}>验收成果</Button>}
</span>
),
},
];
const helper = useCallback(
(label, name, rules, widget, initialValue) => (
<Form.Item label={label}>
{getFieldDecorator(name, { rules, initialValue, validateFirst: true, })(widget)}
</Form.Item>
), []);
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 12 },
sm: { span: 18 },
},
};
return (
<React.Fragment>
<div className="choose-status">
{
chooseStatusArr.map((item, i) => {
return <Button className={classnames({ mr10: true, active: status === i })} type="dashed" key={i} onClick={() => { setStatus(i) }}> {item}</Button>
})
}
</div>
<Table
rowKey={(row) => row.id}
dataSource={requirementList}
columns={columns}
pagination={false}
/>
<div className="edu-txt-center mt10">
<Pagination
showQuickJumper
onChange={(page) => { setCurPage(page) }}
current={curPage}
total={total}
/>
</div>
<Modal
title="需求审核"
visible={checkVisible}
onOk={checkItem}
onCancel={() => { setCheckVisible(false) }}
className="form-edit-modal"
>
{checkVisible && <Form {...formItemLayout}>
<Form.Item label="审批结果:">
<Radio.Group value={checkStatus} onChange={(e) => { setCheckStatus(e.target.value) }}>
<Radio className="columsRadio" value={1}>
同意
</Radio>
<Radio className="columsRadio" value={4}>
拒绝
</Radio>
</Radio.Group>
</Form.Item>
{helper(
"指派用户:",
"vertifyUserAccount",
[{ required: checkStatus === 1, message: "请指派用户" }],
<Select placeholder="请指派用户"
className="edit-input"
showSearch
labelInValue
disabled={checkStatus === 4}
filterOption={(input, option) =>
option.props && option.props.children && (option.props.children.indexOf(input) > -1 || option.props.children.toLowerCase().indexOf(input.toLowerCase()) > -1)
}
>
{
allUsers.map((item, i) => {
return <Option key={item.id} value={item.login}>{item.nickName}</Option>
})
}
</Select>
)}
{helper(
"审核意见:",
"vertifyComments",
[{ required: true, message: "请输入审核意见" }],
<TextArea
className="edit-input"
placeholder="请输入审核意见"
/>
)}
</Form>}
</Modal>
<Modal
title="验收成果"
visible={acceptVisible}
onOk={acceptItem}
onCancel={() => { setAcceptVisible(false) }}
className="form-edit-modal"
>
{acceptVisible && <Form {...formItemLayout}>
{helper(
"验收结果:",
"status",
[{ required: true, message: "请选择验收结果" }],
<Radio.Group>
<Radio className="columsRadio" value={3}>
同意
</Radio>
<Radio className="columsRadio" value={7}>
拒绝
</Radio>
</Radio.Group>
)}
{helper(
"验收意见:",
"checkComments",
[],
<TextArea
className="edit-input"
placeholder="请输入验收意见"
/>
)}
</Form>}
</Modal>
</React.Fragment>
)
})
)

View File

@ -0,0 +1,25 @@
import React from 'react';
import { Tabs } from 'antd';
import AllRequire from './allRequire';
import MyRequire from './myRequire';
import './index.scss';
const { TabPane } = Tabs;
export default (props) => {
return (
<div className="centerbox">
<Tabs className="tabs-style" type="card">
<TabPane tab="全部需求" key="1">
<AllRequire {...props}/>
</TabPane>
<TabPane tab="我的需求" key="2">
<MyRequire {...props}/>
</TabPane>
</Tabs>
</div>
)
}

View File

@ -0,0 +1,54 @@
// Tabs
.tabs-style{
background: #fff;
border: 1px solid #dcdfe6;
-webkit-box-shadow: 0 2px 4px 0 rgba(0,0,0,.12), 0 0 6px 0 rgba(0,0,0,.04);
box-shadow: 0 2px 4px 0 rgba(0,0,0,.12), 0 0 6px 0 rgba(0,0,0,.04);
padding-bottom: 1vw;
.ant-tabs-tab{
margin: 0 !important;
}
.ant-tabs-tab{
border-top:0 !important;
border-left:0 !important;
border-radius:0 !important;
}
.ant-tabs-nav .ant-tabs-tab{
padding:19px 0px;
margin-right: 40px;
}
.ant-tabs-ink-bar{
width:25px!important;
bottom: 10px;
}
}
.choose-status{
padding: 1em;
background: #fff;
}
.ant-btn-info{
color: #fff;
background-color: #909399;
border-color: #909399;
}
.ant-btn-success{
color: #fff;
background-color: #67c23a;
border-color: #67c23a;
}
.mt10{
margin-top:10px;
}
.form-edit-modal{
.ant-col{
display: inline-block;
}
.edit-input{
width: 300px;
}
}

View File

@ -0,0 +1,80 @@
import React, { useEffect, useState } from 'react';
import classnames from 'classnames';
import { Pagination, Button } from 'antd';
import ItemList from '../../components/itemList';
import DelModal from '../../components/DelModal';
import { deleteRequirement, getRequirementList } from '../api';
export default (props) => {
const [requirementList, setRequirementList] = useState([]);
const [curPage, setCurPage] = useState(1);
const [total, setTotal] = useState(0);
// const [queryType, setQueryType] = useState("my");
const [queryType, setQueryType] = useState("all");
useEffect(() => {
getRequireList();
}, [curPage, queryType]);
function getRequireList() {
const params = {
curPage,
pageSize: 10,
status: "",
tag: "",
title: "",
typeName: "",
currentUser: "",
queryType, //all: my: tome:
};
getRequirementList(params).then(data => {
setRequirementList(data.rows);
setTotal(data.total);
})
}
function detailItem(id) {
props.history.push(`/requirement/requireDetail/${id}`);
}
function editItem(id) {
props.history.push(`/requirement/requireEdit/${id}`);
}
function deletItem(id) {
DelModal(() => {
deleteRequirement(id).then(res => {
if (res.code === '1') {
props.showNotification("删除成功");
getRequireList();
} else {
props.showNotification("删除失败");
}
})
}, '此操作将删除该记录, 是否继续?');
}
return (
<React.Fragment>
<div className="choose-status">
<Button className={classnames({ mr10: true, active: queryType === "my" })} type="dashed" key="my" onClick={() => { setQueryType("my") }}>我创建的</Button>
<Button className={classnames({ mr10: true, active: queryType === "tome" })} type="dashed" key="tome" onClick={() => { setQueryType("tome") }}> 指派给我的 </Button>
</div>
<ItemList
list={requirementList}
itemClick={detailItem}
deletItem={queryType === "my" && deletItem}
editItem={queryType === "my" && editItem}
activeByStatus={queryType === "my"}
/>
<div className="edu-txt-center mt10">
<Pagination
showQuickJumper
onChange={(page) => { setCurPage(page) }}
current={curPage}
total={total}
/>
</div>
</React.Fragment>
)
}

85
src/fetch.js Normal file
View File

@ -0,0 +1,85 @@
import axios from 'axios';
import cookie from 'react-cookies';
export const httpUrl='http://117.50.100.12:8001';
const TokenKey = 'autologin_forge_military';
axios.defaults.withCredentials = true;
// 创建axios实例
const service = axios.create({
// baseURL: process.env.BASE_API, // api的base_url
baseURL:httpUrl,
timeout: 5000 // 请求超时时间
// headers:{'X-Custom-Header': 'foobar'} // 请求头
});
// request拦截器
service.interceptors.request.use(config => {
if (cookie.load(TokenKey)) {
config.headers['Authorization'] = cookie.load(TokenKey); // 让每个请求携带自定义token 请根据实际情况自行修改
// config.headers['Authorization'] = 'autologin_forge_military=' + cookie.load(TokenKey); // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config;
}, error => {
// Do something with request error
console.log(error); // for debug
Promise.reject(error);
});
// respone拦截器
service.interceptors.response.use(
response => {
/**
* 下面的注释为通过response自定义code来标示请求状态当code返回如下情况为权限有问题登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
const res = response;
if (res.status === 400) {
// Message({
// message: '请求错误',
// type: 'error',
// duration: 5 * 1000
// });
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('error');
}
if (res.status === 401) {
// Message({
// message: '未授权,请登录!',
// type: 'error',
// duration: 5 * 1000
// });
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('error');
}
if (res.status === 40001) {
// Message({
// message: '账户或密码错误!',
// type: 'warning'
// });
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('error');
}
if (response.status !== 200 && res.status !== 200) {
// Message({
// message: res.message,
// type: 'error',
// duration: 7 * 1000
// });
} else {
return response.data;
}
},
error => {
if (error.response&& error.response.status === 401 && !error.config.url.includes('/auth-server/oauth/token')) {
return cookie.remove(TokenKey);
// that.$router.push('/Login');
} else {
// that.$router.push('/Login');
}
return Promise.reject(error);
}
);
export default service;

View File

@ -44,6 +44,7 @@ function Index(props){
function getData(){
const url = https +`/api/project/achievement/`;
axios.defaults.withCredentials = true;
axios.get(url,{
params:{
projectId:repo_id,