feat:添加日历展示任务及顶部按钮编排

This commit is contained in:
shixiaohua 2024-05-29 16:44:23 +08:00
parent 97071b464b
commit f76c460fd9
6 changed files with 237 additions and 144 deletions

View File

@ -12,6 +12,10 @@
2. 向左紧急 2. 向左紧急
![四象线](four.png) ![四象线](four.png)
## 日历
1. 月,周,日展示
2. 只有期望开始时间和期望结束时间都填写的时候才会在日历中展示
3.
## 项目启动 ## 项目启动
### 后端服务启动 ### 后端服务启动
1. 后端启动应用需求dockerredismysql8。 1. 后端启动应用需求dockerredismysql8。

View File

@ -44,7 +44,7 @@ export type DataType ={
expectedTimeRange?:(string|Dayjs|undefined)[]; expectedTimeRange?:(string|Dayjs|undefined)[];
actualStartTime?:Date; actualStartTime?:Date;
actualEndTime?:Date; actualEndTime?:Date;
actualTimeRange?:(string|Dayjs)[] actualTimeRange?:(string|Dayjs|undefined)[]
children: DataType[]|undefined; children: DataType[]|undefined;
} }
export type DictType={ export type DictType={
@ -54,3 +54,8 @@ export type DictType={
order:number; order:number;
color:string; color:string;
} }
export type SearchObject={
name: string,
value: any,
operateType:string,
}

View File

@ -4,6 +4,7 @@ import {DownOutlined, QuestionCircleOutlined} from "@ant-design/icons";
import {DetailForm} from "@/ui/task/four/DetailForm"; import {DetailForm} from "@/ui/task/four/DetailForm";
import {commonUpdate, deleteTask, OPERATION_BUTTON_TYPE} from "@/lib/task/project/data"; import {commonUpdate, deleteTask, OPERATION_BUTTON_TYPE} from "@/lib/task/project/data";
import Link from "next/link"; import Link from "next/link";
import {DetailModelForm} from "@/ui/task/project/DetailModelForm";
export interface OperationButtonProps { export interface OperationButtonProps {
itemId: number, itemId: number,
@ -14,7 +15,7 @@ export interface OperationButtonProps {
} }
interface OperationModelProps { interface OperationModelProps {
operationId: number | undefined, operationId: number,
pPid: number, pPid: number,
pid: number, pid: number,
openModal: boolean openModal: boolean
@ -27,7 +28,7 @@ class OperationButton extends React.Component<OperationButtonProps, OperationMod
this.state = { this.state = {
pid: props.pid, pid: props.pid,
pPid: props.pPid, pPid: props.pPid,
operationId: undefined, operationId: 0,
openModal: false openModal: false
}; };
} }
@ -129,42 +130,16 @@ class OperationButton extends React.Component<OperationButtonProps, OperationMod
</Space> </Space>
</a> </a>
</Dropdown> </Dropdown>
<Modal <DetailModelForm
maskClosable={false} haveButton={false}
destroyOnClose={true} itemId={this.props.itemId}
open={this.state.openModal} pPid={this.props.pPid}
title={this.state.operationId === OPERATION_BUTTON_TYPE.DETAIL ? '任务详情' : operationId={this.state.operationId}
description={this.state.operationId === OPERATION_BUTTON_TYPE.DETAIL ? '任务详情' :
this.state.operationId === OPERATION_BUTTON_TYPE.ADD_CHILD ? '添加支线任务' : this.state.operationId === OPERATION_BUTTON_TYPE.ADD_CHILD ? '添加支线任务' :
this.state.operationId === OPERATION_BUTTON_TYPE.UPDATE ? '修改任务' : '未知操作'} this.state.operationId === OPERATION_BUTTON_TYPE.UPDATE ? '修改任务' : '未知操作'}
// open={open} open={this.state.openModal}
// onOk={handleOk} reloadData={handleCancel}/>
onCancel={handleCancel}
footer={[]}
width={800}
// footer={[
// <Button key="back" onClick={handleCancel}>
// Return
// </Button>,
// <Button key="submit" type="primary" loading={loading} onClick={handleOk}>
// Submit
// </Button>,
// <Button
// key="link"
// href="https://google.com"
// type="primary"
// loading={loading}
// onClick={handleOk}
// >
// Search on Google
// </Button>,
// ]}
>
<DetailForm itemId={this.props.itemId}
operationId={this.state.operationId}
handleCancel={handleCancel}
pPid={this.props.pPid}
/>
</Modal>
</Fragment> </Fragment>
} }
} }

