diff --git a/package.json b/package.json index 6a14dcc..91d3109 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,10 @@ "version": "1.0.0", "description": "a react gantt timeline calendar", "keywords": [ - "npm", - "template" + "react", + "gantt", + "timeline", + "calendar" ], "homepage": "https://github.com/eternallycyf/ims-gantt-timeline-calendar", "bugs": { @@ -66,13 +68,15 @@ ] }, "resolutions": { - "dumi": "2.2.17" + "dumi": "~2.2.17" }, "dependencies": { "@babel/runtime": "^7.23.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "zrender": "^5.5.0" + "react-use": "^17.5.0", + "zrender": "^5.4.4", + "zustand": "^4.5.2" }, "devDependencies": { "@ant-design/icons": "^5.2.6", @@ -91,7 +95,7 @@ "concurrently": "^7", "conventional-changelog-gitmoji-config": "^1", "cross-env": "^7", - "dumi": "2.2.17", + "dumi": "~2.2.17", "dumi-theme-antd-style": "^0.31.0", "eslint": "^8", "father": "^4", diff --git a/src/components/Hello/demo/index.tsx b/src/components/Hello/demo/index.tsx deleted file mode 100644 index eb7974c..0000000 --- a/src/components/Hello/demo/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { Hello } from 'ims-gantt-timeline-calendar'; - -export default Hello; diff --git a/src/components/Hello/index.md b/src/components/Hello/index.md deleted file mode 100644 index 8b3ff89..0000000 --- a/src/components/Hello/index.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Hello -description: Hello -toc: content -group: - title: 分组 - order: 0 -demo: - cols: 2 ---- - -## Hello - - diff --git a/src/components/Hello/index.tsx b/src/components/Hello/index.tsx deleted file mode 100644 index fecc853..0000000 --- a/src/components/Hello/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Hello = () => { - return 'word'; -}; - -export default Hello; diff --git a/src/components/TimeLine/TimeLine.tsx b/src/components/TimeLine/TimeLine.tsx new file mode 100644 index 0000000..f42de57 --- /dev/null +++ b/src/components/TimeLine/TimeLine.tsx @@ -0,0 +1,246 @@ +import { FC, useEffect } from 'react'; +import { useLocation } from 'react-use'; +import * as zrender from 'zrender'; +import { div } from 'zrender/lib/core/vector'; +import './index.less'; +import type { TimeLineProps } from './interface'; +import { useSearchParams, useSearchParamsActions } from './store/useGanttChartStore'; +import { getRandomColor } from './utils'; +import { hachureLines } from './utils/hachure'; +import { isHoliday } from './utils/holidays'; +import { getRealDuration } from './utils/task'; + +const TimeLine: FC = (props) => { + const { tasks = [] } = props; + const { refreshSearchParamsStore } = useSearchParamsActions(); + const { + initChartStartX, + initChartStartY, + timeScaleHeight, + milestoneTopHeight, + unitWidth, + barHeight, + barMargin, + halfUnitWidth, + taskNamePaddingLeft, + } = useSearchParams(); + + useEffect(() => { + refreshSearchParamsStore(); + }, [location]); + + const handleInitTimeLine = () => { + let container = document.getElementById('TimeLine'); + let zr = zrender.init(container); + // margin left to the container + const chartStartX = initChartStartX; + // margin top to the container + const chartStartY = Math.max(initChartStartY, timeScaleHeight + milestoneTopHeight); + + // 1. 拿到画布的宽 + const canvasWidth = zr.getWidth()!; + const canvasHeight = zr.getHeight(); + // 2. 计算需要画多少格 + const timeScaleWidth = Math.ceil(canvasWidth / unitWidth); + + // 3. 画时间轴的矩形,设置位置x,y,长宽,给一个背景色填充 + const timeScale = new zrender.Rect({ + shape: { + x: chartStartX, + y: chartStartY - timeScaleHeight, + width: timeScaleWidth * unitWidth, + height: timeScaleHeight, + }, + style: { + fill: 'rgba(255, 0,0, .2)', + }, + }); + zr.add(timeScale); + + const lastScrollX = 0; + const gridStartX = chartStartX; + const gridEndX = timeScaleWidth * unitWidth; + const gridLineCount = timeScaleWidth + 1; + const deltaScrollX = Math.floor(lastScrollX / unitWidth); + + // 3. 遍历要画的线的个数 + for (let i = 0 + deltaScrollX, count = 0; count < gridLineCount; i++, count++) { + const gridX = gridStartX + i * unitWidth; + // 4. 画一根线,从(x1, y1) -> (x2, y2) + const gridLine = new zrender.Line({ + shape: { + x1: gridX, + y1: chartStartY - timeScaleHeight, + x2: gridX, + y2: chartStartY + (barHeight + barMargin) * tasks.length, + }, + style: { + stroke: 'lightgray', + }, + }); + // 1. 线比格子多1,所以要提前1步退出 + if (count < gridLineCount - 1) { + // MARK: 同一个遍历画「休息日斜线」 + const now = +new Date('2024-01-01'); + const currentDate = now + i * 60 * 1000 * 60 * 24; + const dateInfo = isHoliday(currentDate); + // 是休息日的话画斜线 + if (dateInfo.isHoliday) { + try { + // 返回要画的线的开始、结束坐标 + const lines = hachureLines( + [ + [chartStartX + i * unitWidth, chartStartY], + [chartStartX + i * unitWidth + unitWidth, chartStartY], + [ + chartStartX + i * unitWidth + unitWidth, + chartStartY + (barHeight + barMargin) * tasks.length, + ], + [chartStartX + i * unitWidth, chartStartY + (barHeight + barMargin) * tasks.length], + ], + 10, + 45, + ); + // 用zrender画线段,描边上色 + lines.forEach((line) => { + const [x1, y1] = line[0]; + const [x2, y2] = line[1]; + const l = new zrender.Line({ + shape: { + x1, + y1, + x2, + y2, + }, + style: { + stroke: 'rgba(221, 221, 221, 0.7)', + }, + }); + zr.add(l); + }); + } catch (error) { + console.log(error); + } + } + + // 2. 画基本文本,可以直接改成日期名字,这里直接写遍历的index + const dateText = new zrender.Text({ + style: { + // text: i, + text: dateInfo.dateString, + x: gridX, + y: chartStartY - timeScaleHeight, + }, + z: 1, + }); + // 3. 为了居中,要算出文本的宽高 + const { width, height } = dateText.getBoundingRect(); + // 4. 重新设置日期文本位置,居中 + dateText.attr({ + style: { + x: gridX - width / 2 + halfUnitWidth, + y: chartStartY - timeScaleHeight - height / 2 + timeScaleHeight / 2, + }, + }); + // 5. 加到zrender实例中 + zr.add(dateText); + } + + zr.add(gridLine); + } + + // 1. 遍历tasks数组 + tasks.forEach(function (task, index) { + // 2. 因为有最后一行是空行,没有任务,用来创建新任务,轮空不画 + if (!task?.name) return; + // 3. 计算任务的绘制位置和矩形宽高 + const x = chartStartX + task.start * unitWidth; + const y = chartStartY + (barHeight + barMargin) * index; + const width = task.duration * unitWidth; + const taskBarRect = { + width, + height: barHeight, + }; + // 4. 建一个组,设置可以拖拽 (感谢这个属性,后续交互省了很多力气,还可以设置只能垂直或者水平拖动) + const group = new zrender.Group({ + x, + y, + draggable: true, // Enable draggable for the group + // draggable: "horizontal", // Enable draggable for the group + }); + // 5. 创建任务跨度矩形 + const rect = new zrender.Rect({ + shape: { + x: 0, + y: 0, + width: width, + height: barHeight, + r: 6, + }, + style: { + fill: task.fillColor, + }, + cursor: 'move', + }); + // 6. 加到组里 + group.add(rect); + + // Create a text shape for task name + const taskName = new zrender.Text({ + style: { + text: task.name, + x: taskNamePaddingLeft, + y: barHeight / 2 - 12 / 2, + textFill: 'white', + textAlign: 'left', + textVerticalAlign: 'middle', + fill: 'white', + } as any, + cursor: 'move', + }); + group.add(taskName); + // Create a text shape for resource assignment + const resourceText = new zrender.Text({ + style: { + text: 'Assigned to: ' + task.resource, + x: 0 + width + 5, + y: barHeight / 2 + 0 - 12 / 2, + textFill: 'black', + } as any, + cursor: 'normal', + }); + group.add(resourceText); + const taskDurationText = new zrender.Text({ + style: { + text: `${getRealDuration(task, false)}天`, + x: width - taskNamePaddingLeft, + y: barHeight / 2 - 12 / 2, + textFill: 'white', + textAlign: 'left', + textVerticalAlign: 'middle', + fill: 'white', + } as any, + cursor: 'move', + }); + const { width: taskDurationTextWidth } = taskDurationText.getBoundingRect(); + taskDurationText.attr({ + style: { + x: width - taskDurationTextWidth - taskNamePaddingLeft, + }, + }); + group.add(taskDurationText); + + zr.add(group); + }); + }; + + useEffect(handleInitTimeLine, []); + + return ( +
+
+
+ ); +}; + +export default TimeLine; diff --git a/src/components/TimeLine/demo/index.less b/src/components/TimeLine/demo/index.less new file mode 100644 index 0000000..dd6f02e --- /dev/null +++ b/src/components/TimeLine/demo/index.less @@ -0,0 +1,81 @@ +#zrender-container, +#off-screen { + outline: 1px solid red; + /* margin: 20px 0 0 150px; */ +} + +.popup-wrapper { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + background-color: rgba(255, 0, 0, 0.3); + /* display: flex; */ + align-items: center; + justify-content: center; + flex-direction: column; + display: none; +} + +.popup-wrapper .popup-wrapper__body { + width: 337px; +} + +.popup-wrapper .popup-wrapper__body .button-wrapper button { + display: block; +} + +.popup-wrapper .popup-wrapper__body textarea { + display: block; + width: 100%; +} + +.popup-wrapper .popup-wrapper__body .button-wrapper { + display: flex; + justify-content: space-between; +} + +.popup-wrapper .popup-wrapper__body .button-wrapper button { + width: 100px; + height: 32px; + cursor: pointer; +} + +.popup-wrapper .popup-wrapper__body .button-wrapper button.delete:hover { + background-color: #f00; + outline: none; + border-color: #f00; + color: #fff; +} + +.popup-wrapper .popup-wrapper__body .button-wrapper button.copy:hover { + background-color: #86a5e5; + outline: none; + border-color: #86a5e5; + color: #fff; +} + +.popup-wrapper .popup-wrapper__body .button-wrapper button.modify:hover { + background-color: #1f8a6f; + outline: none; + border-color: #1f8a6f; + color: #fff; +} + +#color-picker { + display: flex; + margin: 5px 0; +} + +#color-picker>div { + width: 80px; + height: 30px; + margin-right: 10px; +} + +#color-picker>div:hover { + cursor: pointer; + background-color: attr(data-color) !important; +} \ No newline at end of file diff --git a/src/components/TimeLine/demo/index.tsx b/src/components/TimeLine/demo/index.tsx new file mode 100644 index 0000000..0fb0931 --- /dev/null +++ b/src/components/TimeLine/demo/index.tsx @@ -0,0 +1,26 @@ +import { TimeLine, getRandomColor } from 'ims-gantt-timeline-calendar'; +import './index.less'; + +const tasks = [ + { name: 'Task 1', start: 0, duration: 3, resource: 'John', fillColor: getRandomColor() }, + { name: 'Task 2', start: 2, duration: 4, resource: 'Jane', fillColor: getRandomColor() }, + { + name: 'Task 3 long long long', + start: 7, + duration: 1, + resource: 'Bob', + fillColor: getRandomColor(), + }, + { name: 'Task 4', start: 8, duration: 2, resource: 'Bose', fillColor: getRandomColor() }, + { name: 'Task 5', start: 10, duration: 8, resource: 'Uno', fillColor: getRandomColor() }, + {}, + // Add more tasks as needed +]; + +export default () => { + return ( + <> + + + ); +}; diff --git a/src/components/TimeLine/hooks/index.ts b/src/components/TimeLine/hooks/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TimeLine/index.less b/src/components/TimeLine/index.less new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TimeLine/index.md b/src/components/TimeLine/index.md new file mode 100644 index 0000000..35ec35e --- /dev/null +++ b/src/components/TimeLine/index.md @@ -0,0 +1,13 @@ +--- +title: TimeLine +toc: content +group: + title: 分组 + order: 0 +demo: + cols: 2 +--- + +## TimeLine + + diff --git a/src/components/TimeLine/index.ts b/src/components/TimeLine/index.ts new file mode 100644 index 0000000..5fe93dd --- /dev/null +++ b/src/components/TimeLine/index.ts @@ -0,0 +1,2 @@ +import TimeLine from './TimeLine'; +export default TimeLine; diff --git a/src/components/TimeLine/interface.ts b/src/components/TimeLine/interface.ts new file mode 100644 index 0000000..5c5906a --- /dev/null +++ b/src/components/TimeLine/interface.ts @@ -0,0 +1,3 @@ +export interface TimeLineProps { + tasks?: any[]; +} diff --git a/src/components/TimeLine/store/useGanttChartStore.ts b/src/components/TimeLine/store/useGanttChartStore.ts new file mode 100644 index 0000000..e36fba3 --- /dev/null +++ b/src/components/TimeLine/store/useGanttChartStore.ts @@ -0,0 +1,106 @@ +import { create } from 'zustand'; +import { StorageEnum } from '../type/enum'; +import { GetUrlParms } from '../utils'; +import { getItem, removeItem, setItem } from '../utils/storage'; + +export interface SearchParamsType { + currentGroup: any; + debug: boolean; + taskOwner: string; + unitWidth: number; + halfUnitWidth: number; + taskNamePaddingLeft: number; + initChartStartX: number; + initChartStartY: number; + timeScaleHeight: number; + milestoneTopHeight: number; + barHeight: number; + barMargin: number; + scrollSpeed: number; + includeHoliday: boolean; + useLocal: boolean; + useRemote: boolean; + view: string; + mockTaskSize: number; + todayOffset: number; + initLastScrollX: number; + filter: React.CSSProperties['color']; + showFilter: boolean; + arrowSize: number; + showArrow: boolean; +} + +interface SearchParamsStore { + searchParams: SearchParamsType; + actions: { + refreshSearchParamsStore: () => void; + setSearchParamsStore: (settings: Partial) => void; + clearSearchParamsStore: () => void; + }; +} + +const useSearchParamsStore = create((set, get) => { + const unitWidth = 160; + const halfUnitWidth = 160 / 2; + const barHeight = 30; + const todayOffset = Math.floor((+new Date() - +new Date('2024-01-01')) / (60 * 60 * 24 * 1000)); + + return { + searchParams: getItem(StorageEnum.searchParams) || { + currentGroup: null, + debug: false, + taskOwner: 'alexq', + unitWidth, + halfUnitWidth, + taskNamePaddingLeft: 15, + initChartStartX: 1, + initChartStartY: 50, + timeScaleHeight: 20, + milestoneTopHeight: 20, + barHeight: 30, + barMargin: 1, + scrollSpeed: 35, + includeHoliday: false, + useLocal: false, + useRemote: false, + view: '', + mockTaskSize: 0, + todayOffset: todayOffset, + initLastScrollX: (todayOffset - 1) * unitWidth, + filter: undefined, + showFilter: false, + arrowSize: (barHeight / 3) * 2, + showArrow: true, + }, + actions: { + refreshSearchParamsStore: () => { + const params = GetUrlParms(); + const { [StorageEnum.searchParams]: defaultSearchParams } = get(); + const newParams = { + ...defaultSearchParams, + ...params, + halfUnitWidth: (params?.unitWidth || unitWidth) / 2, + barMargin: params?.debug ? 10 : params?.barMargin ?? 1, + view: params?.view ?? '', + mockTaskSize: + !params?.useRemote && !params?.useLocal && params?.mockTaskSize + ? Number(params.mockTaskSize) + : 0, + initLastScrollX: (params?.todayOffset - 1) * (params?.unitWidth ?? unitWidth), + }; + set({ [StorageEnum.searchParams]: newParams }); + setItem(StorageEnum.searchParams, newParams); + }, + setSearchParamsStore: (searchParams) => { + const { [StorageEnum.searchParams]: defaultSearchParams } = get(); + const newDefaultSearchParams = { ...defaultSearchParams, ...searchParams }; + set({ [StorageEnum.searchParams]: newDefaultSearchParams }); + setItem(StorageEnum.searchParams, newDefaultSearchParams); + }, + clearSearchParamsStore: () => removeItem(StorageEnum.searchParams), + }, + }; +}); + +export const useSearchParams = () => useSearchParamsStore((state) => state.searchParams); +export const useSearchParamsActions = () => useSearchParamsStore((state) => state.actions); diff --git a/src/components/TimeLine/type/enum.ts b/src/components/TimeLine/type/enum.ts new file mode 100644 index 0000000..e972db6 --- /dev/null +++ b/src/components/TimeLine/type/enum.ts @@ -0,0 +1,3 @@ +export enum StorageEnum { + searchParams = 'searchParams', +} diff --git a/src/components/TimeLine/type/utils.ts b/src/components/TimeLine/type/utils.ts new file mode 100644 index 0000000..8f008d1 --- /dev/null +++ b/src/components/TimeLine/type/utils.ts @@ -0,0 +1,15 @@ +// // 获取枚举的 value +// type EnumValues = `${StorageEnum}` + +// // 获取枚举的 key +// type EnumKeys = keyof typeof StorageEnum + +/** https://github.com/Microsoft/TypeScript/issues/29729 */ +export type LiteralUnion = T | (string & {}); + +export type ValuesOf = T[keyof T]; + +export type AddStringPrefix< + Str extends string, + String extends string = '', +> = Str extends `${infer Rest}` ? `${String}${Rest}` : Str; diff --git a/src/components/TimeLine/utils/hachure.ts b/src/components/TimeLine/utils/hachure.ts new file mode 100644 index 0000000..86fc9ab --- /dev/null +++ b/src/components/TimeLine/utils/hachure.ts @@ -0,0 +1,158 @@ +function rotatePoints(points: any[], center: any[], degrees: any) { + if (points && points.length) { + const [cx, cy] = center; + const angle = (Math.PI / 180) * degrees; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + for (const p of points) { + const [x, y] = p; + p[0] = (x - cx) * cos - (y - cy) * sin + cx; + p[1] = (x - cx) * sin + (y - cy) * cos + cy; + } + } +} + +function rotateLines(lines: any[], center: any[], degrees: any) { + const points: any[] = []; + lines.forEach((line: any) => points.push(...line)); + rotatePoints(points, center, degrees); +} + +function areSamePoints(p1: any[], p2: any[]) { + return p1[0] === p2[0] && p1[1] === p2[1]; +} + +export function hachureLines( + polygons: any[], + hachureGap: any, + hachureAngle: any, + hachureStepOffset = 1, +) { + const angle = hachureAngle; + const gap = Math.max(hachureGap, 0.1); + const polygonList = + polygons[0] && polygons[0][0] && typeof polygons[0][0] === 'number' ? [polygons] : polygons; + const rotationCenter = [0, 0]; + if (angle) { + for (const polygon of polygonList) { + rotatePoints(polygon, rotationCenter, angle); + } + } + const lines = straightHachureLines(polygonList, gap, hachureStepOffset); + if (angle) { + for (const polygon of polygonList) { + rotatePoints(polygon, rotationCenter, -angle); + } + rotateLines(lines, rotationCenter, -angle); + } + return lines; +} +function straightHachureLines(polygons: any[], gap: number, hachureStepOffset: number) { + const vertexArray: any[] = []; + for (const polygon of polygons) { + const vertices = [...polygon]; + if (!areSamePoints(vertices[0], vertices[vertices.length - 1])) { + vertices.push([vertices[0][0], vertices[0][1]]); + } + if (vertices.length > 2) { + vertexArray.push(vertices); + } + } + const lines: any[] = []; + // eslint-disable-next-line no-param-reassign + gap = Math.max(gap, 0.1); + // Create sorted edges table + const edges: any[] = []; + for (const vertices of vertexArray) { + for (let i = 0; i < vertices.length - 1; i++) { + const p1 = vertices[i]; + const p2 = vertices[i + 1]; + if (p1[1] !== p2[1]) { + const ymin = Math.min(p1[1], p2[1]); + edges.push({ + ymin, + ymax: Math.max(p1[1], p2[1]), + x: ymin === p1[1] ? p1[0] : p2[0], + islope: (p2[0] - p1[0]) / (p2[1] - p1[1]), + }); + } + } + } + edges.sort((e1, e2) => { + if (e1.ymin < e2.ymin) { + return -1; + } + if (e1.ymin > e2.ymin) { + return 1; + } + if (e1.x < e2.x) { + return -1; + } + if (e1.x > e2.x) { + return 1; + } + if (e1.ymax === e2.ymax) { + return 0; + } + return (e1.ymax - e2.ymax) / Math.abs(e1.ymax - e2.ymax); + }); + if (!edges.length) { + return lines; + } + // Start scanning + let activeEdges: any[] = []; + let y = edges[0].ymin; + let iteration = 0; + while (activeEdges.length || edges.length) { + if (edges.length) { + let ix = -1; + for (let i = 0; i < edges.length; i++) { + if (edges[i].ymin > y) { + break; + } + ix = i; + } + const removed = edges.splice(0, ix + 1); + // eslint-disable-next-line @typescript-eslint/no-loop-func + removed.forEach((edge) => { + activeEdges.push({ s: y, edge }); + }); + } + // eslint-disable-next-line @typescript-eslint/no-loop-func + activeEdges = activeEdges.filter((ae) => { + if (ae.edge.ymax <= y) { + return false; + } + return true; + }); + activeEdges.sort((ae1, ae2) => { + if (ae1.edge.x === ae2.edge.x) { + return 0; + } + return (ae1.edge.x - ae2.edge.x) / Math.abs(ae1.edge.x - ae2.edge.x); + }); + // fill between the edges + if (hachureStepOffset !== 1 || iteration % gap === 0) { + if (activeEdges.length > 1) { + for (let i = 0; i < activeEdges.length; i = i + 2) { + const nexti = i + 1; + if (nexti >= activeEdges.length) { + break; + } + const ce = activeEdges[i].edge; + const ne = activeEdges[nexti].edge; + lines.push([ + [Math.round(ce.x), y], + [Math.round(ne.x), y], + ]); + } + } + } + y += hachureStepOffset; + activeEdges.forEach((ae) => { + ae.edge.x = ae.edge.x + hachureStepOffset * ae.edge.islope; + }); + iteration++; + } + return lines; +} diff --git a/src/components/TimeLine/utils/holidays.ts b/src/components/TimeLine/utils/holidays.ts new file mode 100644 index 0000000..eb45487 --- /dev/null +++ b/src/components/TimeLine/utils/holidays.ts @@ -0,0 +1,275 @@ +const holidays = { + '01-01': { + holiday: true, + name: '元旦', + wage: 3, + date: '2024-01-01', + rest: 1, + }, + '02-04': { + holiday: false, + name: '春节前补班', + wage: 1, + after: false, + target: '春节', + date: '2024-02-04', + rest: 34, + }, + '02-10': { + holiday: true, + name: '初一', + wage: 3, + date: '2024-02-10', + rest: 40, + }, + '02-11': { + holiday: true, + name: '初二', + wage: 3, + date: '2024-02-11', + }, + '02-12': { + holiday: true, + name: '初三', + wage: 3, + date: '2024-02-12', + }, + '02-13': { + holiday: true, + name: '初四', + wage: 2, + date: '2024-02-13', + }, + '02-14': { + holiday: true, + name: '初五', + wage: 2, + date: '2024-02-14', + }, + '02-15': { + holiday: true, + name: '初六', + wage: 2, + date: '2024-02-15', + }, + '02-16': { + holiday: true, + name: '初七', + wage: 2, + date: '2024-02-16', + }, + '02-17': { + holiday: true, + name: '初八', + wage: 2, + date: '2024-02-17', + }, + '02-18': { + holiday: false, + name: '春节后补班', + wage: 1, + after: true, + target: '春节', + date: '2024-02-18', + }, + '04-04': { + holiday: true, + name: '清明节', + wage: 3, + date: '2024-04-04', + rest: 46, + }, + '04-05': { + holiday: true, + name: '清明节', + wage: 2, + date: '2024-04-05', + }, + '04-06': { + holiday: true, + name: '清明节', + wage: 2, + date: '2024-04-06', + }, + '04-07': { + holiday: false, + name: '清明节后补班', + wage: 1, + target: '清明节', + after: true, + date: '2024-04-07', + }, + '04-28': { + holiday: false, + name: '劳动节前补班', + wage: 1, + target: '劳动节', + after: false, + date: '2024-04-28', + }, + '05-01': { + holiday: true, + name: '劳动节', + wage: 3, + date: '2024-05-01', + }, + '05-02': { + holiday: true, + name: '劳动节', + wage: 2, + date: '2024-05-02', + }, + '05-03': { + holiday: true, + name: '劳动节', + wage: 3, + date: '2024-05-03', + }, + '05-04': { + holiday: true, + name: '劳动节', + wage: 3, + date: '2024-05-04', + }, + '05-05': { + holiday: true, + name: '劳动节', + wage: 3, + date: '2024-05-05', + }, + '05-11': { + holiday: false, + name: '劳动节后补班', + after: true, + wage: 1, + target: '劳动节', + date: '2024-05-11', + }, + '06-08': { + holiday: true, + name: '端午节', + wage: 2, + date: '2024-06-08', + }, + '06-09': { + holiday: true, + name: '端午节', + wage: 2, + date: '2024-06-09', + }, + '06-10': { + holiday: true, + name: '端午节', + wage: 3, + date: '2024-06-10', + }, + '09-14': { + holiday: false, + name: '中秋节前补班', + after: false, + wage: 1, + target: '中秋节', + date: '2024-09-14', + }, + '09-15': { + holiday: true, + name: '中秋节', + wage: 2, + date: '2024-09-15', + }, + '09-16': { + holiday: true, + name: '中秋节', + wage: 2, + date: '2024-09-16', + }, + '09-17': { + holiday: true, + name: '中秋节', + wage: 3, + date: '2024-09-17', + }, + '09-29': { + holiday: false, + name: '国庆节前补班', + after: false, + wage: 1, + target: '国庆节', + date: '2024-09-29', + }, + '10-01': { + holiday: true, + name: '国庆节', + wage: 3, + date: '2024-10-01', + }, + '10-02': { + holiday: true, + name: '国庆节', + wage: 3, + date: '2024-10-02', + rest: 1, + }, + '10-03': { + holiday: true, + name: '国庆节', + wage: 3, + date: '2024-10-03', + }, + '10-04': { + holiday: true, + name: '国庆节', + wage: 2, + date: '2024-10-04', + }, + '10-05': { + holiday: true, + name: '国庆节', + wage: 2, + date: '2024-10-05', + }, + '10-06': { + holiday: true, + name: '国庆节', + wage: 2, + date: '2024-10-06', + rest: 1, + }, + '10-07': { + holiday: true, + name: '国庆节', + wage: 2, + date: '2024-10-07', + rest: 1, + }, + '10-12': { + holiday: false, + after: true, + wage: 1, + name: '国庆节后补班', + target: '国庆节', + date: '2024-10-12', + }, +}; + +export function isHoliday(dateString: any) { + const d = new Date(dateString); + const month = d.getMonth() + 1 + ''; + const monthWithPadding = month.padStart(2, '0'); + const day = d.getDate() + ''; + const dayWithPadding = day.padStart(2, '0'); + const date_key = `${monthWithPadding}-${dayWithPadding}`; + const isWeekend = [0, 6].indexOf(d.getDay()) != -1; + //@ts-ignore + if (holidays[date_key]) { + return { + //@ts-ignore + isHoliday: holidays[date_key].holiday, + dateString: `${month}-${day}`, + }; + } + return { + isHoliday: isWeekend, + dateString: `${month}-${day}`, + }; +} diff --git a/src/components/TimeLine/utils/index.ts b/src/components/TimeLine/utils/index.ts new file mode 100644 index 0000000..d9268ca --- /dev/null +++ b/src/components/TimeLine/utils/index.ts @@ -0,0 +1,42 @@ +import type { SearchParamsType } from '../store/useGanttChartStore'; + +export function getParamsFromSearch(key: SearchParamsType, autoConvert = true) { + const params = new URLSearchParams(location.search); + return params.get(key as unknown as string) && autoConvert + ? Number(params.get(key as unknown as string)) + : params.get(key as unknown as string); +} + +export function GetUrlParms(): Record { + let args: any = new Object(); + let query = decodeURIComponent(location.search).substring(1); + let pairs = query.split('&'); + for (let i = 0; i < pairs.length; i++) { + let pos = pairs[i].indexOf('='); + if (pos == -1) continue; + let argname = pairs[i].substring(0, pos); + let value = pairs[i].substring(pos + 1); + args[argname] = unescape(value); + } + return args; +} + +export function getRandomColor() { + // Generate random values for red, green, and blue components + const r = Math.floor(Math.random() * 256); + const g = Math.floor(Math.random() * 256); + const b = Math.floor(Math.random() * 256); + + // Convert values to hexadecimal and format the color + const hexColor = + '#' + + r.toString(16).padStart(2, '0') + + g.toString(16).padStart(2, '0') + + b.toString(16).padStart(2, '0'); + + return hexColor; +} + +export const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent, +); diff --git a/src/components/TimeLine/utils/storage.ts b/src/components/TimeLine/utils/storage.ts new file mode 100644 index 0000000..847efd7 --- /dev/null +++ b/src/components/TimeLine/utils/storage.ts @@ -0,0 +1,26 @@ +import type { StorageEnum } from '../type/enum'; + +export function getItem(key: StorageEnum): T | null { + let value = null; + try { + const result = window.localStorage.getItem(key); + if (result) value = JSON.parse(result); + } catch (error) { + console.error(error); + } + return value; +} + +export function getStringItem(key: StorageEnum): string | null { + return localStorage.getItem(key); +} + +export function setItem(key: StorageEnum, value: T): void { + localStorage.setItem(key, JSON.stringify(value)); +} +export function removeItem(key: StorageEnum): void { + localStorage.removeItem(key); +} +export function clearItems() { + localStorage.clear(); +} diff --git a/src/components/TimeLine/utils/task.ts b/src/components/TimeLine/utils/task.ts new file mode 100644 index 0000000..47acc18 --- /dev/null +++ b/src/components/TimeLine/utils/task.ts @@ -0,0 +1,17 @@ +import { isHoliday } from './holidays'; + +const taskStartDate = +new Date('2024-01-01'); +const taskDayCount = 60 * 60 * 24 * 1000; + +// 获取天数 +export function getRealDuration(task: any, includeHoliday: any) { + const { start, duration } = task; + if (includeHoliday) return task.duration; + let res = 0; + const endLen = duration + start; + for (let i = start; i < endLen; i++) { + if (isHoliday(new Date(taskStartDate + taskDayCount * i)).isHoliday) continue; + res++; + } + return res; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9012c1c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export { default as TimeLine } from './components/TimeLine'; +export * from './components/TimeLine/interface'; +export * from './components/TimeLine/utils'; diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 91bafd0..0000000 --- a/src/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default as Hello } from './components/Hello';