当时看到这个原型我就头皮发麻,疯狂吐槽产品设计这么难得UI。
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { DownOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import interact from 'interactjs';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import ChangeRequestDetailsModal from './modals/changeRequestDetails.vue';
import store from '@/store';
import { replyCommentT } from '@/api/rotation/myRotationPlan';
import { useLanguage } from '@/hooks';
import { WeekDaysMap, workTagsEnum } from '@/types';
import { formatTime } from '../hooks';
dayjs.extend(isBetween);
const { t: $t } = useI18n();
interface IProps {
id: string;
title?: string;
isDetail?: boolean;
isReadOnly?: boolean;
isB2B?: boolean;
hasB2B?: boolean;
gapDays?: number;
overlapDays?: number;
dataBeginDate?: string | null;
isExceed9Weeks?: boolean;
isFailed2Modified?: boolean;
isFewerWorkingDay?: boolean;
isFill?: boolean;
requireWorkingDays?: number;
canOverlapDays?: number;
timesOfYear?: number;
joinInfo?: {
joinDate: string;
joinStatus: number;
} | null;
employRpDetail?: any;
workPatternTitle?: string;
}
const props = withDefaults(defineProps<IProps>(), {
isDetail: false,
isReadOnly: false,
isB2B: false,
hasB2B: false,
gapDays: 0,
overlapDays: 0,
dataBeginDate: null,
isExceed9Weeks: false,
isFailed2Modified: false,
isFewerWorkingDay: false,
isFill: true,
requireWorkingDays: 0,
canOverlapDays: 0,
timesOfYear: 2,
joinInfo: null,
employRpDetail: null,
});
const tips = computed(() => {
return {
M: $t('attendance.common.M'),
R: $t('attendance.common.R'),
T: $t('attendance.common.T'),
};
});
const containerRef = ref();
interface IDay {
day: number;
duration: null | string;
disabled: boolean;
note: string;
step: number;
week: string;
}
interface IMonth {
month: number;
monthLabel: string;
monthDayCount: number;
days: IDay[];
}
const months = ref<IMonth[]>([]);
const pureMonthData = ref<
{
month: number;
days: { day: number; disabled: boolean }[];
}[]
>([]);
const days = ref(Array.from({ length: 31 }, (_, i) => i + 1));
let year = 0;
let month = 0;
/**
* 动态获取上/下半年的月份和天数
*/
const getMonthData = () => {
const allMonthNames = [
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
].map((item) => `attendance.common.${item}`);
const allMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // 平年天数
if (props.joinInfo && props.joinInfo.joinStatus == 0 && !props.isB2B) {
const { joinDate } = props.joinInfo;
const [joinYear, joinMonth] = joinDate.split('-');
year = +joinYear;
month = parseInt(joinMonth) - 1;
let timesOfYear = props.timesOfYear; // 存在1 2 4 三种, 1渲染全年 2根据月份渲染上/下半年 4渲染季节
let start = +joinMonth - 1,
end = 11;
if (timesOfYear == 1) {
end = 11;
} else if (timesOfYear == 2) {
if (month < 6) {
end = 5;
} else {
end = 11;
}
} else if (timesOfYear == 4) {
// 1-3 渲染 4-6 , 4-6渲染7-9, 7-9渲染 10-12 , 10-12 渲染 1-3
if (month < 3) {
end = 2;
} else if (month < 6) {
end = 5;
} else if (month < 9) {
end = 8;
} else {
end = 11;
}
}
// 判断是否为闰年
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 470 === 0;
if (isLeapYear) {
allMonthDays[1] = 29; // 闰年2月天数
}
let monthNames = [];
let monthDays = [];
// 遍历每个月生成数据
for (let i = +joinMonth; i <= +joinMonth; i++) {
const currentMonthDays = allMonthDays[i];
let days = [];
if (i === month) {
// 如果是入职的第一个月,从入职日开始算起
for (let j = +joinMonth; j <= currentMonthDays; j++) {
days.push(j);
}
} else {
// 其他月份全部天数
for (let j = 1; j <= currentMonthDays; j++) {
days.push(j);
}
}
monthNames.push(allMonthNames[i]);
monthDays.push(days);
}
// 获取指定半年的月份名称和天数
monthNames = allMonthNames.slice(start, end + 1);
monthDays = allMonthDays.slice(start, end + 1);
return {
monthNames,
monthDays,
startMonth: start + 1,
startDate: props.joinInfo.joinDate,
endDate: `${year}-${`${end + 1}`.padStart(2, '0')}-${`${
monthDays[monthDays.length - 1]
}`.padStart(2, '0')}`,
};
} else {
// 有数据时就根据数据生成月份数据,无则获取当前月份的月份数据
if (props.dataBeginDate) {
const [BYear, BMonth] = props.dataBeginDate.split('-');
year = +BYear;
month = parseInt(BMonth) - 1;
}
/**
* @description 上半季度填写的是下半年7-12月分轮班, 下半季度填写的是明年1-6月排班
*/
let timesOfYear = props.timesOfYear; // 存在1 2 4 三种, 1渲染全年 2根据月份渲染上/下半年 4渲染季节
let start = 0,
end = 11;
if (timesOfYear == 1) {
year += 1;
start = 0;
end = 11;
} else if (timesOfYear == 2) {
if (month < 6) {
start = 6;
end = 11;
} else {
year += 1;
start = 0;
end = 5;
}
} else if (timesOfYear == 4) {
// 1-3 渲染 4-6 , 4-6渲染7-9, 7-9渲染 10-12 , 10-12 渲染 1-3
if (month < 3) {
start = 3;
end = 5;
} else if (month < 6) {
start = 6;
end = 8;
} else if (month < 9) {
start = 9;
end = 11;
} else {
year += 1;
start = 0;
end = 2;
}
}
// 判断是否为闰年
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 470 === 0;
if (isLeapYear) {
allMonthDays[1] = 29; // 闰年2月天数
}
// 获取指定半年的月份名称和天数
const monthNames = allMonthNames.slice(start, end + 1);
const monthDays = allMonthDays.slice(start, end + 1);
return {
monthNames,
monthDays,
startMonth: start + 1,
startDate: `${year}-${`${start + 1}`.padStart(2, '0')}-01`,
endDate: `${year}-${`${end + 1}`.padStart(2, '0')}-${`${
monthDays[monthDays.length - 1]
}`.padStart(2, '0')}`,
};
}
};
interface IEmit {
(event: 'getMonthData', value: any): void;
}
const $emit = defineEmits<IEmit>();
/**
* @description 生成月份数据
*/
const generateMonthsData = () => {
const { monthNames, monthDays, startMonth } = getMonthData();
// 返回月份数据
$emit('getMonthData', { ...getMonthData(), year });
const tmpArr = [];
const pureData = [];
for (let i = 0; i < monthNames.length; i++) {
const daysArray = Array.from({ length: monthDays[i] }, (_, dayIndex) => ({
day: dayIndex + 1,
duration: null,
disabled:
dayIndex + 1 > monthDays[i] ||
(!props.isB2B &&
props.joinInfo &&
dayjs(`${year}-${i + startMonth}-${dayIndex + 1}`).isBefore(
dayjs(props.joinInfo.joinDate),
)),
note: '',
step: 0,
week: WeekDaysMap[
dayjs(`${year}-${i + startMonth}-${dayIndex + 1}`).day()
],
})) as IDay[];
tmpArr.push({
month: i + startMonth,
monthLabel: monthNames[i],
monthDayCount: monthDays[i],
days: daysArray,
});
pureData.push({
month: i + startMonth,
days: daysArray.map((item) => {
return {
day: item.day,
disabled: item.disabled,
};
}),
});
}
months.value = tmpArr;
pureMonthData.value = pureData;
return new Promise((resolve) => {
resolve(true);
});
};
const isDragging = ref(false);
const currentCell = ref<null | string>(null);
const monthEleWidth = 119;
const dayEleWidth = 47;
const dayEleHeight = 30;
const isAddComment = ref(false);
const editActiveIndex = ref<string | null>(null);
const activeCellIndex = ref<string | null>(null);
const activeDayIndex = ref<string | null>(null);
const { isArabic } = useLanguage();
/**
* @description 单元格绑定拖动事件
*/
const bindResizableEvent = () => {
const edges = {
arabic: { right: false, left: true },
default: { right: true, left: false },
};
interact(`#${props.id} .duration`).resizable({
edges: {
top: false,
bottom: false,
...edges[isArabic.value ? 'arabic' : 'default'],
},
modifiers: [
interact.modifiers.restrictSize({
min: { width: dayEleWidth, height: dayEleHeight },
}),
],
listeners: {
start(event) {
isDragging.value = true;
const target = event.target;
target.style.zIndex = 101;
currentCell.value = target.dataset.current;
isAddComment.value = false;
editActiveIndex.value = null;
},
move(event) {
var target = event.target;
const count = +target.dataset.count; // 月份最大天数
const [month, day] = target.dataset.current
.split('-')
.map((item: string) => +item); // 当前操作时间 月,日
let { x, y } = event.target.dataset;
x = (parseFloat(x) || 0) + event.deltaRect.left;
y = (parseFloat(y) || 0) + event.deltaRect.top;
// 查找下一个元素,限制其最大拖动宽度
let nextTarget = null;
let nextTargetOffsetLeft = 0;
const durationGroup = target
.closest('.gantt-row')
.querySelectorAll(`#${props.id} .duration`);
const currentTargetIndex = Object.values(durationGroup).findIndex(
(item: any) => item.dataset.current == `${month}-${day}`,
);
if (currentTargetIndex + 1 < durationGroup.length) {
nextTarget = Object.values(durationGroup)[
currentTargetIndex + 1
] as any;
// eslint-disable-next-line no-unused-vars
const [_, nextTargetDay] = nextTarget.dataset.current
.split('-')
.map((item: string) => +item); // 当前操作时间 月,日
nextTargetOffsetLeft =
containerRef.value.clientWidth -
monthEleWidth -
(count - nextTargetDay + (31 - count)) * dayEleWidth -
day * dayEleWidth;
}
// 无下一个元素时,最大拖动宽度
const maxWidthWhenNoNext = (count - day + 1) * dayEleWidth;
// 限制最大拖动宽度
const width = Math.min(
Math.round(event.rect.width / dayEleWidth) * dayEleWidth,
nextTarget ? nextTargetOffsetLeft : maxWidthWhenNoNext,
);
const height = (target.style.height = event.rect.height + 'px');
const dragArea = target.querySelector('.drag_area');
event.target.style.width = `${width}px`;
event.target.style.height = `${height}px`;
dragArea.style.width = `${width - dayEleWidth}px`;
dragArea.style.height = `${height}px`;
event.target.dataset.x = x;
event.target.dataset.y = y;
},
end(event) {
isDragging.value = false;
event.target.style.zIndex = '';
currentCell.value = null;
const dragArea = event.target.querySelector('.drag_area');
// 拖动了几个格子
const dragStep = dragArea.clientWidth / dayEleWidth;
const [month, day] = event.target.dataset.current
.split('-')
.map((item: string) => +item); // 当前操作时间 月,日
const monthInfo = months.value.find((k: IMonth) => k.month == month);
if (monthInfo) {
const dayInfo = monthInfo.days.find((k: IDay) => k.day == day);
if (dayInfo) {
dayInfo.step = dragStep;
}
}
},
},
});
};
/**
* 重置初始值
*/
const resetDefault = () => {
activeCellIndex.value = null;
activeDayIndex.value = null;
isAddComment.value = false;
editActiveIndex.value = null;
};
/**
* @description 添加全局点击事件监听器
*/
const addGlobalClickListener = () => {
document.addEventListener('click', function (event) {
// 检查点击的目标对象及其祖先元素是否包含.duration类
let targetElement = event.target as any;
let containsDurationClass = false;
while (targetElement) {
if (targetElement.classList && targetElement.classList.contains('day')) {
containsDurationClass = true;
break;
}
targetElement = targetElement.parentElement;
}
// 如果点击的对象不包含.duration类,则清空容器内容
if (!containsDurationClass) {
resetDefault();
}
});
};
onMounted(() => {
// 添加全局点击事件监听器
addGlobalClickListener();
});
//是否渲染完成
const isComplete = ref(false);
watch(
() => props.dataBeginDate,
(newValue, oldValue) => {
if (newValue && newValue != oldValue) {
// 生成月份数据
generateMonthsData()
.then(() => {
// 单元格绑定拖动事件
bindResizableEvent();
})
.finally(() => {
isComplete.value = true;
});
}
},
{ immediate: true },
);
const { proxy } = getCurrentInstance() as any;
watch(
() => proxy.$i18n.locale,
(newLocale, oldLocale) => {
if (newLocale !== oldLocale) {
bindResizableEvent();
}
},
{
immediate: true,
},
);
// 轮班类型
const rotationPlanTypes = [
{ label: 'M', value: 'M' },
{ label: 'R', value: 'R' },
{ label: 'T (A)', value: 'Arrive_T' },
{ label: 'T (L)', value: 'Leave_T' },
];
/**
* @description 最外层cell点击事件
*/
const handleCellClick = (month: IMonth, day: IDay) => {
if (!isDragging.value) {
activeCellIndex.value = `${month.month}-cell-${day.day}`;
isAddComment.value = false;
editActiveIndex.value = null;
}
};
/**
* @description 鼠标左击事件
*/
const handleDayClick = (month: IMonth, day: IDay) => {
activeDayIndex.value = `${month.monthLabel}-day-${day.day}`;
activeCellIndex.value = null;
isAddComment.value = false;
editActiveIndex.value = null;
};
/**
* @description 鼠标右击事件
*/
const handleRightDayClick = (event: Event, month: IMonth, day: IDay) => {
isAddComment.value = true;
editActiveIndex.value = `${month.month}-edit-${day.day}`;
// input focus
const target = event.target as any;
if (target) {
setTimeout(() => {
const input = target.closest('.duration').querySelector('input');
if (input) {
input.focus();
}
}, 200);
}
};
const hoverDayIndex = ref<string | null>(null);
const handleDayMouseenter = (month: IMonth, day: IDay) => {
hoverDayIndex.value = `${month.monthLabel}${day.day}`;
};
const handleDayMouseleave = () => {
hoverDayIndex.value = null;
activeDayIndex.value = null;
};
/**
* @description 选择类型
*/
const handlePlanTypeClick = (day: IDay, duration: string) => {
day.duration = duration;
activeDayIndex.value = null;
};
/**
* @description 删除durantion
*/
const keyboardDownHandle = (evt: KeyboardEvent) => {
if (activeCellIndex.value && ['Delete', 'Backspace'].includes(evt.key)) {
const [month, day] = activeCellIndex.value
.split('-cell-')
.map((item: string) => +item);
const current = months.value
.filter((item: any) => item.month == month)[0]
.days.filter((k: any) => k.day == day)[0];
current.duration = null;
current.step = 0;
current.note = '';
}
};
onMounted(() => {
document.addEventListener('keydown', keyboardDownHandle, true);
});
onUnmounted(() => {
document.removeEventListener('keydown', keyboardDownHandle, true);
});
/**
* @description 导出数据
*/
const exportData = (tagWork?: { [key: string]: string }) => {
const result = months.value.reduce((pre: any, month: IMonth) => {
const arr = month.days.reduce((blocks: any, day: IDay) => {
if (day.duration) {
blocks.push({
beginDate: `${month.month}-${day.day}`,
endDate: `${month.month}-${+day.day + +day.step}`,
workTag: tagWork ? +tagWork[day.duration as string] : day.duration,
remark: day.note,
});
}
return blocks;
}, []);
return [...pre, ...arr];
}, []);
return { year, rpData: result };
};
/**
* @description 数据回显
*/
const importData = debounce((data: any) => {
data.forEach((item: any) => {
const { beginDate, endDate, workTag, remark } = item;
const curMonth = new Date(beginDate).getMonth() + 1;
const startDay = new Date(beginDate).getDate();
const diffInDays = dayjs(endDate).diff(dayjs(beginDate), 'day');
const month = months.value.find((k: any) => k.month == curMonth);
if (month) {
const day = month.days.find((day: any) => day.day == startDay) as IDay;
day.duration = workTag;
day.note = remark ?? '';
day.step = diffInDays;
requestAnimationFrame(() => {
const durationEle = document.querySelector(
`#${props.id} .duration[data-current="${curMonth}-${startDay}"]`,
) as HTMLElement;
if (durationEle) {
const newWidth = diffInDays * dayEleWidth + dayEleWidth;
const dragArea = durationEle.getElementsByClassName(
'drag_area',
)[0] as HTMLElement;
// 只有在宽度变化时才修改样式,避免不必要的更新
if (durationEle.style.width !== `${newWidth}px`) {
durationEle.style.width = `${newWidth}px`;
}
if (
dragArea &&
dragArea.style.width !== `${diffInDays * dayEleWidth}px`
) {
dragArea.style.width = `${diffInDays * dayEleWidth}px`;
}
}
});
}
});
}, 500);
const workPattern = computed(() => {
if (props.employRpDetail) {
const { workPatternDTO, workPattern, b2bWorkPattern, workPatternName } =
props.employRpDetail;
if (props.isB2B) {
return b2bWorkPattern ?? '--';
} else {
return (workPattern || workPatternDTO?.name || workPatternName) ?? '--';
}
}
return '';
});
/**
* @description Total Working Days
*/
const totalWorkingDays = computed(() => {
return months.value.reduce((pre: number, month: IMonth) => {
const workingDays = month.days.reduce((workingDays: number, day: IDay) => {
if (day.duration && day.duration == 'M') {
workingDays += day.step + 1;
}
if (
day.duration &&
(day.duration == 'Leave_T' || day.duration == 'Arrive_T')
) {
workingDays += day.step + 0.5;
}
return workingDays;
}, 0);
return pre + workingDays;
}, 0);
});
/**
* @description Total Off Days
*/
const totalOffDays = computed(() => {
return months.value.reduce((pre: number, month: IMonth) => {
const workingDays = month.days.reduce((workingDays: number, day: IDay) => {
if (day.duration && day.duration == 'R') {
workingDays += day.step + 1;
}
if (
day.duration &&
(day.duration == 'Leave_T' || day.duration == 'Arrive_T')
) {
workingDays += day.step + 0.5;
}
return workingDays;
}, 0);
return pre + workingDays;
}, 0);
});
// 修改历史数组
const planChangedArr = ref<any>([]);
const ganttBodyRef = ref();
/**
* @description Details时显示plan changed历史
*/
const displayPlanChanged = (data: any) => {
if (!data || !data.length) return;
setTimeout(() => {
const parentRect = ganttBodyRef.value.getBoundingClientRect();
const result = data.reduce((pre: any, cur: any) => {
const { beginDate, endDate } = cur;
// 1.1 查找开始位置,获取left
const [startMonth, startDay] = dayjs(beginDate).format('M-D').split('-');
const dayEle = document.querySelector(
`.day[data-current="${startMonth}-${startDay}"]`,
) as Element;
const { left, top } = dayEle.getBoundingClientRect();
cur['left'] = left - parentRect.left;
cur['top'] = top - parentRect.top;
// 2.2 计算两个日期相差天数,获得元素宽
const date1 = dayjs(beginDate, 'YYYY-MM-DD');
const date2 = dayjs(endDate, 'YYYY-MM-DD');
const diffDays = date2.diff(date1, 'day') + 1;
cur['width'] = diffDays * dayEleWidth;
pre.push(cur);
return pre;
}, []);
planChangedArr.value = result;
});
};
const open = ref(false);
const currentPlanChangedItem = ref<any>(null);
/**
* @description 打开Change Request Details 弹窗
*/
const openRequestDetail = (item: any) => {
open.value = true;
currentPlanChangedItem.value = item;
};
// ============================================ comment ====
const username = store.getters.getUserInfo.username;
const comment = ref('');
const commentArr = ref<any>([]);
/**
* @description Details时显示plan changed历史
*/
const displayCommentBlock = (data: any) => {
if (!data || !data.length) return;
setTimeout(() => {
const parentRect = ganttBodyRef.value.getBoundingClientRect();
const result = data.reduce((pre: any, cur: any) => {
if (cur.commentList && cur.commentList.length) {
const { beginDate, endDate } = cur;
// 1.1 查找开始位置,获取left
const [startMonth, startDay] = dayjs(beginDate)
.format('M-D')
.split('-');
const dayEle = document.querySelector(
`.day[data-current="${startMonth}-${startDay}"]`,
) as Element;
const { left, top } = dayEle.getBoundingClientRect();
cur['left'] = left - parentRect.left;
cur['top'] = top - parentRect.top;
// 2.2 计算两个日期相差天数,获得元素宽
const date1 = dayjs(beginDate, 'YYYY-MM-DD');
const date2 = dayjs(endDate, 'YYYY-MM-DD');
const diffDays = date2.diff(date1, 'day') + 1;
cur['width'] = diffDays * dayEleWidth;
pre.push(cur);
}
return pre;
}, []);
commentArr.value = result;
});
};
//================================================ Overlap & Gap 判断 ===================
const overlapArr = ref<any>([]);
/**
* @description 显示重叠日期
*/
const displayOverlapBlock = (data: any) => {
if (!data) return;
const parentRect = ganttBodyRef.value.getBoundingClientRect();
const result = data.reduce((pre: any, cur: any) => {
const { beginDate, endDate } = cur;
// 1.1 查找开始位置,获取left
const [startMonth, startDay] = dayjs(beginDate).format('M-D').split('-');
const dayEle = document.querySelector(
`.day[data-current="${startMonth}-${startDay}"]`,
) as Element;
const { left, top } = dayEle.getBoundingClientRect();
cur['left'] = left - parentRect.left;
cur['top'] = top - parentRect.top;
// 2.2 计算两个日期相差天数,获得元素宽
const date1 = dayjs(beginDate, 'YYYY-MM-DD');
const date2 = dayjs(endDate, 'YYYY-MM-DD');
const diffDays = date2.diff(date1, 'day') + 1;
cur['width'] = diffDays * dayEleWidth;
pre.push(cur);
return pre;
}, []);
overlapArr.value = result;
};
const gapArr = ref<any>([]);
/**
* @description 显示Gap日期
*/
const displayGapBlock = (data: any) => {
if (!data) return;
const parentRect = ganttBodyRef.value.getBoundingClientRect();
const result = data.reduce((pre: any, cur: any) => {
const { beginDate, endDate } = cur;
// 1.1 查找开始位置,获取left
const [startMonth, startDay] = dayjs(beginDate).format('M-D').split('-');
const dayEle = document.querySelector(
`.day[data-current="${startMonth}-${startDay}"]`,
) as Element;
const { left, top } = dayEle.getBoundingClientRect();
cur['left'] = left - parentRect.left;
cur['top'] = top - parentRect.top;
// 2.2 计算两个日期相差天数,获得元素宽
const date1 = dayjs(beginDate, 'YYYY-MM-DD');
const date2 = dayjs(endDate, 'YYYY-MM-DD');
const diffDays = date2.diff(date1, 'day') + 1;
cur['width'] = diffDays * dayEleWidth;
pre.push(cur);
return pre;
}, []);
gapArr.value = result;
};
defineExpose({
isComplete,
totalWorkingDays,
totalOffDays,
exportData,
importData,
displayPlanChanged,
displayCommentBlock,
displayOverlapBlock,
displayGapBlock,
});
/**
* @description 回复评论
*/
const replyComment = (record: any) => {
if (comment.value.length && record) {
replyCommentT({
beginDate: record.beginDate,
endDate: record.endDate,
planDetailId: record.id,
content: comment.value,
}).then((res) => {
if (res.code == 200) {
record.commentList.push({
comment: comment.value,
createTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
employeeName: username,
});
comment.value = '';
}
});
}
};
/**
* @description 初始化数据
*/
const resetData = () => {
generateMonthsData();
};
const calculateExecutedDays = (
{
workDays,
workTag,
}: {
workDays: number;
workTag: string;
},
workPattern: any,
): { [key: string]: number } => {
const executedDays = {
arrivalTravelDays: 0,
workDays: 0,
leaveTravelDays: 0,
rotationDays: 0,
};
switch (workTag) {
case 'Arrive_T':
executedDays.arrivalTravelDays = workPattern.arrivalTravelDays;
break;
case 'M':
executedDays.arrivalTravelDays = workPattern.arrivalTravelDays;
executedDays.workDays = workDays;
break;
case 'Leave_T':
executedDays.arrivalTravelDays = workPattern.arrivalTravelDays;
executedDays.workDays = workPattern.workDays;
executedDays.leaveTravelDays = workPattern.leaveTravelDays;
break;
case 'R':
executedDays.arrivalTravelDays = workPattern.arrivalTravelDays;
executedDays.workDays = workPattern.workDays;
executedDays.leaveTravelDays = workPattern.leaveTravelDays;
executedDays.rotationDays = workDays;
break;
default:
throw new Error('Unknown workTag');
}
return executedDays;
};
/**
* @description 自动填充RP
*/
const autofill = () => {
if (!isComplete.value) return;
resetData();
const {
workPatternDTO,
lastRotationPlanDetailDTO,
b2bWorkPattern,
workPattern,
} = props.employRpDetail;
// 月份数据
const calendarData = pureMonthData.value;
// workPattern工作模式
const WorkPatternMap = {
arrivalTravelDays: workPatternDTO?.arrivalTravelDays || 0,
workDays: workPatternDTO?.workDays || 0,
leaveTravelDays: workPatternDTO?.leaveTravelDays || 0,
rotationDays: workPatternDTO?.rotationDays || 0,
};
const workTags = {
arrivalTravelDays: 'Arrive_T',
workDays: 'M',
leaveTravelDays: 'Leave_T',
rotationDays: 'R',
};
const results = [] as {
beginDate: string;
endDate: string;
workTag: string;
}[];
let originalWorkPattern = { ...WorkPatternMap };
// 1. 对上个季度最后一段数据进行处理, 如果workDay/workTag为null, 则不处理
const lastQuarterData = lastRotationPlanDetailDTO;
if (lastQuarterData.workDays && lastQuarterData.workTag) {
const executedDays = calculateExecutedDays(lastQuarterData, WorkPatternMap);
originalWorkPattern = Object.keys(originalWorkPattern).reduce(
(acc: any, key) => {
acc[key] -= executedDays[key] || 0;
return acc;
},
originalWorkPattern,
);
}
// 2. 生成RP数据
const addResult = (beginDate: string, workTag: string) => {
const dateString = dayjs(beginDate).format('YYYY-MM-DD');
const lastEntry = results[results.length - 1];
if (lastEntry && lastEntry.workTag === workTag) {
lastEntry.endDate = dateString; // 更新最后一个条目的endDate
} else {
results.push({ beginDate: dateString, endDate: dateString, workTag });
}
};
// 是否b2b有数据
const hasB2BData =
props.employRpDetail.b2bEmployeeRotationPlanDetailDTOList &&
props.employRpDetail.b2bEmployeeRotationPlanDetailDTOList.length > 0;
const [M, R] = workPattern.split('/');
// 有b2b数据,根据b2b的数据反向生成数据 T_A -> T_L, M -> R
// 对称的才进行反向生成 2024/11/18
if (hasB2BData && b2bWorkPattern === workPattern && +M == +R) {
const { b2bEmployeeRotationPlanDetailDTOList } = props.employRpDetail;
calendarData.forEach((month) => {
let currentDayIndex = 0;
while (currentDayIndex < month.days.length) {
const day = month.days[currentDayIndex];
const dateString = `${year}-${String(month.month).padStart(
2,
'0',
)}-${String(day.day).padStart(2, '0')}`;
// 对disabled的日期进行跳过,因为可能为新员工,入职日期不一定是从1号开始的,disabled在之前的方法中已进行了判断,所以这里无需判断
if (day.disabled) {
currentDayIndex++;
continue;
}
const targetDate = dayjs(dateString);
const result = b2bEmployeeRotationPlanDetailDTOList.find(
(item: any) => {
const beginDate = dayjs(item.beginDate);
const endDate = dayjs(item.endDate);
return targetDate.isBetween(beginDate, endDate, null, '[]'); // '[]' 表示包含边界
},
);
const reverseWorkTag: any = {
[workTagsEnum.arrive]: workTagsEnum.leave, // T_A -> T_L,
[workTagsEnum.m]: workTagsEnum.r, // M -> R,
[workTagsEnum.leave]: workTagsEnum.arrive, // T_L -> T_A,
[workTagsEnum.r]: workTagsEnum.m, // R -> M,
};
const WorkTagMap: any = {
[workTagsEnum.arrive]: 'Arrive_T',
[workTagsEnum.m]: 'M',
[workTagsEnum.leave]: 'Leave_T',
[workTagsEnum.r]: 'R',
};
if (result) {
addResult(dateString, WorkTagMap[reverseWorkTag[result.workTag]]);
}
currentDayIndex++;
}
});
// 如果上一期没数据, 将第一天workTag改为Arrive_T类型
if (!lastQuarterData.workDays || !lastQuarterData.workTag) {
const earliestDate = results[0].beginDate;
results[0].beginDate = dayjs(earliestDate)
.add(1, 'day')
.format('YYYY-MM-DD');
results.unshift({
beginDate: earliestDate,
endDate: earliestDate,
workTag: 'Arrive_T',
});
}
} else {
calendarData.forEach((month) => {
let currentDayIndex = 0;
while (currentDayIndex < month.days.length) {
const day = month.days[currentDayIndex];
const dateString = `${year}-${String(month.month).padStart(
2,
'0',
)}-${String(day.day).padStart(2, '0')}`;
// 对disabled的日期进行跳过,因为可能为新员工,入职日期不一定是从1号开始的,disabled在之前的方法中已进行了判断,所以这里无需判断
if (day.disabled) {
currentDayIndex++;
continue;
}
// 有b2b数据,根据b2b的数据反向生成数据 T_A -> T_L , M -> R
if (originalWorkPattern.arrivalTravelDays > 0) {
addResult(dateString, workTags.arrivalTravelDays);
originalWorkPattern.arrivalTravelDays--;
} else if (originalWorkPattern.workDays > 0) {
addResult(dateString, workTags.workDays);
originalWorkPattern.workDays--;
} else if (originalWorkPattern.leaveTravelDays > 0) {
addResult(dateString, workTags.leaveTravelDays);
originalWorkPattern.leaveTravelDays--;
} else if (originalWorkPattern.rotationDays > 0) {
addResult(dateString, workTags.rotationDays);
originalWorkPattern.rotationDays--;
}
// 如果所有工作模式都已执行完,则重置
if (Object.values(originalWorkPattern).every((v) => v === 0)) {
originalWorkPattern = { ...WorkPatternMap };
}
currentDayIndex++;
}
});
}
// 3. 对跨月的数据进行拆分,考虑跨多个月的情况
const finalResults = [] as {
beginDate: string;
endDate: string;
workTag: string;
}[];
// 辅助函数,用于创建结果并推入finalResults
const pushResult = (beginDate: string, endDate: string, workTag: string) => {
finalResults.push({
beginDate: dayjs(beginDate).format('YYYY-MM-DD'),
endDate: dayjs(endDate).format('YYYY-MM-DD'),
workTag,
});
};
results.forEach((result) => {
let beginDate = dayjs(result.beginDate);
let endDate = dayjs(result.endDate);
// 如果开始日期和结束日期不在同一月份,进行跨月拆分
while (!beginDate.isSame(endDate, 'month')) {
// 当前月的最后一天
const midMonthLastDay = beginDate.endOf('month');
// 将从beginDate到当前月的最后一天的数据推入结果
pushResult(
beginDate.format('YYYY-MM-DD'),
midMonthLastDay.format('YYYY-MM-DD'),
result.workTag,
);
// 更新beginDate为下个月的第一天
beginDate = midMonthLastDay.add(1, 'day');
}
// 如果在同一月份,或者已经处理到最后一个月,将剩余的日期段推入结果
pushResult(
beginDate.format('YYYY-MM-DD'),
endDate.format('YYYY-MM-DD'),
result.workTag,
);
});
// 输出结果
importData(finalResults);
};
const workPatternTitle = computed(() => {
if (props.workPatternTitle) {
return props.workPatternTitle;
}
return $t('attendance.rotation.workPattern');
});
</script>
<template>
<div :id="props.id" class="rotationPlanDesign">
<div class="rotationPlanDesign-header">
<div class="first-floor">
<div class="title">
{{ props.title ?? $t('attendance.rotation.planDesign.title') }}
</div>
<div class="tips">
<div v-for="(value, key) in tips" :key="key" class="tip-item">
<span class="icon" :class="[key]">{{ key }}</span>
<span class="msg">{{ value }}</span>
</div>
</div>
</div>
<div class="last-floor" :class="{ detail: props.isDetail }">
<div class="last-floor-left">
<span class="total">
<span class="key"> {{ workPatternTitle }}: </span>
<span class="value">{{ workPattern }}</span>
</span>
<span class="total">
<span class="key">
{{ $t('attendance.rotation.planDesign.workingDays') }}:
</span>
<span class="value">{{ totalWorkingDays }}</span>
</span>
<span class="total">
<span class="key">
{{ $t('attendance.rotation.planDesign.offDays') }}:
</span>
<span class="value">{{ totalOffDays }}</span>
</span>
<template
v-if="
props.hasB2B &&
['28/28', '14/14', '7/7'].includes(
props.employRpDetail.workPattern,
)
"
>
<span v-if="props.overlapDays > 0" class="total">
<span class="key">
{{ $t('attendance.rotation.planDesign.overlap') }}:
</span>
<span class="value" style="color: #cb0000">{{
props.overlapDays
}}</span>
</span>
<span v-if="props.gapDays > 0" class="total">
<span class="key">
{{ $t('attendance.rotation.planDesign.gap') }}:
</span>
<span class="value" style="color: #cb0000">{{
props.gapDays
}}</span>
</span>
</template>
</div>
<div class="last-floor-right">
<template v-if="!props.isB2B && !props.isReadOnly && !props.isDetail">
<span
v-if="!totalWorkingDays && !totalOffDays"
class="autofill"
@click="autofill"
>{{ $t('attendance.rotation.planDesign.autoFill') }}
</span>
<template v-else>
<a-popconfirm
ok-text="OK"
cancel-text="Cancel"
placement="left"
@confirm="autofill"
>
<template #icon>
<ExclamationCircleOutlined style="color: red" />
</template>
<template #title>
<span style="color: #131211">
<i18n-t
keypath="attendance.rotation.planDesign.autoFillTip"
>
<template #br>
<br />
</template>
</i18n-t>
</span>
</template>
<span class="autofill">{{
$t('attendance.rotation.planDesign.autoFill')
}}</span>
</a-popconfirm>
</template>
</template>
</div>
</div>
<div v-if="props.isExceed9Weeks" class="overlap-tips">
{{ $t('attendance.rotation.planDesign.exceed9WeeksTip') }}
</div>
<div v-if="props.isFailed2Modified" class="overlap-tips">
{{ $t('attendance.rotation.planDesign.failed2Modified') }}
</div>
<div v-if="!props.isFill" class="overlap-tips">
{{ $t('attendance.rotation.planDesign.notFillTip') }}
</div>
<div
v-if="props.overlapDays > props.canOverlapDays || props.gapDays > 0"
class="overlap-tips"
>
{{ $t('attendance.rotation.planDesign.overlapOrGapTip') }}
</div>
<div
v-if="props.isFewerWorkingDay"
class="overlap-tips"
style="color: #ff7a00"
>
{{
$t('attendance.rotation.planDesign.fewerWorkingDayTip', {
requireWorkingDays: props.requireWorkingDays,
})
}}
</div>
</div>
<div
class="rotationPlanDesign-content"
:style="{ transform: isArabic ? 'rotateY(180deg)' : 'none' }"
>
<div
ref="containerRef"
class="gantt-container"
:class="{ 'read-only': isReadOnly }"
>
<div class="gantt-header">
<div class="gantt-cell empty"></div>
<div v-for="dayItem in days" :key="dayItem" class="gantt-cell">
{{ dayItem }}
</div>
</div>
<div ref="ganttBodyRef" class="gantt-body">
<div
v-for="(month, monthIndex) in months"
:key="month.month"
class="gantt-row"
>
<div class="gantt-cell month">{{ $t(month.monthLabel) }}.</div>
<div
v-for="day in month.days"
:key="day.day"
class="gantt-cell day"
:class="{ disabled: day.disabled }"
:data-current="`${month.month}-${day.day}`"
>
<span class="week">{{ $t(day.week) }}</span>
<div
v-if="!day.disabled"
class="action"
@mouseenter="handleDayMouseenter(month, day)"
@mouseleave="handleDayMouseleave()"
>
<template
v-if="
hoverDayIndex == `${month.monthLabel}${day.day}` &&
!isDragging
"
>
<div
v-if="!day.duration"
class="grid dropdown"
:class="{
active: hoverDayIndex == `${month.monthLabel}${day.day}`,
}"
@click="handleDayClick(month, day)"
>
<DownOutlined />
</div>
</template>
<div
v-if="day.duration"
class="grid duration"
:class="{
[day.duration]: true,
hideAfter:
isDragging && currentCell !== `${month.month}-${day.day}`,
active:
activeCellIndex === `${month.month}-cell-${day.day}` &&
!isAddComment,
addCommentActive:
isAddComment &&
editActiveIndex === `${month.month}-edit-${day.day}`,
hasComment:
day.note.length &&
editActiveIndex !== `${month.month}-edit-${day.day}`,
}"
:data-count="month.monthDayCount"
:data-current="`${month.month}-${day.day}`"
:data-duration="day.duration"
@click="handleCellClick(month, day)"
@click.right.prevent="
(e) => handleRightDayClick(e, month, day)
"
>
<a-tooltip
color="#fff"
:overlay-inner-style="{
color: '#66809E',
padding: '8px 12px 2px',
fontSize: '12px',
width: '389px',
}"
:placement="monthIndex > 3 ? 'bottom' : 'top'"
>
<template #title>
<ul style="padding: 0; list-style: none">
<li style="text-align: left">
• {{ $t('attendance.rotation.planDesign.dragEdge') }}
</li>
<li style="text-align: left">
•
{{
$t('attendance.rotation.planDesign.clickRightMouse')
}}
</li>
<li style="text-align: left">
•
{{
$t('attendance.rotation.planDesign.deleteDuration')
}}
</li>
</ul>
</template>
<div
class="selectedCell"
:style="{
transform: isArabic ? 'rotateY(180deg)' : 'none',
}"
@click="handleDayClick(month, day)"
>
{{
['Arrive_T', 'Leave_T'].includes(day.duration)
? 'T'
: day.duration
}}
<span
v-if="
(isAddComment &&
editActiveIndex ===
`${month.month}-edit-${day.day}`) ||
day.note
"
>:
</span>
</div>
</a-tooltip>
<div
class="drag_area"
:title="$t('attendance.rotation.planDesign.addNote')"
>
<a-input
v-if="
isAddComment &&
editActiveIndex === `${month.month}-edit-${day.day}`
"
v-model:value="day.note"
:placeholder="
$t('attendance.rotation.planDesign.addANote')
"
@click.stop
></a-input>
<div
v-if="
day.note.length &&
editActiveIndex !== `${month.month}-edit-${day.day}`
"
class="note"
:title="day.note"
>
{{ day.note }}
</div>
</div>
</div>
<!-- 下拉列表 -->
<ul
v-if="activeDayIndex == `${month.monthLabel}-day-${day.day}`"
class="plan_list"
:class="{ up: monthIndex > 3 }"
>
<li
v-for="item in rotationPlanTypes"
:key="item.value"
class="plan_list_item"
:class="{ active: item.value === day.duration }"
@click="handlePlanTypeClick(day, item.value)"
>
{{ item.label }}
</li>
</ul>
</div>
</div>
</div>
<!-- 显示Rotation plan changed -->
<template v-if="planChangedArr.length">
<div
v-for="(item, idx) in planChangedArr"
:key="idx"
class="planChangedItem"
:style="{
width: `${item.width}px`,
left: `${item.left}px`,
top: `${item.top}px`,
}"
>
<a-tooltip
color="#fff"
:overlay-inner-style="{
width: '371px',
}"
>
<template #title>
<div class="rotationPlanChanged">
<div class="rotationPlanChanged-header">
<span class="title">{{
$t('attendance.rotation.planDesign.planChangedTitle')
}}</span>
<span class="time">{{ item.createTime || '-' }}</span>
</div>
<div class="rotationPlanChanged-operation">
<span @click="openRequestDetail(item)">
{{ $t('attendance.rotation.planDesign.viewDetails') }}
</span>
</div>
<div class="rotationPlanChanged-content">
<div
v-for="(k, index) in item.changeHistory"
:key="index"
class="history-item"
>
{{ dayjs(k.beginDate).format('MM/DD/YYYY') }} -
{{ dayjs(k.endDate).format('MM/DD/YYYY') }}, From
<span :class="[`${k.from}`]">{{ k.from }}</span> to
<span :class="[`${k.to}`]">{{ k.to }}</span>
</div>
</div>
</div>
</template>
<div style="width: 100%; height: 100%"></div>
</a-tooltip>
</div>
</template>
<!-- 显示comment -->
<template v-if="commentArr.length">
<div
v-for="(item, idx) in commentArr"
:key="idx"
class="planChangedItem"
:style="{
width: `${item.width}px`,
left: `${item.left}px`,
top: `${item.top}px`,
}"
>
<a-tooltip
color="#fff"
:overlay-inner-style="{
width: '297px',
}"
@open-change="comment = ''"
>
<template #title>
<div class="commentBox">
<div class="comment">
<div
v-for="(k, index) in item.commentList"
:key="index"
class="comment-item"
>
<div class="top">
<span class="nameT">
{{
k.employeeName &&
k.employeeName.slice(0, 1).toUpperCase()
}}
</span>
<span class="username">{{ k.employeeName }}</span>
<span class="time">{{
formatTime(k.createTime)
}}</span>
</div>
<div class="bottom">
<div class="msg">
{{ k.comment }}
</div>
</div>
</div>
</div>
<div class="reply">
<span class="nameT">
{{ username.slice(0, 1).toUpperCase() }}
</span>
<div class="input">
<a-input
v-model:value="comment"
placeholder="Reply"
@click.stop
></a-input>
<svg-icon
name="replyComment"
width="17"
height="14"
class="icon"
@click="replyComment(item)"
></svg-icon>
</div>
</div>
</div>
</template>
<div style="width: 100%; height: 100%"></div>
</a-tooltip>
</div>
</template>
<!-- 显示Overlap -->
<template v-if="overlapArr.length && props.isB2B">
<div
v-for="(item, idx) in overlapArr"
:key="idx"
class="overlapAndGapItem"
:style="{
width: `${item.width}px`,
left: `${item.left}px`,
top: `${item.top}px`,
}"
>
<svg-icon
name="overlapAndGapErrorIcon"
width="14.2"
height="12px"
class="icon"
/>
{{ $t('attendance.rotation.planDesign.overlap') }}
</div>
</template>
<!-- 显示Gap -->
<template v-if="gapArr.length && props.isB2B">
<div
v-for="(item, idx) in gapArr"
:key="idx"
class="overlapAndGapItem"
:style="{
width: `${item.width}px`,
left: `${item.left}px`,
top: `${item.top}px`,
}"
>
<svg-icon
name="overlapAndGapErrorIcon"
width="14.2"
height="12px"
class="icon"
/>
{{ $t('attendance.rotation.planDesign.gap') }}
</div>
</template>
</div>
</div>
</div>
</div>
<ChangeRequestDetailsModal
v-if="open"
v-model:open="open"
:current-data="currentPlanChangedItem"
:query-type="0"
/>
</template>
<style scoped lang="scss">
.rotationPlanDesign {
padding: 32px 46px;
position: relative;
// overflow-x: none;
&-header {
display: flex;
flex-direction: column;
margin-bottom: 12px;
position: relative;
.first-floor {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
.title {
font-size: 16px;
font-weight: 600;
color: #66809e;
}
.tips {
display: flex;
.tip-item {
display: flex;
align-items: center;
margin-inline-end: 36px;
&:last-child {
margin-inline-end: 0;
}
.icon {
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
color: #66809e;
background: rgba(102, 128, 158, 0.1);
&.R {
background: rgba(47, 138, 57, 0.15);
color: #2d8a39;
}
&.T {
background: rgba(238, 166, 0, 0.15);
color: #eea600;
}
}
.msg {
margin-inline-start: 14px;
font-size: 12px;
font-weight: 470;
}
}
}
}
.last-floor {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
&-left {
.total {
margin-inline-end: 36px;
font-size: 16px;
.key {
font-weight: 470;
color: #66809e;
padding-inline-end: 4px;
}
.value {
font-weight: 600;
color: #131211;
}
}
}
&-right {
.autofill {
color: #ff7a00;
font-size: 12px;
font-weight: 500;
text-decoration: underline;
cursor: pointer;
&:hover {
color: #cb6100;
}
}
}
&.detail {
position: absolute;
top: 4px;
}
}
.overlap-tips {
margin-bottom: 16px;
font-size: 14px;
color: #cb0000;
}
}
&-content {
display: flex;
align-items: center;
justify-content: space-around;
overflow-x: auto; /* 水平滚动条 */
direction: ltr !important;
.gantt-container {
width: 1576px;
position: relative;
&.read-only::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
z-index: 99;
top: 0;
bottom: 0;
}
}
.gantt-header,
.gantt-row {
display: flex;
}
.gantt-header {
.gantt-cell {
width: 47px;
height: 34px;
color: #ff7a00;
font-weight: 600;
font-size: 12px;
user-select: none;
}
.empty {
width: 75px;
height: 30px;
padding-inline-end: 15px;
box-sizing: content-box;
}
}
.gantt-cell {
display: flex;
align-items: center;
justify-content: center;
}
.gantt-body {
position: relative;
padding-top: 12px;
&::after {
content: '';
position: absolute;
width: 1px;
height: 100%;
background: #eaedf7;
left: 89px;
top: 0;
}
.planChangedItem {
width: 47px;
height: 34px;
cursor: pointer;
position: absolute;
top: 14px;
z-index: 2002;
border: 1px dashed #cb0000;
&::after {
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-top: 8px solid #cb0000;
}
}
.overlapAndGapItem {
width: 47px;
height: 34px;
position: absolute;
top: 14px;
z-index: 2002;
background: rgba(203, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #cb0000;
font-weight: bold;
overflow: hidden;
.icon {
margin-inline-end: 4px;
}
}
}
.gantt-row {
.gantt-cell {
&.disabled {
cursor: not-allowed !important;
}
&.month {
width: 69px;
height: 30px;
font-weight: bold;
font-size: 12px;
font-weight: 600;
justify-content: flex-end;
user-select: none;
padding: 14px 0;
padding-inline-end: 20px;
box-sizing: content-box;
}
&.day {
box-sizing: content-box;
margin: 12px 0;
width: 47px;
border-radius: 2px;
cursor: pointer;
position: relative;
user-select: none;
.week {
position: absolute;
top: -18px;
color: #a7b1c2;
font-size: 12px;
font-weight: 600;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: initial;
}
&::after {
content: '';
position: absolute;
right: 0;
top: -25px;
width: 1px;
height: 120%;
background-color: rgba(231, 235, 242, 0.5);
height: 71px;
}
&:nth-child(4n + 1) {
&::after {
background-color: #eaedf7;
}
}
.action {
width: 100%;
height: 100%;
display: flex;
.grid {
width: 100%;
height: 100%;
text-align: center;
.anticon {
font-size: 12px;
color: #66809e;
}
&.dropdown {
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: #f0f3f6;
}
}
}
.duration {
background-color: rgba(102, 128, 158, 0.1);
color: #66809e;
font-weight: 700;
font-size: 13px;
border-radius: 2px;
display: flex;
position: absolute;
top: 0;
left: 0;
z-index: 3;
border: 1px dashed transparent;
&.active {
border-color: #ff8a20;
}
.selectedCell {
width: 47px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 4;
}
.drag_area {
// padding: 4px;
position: absolute;
top: 0;
left: 47px;
height: 100%;
text-align: left;
.note {
max-width: 90%;
height: 100%;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
overflow: hidden;
line-height: 32px;
}
}
&.R {
background-color: rgba(224, 238, 226, 0.8);
color: #2d8a39;
&::after {
border-color: #2d8a39;
}
}
&.Arrive_T,
&.Leave_T {
background-color: rgba(253, 242, 217, 0.8);
color: #eea600;
&::after {
border-color: #eea600;
}
}
&:hover,
&:active {
z-index: 100;
background-color: rgba(102, 128, 158, 0.15);
&.R {
background-color: rgba(224, 238, 226, 1);
}
&.Arrive_T,
&.Leave_T {
background-color: rgba(253, 242, 217, 1);
}
&::after {
display: block;
}
}
&.hideAfter {
&::after {
display: none;
}
}
&::after {
content: '';
width: 8px;
height: 8px;
border: 1px solid #66809e;
position: absolute;
border-radius: 50%;
right: -4px;
top: calc(15px - 4px);
display: none;
z-index: 4;
background-color: #fff;
}
}
.addCommentActive {
border-color: #cb0000 !important;
border-style: solid;
background: #cb000026 !important;
color: #cb0000 !important;
.drag_area {
display: flex;
align-items: center;
padding-inline-start: 10px;
&::before {
content: '';
width: 2px;
position: absolute;
top: 6px;
left: -3px;
height: 18px;
background: #ff7a00;
}
.ant-input {
background: transparent;
border: none;
padding: 0;
font-weight: 600;
color: #66809e;
border-radius: 0;
font-size: 13px;
&:focus {
box-shadow: none;
}
&::placeholder {
font-size: 13px;
font-weight: 600;
color: #66809e;
opacity: 0.8;
}
}
}
&::after {
border-color: #cb0000 !important;
}
}
.hasComment {
border-color: transparent;
background: #cb000026 !important;
color: #cb0000 !important;
&::after {
border-color: #cb0000 !important;
}
}
.plan_list {
position: absolute;
left: 0;
top: 30px;
width: 100%;
z-index: 1072;
border-radius: 2px;
background: #fff;
padding: 0;
padding-top: 2px;
&.up {
top: auto;
bottom: 23px;
left: auto;
}
&_item {
width: 100%;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
background: #e8ecf0;
color: #66809e;
border-bottom: 1px solid #dae0e6;
font-weight: 500;
cursor: pointer;
&:first-child {
border-radius: 2px 2px 0 0;
}
&:last-child {
border-radius: 0 0 2px 2px;
}
&:hover {
background: #66809e;
color: #fff;
}
&.active {
background: #66809e;
color: #fff;
}
}
}
}
}
}
}
}
}
</style>
<style lang="scss">
.rotationPlanChanged {
padding: 10px 10px;
&-header {
margin-bottom: 10px;
font-size: 12px;
.title {
color: #131211;
font-weight: 700;
}
.time {
padding-inline-start: 8px;
color: #66809e;
}
}
&-operation {
color: #66809e;
margin-bottom: 10px;
span {
text-decoration: underline;
font-style: italic;
cursor: pointer;
}
}
&-content {
.history-item {
color: #131211;
padding-bottom: 8px;
span {
padding: 0 2px;
font-weight: 600;
color: #66809e;
&.Arrive_T,
&.Leave_T {
color: #eea600;
}
&.R {
color: #2d8a39;
}
}
}
}
}
.commentBox {
padding: 16px;
border-radius: 8px;
background: #fff;
position: relative;
.closeIcon {
position: absolute;
font-size: 12px;
color: #272d37;
top: 10px;
right: 10px;
cursor: pointer;
}
.nameT {
width: 28px;
height: 28px;
background: #fff9df;
border-radius: 50%;
color: #eea600;
font-weight: 700;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-inline-end: 12px;
}
.comment {
max-height: 150px;
overflow-y: auto;
margin-bottom: 12px;
&::-webkit-scrollbar {
width: 8px !important;
}
&-item {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.top {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 700;
margin-bottom: 8x;
.username {
color: #131211;
margin-inline-end: 8px;
}
.time {
color: #66809e;
}
}
.bottom {
padding-inline-start: 40px;
.msg {
color: #131211;
font-size: 12px;
font-weight: 400;
text-wrap: wrap;
line-height: 20px;
}
}
}
}
.reply {
display: flex;
align-items: center;
.ant-input {
background: transparent;
border: none;
padding: 0;
font-weight: 600;
color: #66809e;
border-radius: 0;
font-size: 13px;
&:focus {
box-shadow: none;
}
&::placeholder {
font-size: 12px;
font-weight: 400;
color: #a7b1c2;
opacity: 0.8;
}
}
.input {
flex: 1;
display: flex;
align-items: center;
border: 1px solid #dae0e6;
border-radius: 4px;
padding: 4px 12px;
.icon {
cursor: pointer;
}
}
}
}
</style>
原理其实跟我第一篇文章列表中日历差不多,只不过这个比较复杂。功能点也比较多。项目定制化非常强,发表几个代表性得东西。