View File

@ -1,4 +1,4 @@
import React, {useContext} from "react"; import React, {Fragment, useContext} from "react";
import {Button, DatePicker, Select, Space} from "antd"; import {Button, DatePicker, Select, Space} from "antd";
import {usePathname, useRouter} from "next/navigation"; import {usePathname, useRouter} from "next/navigation";
import {DetailModelForm} from "@/ui/task/project/DetailModelForm"; import {DetailModelForm} from "@/ui/task/project/DetailModelForm";
@ -23,33 +23,39 @@ export const TitleOperation: React.FC<TitleOperationProps> = ({
console.log('usePathname()', usePathname()); console.log('usePathname()', usePathname());
const data = useContext(LocalContext); const data = useContext(LocalContext);
const {RangePicker} = DatePicker; const {RangePicker} = DatePicker;
const expectStartTimeParseResult:RequestDateType[] = data.expectedStartTime.length>0?JSON.parse(data.expectedStartTime):[undefined,undefined] const expectStartTimeParseResult: RequestDateType[] = data.expectedStartTime.length > 0 ? JSON.parse(data.expectedStartTime) : [undefined, undefined]
expectStartTimeParseResult.map(item => item&&item.value ? dayjs(item.value.toString()) : undefined) expectStartTimeParseResult.map(item => item && item.value ? dayjs(item.value.toString()) : undefined)
const defaultExpectStartTime:[start: Dayjs | null | undefined, end: Dayjs | null | undefined] = [ const defaultExpectStartTime: [start: Dayjs | null | undefined, end: Dayjs | null | undefined] = [
expectStartTimeParseResult[0]&&expectStartTimeParseResult[0].value ? dayjs(expectStartTimeParseResult[0].value.toString()) : undefined, expectStartTimeParseResult[0] && expectStartTimeParseResult[0].value ? dayjs(expectStartTimeParseResult[0].value.toString()) : undefined,
expectStartTimeParseResult[1]&&expectStartTimeParseResult[1].value ? dayjs(expectStartTimeParseResult[1].value.toString()) : undefined expectStartTimeParseResult[1] && expectStartTimeParseResult[1].value ? dayjs(expectStartTimeParseResult[1].value.toString()) : undefined
]; ];
return <Space style={{marginTop: 0 ,"height": "42px", "alignContent": "center"}}> return <Space style={{marginTop: 0, "height": "42px", "alignContent": "center"}}>
<DetailModelForm operationId={OPERATION_BUTTON_TYPE.ADD} description='添加主线任务' reloadData={refreshData}/> <DetailModelForm haveButton={true} open={false} operationId={OPERATION_BUTTON_TYPE.ADD}
{usePathname().startsWith("/task/project") ? description='添加主线任务' reloadData={refreshData}/>
<> {
<Button type="primary" onClick={() => { !usePathname().startsWith("/task/project") &&
replace("/task/four"); <Button type="primary" onClick={() => {
// setCurrentPath("/task/four"); replace("/task/project");
}}></Button> // setCurrentPath("/task/project")
<Button type="primary" onClick={() => { }}></Button>
replace("/task/calendar"); }
// setCurrentPath("/task/project") {
}}></Button> !usePathname().startsWith("/task/four") &&
</> : <> <Button type="primary" onClick={() => {
<Button type="primary" onClick={() => { replace("/task/four");
replace("/task/project"); // setCurrentPath("/task/four");
// setCurrentPath("/task/project") }}></Button>
}}></Button> }
<Button type="primary" onClick={() => { {
replace("/task/calendar"); !usePathname().startsWith("/task/calendar") &&
// setCurrentPath("/task/project") <Button type="primary" onClick={() => {
}}></Button> replace("/task/calendar");
// setCurrentPath("/task/project")
}}></Button>
}
{
/*日历需要状态*/
!usePathname().startsWith("/task/project") && <Fragment>
<span style={{whiteSpace: 'nowrap'}}>:</span> <span style={{whiteSpace: 'nowrap'}}>:</span>
<Select <Select
mode="multiple" mode="multiple"
@ -65,6 +71,11 @@ export const TitleOperation: React.FC<TitleOperationProps> = ({
return {label: item.name, value: item.code} return {label: item.name, value: item.code}
})} })}
/> />
</Fragment>
}
{
/*四相线需要状态时间*/
usePathname().startsWith("/task/four") && <Fragment>
<span style={{whiteSpace: 'nowrap'}}>:</span> <span style={{whiteSpace: 'nowrap'}}>:</span>
<RangePicker <RangePicker
placeholder={['开始时间', '结束时间']} placeholder={['开始时间', '结束时间']}
@ -83,7 +94,7 @@ export const TitleOperation: React.FC<TitleOperationProps> = ({
'value': dates[0], 'value': dates[0],
'operateType': ">=" 'operateType': ">="
}); });
}else{ } else {
expectStartTimeList.push(undefined) expectStartTimeList.push(undefined)
} }
if (dates[1]) { if (dates[1]) {
@ -92,13 +103,15 @@ export const TitleOperation: React.FC<TitleOperationProps> = ({
'value': dates[1].add(1, 'day'), 'value': dates[1].add(1, 'day'),
'operateType': "<" 'operateType': "<"
}) })
}else{ } else {
expectStartTimeList.push(undefined) expectStartTimeList.push(undefined)
} }
setExpectedStartTime(JSON.stringify(expectStartTimeList)) setExpectedStartTime(JSON.stringify(expectStartTimeList))
}} }}
/> />
</> </Fragment>
} }
</Space> </Space>
} }

