diff --git a/src/components/DiaryOption.tsx b/src/components/DiaryOption.tsx index bf55ddc..edca23f 100644 --- a/src/components/DiaryOption.tsx +++ b/src/components/DiaryOption.tsx @@ -100,7 +100,7 @@ const DiaryOption = (props: SelectDiary) => { if (!open) { return } - setSendValueFlag(true) + setSelectLoading(true) const fakeDataUrl = process.env.NEXT_PUBLIC_TODO_REQUEST_URL + `/task/message/diary/select`; fetch(fakeDataUrl, { method: 'POST', headers: { @@ -123,7 +123,7 @@ const DiaryOption = (props: SelectDiary) => { listRef.current.scrollTo({top: 9999999}); } } - setSendValueFlag(false) + setSelectLoading(false) }); }; @@ -190,7 +190,7 @@ const DiaryOption = (props: SelectDiary) => { // 滚动处理 end // 点击操作 start const [clickTaskDiary, setClickTaskDiary] = useState() - const onClickTAskDiary = (item: ListDiary, operate: string) => { + const onClickTaskDiary = (item: ListDiary, operate: string) => { if (clickTaskDiary == item && operate == 'L') { setClickTaskDiary(undefined) } else { @@ -368,7 +368,7 @@ const DiaryOption = (props: SelectDiary) => { - + { >
onClickTAskDiary(item, "L")} - onContextMenu={() => onClickTAskDiary(item, "R")}> + onClick={() => onClickTaskDiary(item, "L")} + onContextMenu={() => onClickTaskDiary(item, "R")}> parseInt(m)); + return `在${minutes.join('、')}分`; + } + return `在${parseInt(m)}分`; + } + + // 解析小时 + function parseHour(h: string) { + if (h === '*') return '每小时'; + if (h.includes('/')) { + const interval = h.split('/')[1]; + return `每${interval}小时`; + } + if (h.includes(',')) { + const hours = h.split(',').map(h => parseInt(h)); + return `在${hours.join('、')}点`; + } + return `在${parseInt(h)}点`; + } + + // 解析日期 + function parseDayOfMonth(d: string) { + if (d === '*') return ''; + if (d === 'L') return '每月最后一天'; + if (d.includes('W')) { + const day = d.replace('W', ''); + return `在每月${day}日最近的工作日`; + } + if (d.includes('/')) { + const interval = d.split('/')[1]; + return `每${interval}天`; + } + if (d.includes(',')) { + const days = d.split(',').map(d => parseInt(d)); + return `在每月${days.join('、')}日`; + } + return `在每月${parseInt(d)}日`; + } + + // 解析月份 + function parseMonth(m: string) { + if (m === '*') return ''; + const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', + '七月', '八月', '九月', '十月', '十一月', '十二月']; + if (m.includes('/')) { + const interval = m.split('/')[1]; + return `每${interval}个月`; + } + if (m.includes(',')) { + const months = m.split(',').map(m => monthNames[parseInt(m) - 1]); + return `在${months.join('、')}`; + } + return `在${monthNames[parseInt(m) - 1]}`; + } + + // 解析星期 + function parseDayOfWeek(d: string) { + if (d === '*' || d === '?') return ''; + const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; + if (d.includes('L')) { + const dayIndex = parseInt(d.replace('L', '')); + return `每月最后一个${dayNames[dayIndex]}`; + } + if (d.includes('#')) { + const [dayIndex, weekNum] = d.split('#').map(Number); + return `每月第${weekNum}个${dayNames[dayIndex]}`; + } + if (d.includes('/')) { + const interval = d.split('/')[1]; + return `每${interval}天(周)`; + } + if (d.includes(',')) { + const days = d.split(',').map(d => dayNames[parseInt(d)]); + return `在每周${days.join('、')}`; + } + return `在每周${dayNames[parseInt(d)]}`; + } + + // 构建描述 + const minuteDesc = parseMinute(minute); + const hourDesc = parseHour(hour); + const dayOfMonthDesc = parseDayOfMonth(dayOfMonth); + const monthDesc = parseMonth(month); + const dayOfWeekDesc = parseDayOfWeek(dayOfWeek); + + // 处理整点时间 + if (minute === '0' && hour !== '*') { + // 每天固定时间 + if (dayOfMonth === '*' && month === '*' && (dayOfWeek === '*' || dayOfWeek === '?')) { + return `每天${hourDesc}`; + } + return `${hourDesc} ${dayOfMonthDesc} ${monthDesc} ${dayOfWeekDesc}`.replace(/\s+/g, ' ').trim(); + } + + // 组合描述 + let description = `${minuteDesc} ${hourDesc} ${dayOfMonthDesc} ${monthDesc} ${dayOfWeekDesc}`; + + // 清理多余空格和空部分 + description = description.replace(/\s+/g, ' ').trim(); + description = description.replace(/\s\s+/g, ' '); + + // 处理一些常见模式 + if (description === '每分钟 每小时') return '每分钟'; + if (description.startsWith('每分钟 在')) return description.replace('每分钟', '每分钟的'); + + // 处理空描述 + if (!description) return '非法的Cron表达式'; + + // 优化描述 + description = description + .replace(/在每月(\d+)日 在每周/g, '在每月$1日且') + .replace(/在每月(\d+)日 在/g, '在每月$1日') + .replace(/在每月/g, '每月') + .replace(/在每周/g, '每周') + .replace(/在/g, ''); + + return description; +} + const CronGenerator: React.FC = ({setCronFunction, cron}) => { - const [showModal,setShowModal] = useState(false); + const [showModal, setShowModal] = useState(false); const [current, setCurrent] = useState(0); const [cronSeconds, setCronSeconds] = useState(cron ? cron.split(' ')[0] : '*'); const [cronMinutes, setCronMinutes] = useState(cron ? cron.split(' ')[1] : '*'); @@ -24,7 +161,9 @@ const CronGenerator: React.FC = ({setCronFunction, cron}) => const [fullCronExpression, setFullCronExpression] = useState( `${cronMinutes} ${cronHours} ${cronDayOfMonth} ${cronMonth} ${cronDayOfWeek}`.trim() ); + const [showTimeListModal, setShowTimeListModal] = useState(false); + const [canReadCron,setCanReadCron]=useState() const titleItem = ['具体时间', '周期', '自定义cron']; useEffect(() => { @@ -34,17 +173,20 @@ const CronGenerator: React.FC = ({setCronFunction, cron}) => const onClickItem = (index: number) => { setCurrent(index); }; + useEffect(() => { + setCanReadCron(cronToChinese(fullCronExpression)) + }, [fullCronExpression]); const onClickConfirmCron = () => { if (fullCronExpression) { try { - // Replace generateNextTimeAPI with your actual API call - // generateNextTimeAPI('0 ' + fullCronExpression).then(res => { - // setNextOccurrences(res.map((next: string) => dayjs(next))); - // setCronFunction(false); - // }); + generateNextTimeAPI('0 ' + fullCronExpression).then(res => { + setNextOccurrences(res.data.data.map((next: string) => dayjs(next))); + setCronFunction(false); + }); console.log('Cron confirmed:', fullCronExpression); setCronFunction(false); + setShowModal(false) } catch (error) { console.error('Cron expression is invalid', error); } @@ -69,10 +211,10 @@ const CronGenerator: React.FC = ({setCronFunction, cron}) => return; } console.log('cron' + fullCronExpression); - // Replace generateNextTimeAPI with your actual API call - // generateNextTimeAPI('0 ' + fullCronExpression).then(res => { - // setNextOccurrences(res.map((next: string) => dayjs(next))); - // }); + generateNextTimeAPI('0 ' + fullCronExpression).then(res => { + setNextOccurrences(res.data.data.map((next: string) => dayjs(next))); + }); + setShowTimeListModal(true) } catch (error) { console.error('Cron expression is invalid', error); } @@ -125,142 +267,148 @@ const CronGenerator: React.FC = ({setCronFunction, cron}) => return ( - setShowModal(false)} + onOk={onClickConfirmCron} + onCancel={() => setShowModal(false)} > -
-
-
- onClickItem(titleItem.indexOf(value as string))} - block - /> -
-
- {current === 0 && ( - <> -
-
分钟(0-59)
- - setCronMinutes(e.target.value)} - placeholder="*" - /> -
- -
-
小时(0-23)
- - setCronHours(e.target.value)} - placeholder="*" - /> -
- -
-
日期(1-31)
- setCronDayOfMonth(e.target.value)} - placeholder="*" - /> -
- -
-
月份(1-12)
- setCronMonth(e.target.value)} - placeholder="*" - /> -
- -
-
星期(0-6,0=周日)
- setCronDayOfWeek(e.target.value)} - placeholder="*" - /> -
- - )} - - {current === 1 && ( -
-
- setEveryNumber(parseInt(e.target.value) || 1)} - placeholder="1" - /> -
- - - - - -
- )} - {current === 2 && ( -
-
!!!目前只支持5位设定,分钟、小时、日、月、星期
- setFullCronExpression(e.target.value)}/> -
- )} -
-
- - -
+ {/*
*/} +
+
+ onClickItem(titleItem.indexOf(value as string))} + block + />
- - {nextOccurrences.length > 0 && ( -
-
最近触发时间:
- {nextOccurrences.map((item, index) => ( -
-
{dayjs(item).format('YYYY-MM-DD HH:mm')}
+
+ {current === 0 && ( + <> +
+
分钟(0-59)
+ + setCronMinutes(e.target.value)} + placeholder="*" + />
- ))} -
- )} - + +
+
小时(0-23)
+ + setCronHours(e.target.value)} + placeholder="*" + /> +
+ +
+
日期(1-31)
+ setCronDayOfMonth(e.target.value)} + placeholder="*" + /> +
+ +
+
月份(1-12)
+ setCronMonth(e.target.value)} + placeholder="*" + /> +
+ +
+
星期(0-6,0=周日)
+ setCronDayOfWeek(e.target.value)} + placeholder="*" + /> +
+ + )} + + {current === 1 && ( +
+
+ setEveryNumber(parseInt(e.target.value) || 1)} + placeholder="1" + /> +
+ + + + + +
+ )} + {current === 2 && ( +
+
!!!目前只支持5位设定,分钟、小时、日、月、星期
+ setFullCronExpression(e.target.value)}/> +
+ )} +
+
+ + +
+ + {nextOccurrences.length > 0 && ( + //
+ //
最近触发时间:
+ setShowTimeListModal(false)} + onCancel={() => setShowTimeListModal(false)} + > + {nextOccurrences.map((item, index) => ( +
+
{dayjs(item).format('YYYY-MM-DD HH:mm')}
+
+ ))} +
+ //
+ )} + {/*
*/} diff --git a/src/components/StepSort.tsx b/src/components/StepSort.tsx index 889fad2..a865885 100644 --- a/src/components/StepSort.tsx +++ b/src/components/StepSort.tsx @@ -1,27 +1,25 @@ import React, {CSSProperties, Fragment, useEffect, useState} from "react"; import {DragDropContext, Droppable, Draggable, DropResult, DraggingStyle, NotDraggingStyle} from "react-beautiful-dnd"; -import {TaskStepSortVO} from "@/components/type/TaskSort.d"; -import {Button, Drawer, message, Modal} from "antd"; +import {TaskStepSortOperateVO, TaskStepSortVO} from "@/components/type/TaskSort.d"; +import {Button, Drawer, Dropdown, MenuProps, message, Modal, Popconfirm} from "antd"; import TextArea from "antd/es/input/TextArea"; -import {addStepItemAPI} from "@/components/service/StepSort"; +import { + addStepItemAPI, + deleteStepItemAPI, insertStepItemAPI, + updateStepItemAPI, + updateStepItemIndexAPI +} from "@/components/service/StepSort"; +import {QuestionCircleOutlined} from "@ant-design/icons"; const reorder = (list: TaskStepSortVO[], startIndex: number, endIndex: number) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); - return result; }; const grid = 8; -const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined): CSSProperties => ({ - // some basic styles to make the items look a bit nicer - userSelect: "none", - // padding: grid, - margin: `0 0 ${grid}px 0`, - background: isDragging ? "lightgreen" : "white", - ...draggableStyle -}); + const getListStyle = (isDraggingOver: boolean) => ({ background: isDraggingOver ? "lightblue" : "white", width: "100%" @@ -32,51 +30,200 @@ const StepSort = (props: { taskId: string, stepList: TaskStepSortVO[] }) => { // 抽屉 start const [open, setOpen] = useState(false); const [dialogueOpen, setDialogueOpen] = useState(false); - const [sortItemList, setSortItemList] = useState([]); - const [modalText, setModalText] = useState({ + const [clickTaskSortItem, setClickTaskSortItem] = useState(); + const [clickSortItemIndex, setClickSortItemIndex] + = useState(undefined); + const initModalText = { id: "", stepDesc: undefined, sortIndex: undefined, taskId: props.taskId, - }); + }; + const [modalText, setModalText] = useState(initModalText); + const items: MenuProps['items'] = [ + { + key: "0", label: "上方插入", onClick: () => { + setDialogueOpen(true) + setModalText({ + id: "", + stepDesc: undefined, + sortIndex: undefined, + taskId: props.taskId, + lastId:clickSortItemIndex==0?undefined:state[clickSortItemIndex!-1].id, + nextId:clickTaskSortItem?.id, + operate:"last" + }) + } + }, + { + key: "1", label: "下方插入", onClick: () => { + setDialogueOpen(true) + setModalText({ + id: "", + stepDesc: undefined, + sortIndex: undefined, + taskId: props.taskId, + lastId:clickTaskSortItem?.id, + nextId:clickSortItemIndex==state.length-1?undefined:state[clickSortItemIndex!+1].id, + operate:"next" + }) + } + }, + { + key: "2", label: "修改步骤", onClick: () => { + setDialogueOpen(true) + setModalText(clickTaskSortItem!) + } + }, + { + key: "3", label: + } + okText="确认" + cancelText="取消" + onConfirm={() => { + deleteStepItemAPI(clickTaskSortItem!.id).then((response => { + console.log('response', response) + if (response.data.status.success) { + state.splice(clickSortItemIndex!,1); + setState([...state]) + props.stepList.length = 0 + props.stepList.push(...state) + message.success("删除步骤成功") + } + })); + }} + >删除步骤 + }, + ] useEffect(() => { // 根据任务id查找步骤 }, []); - function onDragEnd(result: DropResult) { + const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined, itemId: string): CSSProperties => ({ + // some basic styles to make the items look a bit nicer + userSelect: "none", + // padding: grid, + margin: `0 0 ${grid}px 0`, + color: clickTaskSortItem && clickTaskSortItem.id == itemId ? "white" : "black", + background: isDragging ? "lightgreen" : clickTaskSortItem && clickTaskSortItem.id == itemId ? "#1677ff" : "white", + ...draggableStyle + }); + + async function onDragEnd(result: DropResult) { + console.log({result}) const {source, destination} = result; // dropped outside the list if (!destination) { return; } + if (source.index == destination.index) { + return; + } + let lastId = "", nextId = ""; + if (source.index > destination.index) { + if (destination.index != 0) { + lastId = state[destination.index - 1].id; + } + nextId = state[destination.index].id + } else { + lastId = state[destination.index].id; + if (destination.index != state.length - 1) { + nextId = state[destination.index + 1].id; + } + } + updateStepItemIndexAPI(lastId, state[source.index].id, nextId) const items = reorder(state, source.index, destination.index); let newState = [...state]; newState = items; setState(newState); - props.stepList.length=0 + props.stepList.length = 0 props.stepList.push(...newState) + message.info("移动成功"); + } + + const onClickTaskSortItem = (item: TaskStepSortVO, operate: string,index:number) => { + if (clickTaskSortItem == item && operate == 'L') { + setClickTaskSortItem(undefined) + setClickSortItemIndex(undefined) + } else { + setClickTaskSortItem(item) + setClickSortItemIndex(index) + } } const [confirmButtonLoading, setConfirmButtonLoading] = useState(false); const confirmModalTextArea = () => { setConfirmButtonLoading(true) - addStepItemAPI(modalText).then(res => { - if (res.data.status.success) { - message.info("添加步骤成功") - setModalText( - {...modalText, stepDesc: undefined}) - setState([...state, res.data.data]) - props.stepList.length=0 - props.stepList.push(...state, res.data.data) - setDialogueOpen(false); - } else { - message.error(res.data.status.message) - } - }).finally(()=>{ - setConfirmButtonLoading(false) - }) + // 修改 + if (modalText.id){ + updateStepItemAPI(modalText).then(res=>{ + if (res.data.status.success) { + message.info("修改步骤成功") + setModalText(initModalText) + state.splice(clickSortItemIndex!,1,res.data.data) + setState([...state]) + props.stepList.length = 0 + props.stepList.push(...state) + setDialogueOpen(false); + } else { + message.error(res.data.status.message) + } + }).finally(() => { + setConfirmButtonLoading(false) + }) + }else if(modalText.operate){ + insertStepItemAPI(modalText).then(res => { + if (res.data.status.success) { + message.info("添加步骤成功") + setModalText(initModalText) + let result:TaskStepSortVO[]=[]; + if('next'==modalText.operate){ + state.splice(clickSortItemIndex!,0,res.data.data) + }else if ('last'==modalText.operate){ + if (clickSortItemIndex==0){ + result = [res.data.data,...state] + }else { + state.splice(clickSortItemIndex!-1,0,res.data.data) + } + } + if (result.length>0){ + props.stepList.length = 0 + props.stepList.push(...result) + setState(result) + }else { + props.stepList.length = 0 + props.stepList.push(...state) + setState(state) + } + + setDialogueOpen(false); + } else { + message.error(res.data.status.message) + } + }).finally(() => { + setConfirmButtonLoading(false) + }) + }else { + addStepItemAPI(modalText).then(res => { + if (res.data.status.success) { + message.info("添加步骤成功") + setModalText(initModalText) + setState([...state, res.data.data]) + props.stepList.length = 0 + props.stepList.push(...state, res.data.data) + setDialogueOpen(false); + } else { + message.error(res.data.status.message) + } + }).finally(() => { + setConfirmButtonLoading(false) + }) + } } return ( @@ -103,7 +250,7 @@ const StepSort = (props: { taskId: string, stepList: TaskStepSortVO[] }) => { bottom: "0px" } }} - value={modalText.sortIndex} + value={modalText.stepDesc} maxLength={255} autoFocus onChange={(e) => setModalText( {...modalText, stepDesc: e.target.value})} @@ -131,51 +278,59 @@ const StepSort = (props: { taskId: string, stepList: TaskStepSortVO[] }) => { open={open} footer={} > -
- - - {(provided, snapshot) => ( -
- {state.map((item, index) => ( - - {(provided, snapshot) => ( -
-
- {`步骤${index + 1}`} -
+ +
+ + + {(provided, snapshot) => ( +
+ {state.map((item, index) => ( + + {(provided, snapshot) => (
onClickTaskSortItem(item, "L",index)} + onContextMenu={() => onClickTaskSortItem(item, "R",index)} > -
- {item.stepDesc} +
+ {`步骤${index + 1}`} +
+
+
+ {item.stepDesc} +
-
- )} -
- ))} - {provided.placeholder} -
- )} -
-
-
+ )} + + ))} + {provided.placeholder} +
+ )} + + +
+ ); diff --git a/src/components/service/ScheduleTask.tsx b/src/components/service/ScheduleTask.tsx new file mode 100644 index 0000000..849afbe --- /dev/null +++ b/src/components/service/ScheduleTask.tsx @@ -0,0 +1,8 @@ +import {AxiosResponse} from "axios"; +import {ResponseVO} from "@/lib/definitions"; +import {httpReq} from "@/utils/axiosReq"; + +export const generateNextTimeAPI = (cron: string): Promise>> => { + return httpReq.get(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + + `/task/schedule/nextTime?cron=${cron}`) +} \ No newline at end of file diff --git a/src/components/service/StepSort.tsx b/src/components/service/StepSort.tsx index 3a44838..1bc72e5 100644 --- a/src/components/service/StepSort.tsx +++ b/src/components/service/StepSort.tsx @@ -6,4 +6,21 @@ import {TaskStepSortVO} from "@/components/type/TaskSort.d"; export const addStepItemAPI= (data:TaskStepSortVO):Promise>> =>{ return httpReq.post(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + `/task/step/sort/item`, data) +} +export const insertStepItemAPI= (data:TaskStepSortVO):Promise>> =>{ + return httpReq.post(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + `/task/step/sort/insert/item`, + data) +} +export const updateStepItemAPI= (data:TaskStepSortVO):Promise>> =>{ + return httpReq.put(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + `/task/step/sort/item`, + data) +} +export const updateStepItemIndexAPI=(lastId:string,currentId:string ,nextId:string)=>{ + return httpReq.get(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + + `/task/step/sort/update/index?lastId=${lastId}¤tId=${currentId}&nextId=${nextId}`) +} + +export const deleteStepItemAPI=(id:string)=>{ + return httpReq.delete(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + + `/task/step/sort/item?id=${id}`) } \ No newline at end of file diff --git a/src/components/type/TaskSort.d.tsx b/src/components/type/TaskSort.d.tsx index 0fe2b30..52e49db 100644 --- a/src/components/type/TaskSort.d.tsx +++ b/src/components/type/TaskSort.d.tsx @@ -2,5 +2,11 @@ export type TaskStepSortVO = { id: string, sortIndex: number|undefined, stepDesc: string|undefined, - taskId:string -} \ No newline at end of file + taskId:string, +} +export type TaskStepSortOperateVO=TaskStepSortVO&{ + lastId?:string, + nextId?:string, + [key: string]: unknown; // 允许任意额外的 string 类型字段,值类型为 unknown +} +// & Record; \ No newline at end of file diff --git a/src/lib/definitions.ts b/src/lib/definitions.ts index f4c477c..2d02cac 100644 --- a/src/lib/definitions.ts +++ b/src/lib/definitions.ts @@ -1,6 +1,7 @@ import React from "react"; import {Dayjs} from "dayjs"; import {TaskStepSortVO} from "@/components/type/TaskSort.d"; +import {Data} from "@dnd-kit/core"; export type Request={ data:T, @@ -40,6 +41,9 @@ export type TaskMessage ={ stepList?:TaskStepSortVO[]; } +// export type TaskSortMessage=Data&{ +// stepList?:TaskStepSortVO[]; +// } export type DataType = TaskMessage&{ key: React.ReactNode; diff --git a/src/ui/task/project/DetailModelForm.tsx b/src/ui/task/project/DetailModelForm.tsx index 5cc9ff8..ed2fc70 100644 --- a/src/ui/task/project/DetailModelForm.tsx +++ b/src/ui/task/project/DetailModelForm.tsx @@ -55,7 +55,7 @@ export const DetailModelForm: React.FC = (props) => { // 团队第一层 pid必须为0 const [taskType, setTaskType] = useState('0') const [spinning, setSpinning] = useState(true) - const [operationRequest,setOperationRequest] = useState(false) + const [operationRequest, setOperationRequest] = useState(false) useEffect(() => { if (props.itemId != undefined && ( props.operationId === OPERATION_BUTTON_TYPE.DETAIL || props.operationId === OPERATION_BUTTON_TYPE.UPDATE)) { @@ -117,10 +117,10 @@ export const DetailModelForm: React.FC = (props) => { // 推荐使用 request,或者 initialValues ?
: null return ( - + title={props.description} - open={!spinning&&props.open && !props.haveButton} + open={!spinning && props.open && !props.haveButton} trigger={props.haveButton ? ) } if (taskType == '2') { - result.push() + result.push() } return result; }, @@ -433,8 +433,9 @@ export const DetailModelForm: React.FC = (props) => { disabled={editFormDisable} /> - {taskType=="3"&&{} + {taskType == "3" && { + } }/>}