diff --git a/package.json b/package.json index 05e54180e333e016861f725422283e2fa6f925e4..6566574b98dda08842b9bfdd0a076e363225f376 100644 --- a/package.json +++ b/package.json @@ -12,4 +12,4 @@ "workspaces": [ "packages/*" ] -} +} \ No newline at end of file diff --git a/packages/devui-vue/devui/gantt/__tests__/gantt.spec.ts b/packages/devui-vue/devui/gantt/__tests__/gantt.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..321977ee72742e59bc55eac0369e0fb6d5c23e63 --- /dev/null +++ b/packages/devui-vue/devui/gantt/__tests__/gantt.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils'; +import { Gantt } from '../index'; + +describe('gantt test', () => { + it('gantt init render', async () => { + // todo + }) +}) diff --git a/packages/devui-vue/devui/gantt/index.ts b/packages/devui-vue/devui/gantt/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f79701c051486e61388229d57d20d25aa43a2b5 --- /dev/null +++ b/packages/devui-vue/devui/gantt/index.ts @@ -0,0 +1,21 @@ +import type { App } from 'vue' +import Gantt from './src/gantt' +import GanttTools from './src/gantt-tools' +import ganttMarkerDirective from './src/gantt-scale/gantt-marker-directive' + +Gantt.install = function (app: App): void { + app.component(Gantt.name, Gantt) + app.component(GanttTools.name, GanttTools) + app.directive('gantt-marker', ganttMarkerDirective) +} + +export { Gantt } + +export default { + title: 'Gantt 甘特图', + category: '数据展示', + status: '10%', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Gantt as any) + }, +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-bar-parent/gantt-bar-parent.scss b/packages/devui-vue/devui/gantt/src/gantt-bar-parent/gantt-bar-parent.scss new file mode 100644 index 0000000000000000000000000000000000000000..6aaae086d6d1b641ebe1b89eebd7103a5a5a07e3 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-bar-parent/gantt-bar-parent.scss @@ -0,0 +1,73 @@ +@import '../../style/theme/color'; + +.devui-gantt-bar-parent { + width: 100%; + box-sizing: border-box; + height: 24px; + z-index: 3; + position: relative; + + .devui-gantt-bar-rail { + position: absolute; + background: #eaecf0; + height: 12px; + width: 100%; + margin-top: 4px; + } + + .devui-gantt-bar-rail::before { + width: 0; + height: 0; + border: 3px transparent solid; + border-top-color: #eaecf0; + border-left-color: #eaecf0; + position: absolute; + left: 0; + bottom: -6px; + content: ''; + } + + .devui-gantt-bar-rail::after { + width: 0; + height: 0; + border: 3px transparent solid; + border-top-color: #eaecf0; + border-right-color: #eaecf0; + position: absolute; + right: 0; + bottom: -6px; + content: ''; + } + + .devui-gantt-bar-track { + position: absolute; + background-color: #cacfd8; + height: 12px; + margin-top: 4px; + width: 0; + } + + .devui-gantt-bar-track.head::before { + width: 0; + height: 0; + border: 3px transparent solid; + border-top-color: #cacfd8; + border-left-color: #cacfd8; + position: absolute; + left: 0; + bottom: -6px; + content: ''; + } + + .devui-gantt-bar-track.tail::after { + width: 0; + height: 0; + border: 3px transparent solid; + border-top-color: #cacfd8; + border-right-color: #cacfd8; + position: absolute; + right: 0; + bottom: -6px; + content: ''; + } +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-bar-parent/index.tsx b/packages/devui-vue/devui/gantt/src/gantt-bar-parent/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c20a7cad965dfeb139ee3de410a89866cbdff53c --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-bar-parent/index.tsx @@ -0,0 +1,154 @@ +import {defineComponent , ref , toRefs } from 'vue'; +import {GanttService} from '../gantt-service' +import { Subscription } from 'rxjs'; +// import { GanttProps } from '../gantt-types' +import './gantt-bar-parent.scss'; +const ganttService = new GanttService() +export default defineComponent({ + name:'DGanttBarParent', + props:{ + // 开始时间 + startDate:{ + type:Date, + }, + // 结束时间 + endDate:{ + type:Date + }, + // 进度 + progressRate:{ + type:Number, + default:0 + }, + data:{}, + id:{ + type:String, + }, + tip:{ + type:String, + }, + ganttScaleStatusHandler:{ + type:Subscription + } + }, + setup(props){ + const {startDate,endDate,data,id,tip } = toRefs(props) + let { progressRate, ganttScaleStatusHandler } = toRefs(props) + // const ganttScaleStatusHandler:Subscription = ref() + const tipHovered = ref(false) + const percentage = ref(0) + let left = ref(0) + let width = ref(0) + let max = 100 + let min = 0 + let duration = ref('') + + const setValue = (value: number | null): void => { + if (progressRate !== value) { + progressRate = value; + updateTrackAndHandle(); + } + } + const ensureValueInRange = (value: number | null): number => { + let safeValue; + if (!valueMustBeValid(value)) { + safeValue = min; + } else { + safeValue = clamp(min, value as number, max); + } + return safeValue; + } + const valueMustBeValid = (value: number): boolean => { + return !isNaN(typeof value !== 'number' ? parseFloat(value) : value); + } + const clamp = (min: number, n: number, max: number) => { + return Math.max(min, Math.min(n, max)); + } + const updateTrackAndHandle = (): void => { + const value = progressRate; + const offset = valueToOffset(value); + updateStyle(offset / 100); + this.cdr.markForCheck(); + } + const valueToOffset = (value: number): number => { + return ((value - min.value) / (max.value - min.value)) * 100; + } + const updateStyle = (percentage) => { + percentage = Math.min(1, Math.max(0, percentage)); + if (this.ganttBarTrack && this.ganttBarTrack.nativeElement) { + this.ganttBarTrack.nativeElement.style.width = `${percentage * 100}%`; + } + + if (this.ganttBarProgress && this.ganttBarProgress.nativeElement) { + this.ganttBarProgress.nativeElement.style.left = `${percentage * 100}%`; + } + } + const onInit = () => { + if (progressRate === null) { + this.setValue(this.ensureValueInRange(null)); + } + + duration = ganttService.getDuration(startDate, endDate) + 'd'; + + ganttScaleStatusHandler = ganttService.ganttScaleConfigChange.subscribe((config) => { + if (config.startDate) { + left = ganttService.getDatePostionOffset(startDate); + } + if (config.unit) { + left = ganttService.getDatePostionOffset(startDate); + width = ganttService.getDurationWidth(startDate, endDate); + } + }) as Subscription; + } + + const ngOnChanges = (changes) => { + if (changes['progressRate'] && progressRate > 0) { + updateTrackAndHandle(); + } + + if (changes['startDate']) { + left = ganttService.getDatePostionOffset(startDate); + width = ganttService.getDurationWidth(startDate, endDate); + } + + if (changes['endDate']) { + width = ganttService.getDurationWidth(startDate, endDate); + } + } + + const AfterViewInit = () => { + if (progressRate && progressRate > 0) { + updateTrackAndHandle(); + } + } + + const OnDestroy= (): void => { + if (ganttScaleStatusHandler.value) { + ganttScaleStatusHandler.value.unsubscribe(); + ganttScaleStatusHandler = null; + } + } + + return { + left, + width + } + }, + render() { + const { + left, + width + } = this + const style = { + position:'', + left:left, + width:width + } + return ( +
+
+
+
+ ) + }, +}) \ No newline at end of file diff --git a/packages/devui-vue/devui/gantt/src/gantt-bar/gantt-bar.scss b/packages/devui-vue/devui/gantt/src/gantt-bar/gantt-bar.scss new file mode 100644 index 0000000000000000000000000000000000000000..2e593ef9bf3005ea9d59bda286a691834ade1ff1 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-bar/gantt-bar.scss @@ -0,0 +1,3 @@ +.gantt-bar { + position: relative; +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-bar/index.tsx b/packages/devui-vue/devui/gantt/src/gantt-bar/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8370cf8053a560f2e088c9b751458a09ae8d6fc3 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-bar/index.tsx @@ -0,0 +1,14 @@ +import {defineComponent} from 'vue' + +import './gantt-bar.scss' + + +export default defineComponent({ + name:'GantBar', + props:{}, + render(){ + return ( +
+ ) + } +}) \ No newline at end of file diff --git a/packages/devui-vue/devui/gantt/src/gantt-milestone/gantt-milestone.scss b/packages/devui-vue/devui/gantt/src/gantt-milestone/gantt-milestone.scss new file mode 100644 index 0000000000000000000000000000000000000000..0abd57b6c0ca72b6175bc6e086a8da449b0e0c8f --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-milestone/gantt-milestone.scss @@ -0,0 +1,17 @@ +.devui-gantt-milestone { + position: relative; + height: 24px; + line-height: 24px; + display: inline-block; + + & span { + display: inline-block; + vertical-align: middle; + } + + & .icon { + width: 20px; + height: 24px; + margin-right: 4px; + } +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-milestone/index.tsx b/packages/devui-vue/devui/gantt/src/gantt-milestone/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..38aae3c74b7cf1c4bc8b97d63cb359b217a9d32e --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-milestone/index.tsx @@ -0,0 +1,32 @@ +import { defineComponent, ref } from 'vue' +import { MilestoneIcon } from './milestone-icon' +import './gantt-milestone.scss' + +export default defineComponent({ + name: 'DGanttMilestone', + props: { + startDate: { + type: Date, + }, + title: { + type: String, + }, + id: { + type: String, + }, + }, + setup(props) { + // todo + }, + render() { + const { title } = this + return ( +
+ + + + {title} +
+ ) + }, +}) diff --git a/packages/devui-vue/devui/gantt/src/gantt-milestone/milestone-icon.tsx b/packages/devui-vue/devui/gantt/src/gantt-milestone/milestone-icon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b0c2b58617aaa127a557ca7c4109ce91c7cff247 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-milestone/milestone-icon.tsx @@ -0,0 +1,212 @@ +export const MilestoneIcon = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) diff --git a/packages/devui-vue/devui/gantt/src/gantt-model.ts b/packages/devui-vue/devui/gantt/src/gantt-model.ts new file mode 100644 index 0000000000000000000000000000000000000000..91b5456bd719e0aaa6b05a966d5f284dcead9459 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-model.ts @@ -0,0 +1,61 @@ +export interface GanttScaleDateInfo { + dayOfMonthLabel: string + dayOfWeekLabel: string + monthLabel: string + yearLabel: string + date: Date + monthStart?: boolean + weekend?: boolean + today?: boolean + highlight?: boolean + highlightStart?: boolean + milestone?: string + scaleStartVisable?: boolean + index?: number +} + +export enum GanttScaleUnit { + day = 'day', + week = 'week', + month = 'month', +} +export interface GanttBarStatus { + focused: boolean + startDate: Date + endDate: Date +} + +export interface GanttScaleConfig { + startDate?: Date + endDate?: Date + unit?: GanttScaleUnit +} + +export enum GanttMarkerType { + milestone = 'milestone', + month = 'month', + week = 'week', +} + +export interface GanttMilestone { + date: Date + lable: string +} + +export interface GanttTaskInfo { + id: string + startDate: Date + endDate: Date + title?: string + progress: string + duration: string + moveOffset?: number + left?: number + width?: number +} + +export enum UnitRole { + day = 10, + week = 20, + month = 30, +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-scale/gantt-marker-directive.ts b/packages/devui-vue/devui/gantt/src/gantt-scale/gantt-marker-directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..9156a42a6dcfda33d1016b61e7a7ed5b9f86568c --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-scale/gantt-marker-directive.ts @@ -0,0 +1,47 @@ +import { GanttScaleUnit } from '../gantt-model' +interface BindingType { + value: { + ganttBarContainerElement: HTMLElement + ganttScaleContainerOffsetLeft: number + monthMark: boolean + weekend: boolean + today: boolean + milestone: string + unit: GanttScaleUnit + date: Date + last: boolean + } +} +const ganttMarkerDirective = { + ganttBarContainerElement: null, + monthMarkElement: null, + weekendElement: null, + todayElement: null, + milestoneElement: null, + monthMark: '', + mounted(el: HTMLElement, binding: BindingType): void { + const { ganttBarContainerElement, monthMark } = binding.value + if (ganttBarContainerElement) { + this.ganttBarContainerElement = ganttBarContainerElement + } + if (monthMark) { + this.monthMark = this.monthMark + } + }, + updated(el: HTMLElement, binding: BindingType): void { + // todo + const { monthMark, weekend, today, milestone, unit } = binding.value + if (monthMark) { + this.initMarkElement() + } + }, + initMarkElement(): void { + if (this.ganttBarContainerElement) { + if (this.monthMark && !this.monthMarkElement) { + // todo + } + } + }, +} + +export default ganttMarkerDirective diff --git a/packages/devui-vue/devui/gantt/src/gantt-scale/gantt-scale.scss b/packages/devui-vue/devui/gantt/src/gantt-scale/gantt-scale.scss new file mode 100644 index 0000000000000000000000000000000000000000..bbb3cc42bc5e6f5ab757065c1f499d2c3d61673d --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-scale/gantt-scale.scss @@ -0,0 +1,175 @@ +@import '../../../style/theme/color'; +@import '../../../style/theme/corner'; +@import '../../../style/core/_font'; + +.devui-gantt-scale-wrapper { + display: block; + height: 36px; + line-height: 18px; +} + +.devui-gantt-scale { + display: inline-block; + color: $devui-placeholder; // TODO: Color-Question + text-align: center; + position: absolute; + height: 36px; + font-weight: normal; + + &.day { + &:not(.milestone) { + &:hover .devui-scale-start { + display: none; + } + } + + &.milestone { + background-image: + linear-gradient( + 180deg, + rgba(254, 204, 85, 0) 0%, + rgba(62, 204, 166, 0.1) 100% + ); + } + } + + & .devui-scale-start { + width: 100%; + height: 18px; + position: absolute; + left: 1px; + white-space: nowrap; + + &.milestone { + color: $devui-success; + } + } + + & .devui-scale-unit { + height: 18px; + position: absolute; + top: 18px; + width: 100%; + + & .border-left { + height: 18px; + border-left: 1px solid $devui-list-item-selected-bg; + } + + & .scale-highlight { + position: absolute; + height: 18px; + border-radius: $devui-border-radius; + background-color: $devui-brand; + padding: 0 4px 0 4px; + + & div { + color: $devui-base-bg; + font-size: $devui-font-size; + font-weight: normal; + } + } + + .today { + background: rgba(255, 121, 14, 0.2); + border-radius: $devui-border-radius; + height: 16px; + } + } + + & .milestone-new { + display: none; + position: absolute; + width: 18px; + height: 18px; + margin-left: 16px; + border: 1px solid $devui-list-item-selected-bg; // TODO: Color-Question + cursor: pointer; + + & div { + line-height: 16px; + } + } + + &:not(.milestone) { + &:hover .milestone-new.day { + display: block; + } + } +} + +.devui-mark-line { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + z-index: 1; + background: $devui-line; + opacity: 0.5; + + &.today { + opacity: 0.2; + background: #ff790e; + + &.day { + margin-left: 24px; + } + + &.week { + margin-left: 9px; + } + + &.month { + margin-left: 4px; + } + } + + &.milestone { + opacity: 0.2; + background: $devui-success; + + &.day { + margin-left: 24px; + } + + &.week { + margin-left: 9px; + } + + &.month { + margin-left: 4px; + } + } +} + +.devui-mark-stripe { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + background: + linear-gradient( + 45deg, + rgba(202, 207, 216, 0.2) 0, + rgba(202, 207, 216, 0.2) 10%, + transparent 10%, + transparent 50%, + rgba(202, 207, 216, 0.2) 50%, + rgba(202, 207, 216, 0.2) 60%, + transparent 60%, + transparent + ); + background-size: 6px 6px; + + &.day { + width: 100px; + } + + &.week { + width: 40px; + } + + &.month { + width: 20px; + } +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-scale/index.tsx b/packages/devui-vue/devui/gantt/src/gantt-scale/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..00cb52decf41882f766ad27872d46c636e9de199 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-scale/index.tsx @@ -0,0 +1,195 @@ +import { defineComponent, ref, PropType, onMounted, toRefs, watch } from 'vue' +import './gantt-scale.scss' +import { + GanttScaleUnit, + GanttMilestone, + GanttScaleDateInfo, +} from '../gantt-model' +import { useScaleData } from './use-scale-data' +import { i18nText } from '../i18n-gantt' +export default defineComponent({ + name: 'DGanttScale', + props: { + /** 视图单位 */ + unit: { + type: String, + default: GanttScaleUnit.day, + }, + height: { + type: Number, + }, + /** 开始时间 */ + startDate: { + type: Date, + }, + /** 结束时间 */ + endDate: { + type: Date, + }, + /** 甘特图时间轴容器左偏移像素 */ + ganttScaleContainerOffsetLeft: { + type: Number, + }, + /** 版本里程碑列表 */ + milestoneList: { + type: Array as PropType, + }, + scrollElement: { + type: Object, + }, + ganttBarContainerElement: { + type: Object, + }, + }, + emits: ['addMilestone'], + setup(props, ctx) { + const { startDate, endDate, milestoneList, scrollElement, unit } = + toRefs(props) + const scaleData = ref([]) + const viewSCaleData = ref([]) + const scaleWidth = ref({ + day: 40, + week: 30, + month: 20, + }) + const highlight = ref(false) + const highlightStartText = ref('') + const highlightEndText = ref('') + const { generateScaleData } = useScaleData(milestoneList) + let viewScaleRange = [0, 0] + const addMilestone = (info: GanttScaleDateInfo) => { + ctx.emit('addMilestone', info) + } + const getViewScaleData = () => { + if (scrollElement.value) { + const containerWidth = scrollElement.value.clientWidth + const scrollLeft = scrollElement.value.scrollLeft + + const start = Math.floor(scrollLeft / scaleWidth.value[unit.value]) + const offset = Math.ceil(containerWidth / scaleWidth.value[unit.value]) + viewScaleRange = [start - 2, start + offset + 2] + viewSCaleData.value = scaleData.value.filter( + (i: GanttScaleDateInfo) => { + return i.index >= viewScaleRange[0] && i.index <= viewScaleRange[1] + } + ) + } + } + onMounted(() => { + if (startDate.value && endDate.value) { + scaleData.value = generateScaleData(startDate.value, endDate.value) + getViewScaleData() + } + }) + + watch( + () => props.scrollElement, + () => { + getViewScaleData() + ;(props.scrollElement as HTMLDivElement).addEventListener( + 'scroll', + () => { + getViewScaleData() + } + ) + } + ) + + return { + viewSCaleData, + scaleWidth, + addMilestone, + highlight, + highlightStartText, + highlightEndText, + } + }, + render() { + const { + unit, + viewSCaleData, + scaleWidth, + addMilestone, + highlight, + highlightStartText, + highlightEndText, + ganttBarContainerElement, + } = this + return ( +
+ {viewSCaleData.map((data, index) => ( +
+
+ {data.milestone && unit === 'day' &&
{data.milestone}
} + {(!data.milestone || unit !== 'day') && + data.scaleStartVisable && + (index === 0 || data.monthStart) + ? unit === 'month' + ? i18nText.zh.yearDisplay(data.yearLabel) + : i18nText.zh.yearAndMonthDisplay( + data.yearLabel, + data.monthLabel + ) + : ''} +
+
+ {highlight && data.highlightStart && ( +
+
{highlightStartText}
+
{highlightEndText}
+
+
+ )} + {(!highlight || !data.highlightStart) && unit === 'day' && ( +
+ {data.today ? i18nText.zh.today : data.dayOfMonthLabel} +
+ )} + {(!highlight || !data.highlightStart) && unit === 'week' && ( +
+ {index === 0 || data.weekend ? data.dayOfMonthLabel : ''} +
+ )} + {(!highlight || !data.highlightStart) && unit === 'month' && ( +
+ {index === 0 || data.monthStart + ? i18nText.zh.monthDisplay(data.monthLabel) + : ''} +
+ )} +
+
addMilestone(data)} + > + +
+
+ ))} +
+ ) + }, +}) diff --git a/packages/devui-vue/devui/gantt/src/gantt-scale/use-scale-data.ts b/packages/devui-vue/devui/gantt/src/gantt-scale/use-scale-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..72f2f14c9f7b7da4184e0273a8d76efa43968e24 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-scale/use-scale-data.ts @@ -0,0 +1,93 @@ +import { Ref } from 'vue' +import { GanttMilestone, GanttScaleDateInfo } from '../gantt-model' +import { isSameDate } from '../utils' + +export const useScaleData = ( + milestoneList: Ref +): { + generateScaleData: (startDate: Date, endDate: Date) => GanttScaleDateInfo[] +} => { + const SCALE_START_LABLE_OFFSET = 7 + + const generateDateInfo = (date: Date, index: number): GanttScaleDateInfo => { + const dateInfo: GanttScaleDateInfo = { + dayOfMonthLabel: '', + dayOfWeekLabel: '', + monthLabel: '', + yearLabel: '', + date: date, + monthStart: false, + weekend: false, + today: false, + milestone: '', + highlightStart: false, + scaleStartVisable: true, + index, + } + const dayOfMonth = date.getDate() + dateInfo.dayOfMonthLabel = dayOfMonth + '' + if (dayOfMonth === 1) { + dateInfo.monthStart = true + } + + const dayOfWeek = date.getDay() + dateInfo.dayOfWeekLabel = dayOfWeek + '' + if (dayOfWeek === 6) { + dateInfo.weekend = true + } + const month = date.getMonth() + 1 + dateInfo.monthLabel = month + '' + const year = date.getFullYear() + dateInfo.yearLabel = year + '' + if (isSameDate(date, new Date())) { + dateInfo.today = true + } + + if ( + new Date( + year, + month - 1, + dayOfMonth + SCALE_START_LABLE_OFFSET + ).getMonth() > + month - 1 + ) { + dateInfo.scaleStartVisable = false + } + if (milestoneList.value) { + milestoneList.value.forEach((milestone) => { + if (milestone.date) { + if (isSameDate(milestone.date, dateInfo.date)) { + dateInfo.milestone = milestone.lable + } + } + }) + } + + return dateInfo + } + + const getNextDay = (date: Date) => { + const nextDayDate = date.setDate(date.getDate() + 1) + return new Date(nextDayDate) + } + const generateScaleData = ( + startDate: Date, + endDate: Date + ): GanttScaleDateInfo[] => { + const scaleData = [] + let handleDate = startDate + let index = 0 + while (!isSameDate(handleDate, endDate)) { + const dateInfo = generateDateInfo(handleDate, index) + scaleData.push(dateInfo) + handleDate = getNextDay(new Date(handleDate)) + index++ + } + console.log({ scaleData }) + return scaleData + } + + return { + generateScaleData, + } +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-service.ts b/packages/devui-vue/devui/gantt/src/gantt-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..22e888132113beb67b1b6428d2fc1f9846a6af70 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-service.ts @@ -0,0 +1,110 @@ +import { fromEvent, merge, Observable, ReplaySubject, Subject } from 'rxjs'; +import { pluck } from 'rxjs/operators'; +import { GanttBarStatus, GanttScaleConfig, GanttScaleUnit } from './gantt-model'; +export class GanttService { + static DAY_DURATION = 24 * 60 * 60 * 1000; + scaleUnit = GanttScaleUnit.day; + scaleStartDate: Date; + scaleEndDate: Date; + ganttBarStatusChange = new Subject(); + ganttScaleConfigChange = new ReplaySubject(1); + + mouseDownListener: Observable; + mouseMoveListener = new Observable(); + mouseEndListener = new Observable(); + + changeGanttBarStatus(status: GanttBarStatus):void { + this.ganttBarStatusChange.next(status); + } + + registContainerEvents(scrollContainer: HTMLElement):void { + // 背景拖拽 + this.mouseDownListener = fromEvent(scrollContainer, 'mousedown').pipe(pluck('pageX')); + + this.mouseMoveListener = fromEvent(scrollContainer, 'mousemove').pipe(pluck('pageX')); + + this.mouseEndListener = merge(fromEvent(scrollContainer, 'mouseup'), fromEvent(scrollContainer, 'mouseout')).pipe( + pluck('pageX') + ); + } + + changeGanttScaleConfig(status: GanttScaleConfig):void { + this.ganttScaleConfigChange.next(status); + } + + setScaleConfig(config: GanttScaleConfig):void { + if (config.startDate) { + this.scaleStartDate = config.startDate; + } + if (config.endDate) { + this.scaleEndDate = config.endDate; + } + if (config.unit) { + this.scaleUnit = config.unit; + } + this.changeGanttScaleConfig(config); + } + + getScaleUnitPixel():number { + switch (this.scaleUnit) { + case GanttScaleUnit.day: + return 40; + break; + case GanttScaleUnit.week: + return 30; + break; + case GanttScaleUnit.month: + return 20; + break; + default: + break; + } + } + + getDatePostionOffset(date: Date): number { + if (date && this.scaleStartDate) { + const timeOffset = date.getTime() - this.scaleStartDate.getTime(); + const units = timeOffset / GanttService.DAY_DURATION; + return units * this.getScaleUnitPixel(); + } + } + + getDuration(startDate: Date, endDate: Date): number { + if (startDate && endDate) { + const timeOffset = endDate.getTime() - startDate.getTime(); + const duration = timeOffset / GanttService.DAY_DURATION + 1; + return Math.round(duration); + } + } + + getDurationWidth(startDate: Date, endDate: Date): number { + if (startDate && endDate) { + const duration = this.getDuration(startDate, endDate); + return duration * this.getScaleUnitPixel(); + } + } + + isSomeDate(date: Date, compareDate: Date):boolean { + if (date.getFullYear() !== compareDate.getFullYear()) { + return false; + } + + if (date.getMonth() !== compareDate.getMonth()) { + return false; + } + + if (date.getDate() !== compareDate.getDate()) { + return false; + } + return true; + } + + roundDate(date: Date):void { + if (date.getHours() >= 12) { + date.setDate(date.getDate() + 1); + date.setHours(0, 0, 0); + } else if (date.getHours() < 12) { + date.setHours(0, 0, 0); + } + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/gantt/src/gantt-tools/gantt-tools.scss b/packages/devui-vue/devui/gantt/src/gantt-tools/gantt-tools.scss new file mode 100644 index 0000000000000000000000000000000000000000..45137d7795db0dea315b6fda1c71fb61e2a2eb2d --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-tools/gantt-tools.scss @@ -0,0 +1,79 @@ +@import '../../../style/theme/color'; +@import '../../../style/theme/corner'; +@import '../../../style/core/_font'; +@import '../../../style/theme/shadow'; + +.tools-container { + position: absolute; + top: 70px; + right: 20px; + z-index: 10; + + .devui-dropdown-origin { + border: 0; + + &:hover { + color: $devui-link !important; + } + } + + .devui-btn { + height: 32px !important; + color: $devui-text !important; + padding: 0 8px !important; + min-width: 50px; + + &:hover { + color: $devui-link !important; + } + } + + .tool { + &.minus, + &.add { + .devui-btn { + min-width: 30px; + } + } + } + + .devui-select-selection { + width: 90px; + + .devui-select-input { + height: 32px; + } + } +} + +.tool { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + margin-left: 12px; + background-color: $devui-base-bg; + box-shadow: $devui-shadow-length-base rgba(81, 112, 255, 0.4); + cursor: pointer; + + &.disabled { + opacity: 0.5; + } + + span { + border: 0 !important; + } + + .switch-view { + padding: 0 8px; + + &:hover { + color: $devui-link !important; + } + } +} + +.devui-dropdown-menu { + top: 10px !important; + left: -6px !important; +} diff --git a/packages/devui-vue/devui/gantt/src/gantt-tools/index.tsx b/packages/devui-vue/devui/gantt/src/gantt-tools/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..070283dcd238d8814b02daeac1c0fff59eb5a634 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-tools/index.tsx @@ -0,0 +1,107 @@ +import { defineComponent, ref } from 'vue' +import './gantt-tools.scss' +import { GanttScaleUnit } from '../gantt-model' + +export default defineComponent({ + name: 'DGanttTools', + props: { + unit: { + type: String, + default: null, + }, + isFullScreen: { + type: Boolean, + default: false, + }, + }, + emits: ['goToday', 'reduceUnit', 'increaseUnit', 'switchView'], + setup(props, ctx) { + const currentUnitLabel = ref(props.unit) + const views = ref([ + { + name: 'Day', + value: 'day', + }, + { + name: 'Week', + value: 'week', + }, + { + name: 'Month', + value: 'month', + }, + ]) + const actionHandle = (type: string) => { + switch (type) { + case 'today': + ctx.emit('goToday') + break + case 'reduce': + ctx.emit('reduceUnit') + break + case 'increase': + ctx.emit('increaseUnit') + break + } + } + const selectView = (selectItem: { name: string; value: string; }) => { + ctx.emit('switchView', selectItem.value) + } + return { + actionHandle, + currentUnitLabel, + views, + selectView, + } + }, + render() { + const { isFullScreen, actionHandle, views, selectView, $slots } = this + + return ( +
+ actionHandle('today')} + class="tool" + > + Today + +
+ +
+ actionHandle('reduce')} + > + + + actionHandle('increase')} + > + + + {$slots.default && $slots.default()} +
+ ) + }, +}) diff --git a/packages/devui-vue/devui/gantt/src/gantt-types.ts b/packages/devui-vue/devui/gantt/src/gantt-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..02cf168da298fa82acf8186df9e073f036a8e974 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt-types.ts @@ -0,0 +1,20 @@ +import type { PropType, ExtractPropTypes } from 'vue' +import { GanttScaleUnit } from './gantt-model' +export const ganttProps = { + startDate: { + type: Date, + }, + endDate: { + type: Date, + }, + unit: { + type: String as PropType, + default: GanttScaleUnit.day, + }, + progressRate:{ + type:Number + }, +} as const + + +export type GanttProps = ExtractPropTypes diff --git a/packages/devui-vue/devui/gantt/src/gantt.scss b/packages/devui-vue/devui/gantt/src/gantt.scss new file mode 100644 index 0000000000000000000000000000000000000000..6f2c3957f3cef96943163fd866472e1a9d5481ae --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt.scss @@ -0,0 +1,36 @@ +@import '../../styles-var/devui-var.scss'; + +.gantt-container { + overflow: scroll; + + .header { + position: relative; + border-bottom: 1px solid $devui-dividing-line; + } + + .body { + position: relative; + min-height: 400px; + height: 100%; + + .item { + height: 40px; + padding-top: 8px; + } + } +} + +.tool { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + margin-left: 12px; + background-color: $devui-base-bg; + box-shadow: $devui-shadow-length-base rgba(81, 112, 255, 0.4); + cursor: pointer; + + span { + border: 0 !important; + } +} diff --git a/packages/devui-vue/devui/gantt/src/gantt.tsx b/packages/devui-vue/devui/gantt/src/gantt.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e2ddae9b4d533172827a2cc52ed002bf0aa50703 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/gantt.tsx @@ -0,0 +1,50 @@ +import { defineComponent, onMounted, ref, toRefs } from 'vue' +import DGanttScale from './gantt-scale/index' +import DGanttTools from './gantt-tools/index' +import { ganttProps, GanttProps } from './gantt-types' +import './gantt.scss' +import { useGantt } from './use-gantt' +export default defineComponent({ + name: 'DGantt', + components: { DGanttScale, DGanttTools }, + props: ganttProps, + setup(props: GanttProps, ctx) { + const { startDate, endDate } = toRefs(props) + const ganttContainer = ref() + const ganttScaleWidth = ref() + const { getDurationWidth } = useGantt() + onMounted(() => { + ganttScaleWidth.value = getDurationWidth(startDate.value, endDate.value) + }) + return { + ganttContainer, + ganttScaleWidth, + } + }, + render() { + const { + $slots, + startDate, + endDate, + unit, + ganttContainer, + ganttScaleWidth, + } = this + return ( +
+
+
+ +
+ +
+
+
+ ) + }, +}) diff --git a/packages/devui-vue/devui/gantt/src/i18n-gantt.ts b/packages/devui-vue/devui/gantt/src/i18n-gantt.ts new file mode 100644 index 0000000000000000000000000000000000000000..82d0363b64a325fc836d91ee3392e2fea130552c --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/i18n-gantt.ts @@ -0,0 +1,54 @@ +export const i18nText = { + en: { + today: 'today', + monthsOfYear: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ], + yearDisplay(year: string): string { + return `${year}` + }, + monthDisplay(strMonthIndex: string): string { + return this.monthsOfYear[Number(strMonthIndex) - 1] + }, + yearAndMonthDisplay(year: string, strMonthIndex: string): string { + return this.yearDisplay(year) + this.monthDisplay(strMonthIndex) + }, + }, + zh: { + today: '今天', + monthsOfYear: [ + '1月', + '2月', + '3月', + '4月', + '5月', + '6月', + '7月', + '8月', + '9月', + '10月', + '11月', + '12月', + ], + yearDisplay(year: string): string { + return `${year}年` + }, + monthDisplay(strMonthIndex: string): string { + return this.monthsOfYear[Number(strMonthIndex) - 1] + }, + yearAndMonthDisplay(year: string, strMonthIndex: string): string { + return this.yearDisplay(year) + this.monthDisplay(strMonthIndex) + }, + }, +} diff --git a/packages/devui-vue/devui/gantt/src/use-gantt.ts b/packages/devui-vue/devui/gantt/src/use-gantt.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a59908c97f844955881aeb4ae331440c6d69955 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/use-gantt.ts @@ -0,0 +1,36 @@ +import { GanttScaleUnit } from './gantt-model' +export const useGantt = (scaleUnit = GanttScaleUnit.day) => { + const DAY_DURATION = 24 * 60 * 60 * 1000 + + const getScaleUnitPixel = () => { + switch (scaleUnit) { + case GanttScaleUnit.day: + return 40 + case GanttScaleUnit.week: + return 30 + case GanttScaleUnit.month: + return 20 + default: + break + } + } + + const getDuration = (startDate: Date, endDate: Date): number => { + if (startDate && endDate) { + const timeOffset = endDate.getTime() - startDate.getTime() + const duration = timeOffset / DAY_DURATION + 1 + console.log('duration => ', duration) + + return Math.round(duration) + } + } + const getDurationWidth = (startDate: Date, endDate: Date): number => { + if (startDate && endDate) { + const duration = getDuration(startDate, endDate) + return duration * getScaleUnitPixel() + } + } + return { + getDurationWidth, + } +} diff --git a/packages/devui-vue/devui/gantt/src/utils.ts b/packages/devui-vue/devui/gantt/src/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..a232b179f9a53f1bb128843d7426c8003a7eccf4 --- /dev/null +++ b/packages/devui-vue/devui/gantt/src/utils.ts @@ -0,0 +1,8 @@ +/** 判断是否是同一天 */ +export const isSameDate = (date: Date, compareDate: Date): boolean => { + return ( + date.getFullYear() === compareDate.getFullYear() && + date.getMonth() === compareDate.getMonth() && + date.getDate() === compareDate.getDate() + ) +} diff --git a/packages/devui-vue/docs/components/gantt/index.md b/packages/devui-vue/docs/components/gantt/index.md new file mode 100644 index 0000000000000000000000000000000000000000..919457f8a05ee5dead1132a3ea002e1ecfda6365 --- /dev/null +++ b/packages/devui-vue/docs/components/gantt/index.md @@ -0,0 +1,65 @@ +# Gantt 甘特图 + +甘特图。 + +### 何时使用 + +当用户需要通过条状图来显示项目,进度和其他时间相关的系统进展的内在关系随着时间进展的情况时。 + +### 基本用法 + +- d-gantt-scale(时间轴)容器作为时间轴标线的定位父级元素,须设置 position 或者是 table、td、th、body 元素。 +- d-gantt-scale(时间轴)容器和 d-gantt-bar(时间条)容器宽度须通过 GanttService 提供的方法根据起止时间计算后设置,初始化之后还须订阅 ganttScaleConfigChange 动态设置。 +- 时间条 move、resize 事件会改变该时间条起止时间和时间轴的起止时间,订阅时间条 resize、move 事件和 ganttScaleConfigChange 来记录变化。 +- 响应时间条 move、resize 事件调整最外层容器的滚动以获得更好的体验。 + +:::demo + +```vue + + + + + +``` + +::: + +### d-gantt + +d-gantt 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| ---- | ---- | ---- | ---- | --------- | ---------- | +| | | | | | | +| | | | | | | +| | | | | | | + +d-gantt 事件 + +| 事件 | 类型 | 说明 | 跳转 Demo | +| ---- | ---- | ---- | --------- | +| | | | | +| | | | | +| | | | |