View File

@ -1,11 +1,14 @@
'use client' 'use client'
import React, {useEffect} from "react"; import React, {useCallback, useContext, useEffect, useMemo} from "react";
import {Calendar, dayjsLocalizer, Event, View} from 'react-big-calendar' import {Calendar, dayjsLocalizer, Event, SlotInfo, View} from 'react-big-calendar'
// https://day.js.org/docs/zh-CN/get-set/get-set // https://day.js.org/docs/zh-CN/get-set/get-set
import dayjs from 'dayjs' import dayjs, {Dayjs} from 'dayjs'
import 'react-big-calendar/lib/css/react-big-calendar.css' import 'react-big-calendar/lib/css/react-big-calendar.css'
import {getTaskTreeResult} from "@/lib/task/project/data"; import {getTaskTreeResult, OPERATION_BUTTON_TYPE} from "@/lib/task/project/data";
import {useSearchParams} from "next/dist/client/components/navigation"; import {useSearchParams} from "next/dist/client/components/navigation";
import {DetailModelForm} from "@/ui/task/project/DetailModelForm";
import {SearchObject} from "@/lib/definitions";
import LocalContext from "@/ui/LocalContent";
/** /**
* https://github.com/jquense/react-big-calendar?tab=readme-ov-file * https://github.com/jquense/react-big-calendar?tab=readme-ov-file
@ -17,73 +20,121 @@ const CalShow: React.FC = () => {
const [view, setView] = React.useState<View>('month'); const [view, setView] = React.useState<View>('month');
const [date, setDate] = React.useState<Date>(new Date()); const [date, setDate] = React.useState<Date>(new Date());
// 展示在页面的任务,默认获取当前月的信息。 // 展示在页面的任务,默认获取当前月的信息。
const [events,setEvents] = React.useState<Event[]>([]); const [events, setEvents] = React.useState<Event[]>([]);
const handleViewChange = (newView:View) => { const [open, setOpen] = React.useState(false);
const [description, setDescription] = React.useState('');
const [operationId, setOperationId] = React.useState(-1);
const [itemId,setItemId] = React.useState(-1);
const [expectedStartTime,setExpectedStartTime] = React.useState<Dayjs>();
const [expectedEndTime,setExpectedEndTime] = React.useState<Dayjs>();
let state:string=useContext(LocalContext).taskState
const handleViewChange = (newView: View) => {
setView(newView); setView(newView);
}; };
var pid = useSearchParams().get('pid'); var pid = useSearchParams().get('pid');
useEffect(() => { const handleNavigate = (newDate: Date) => {
const searchListE=[] console.log('handleNavigate', newDate)
if (pid!=null) {
searchListE.push({name:"pid",value:pid,operateType:"="},{name:'TREE',value:"false",operateType: "TREE"});
}
if (view==='month'){
searchListE.push({name:'expectedStartTime',value:dayjs(date).startOf('month'),operateType:">="})
searchListE.push({name:'expectedStartTime',value:dayjs(date).endOf('month'),operateType:"<="})
}else {
searchListE.push({name:'expectedStartTime',value:dayjs(date).startOf('week'),operateType:">="})
searchListE.push({name:'expectedStartTime',value:dayjs(date).endOf('week'),operateType:"<="})
}
searchListE.push({name:'expectedEndTime',value:dayjs(date).endOf('month'),operateType:"NOT NULL"})
let request = JSON.stringify({
pageSize:9999,
pageNumber:1,
data: searchListE
})
const response = getTaskTreeResult(request)
response.then(responseD=>{
if(responseD.status.success){
setEvents(responseD.data.content.map(taskState=>{
return {
start:dayjs(taskState.expectedStartTime).toDate(),
end:dayjs(taskState.expectedEndTime).toDate(),
title:taskState.name
}
}))
}
})
}, []);
const handleNavigate = (newDate:Date) => {
console.log('handleNavigate',newDate)
setDate(newDate); setDate(newDate);
const searchList=[] const searchList:SearchObject[] = []
if (pid!=null) { if (pid != null) {
searchList.push({name:"pid",value:pid,operateType:"="},{name:'TREE',value:"false",operateType: "TREE"}); searchList.push({name: "pid", value: pid, operateType: "="}, {
name: 'TREE',
value: "false",
operateType: "TREE"
});
} }
searchList.push({name:'expectedStartTime',value:dayjs(newDate).startOf('month'),operateType:">="}) searchList.push({name: "expectedStartTime", value: dayjs(newDate).startOf('month'), operateType: ">="})
searchList.push({name:'expectedStartTime',value:dayjs(newDate).endOf('month'),operateType:"<="}) searchList.push({name: 'expectedStartTime', value: dayjs(newDate).endOf('month'), operateType: "<="})
searchList.push({name:'expectedEndTime',value:dayjs(newDate).endOf('month'),operateType:"NOT NULL"}) loadData(searchList);
let request = JSON.stringify({
pageSize:9999,
pageNumber:1,
data: searchList
})
const response = getTaskTreeResult(request)
response.then(responseD=>{
if(responseD.status.success){
setEvents(responseD.data.content.map(taskState=>{
return {
start:dayjs(taskState.expectedStartTime).toDate(),
end:dayjs(taskState.expectedEndTime).toDate(),
title:taskState.name
}
}))
}
})
}; };
return <div className="App" style={{ height: '90vh' }}> useEffect(() => {
const searchListE = []
if (pid != null) {
searchListE.push({name: "pid", value: pid, operateType: "="}, {
name: 'TREE',
value: "false",
operateType: "TREE"
});
}
if (view === 'month') {
searchListE.push({name: 'expectedStartTime', value: dayjs(date).startOf('month'), operateType: ">="})
searchListE.push({name: 'expectedStartTime', value: dayjs(date).endOf('month'), operateType: "<="})
} else {
searchListE.push({name: 'expectedStartTime', value: dayjs(date).startOf('week'), operateType: ">="})
searchListE.push({name: 'expectedStartTime', value: dayjs(date).endOf('week'), operateType: "<="})
}
loadData(searchListE);
}, [useContext(LocalContext)]);
const message = {
week: '周',
work_week: '工作周',
day: '天',
month: '月',
previous: '前',
next: '后',
today: '当下',
agenda: '日程'
}
const loadData = (searchList:SearchObject[])=>{
if (state.length > 0){
searchList.push({name: 'state', value: state, operateType: "IN"})
}
searchList.push({name: 'expectedEndTime', value: dayjs(date).endOf('month'), operateType: "NOT NULL"})
let request = JSON.stringify({
pageSize: 9999,
pageNumber: 1,
data: searchList
})
getTaskTreeResult(request).then(responseD => {
if (responseD.status.success) {
setEvents(responseD.data.content.map(taskState => {
return {
start: dayjs(taskState.expectedStartTime).toDate(),
end: dayjs(taskState.expectedEndTime).toDate(),
title: taskState.name,
resource:taskState.id
}
}))
}
})
}
const reloadData = ()=>{
setOpen(false)
handleNavigate(expectedStartTime?expectedStartTime.toDate():date)
}
const handleSelectSlot = useCallback(
({start, end}: SlotInfo) => {
setExpectedEndTime(dayjs(end))
setExpectedStartTime(dayjs(start))
setOperationId(OPERATION_BUTTON_TYPE.ADD)
setDescription("添加任务")
setOpen(true);
},
[setEvents]
)
const handleSelectEvent = useCallback(
(event: Event, e: React.SyntheticEvent<HTMLElement>) => {
// window.alert(event.title);
console.log(event)
setOperationId(OPERATION_BUTTON_TYPE.DETAIL)
setDescription("任务详情")
setItemId(event.resource)
setOpen(true);
},
[]
)
const {defaultDate, scrollToTime} = useMemo(
() => ({
defaultDate: new Date(2015, 3, 12),
scrollToTime: new Date(1970, 1, 1, 6),
}),
[]
)
return <div className="App" style={{height: '90vh'}}>
<DetailModelForm operationId={operationId} description={description} open={open} haveButton={false} itemId={itemId}
reloadData={reloadData} expectedStartTime={expectedStartTime} expectedEndTime={expectedEndTime}/>
<Calendar <Calendar
localizer={localizer} localizer={localizer}
events={events} events={events}
@ -92,6 +143,13 @@ const CalShow: React.FC = () => {
// onRangeChange={rangeChange} // onRangeChange={rangeChange}
date={date} date={date}
onNavigate={handleNavigate} onNavigate={handleNavigate}
messages={message}
// 点击任务
onSelectEvent={handleSelectEvent}
// 点击空白处添加任务
onSelectSlot={handleSelectSlot}
selectable
scrollToTime={scrollToTime}
/> />
</div> </div>
} }

View File

@ -7,28 +7,66 @@ import {
ProFormText, ProFormTextArea, ProFormTreeSelect, ProFormText, ProFormTextArea, ProFormTreeSelect,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Button, Form, message } from 'antd'; import { Button, Form, message } from 'antd';
import React, {useState} from "react"; import React, {useEffect, useState} from "react";
import { import {
addTask, addTask, getTask,
getTaskTreeResult, getTaskTreeResult,
OPERATION_BUTTON_TYPE, OPERATION_BUTTON_TYPE,
taskPriorityList, taskPriorityList,
taskStateList taskStateList
} from "@/lib/task/project/data"; } from "@/lib/task/project/data";
import {DataType} from "@/lib/definitions"; import {DataType} from "@/lib/definitions";
import dayjs from "dayjs"; import dayjs, {Dayjs} from "dayjs";
export type DetailModelFormProps={ export type DetailModelFormProps={
// 当前内容id
itemId?: number, itemId?: number,
// 父任务id
pPid?:number, pPid?:number,
// 操作id
operationId: number, operationId: number,
// 标题描述
description:string, description:string,
// 是否打开界面,用于非按钮操作
open:boolean,
// 使用按钮操作
haveButton:boolean,
expectedStartTime?:Dayjs,
expectedEndTime?:Dayjs,
// 重新加载数据
reloadData?: () => void reloadData?: () => void
} }
export type PidSelectTree= { label: string; value: number;pPid:number; children?: PidSelectTree[] } export type PidSelectTree= { label: string; value: number;pPid:number; children?: PidSelectTree[] }
export const DetailModelForm: React.FC<DetailModelFormProps> = (props) => { export const DetailModelForm: React.FC<DetailModelFormProps> = (props) => {
console.log("DetailModelForm:props:",props,props.itemId!=undefined&&(
props.operationId === OPERATION_BUTTON_TYPE.DETAIL || props.operationId === OPERATION_BUTTON_TYPE.UPDATE))
const [form] = Form.useForm<DataType>(); const [form] = Form.useForm<DataType>();
const [pPid, setPPid] = useState<number>(0); const [pPid, setPPid] = useState<number>(0);
useEffect(() => {
if (props.itemId!=undefined&&(
props.operationId === OPERATION_BUTTON_TYPE.DETAIL || props.operationId === OPERATION_BUTTON_TYPE.UPDATE)) {
getTask(props.itemId).then(task => {
console.log('DetailModelForm:getTask(props.itemId)', props.itemId, task);
if (task.status.success) {
// setTaskMessage(task.data)
task.data.state = taskStateList.find(taskState => taskState.code === task.data.state?.toString())?.name;
task.data.priority = taskPriorityList.find(taskPriority => taskPriority.code === task.data.priority?.toString())?.name;
task.data.actualTimeRange = [task.data.actualStartTime ? dayjs(task.data.actualStartTime) : undefined,
task.data.actualEndTime ? dayjs(task.data.actualEndTime) : undefined];
task.data.expectedTimeRange = [task.data.expectedStartTime ? dayjs(task.data.expectedStartTime) : undefined,
task.data.expectedEndTime ? dayjs(task.data.expectedEndTime) : undefined];
form.setFieldsValue(task.data)
} else {
message.error(task.status.message);
props.reloadData?.()
}
})
}else if(props.operationId === OPERATION_BUTTON_TYPE.ADD|| props.operationId === OPERATION_BUTTON_TYPE.ADD_CHILD){
let data={'expectedTimeRange':[props.expectedStartTime?props.expectedStartTime:dayjs(), props.expectedEndTime]};
form.setFieldsValue(data)
}
}, [props])
function childReduce(child:DataType[]):PidSelectTree[]{ function childReduce(child:DataType[]):PidSelectTree[]{
const result:PidSelectTree[] = []; const result:PidSelectTree[] = [];
child.map(data=> { child.map(data=> {
@ -40,26 +78,26 @@ export const DetailModelForm: React.FC<DetailModelFormProps> = (props) => {
}) })
return result; return result;
} }
// 如果不是添加任务需要回显
return ( return (
<ModalForm<DataType> <ModalForm<DataType>
title={ title={props.description}
props.operationId === OPERATION_BUTTON_TYPE.DETAIL ? "任务详情": open={props.open&&!props.haveButton}
props.operationId === OPERATION_BUTTON_TYPE.ADD?"添加任务": trigger={props.haveButton?
props.operationId === OPERATION_BUTTON_TYPE.ADD?"修改任务":''
}
trigger={
<Button type="primary"> <Button type="primary">
<PlusOutlined /> <PlusOutlined />
{props.description} {props.description}
</Button> </Button>:undefined
} }
form={form} form={form}
autoFocusFirstInput autoFocusFirstInput
modalProps={{ modalProps={{
destroyOnClose: true, destroyOnClose: true,
onCancel: () => console.log('run'), onCancel: () => {
console.log('run');
props.reloadData?.();
},
}} }}
// submitTimeout={2000}
onFinish={async (values) => { onFinish={async (values) => {
console.log('Received values of form: ', values); console.log('Received values of form: ', values);
if (values.pid===undefined){ if (values.pid===undefined){
@ -99,7 +137,7 @@ export const DetailModelForm: React.FC<DetailModelFormProps> = (props) => {
}} }}
> >
<ProFormText width="sm" name="id" hidden={true} label="主键" /> <ProFormText width="sm" name="id" hidden={true} label="主键" />
<ProFormText width="sm" name="code" hidden={true} label="任务编码" /> <ProFormText width="sm" name="code" initialValue={props.itemId} hidden={true} label="任务编码" />
<ProFormText width="sm" name="pPid" initialValue={props.pPid} hidden={true} label="祖宗id" /> <ProFormText width="sm" name="pPid" initialValue={props.pPid} hidden={true} label="祖宗id" />
<ProForm.Group> <ProForm.Group>
<ProFormTreeSelect <ProFormTreeSelect