From cf68c5e915f6ee8149df359a07a9f2ec4a1ebcd1 Mon Sep 17 00:00:00 2001 From: 1708-huayu <57060237+1708-huayu@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:01:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=91=A8=E6=9C=9F=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/task/layout.tsx | 4 + src/components/ClickRecord.tsx | 85 ++++++ src/components/SettingCron.tsx | 9 +- src/components/constant/onceConsumeList.json | 242 +++++++++++++++++ src/components/service/ScheduleTask.tsx | 7 +- src/components/type/TaskSchedule.d.tsx | 11 + src/lib/definitions.ts | 1 + src/ui/task/RightOption.tsx | 2 +- src/ui/task/TitleOperation.tsx | 13 +- .../task/project/DetailModelForm.module.css | 3 + src/ui/task/project/DetailModelForm.tsx | 123 ++++++++- src/utils/codeToReadName.tsx | 14 + src/utils/timeFormatUtil.ts | 243 ++++++++++++++++++ 13 files changed, 740 insertions(+), 17 deletions(-) create mode 100644 src/components/ClickRecord.tsx create mode 100644 src/components/constant/onceConsumeList.json create mode 100644 src/components/type/TaskSchedule.d.tsx create mode 100644 src/ui/task/project/DetailModelForm.module.css create mode 100644 src/utils/codeToReadName.tsx create mode 100644 src/utils/timeFormatUtil.ts diff --git a/src/app/task/layout.tsx b/src/app/task/layout.tsx index e39c42d..c4522f1 100644 --- a/src/app/task/layout.tsx +++ b/src/app/task/layout.tsx @@ -28,6 +28,10 @@ export default function Layout({children}: { children: React.ReactNode }) { /* 这里是你的组件 token */ }, }, + token: { + controlItemBgActiveHover:"#4096ff", + controlItemBgActive:"#4096ff" + } }} > = ({taskId, taskName, onceConsume}) => { + const [form] = Form.useForm(); + useEffect(() => { + let data = { + 'recordTimeRange': [onceConsume ? dayjs().subtract(Number(onceConsume.split(",")[0]), onceConsume.split(",")[1] as UnitTypeShort) : dayjs(), dayjs()], + 'timeDifference': onceConsumerRead(onceConsume), + }; + form.setFieldsValue(data) + }, []); + return ( + + + title={`${taskName}打卡`} + layout="horizontal" + trigger={ + } + form={form} + autoFocusFirstInput + modalProps={{ + // destroyOnClose: true, + onCancel: () => console.log('run'), + }} + onFinish={async (values) => { + values.taskId = taskId + if (values.recordTimeRange[0]) { + values.startDate = new Date(values.recordTimeRange[0]) + } + if (values.recordTimeRange[1]) { + values.recordDate = new Date(values.recordTimeRange[1]) + } + await clickRecordAPI(values); + return true + }} + > + { + console.log({dates}) + if (dates && dates[0] && dates[1]) { + form.setFieldValue("timeDifference", betweenTime(dayjs(dates[1]), dayjs(dates[0]))) + } else { + form.setFieldValue("timeDifference", "") + } + } + }} + placeholder={['开始时间', '结束时间']} + allowClear/> + + + + ) +} +export default ClickRecord; diff --git a/src/components/SettingCron.tsx b/src/components/SettingCron.tsx index b11407d..7af585f 100644 --- a/src/components/SettingCron.tsx +++ b/src/components/SettingCron.tsx @@ -9,6 +9,7 @@ type PresetType = 'everyMinute' | 'everyHour' | 'daily' | 'weekly' | 'monthly'; interface CronGeneratorProps { setCronFunction: (data: boolean) => void; cron?: string; + canSetting:boolean; } function cronToChinese(cronExpression: string) { @@ -147,11 +148,11 @@ function cronToChinese(cronExpression: string) { return description; } -const CronGenerator: React.FC = ({setCronFunction, cron}) => { +const CronGenerator: React.FC = ({setCronFunction, cron,canSetting}) => { 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] : '*'); + const [cronMinutes, setCronMinutes] = useState(cron ? cron.split(' ')[1] : '0'); const [cronHours, setCronHours] = useState(cron ? cron.split(' ')[2] : '*'); const [cronDayOfMonth, setCronDayOfMonth] = useState(cron ? cron.split(' ')[3] : '*'); const [cronMonth, setCronMonth] = useState(cron ? cron.split(' ')[4] : '*'); @@ -267,8 +268,8 @@ const CronGenerator: React.FC = ({setCronFunction, cron}) => return ( - {canReadCron} - >> => { return httpReq.get(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + `/task/schedule/nextTime?cron=${cron}`) -} \ No newline at end of file +} +export const clickRecordAPI = (data:TaskScheduleRecordVO)=>{ + return httpReq.post(process.env.NEXT_PUBLIC_TODO_REQUEST_URL + + `/task/schedule/click`,data) +} diff --git a/src/components/type/TaskSchedule.d.tsx b/src/components/type/TaskSchedule.d.tsx new file mode 100644 index 0000000..edd7eb5 --- /dev/null +++ b/src/components/type/TaskSchedule.d.tsx @@ -0,0 +1,11 @@ +export interface TaskScheduleRecordVO{ + id:string, + timeDifference:string, + remarks:string, + startDate:Date, + recordDate:Date, + taskId:string, +} +export interface TaskScheduleRecordForm extends TaskScheduleRecordVO{ + recordTimeRange:Date[] +} diff --git a/src/lib/definitions.ts b/src/lib/definitions.ts index 2d02cac..6c76c71 100644 --- a/src/lib/definitions.ts +++ b/src/lib/definitions.ts @@ -38,6 +38,7 @@ export type TaskMessage ={ fId?:string; fName?:string; taskType?:string; + onceConsume?:string; stepList?:TaskStepSortVO[]; } diff --git a/src/ui/task/RightOption.tsx b/src/ui/task/RightOption.tsx index 79324ea..95676ae 100644 --- a/src/ui/task/RightOption.tsx +++ b/src/ui/task/RightOption.tsx @@ -1,5 +1,5 @@ import React, {Fragment, useState} from 'react'; -import {Form, MenuProps, message, Popconfirm} from 'antd'; +import {ConfigProvider, Form, MenuProps, message, Popconfirm} from 'antd'; import { Dropdown, theme } from 'antd'; import {commonUpdate, deleteTask, OPERATION_BUTTON_TYPE} from "@/lib/task/project/data"; import {QuestionCircleOutlined} from "@ant-design/icons"; diff --git a/src/ui/task/TitleOperation.tsx b/src/ui/task/TitleOperation.tsx index fbca7d1..f129130 100644 --- a/src/ui/task/TitleOperation.tsx +++ b/src/ui/task/TitleOperation.tsx @@ -1,6 +1,6 @@ 'use client' import React, {Fragment, useContext, useEffect} from "react"; -import {Button, DatePicker, Select, Space} from "antd"; +import {Button, Checkbox, CheckboxOptionType, DatePicker, Select, Space} from "antd"; import {usePathname, useRouter} from "next/navigation"; import {DetailModelForm} from "@/ui/task/project/DetailModelForm"; import {OPERATION_BUTTON_TYPE, taskStateList} from "@/lib/task/project/data"; @@ -33,6 +33,11 @@ export const TitleOperation: React.FC = ({ expectStartTimeParseResult[1] && expectStartTimeParseResult[1].value ? dayjs(expectStartTimeParseResult[1].value.toString()) : undefined ]; const pName = useSearchParams().get("pName"); + const typeList: CheckboxOptionType[] = [ + { label: '计划', value: '0,1,2,3', }, + { label: '打卡', value: '4', }, + { label: '事件', value: '5', }, + ]; useEffect(() => { if(pName&&pName!=document.title){ document.title = pName; @@ -63,6 +68,12 @@ export const TitleOperation: React.FC = ({ // setCurrentPath("/task/project") }}>日历显示 } + { + usePathname().startsWith("/task/calendar") && + ( + + ) + } { /*日历需要状态*/ !usePathname().startsWith("/task/project") && diff --git a/src/ui/task/project/DetailModelForm.module.css b/src/ui/task/project/DetailModelForm.module.css new file mode 100644 index 0000000..fa236df --- /dev/null +++ b/src/ui/task/project/DetailModelForm.module.css @@ -0,0 +1,3 @@ +.localDiv{ + margin-bottom: 24px; +} diff --git a/src/ui/task/project/DetailModelForm.tsx b/src/ui/task/project/DetailModelForm.tsx index ed2fc70..84ba0e8 100644 --- a/src/ui/task/project/DetailModelForm.tsx +++ b/src/ui/task/project/DetailModelForm.tsx @@ -6,7 +6,7 @@ import { ProFormSelect, ProFormText, ProFormTextArea, ProFormTreeSelect, } from '@ant-design/pro-components'; -import {Button, Form, message, Popconfirm, Spin} from 'antd'; +import {Button, Form, message, Popconfirm, Select, Space, Spin} from 'antd'; import React, {Fragment, useEffect, useState} from "react"; import { addTask, deleteTask, getTask, @@ -21,7 +21,10 @@ import DiaryOption from "@/components/DiaryOption"; import ShareOption from "@/components/ShareOption"; import StepSort from "@/components/StepSort"; import SettingCron from "@/components/SettingCron"; - +import ClickRecord from "@/components/ClickRecord"; +import onceConsumeList from "@/components/constant/onceConsumeList.json" +import {onceConsumerRead} from "@/utils/codeToReadName"; +import style from "@/ui/task/project/DetailModelForm.module.css" export type DetailModelFormProps = { // 当前内容id itemId?: string, @@ -56,6 +59,7 @@ export const DetailModelForm: React.FC = (props) => { const [taskType, setTaskType] = useState('0') const [spinning, setSpinning] = useState(true) const [operationRequest, setOperationRequest] = useState(false) + const [onceConsumeChange, setOnceConsumeChange] = useState(["1", "h"]) useEffect(() => { if (props.itemId != undefined && ( props.operationId === OPERATION_BUTTON_TYPE.DETAIL || props.operationId === OPERATION_BUTTON_TYPE.UPDATE)) { @@ -77,6 +81,9 @@ export const DetailModelForm: React.FC = (props) => { setTaskType(task.data.taskType) } setRequestTask(task.data) + if (task.data.onceConsume) { + setOnceConsumeChange(task.data.onceConsume.split(",")) + } console.log("form.setFieldsValue(task.data)" + JSON.stringify(task.data)) } else { message.error(task.status.message); @@ -191,6 +198,10 @@ export const DetailModelForm: React.FC = (props) => { if (taskType == '2') { result.push() } + if (taskType == '3') { + result.push() + } return result; }, } : { @@ -296,12 +307,16 @@ export const DetailModelForm: React.FC = (props) => { + (task.value == '1' && (requestTask?.id)) ? {...task, disabled: true} : task)} + // (task.value == '1' ) ? {...task, disabled: true} : task)} width="sm" name="taskType" label="任务类型" initialValue={taskType} - disabled={editFormDisable} + // 不建议类型随意改变 + disabled={requestTask && requestTask.id !== undefined} + // disabled={requestTask && requestTask.id !== undefined && requestTask.taskType == '1'} onChange={(value: string, option) => { setTaskType(value) }} @@ -378,7 +393,52 @@ export const DetailModelForm: React.FC = (props) => { placeholder="请输入任务描述" disabled={editFormDisable} /> +
+ + 提醒: + { + }} + options={[{label: "前", + value: "before",},{label: "后", + value: "after",} + ]} + /> + { + }} + options={[{label: "分钟", value: "m"}, {label: "小时", value: "h"}, { + label: "天", + value: "d" + }]} + /> + +
@@ -421,23 +481,66 @@ export const DetailModelForm: React.FC = (props) => { initialValue={[dayjs(), undefined]} name="expectedTimeRange" label="期望时间" - fieldProps={{allowEmpty: [true, true], showTime: true, needConfirm: true}} + fieldProps={{ + showTime: { + format: 'HH:mm', + }, + format: "YYYY-MM-DD HH:mm", allowEmpty: [true, true], needConfirm: true + }} placeholder={['开始时间', '结束时间']} disabled={editFormDisable} + /> - {taskType == "3" && { - } - }/>} + {taskType == "3" && + + {editFormDisable ? +
+ 单次耗时:{onceConsumerRead(requestTask?.onceConsume)} +
: +
+ + 单次耗时: + { + onceConsumeChange[1] = value + setOnceConsumeChange(onceConsumeChange) + }} + options={[{label: "分钟", value: "m"}, {label: "小时", value: "h"}, { + label: "天", + value: "d" + }]} + /> + +
} + { + }}/>
}
diff --git a/src/utils/codeToReadName.tsx b/src/utils/codeToReadName.tsx new file mode 100644 index 0000000..279c081 --- /dev/null +++ b/src/utils/codeToReadName.tsx @@ -0,0 +1,14 @@ + +export function onceConsumerRead(onceConsume:string|undefined){ + if (onceConsume){ + let result = onceConsume.split(",") + if (result[1]=='m'){ + return result[0]+'分钟' + }else if (result[1]=='h'){ + return result[0]+'小时' + }else if (result[1]=='d'){ + return result[0]+'天' + } + } + return '' +} \ No newline at end of file diff --git a/src/utils/timeFormatUtil.ts b/src/utils/timeFormatUtil.ts new file mode 100644 index 0000000..ae0e0a1 --- /dev/null +++ b/src/utils/timeFormatUtil.ts @@ -0,0 +1,243 @@ +import dayjs, { Dayjs, isDayjs } from "dayjs"; +import utc from "dayjs/plugin/utc"; +dayjs.extend(utc) +const DATE_FORMAT = "YYYY-MM-DD" +// 到秒没啥意义 +const DATE_TIME_FORMAT = "YYYY-MM-DD HH:mm" + +const DATE_TIME_FORMAT_SIMPLE = "MM-DD HH:mm" + +function dayStartUtcFormat(dayjsString: Dayjs | string | Date) { + + if (!dayjsString) { + return + } + let dayJsObj; + if (isDayjs(dayjsString)) { + dayJsObj = dayjsString; + } else { + dayJsObj = dayjs(dayjsString); + } + return dayJsObj.set('h', 0).set('m', 0).set('s', 0).set('ms', 0).utc().format() +} + +function dateStartUtcFormat(dateObject: Dayjs | string | Date) { + if (!dateObject) { + return + } + let dayJsObj; + if (isDayjs(dateObject)) { + dayJsObj = dateObject; + } else { + dayJsObj = dayjs(dateObject); + } + return dayJsObj.set('h', 0).set('m', 0).set('s', 0).set('ms', 0).utc().format() +} + +function nextDateStartUtcFormat(dateObject: Dayjs | string | Date) { + if (!dateObject) { + return + } + let dayJsObj; + if (isDayjs(dateObject)) { + dayJsObj = dateObject; + } else { + dayJsObj = dayjs(dateObject); + } + return dayJsObj.add(1, "d").set('h', 0).set('m', 0).set('s', 0).set('ms', 0).utc().format() +} + +function nextDayStartUtcFormat(dayjsString: Dayjs | string | Date): string | undefined { + if (!dayjsString) { + return + } + let dayJsObj; + if (isDayjs(dayjsString)) { + dayJsObj = dayjsString; + } else { + dayJsObj = dayjs(dayjsString); + } + return dayJsObj.add(1, "d").set('h', 0).set('m', 0).set('s', 0).set('ms', 0).utc().format() +} +function cronToChinese(cronExpression: string) { + // 解析Cron表达式各部分 + const parts = cronExpression.split(' '); + if (parts.length < 5) { + return '非法的Cron表达式'; + } + const [minute, hour, dayOfMonth, month, dayOfWeek] = parts; + + // 解析分钟 + function parseMinute(m: string) { + if (m === '*') return '每分钟'; + if (m === '0') return ''; + if (m.includes('/')) { + const interval = m.split('/')[1]; + return `每${interval}分钟`; + } + if (m.includes(',')) { + const minutes = m.split(',').map(m => 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; +} +function betweenTime(start: Dayjs, end: Dayjs) { + let minutes = Math.ceil(start.second(0).millisecond(0).diff(end.second(0).millisecond(0), "minute", true)); + minutes = Math.abs(minutes) + if (minutes > 0) { + // 计算天数、小时数和剩余的分钟数 + const days = Math.floor(minutes / (24 * 60)); + const remainingHours = Math.floor((minutes % (24 * 60)) / 60); + const remainingMinutes = minutes % 60; + // 构建结果数组 + const parts = []; + if (days > 0) { + parts.push(`${days}天`); + } + if (remainingHours > 0) { + parts.push(`${remainingHours}小时`); + } + if (remainingMinutes > 0 || parts.length === 0) { + parts.push(`${remainingMinutes}分钟`); + } + return parts.join(' ') + } + return "0分钟"; +} +function dayjsWeek(day: Dayjs): string { + let week = day.day() + if (week == 0) { + return '周日' + } else if (week == 1) { + return '周一' + } else if (week == 2) { + return '周二' + } else if (week == 3) { + return '周三' + } else if (week == 4) { + return '周四' + } else if (week == 5) { + return '周五' + } else if (week == 6) { + return '周六' + } + return "" +} +export { + DATE_TIME_FORMAT, DATE_FORMAT, DATE_TIME_FORMAT_SIMPLE, + dayStartUtcFormat, nextDayStartUtcFormat, betweenTime, + dateStartUtcFormat, nextDateStartUtcFormat, cronToChinese, dayjsWeek +} \ No newline at end of file