feat(mes):迁移【排班日历】
parent
d2763dc044
commit
fab333fbb7
|
|
@ -0,0 +1,135 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MesCalCalendarApi } from '#/api/mes/cal/calendar';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { Tag } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { SolarDay } from 'tyme4ts';
|
||||||
|
|
||||||
|
import { MesCalShiftTypeEnum } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
calendarDayMap: Map<string, MesCalCalendarApi.CalendarDay>; // 排班数据
|
||||||
|
day: string; // 日期,格式 yyyy-MM-dd
|
||||||
|
holidaySet: Set<string>; // 节假日集合
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dayNumber = computed(() => props.day.split('-')[2] || ''); // 取日期中的"日"部分展示
|
||||||
|
const isHoliday = computed(() => props.holidaySet.has(props.day));
|
||||||
|
const isWeekend = computed(() => {
|
||||||
|
const weekday = dayjs(props.day).day();
|
||||||
|
return weekday === 0 || weekday === 6; // 0=周日,6=周六
|
||||||
|
});
|
||||||
|
|
||||||
|
const calDay = computed(() => props.calendarDayMap.get(props.day));
|
||||||
|
const teamShifts = computed(() => calDay.value?.teamShifts || []);
|
||||||
|
const shiftType = computed(() => calDay.value?.shiftType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班次标签展示数据:根据 sort 与 shiftType 推导背景色
|
||||||
|
*
|
||||||
|
* 配色规则(sort 对应轮班方式中的班次顺序):
|
||||||
|
* sort=1 白班 → 绿色(#95d475)
|
||||||
|
* sort=2 中班 → 三班倒用橙色(#f0a020),两班倒用灰色(#909399)
|
||||||
|
* sort=3 夜班 → 灰色(#909399)
|
||||||
|
*/
|
||||||
|
const displayShifts = computed(() => {
|
||||||
|
const isThreeShift = shiftType.value === MesCalShiftTypeEnum.THREE;
|
||||||
|
const colorMap: Record<number, string> = {
|
||||||
|
1: 'bg-[#95d475]',
|
||||||
|
2: isThreeShift ? 'bg-[#f0a020]' : 'bg-[#909399]',
|
||||||
|
3: 'bg-[#909399]',
|
||||||
|
};
|
||||||
|
return teamShifts.value
|
||||||
|
.map((item) => {
|
||||||
|
const bgClass = colorMap[item.sort ?? -1];
|
||||||
|
if (!bgClass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
bgClass,
|
||||||
|
key: `${item.teamId}-${item.shiftId}`,
|
||||||
|
label: `${item.shiftName} · ${item.teamName}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((v): v is { bgClass: string; key: string; label: string } => !!v);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 解析当天的农历、节气、节日信息 */
|
||||||
|
const lunarInfo = computed(() => {
|
||||||
|
const [year, month, date] = props.day.split('-').map(Number);
|
||||||
|
try {
|
||||||
|
const solarDay = SolarDay.fromYmd(year!, month!, date!);
|
||||||
|
const lunarDay = solarDay.getLunarDay();
|
||||||
|
const solarFestival = solarDay.getFestival(); // 公历节日,如 元旦
|
||||||
|
const lunarFestival = lunarDay.getFestival(); // 农历节日,如 春节
|
||||||
|
const termDay = solarDay.getTermDay();
|
||||||
|
const termName =
|
||||||
|
termDay.getDayIndex() === 0 ? termDay.getSolarTerm().getName() : '';
|
||||||
|
const lunarMonthName = lunarDay.getLunarMonth().getName();
|
||||||
|
const lunarDayName = lunarDay.getName();
|
||||||
|
return {
|
||||||
|
lunarFestival: lunarFestival ? lunarFestival.getName() : '',
|
||||||
|
lunarText: lunarMonthName + lunarDayName,
|
||||||
|
solarFestival: solarFestival ? solarFestival.getName() : '',
|
||||||
|
termName,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
lunarFestival: '',
|
||||||
|
lunarText: '',
|
||||||
|
solarFestival: '',
|
||||||
|
termName: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 优先级:公历节日 > 农历节日 > 节气 > 农历月日 */
|
||||||
|
const lunarDisplay = computed(() => {
|
||||||
|
const info = lunarInfo.value;
|
||||||
|
return (
|
||||||
|
info.solarFestival || info.lunarFestival || info.termName || info.lunarText
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 当天是否有节日或节气(用于高亮显示农历文字) */
|
||||||
|
const hasFestivalDay = computed(() => {
|
||||||
|
const info = lunarInfo.value;
|
||||||
|
return Boolean(info.solarFestival || info.lunarFestival || info.termName);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col overflow-hidden p-1">
|
||||||
|
<!-- 顶部:日期数字 + 上班/休息标签 -->
|
||||||
|
<div class="flex shrink-0 items-center justify-between">
|
||||||
|
<span class="text-base font-medium" :class="{ 'text-red-500': isWeekend }">
|
||||||
|
{{ dayNumber }}
|
||||||
|
</span>
|
||||||
|
<Tag v-if="isHoliday" color="green" class="!m-0"> 休 </Tag>
|
||||||
|
<Tag v-else color="blue" class="!m-0"> 班 </Tag>
|
||||||
|
</div>
|
||||||
|
<!-- 农历 / 节气 / 节日显示 -->
|
||||||
|
<div
|
||||||
|
class="mt-0.5 shrink-0 truncate text-[11px]"
|
||||||
|
:class="hasFestivalDay ? 'text-green-600' : 'text-muted-foreground'"
|
||||||
|
>
|
||||||
|
{{ lunarDisplay }}
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
班次列表:节假日不显示排班
|
||||||
|
配色规则与背景类由 displayShifts 计算
|
||||||
|
-->
|
||||||
|
<div v-if="!isHoliday" class="mt-0.5 flex flex-col gap-px overflow-hidden">
|
||||||
|
<div
|
||||||
|
v-for="shift in displayShifts"
|
||||||
|
:key="shift.key"
|
||||||
|
class="block w-full truncate rounded-sm px-1 py-px text-[11px] leading-normal text-white"
|
||||||
|
:class="shift.bgClass"
|
||||||
|
>
|
||||||
|
{{ shift.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
/** 配色说明项:色块 + 文案 */
|
||||||
|
const legendItems: Array<{ color: string; label: string }> = [
|
||||||
|
{ color: 'bg-[#95d475]', label: '白班' },
|
||||||
|
{ color: 'bg-[#f0a020]', label: '中班(三班倒)' },
|
||||||
|
{ color: 'bg-[#909399]', label: '中班(两班倒)/ 夜班' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 px-1 py-2 text-xs"
|
||||||
|
>
|
||||||
|
<span class="shrink-0">配色说明:</span>
|
||||||
|
<span
|
||||||
|
v-for="item in legendItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-block h-2.5 w-2.5 shrink-0 rounded-sm"
|
||||||
|
:class="item.color"
|
||||||
|
></span>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<span class="inline-block h-2.5 w-2.5 shrink-0 rounded-sm bg-[#f56c6c] opacity-60"></span>
|
||||||
|
<span class="text-red-500">红色日期</span>
|
||||||
|
= 周末
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<Tag color="green" class="!m-0"> 休 </Tag>
|
||||||
|
= 节假日(不显示排班)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { MesCalCalendarApi } from '#/api/mes/cal/calendar';
|
||||||
|
|
||||||
|
import { Button, Calendar, Spin } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import CalendarDateCell from './calendar-date-cell.vue';
|
||||||
|
import CalendarLegend from './calendar-legend.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
calendarDayMap: Map<string, MesCalCalendarApi.CalendarDay>;
|
||||||
|
holidaySet: Set<string>;
|
||||||
|
loading?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const currentDate = defineModel<Dayjs>('currentDate', { required: true });
|
||||||
|
|
||||||
|
/** 切换到上月 */
|
||||||
|
function handlePrevMonth() {
|
||||||
|
currentDate.value = currentDate.value.subtract(1, 'month');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到下月 */
|
||||||
|
function handleNextMonth() {
|
||||||
|
currentDate.value = currentDate.value.add(1, 'month');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到今天 */
|
||||||
|
function handleToday() {
|
||||||
|
currentDate.value = dayjs();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CalendarLegend />
|
||||||
|
<Spin :spinning="loading" wrapper-class-name="block">
|
||||||
|
<div class="bg-card overflow-hidden rounded-md">
|
||||||
|
<Calendar v-model:value="currentDate" class="mes-calendar-panel">
|
||||||
|
<template #headerRender>
|
||||||
|
<div class="flex items-center justify-between p-3">
|
||||||
|
<div class="text-base font-medium">
|
||||||
|
{{ currentDate.format('YYYY 年 MM 月') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button @click="handlePrevMonth">上月</Button>
|
||||||
|
<Button @click="handleToday">今天</Button>
|
||||||
|
<Button @click="handleNextMonth">下月</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #dateFullCellRender="{ current: date }">
|
||||||
|
<div class="h-[110px] text-left">
|
||||||
|
<CalendarDateCell
|
||||||
|
v-if="date.isSame(currentDate, 'month')"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:day="date.format('YYYY-MM-DD')"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
/>
|
||||||
|
<div v-else class="text-muted-foreground/50 p-1 text-base">
|
||||||
|
{{ date.format('DD') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Calendar>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
仅保留访问 Ant Design Calendar 内部 DOM 的样式,
|
||||||
|
其余已通过 Tailwind 工具类实现
|
||||||
|
-->
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mes-calendar-panel {
|
||||||
|
:deep(.ant-picker-content) {
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
border-left: 1px solid hsl(var(--border));
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border-right: 1px solid hsl(var(--border));
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
border-right: 1px solid hsl(var(--border));
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-picker-cell) {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { MesCalCalendarApi } from '#/api/mes/cal/calendar';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { getCalendarList } from '#/api/mes/cal/calendar';
|
||||||
|
import { getHolidayList } from '#/api/mes/cal/holiday';
|
||||||
|
import { HolidayType } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排班日历通用 composable
|
||||||
|
*
|
||||||
|
* 封装日历组件中通用的响应式状态、假期加载、排班查询、月份切换逻辑
|
||||||
|
*/
|
||||||
|
export function useCalendar() {
|
||||||
|
const loading = ref(false);
|
||||||
|
const currentDate = ref<Dayjs>(dayjs());
|
||||||
|
const calendarDayMap = ref<Map<string, MesCalCalendarApi.CalendarDay>>(
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
const holidaySet = ref(new Set<string>());
|
||||||
|
|
||||||
|
/** 计算当前月份的起止时间 */
|
||||||
|
function getMonthRange() {
|
||||||
|
const startDay = currentDate.value
|
||||||
|
.startOf('month')
|
||||||
|
.format('YYYY-MM-DD 00:00:00');
|
||||||
|
const endDay = currentDate.value
|
||||||
|
.endOf('month')
|
||||||
|
.format('YYYY-MM-DD 23:59:59');
|
||||||
|
return { endDay, startDay };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节假日列表,按当前月份范围加载
|
||||||
|
*
|
||||||
|
* 失败(无权限 / 接口异常)时不抛出,仅清空当月假期标记,
|
||||||
|
* 避免阻断使用方的主数据初始化
|
||||||
|
*/
|
||||||
|
async function loadHolidays() {
|
||||||
|
const { endDay, startDay } = getMonthRange();
|
||||||
|
const days = new Set<string>();
|
||||||
|
try {
|
||||||
|
const list = await getHolidayList({ endDay, startDay });
|
||||||
|
for (const item of list || []) {
|
||||||
|
const day = item.day ? dayjs(item.day).format('YYYY-MM-DD') : '';
|
||||||
|
if (day && item.type === HolidayType.HOLIDAY) {
|
||||||
|
days.add(day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 没有 mes:cal-holiday:query 权限或接口异常时,仅忽略假期标记
|
||||||
|
}
|
||||||
|
holidaySet.value = days;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询排班日历,params 由调用方提供 queryType 相关参数 */
|
||||||
|
async function fetchCalendar(params: Record<string, any>) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { endDay, startDay } = getMonthRange();
|
||||||
|
const list = await getCalendarList({ ...params, endDay, startDay });
|
||||||
|
const map = new Map<string, MesCalCalendarApi.CalendarDay>();
|
||||||
|
for (const item of list || []) {
|
||||||
|
const day = item.day ? dayjs(item.day).format('YYYY-MM-DD') : '';
|
||||||
|
if (day) {
|
||||||
|
map.set(day, { ...item, day });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calendarDayMap.value = map;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,调用回调刷新数据 */
|
||||||
|
function watchMonth(callback: () => void) {
|
||||||
|
watch(
|
||||||
|
() => currentDate.value.format('YYYY-MM'),
|
||||||
|
() => {
|
||||||
|
void loadHolidays();
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Tabs } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import TeamView from './modules/team-view.vue';
|
||||||
|
import TypeView from './modules/type-view.vue';
|
||||||
|
import UserView from './modules/user-view.vue';
|
||||||
|
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
const activeTab = ref<string>('type');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert
|
||||||
|
title="【排班】排班计划、排班日历"
|
||||||
|
url="https://doc.iocoder.cn/mes/cal/calendar/"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="bg-card rounded-md p-3">
|
||||||
|
<Tabs v-model:active-key="activeTab" type="card">
|
||||||
|
<Tabs.TabPane key="type" tab="按分类" force-render>
|
||||||
|
<TypeView />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="team" tab="按班组" force-render>
|
||||||
|
<TeamView />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="user" tab="按个人" force-render>
|
||||||
|
<UserView />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MesCalTeamApi } from '#/api/mes/cal/team';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { getTeamList } from '#/api/mes/cal/team';
|
||||||
|
|
||||||
|
import CalendarPanel from '../components/calendar-panel.vue';
|
||||||
|
import { useCalendar } from '../components/use-calendar';
|
||||||
|
|
||||||
|
const {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
} = useCalendar();
|
||||||
|
|
||||||
|
const teamList = ref<MesCalTeamApi.Team[]>([]);
|
||||||
|
const selectedTeamId = ref<number>();
|
||||||
|
|
||||||
|
/** 查询当前月份的排班日历,按选中班组过滤 */
|
||||||
|
function doFetch() {
|
||||||
|
if (!selectedTeamId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchCalendar({ queryType: 'TEAM', teamId: selectedTeamId.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击左侧班组后切换并刷新日历 */
|
||||||
|
function onSelectTeam(id: number) {
|
||||||
|
selectedTeamId.value = id;
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,重新加载当月排班 */
|
||||||
|
watchMonth(() => {
|
||||||
|
if (selectedTeamId.value) {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 假期列表与班组列表并行加载,避免假期接口无权限或异常时阻断班组排班查询
|
||||||
|
void loadHolidays();
|
||||||
|
teamList.value = await getTeamList();
|
||||||
|
if (teamList.value.length > 0 && teamList.value[0]?.id) {
|
||||||
|
onSelectTeam(teamList.value[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<!-- 左侧:班组列表选择 -->
|
||||||
|
<div class="border-border mr-3 w-[150px] shrink-0 overflow-hidden rounded border">
|
||||||
|
<div
|
||||||
|
v-for="team in teamList"
|
||||||
|
:key="team.id"
|
||||||
|
class="text-foreground border-border hover:bg-muted/50 cursor-pointer border-b px-4 py-2.5 text-sm transition-colors last:border-b-0"
|
||||||
|
:class="
|
||||||
|
selectedTeamId === team.id
|
||||||
|
? 'bg-primary/10 text-primary font-medium'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="onSelectTeam(team.id!)"
|
||||||
|
>
|
||||||
|
{{ team.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:日历 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<CalendarPanel
|
||||||
|
v-model:current-date="currentDate"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DictDataType } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import CalendarPanel from '../components/calendar-panel.vue';
|
||||||
|
import { useCalendar } from '../components/use-calendar';
|
||||||
|
|
||||||
|
const {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
} = useCalendar();
|
||||||
|
|
||||||
|
const typeOptions = ref<DictDataType[]>([]);
|
||||||
|
const selectedType = ref<number>();
|
||||||
|
|
||||||
|
/** 查询当前月份的排班日历,按选中分类过滤 */
|
||||||
|
function doFetch() {
|
||||||
|
if (selectedType.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchCalendar({ calendarType: selectedType.value, queryType: 'TYPE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击左侧分类后切换并刷新日历 */
|
||||||
|
function onSelectType(value: number) {
|
||||||
|
selectedType.value = value;
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,重新加载当月排班 */
|
||||||
|
watchMonth(() => {
|
||||||
|
if (selectedType.value !== undefined) {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 假期列表与排班主数据并行加载,避免假期接口无权限或异常时阻断分类/排班加载
|
||||||
|
void loadHolidays();
|
||||||
|
typeOptions.value = getDictOptions(DICT_TYPE.MES_CAL_CALENDAR_TYPE, 'number');
|
||||||
|
if (typeOptions.value.length > 0) {
|
||||||
|
onSelectType(typeOptions.value[0]!.value as number);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<!-- 左侧:班组类型选择 -->
|
||||||
|
<div class="border-border mr-3 w-[150px] shrink-0 overflow-hidden rounded border">
|
||||||
|
<div
|
||||||
|
v-for="item in typeOptions"
|
||||||
|
:key="item.value as number"
|
||||||
|
class="text-foreground border-border hover:bg-muted/50 cursor-pointer border-b px-4 py-2.5 text-sm transition-colors last:border-b-0"
|
||||||
|
:class="
|
||||||
|
selectedType === item.value
|
||||||
|
? 'bg-primary/10 text-primary font-medium'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="onSelectType(item.value as number)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:日历 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<CalendarPanel
|
||||||
|
v-model:current-date="currentDate"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Button, Form, FormItem, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
|
||||||
|
import CalendarPanel from '../components/calendar-panel.vue';
|
||||||
|
import { useCalendar } from '../components/use-calendar';
|
||||||
|
|
||||||
|
const {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
} = useCalendar();
|
||||||
|
|
||||||
|
const userId = ref<number>();
|
||||||
|
const userOptions = ref<SystemUserApi.User[]>([]);
|
||||||
|
|
||||||
|
/** 查询当前月份的排班日历,按选中人员过滤 */
|
||||||
|
function doFetch() {
|
||||||
|
if (!userId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchCalendar({ queryType: 'USER', userId: userId.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询按钮 / 下拉选人后刷新日历 */
|
||||||
|
function onUserQuery() {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,重新加载当月排班 */
|
||||||
|
watchMonth(() => {
|
||||||
|
if (userId.value) {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 假期列表与人员列表并行加载,避免假期接口无权限或异常时阻断人员排班查询
|
||||||
|
void loadHolidays();
|
||||||
|
userOptions.value = await getSimpleUserList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 顶部:人员选择 -->
|
||||||
|
<Form layout="inline" class="mb-2.5">
|
||||||
|
<FormItem label="人员">
|
||||||
|
<Select
|
||||||
|
v-model:value="userId"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
placeholder="请输入人员姓名搜索"
|
||||||
|
class="!w-[200px]"
|
||||||
|
:options="userOptions"
|
||||||
|
:field-names="{ label: 'nickname', value: 'id' }"
|
||||||
|
:filter-option="
|
||||||
|
(input: string, option: any) =>
|
||||||
|
(option?.nickname ?? '').includes(input)
|
||||||
|
"
|
||||||
|
@change="onUserQuery"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Button type="primary" @click="onUserQuery">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="lucide:search" />
|
||||||
|
</template>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<!-- 日历 -->
|
||||||
|
<CalendarPanel
|
||||||
|
v-model:current-date="currentDate"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MesCalCalendarApi } from '#/api/mes/cal/calendar';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ElTag } from 'element-plus';
|
||||||
|
import { SolarDay } from 'tyme4ts';
|
||||||
|
|
||||||
|
import { MesCalShiftTypeEnum } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
calendarDayMap: Map<string, MesCalCalendarApi.CalendarDay>; // 排班数据
|
||||||
|
day: string; // 日期,格式 yyyy-MM-dd
|
||||||
|
holidaySet: Set<string>; // 节假日集合
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dayNumber = computed(() => props.day.split('-')[2] || '');
|
||||||
|
const isHoliday = computed(() => props.holidaySet.has(props.day));
|
||||||
|
const isWeekend = computed(() => {
|
||||||
|
const weekday = dayjs(props.day).day();
|
||||||
|
return weekday === 0 || weekday === 6; // 0=周日,6=周六
|
||||||
|
});
|
||||||
|
|
||||||
|
const calDay = computed(() => props.calendarDayMap.get(props.day));
|
||||||
|
const teamShifts = computed(() => calDay.value?.teamShifts || []);
|
||||||
|
const shiftType = computed(() => calDay.value?.shiftType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班次标签展示数据:根据 sort 与 shiftType 推导背景色
|
||||||
|
*
|
||||||
|
* 配色规则(sort 对应轮班方式中的班次顺序):
|
||||||
|
* sort=1 白班 → 绿色(#95d475)
|
||||||
|
* sort=2 中班 → 三班倒用橙色(#f0a020),两班倒用灰色(#909399)
|
||||||
|
* sort=3 夜班 → 灰色(#909399)
|
||||||
|
*/
|
||||||
|
const displayShifts = computed(() => {
|
||||||
|
const isThreeShift = shiftType.value === MesCalShiftTypeEnum.THREE;
|
||||||
|
const colorMap: Record<number, string> = {
|
||||||
|
1: 'bg-[#95d475]',
|
||||||
|
2: isThreeShift ? 'bg-[#f0a020]' : 'bg-[#909399]',
|
||||||
|
3: 'bg-[#909399]',
|
||||||
|
};
|
||||||
|
return teamShifts.value
|
||||||
|
.map((item) => {
|
||||||
|
const bgClass = colorMap[item.sort ?? -1];
|
||||||
|
if (!bgClass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
bgClass,
|
||||||
|
key: `${item.teamId}-${item.shiftId}`,
|
||||||
|
label: `${item.shiftName} · ${item.teamName}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((v): v is { bgClass: string; key: string; label: string } => !!v);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 解析当天的农历、节气、节日信息 */
|
||||||
|
const lunarInfo = computed(() => {
|
||||||
|
const [year, month, date] = props.day.split('-').map(Number);
|
||||||
|
try {
|
||||||
|
const solarDay = SolarDay.fromYmd(year!, month!, date!);
|
||||||
|
const lunarDay = solarDay.getLunarDay();
|
||||||
|
const solarFestival = solarDay.getFestival();
|
||||||
|
const lunarFestival = lunarDay.getFestival();
|
||||||
|
const termDay = solarDay.getTermDay();
|
||||||
|
const termName =
|
||||||
|
termDay.getDayIndex() === 0 ? termDay.getSolarTerm().getName() : '';
|
||||||
|
const lunarMonthName = lunarDay.getLunarMonth().getName();
|
||||||
|
const lunarDayName = lunarDay.getName();
|
||||||
|
return {
|
||||||
|
lunarFestival: lunarFestival ? lunarFestival.getName() : '',
|
||||||
|
lunarText: lunarMonthName + lunarDayName,
|
||||||
|
solarFestival: solarFestival ? solarFestival.getName() : '',
|
||||||
|
termName,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
lunarFestival: '',
|
||||||
|
lunarText: '',
|
||||||
|
solarFestival: '',
|
||||||
|
termName: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const lunarDisplay = computed(() => {
|
||||||
|
const info = lunarInfo.value;
|
||||||
|
return (
|
||||||
|
info.solarFestival || info.lunarFestival || info.termName || info.lunarText
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasFestivalDay = computed(() => {
|
||||||
|
const info = lunarInfo.value;
|
||||||
|
return Boolean(info.solarFestival || info.lunarFestival || info.termName);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col overflow-hidden p-1">
|
||||||
|
<!-- 顶部:日期数字 + 上班/休息标签 -->
|
||||||
|
<div class="flex shrink-0 items-center justify-between">
|
||||||
|
<span class="text-base font-medium" :class="{ 'text-red-500': isWeekend }">
|
||||||
|
{{ dayNumber }}
|
||||||
|
</span>
|
||||||
|
<ElTag v-if="isHoliday" effect="dark" size="small" type="success">
|
||||||
|
休
|
||||||
|
</ElTag>
|
||||||
|
<ElTag v-else effect="dark" size="small">班</ElTag>
|
||||||
|
</div>
|
||||||
|
<!-- 农历 / 节气 / 节日显示 -->
|
||||||
|
<div
|
||||||
|
class="mt-0.5 shrink-0 truncate text-[11px]"
|
||||||
|
:class="hasFestivalDay ? 'text-green-600' : 'text-muted-foreground'"
|
||||||
|
>
|
||||||
|
{{ lunarDisplay }}
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
班次列表:节假日不显示排班
|
||||||
|
配色规则与背景类由 displayShifts 计算
|
||||||
|
-->
|
||||||
|
<div v-if="!isHoliday" class="mt-0.5 flex flex-col gap-px overflow-hidden">
|
||||||
|
<div
|
||||||
|
v-for="shift in displayShifts"
|
||||||
|
:key="shift.key"
|
||||||
|
class="block w-full truncate rounded-sm px-1 py-px text-[11px] leading-normal text-white"
|
||||||
|
:class="shift.bgClass"
|
||||||
|
>
|
||||||
|
{{ shift.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ElTag } from 'element-plus';
|
||||||
|
|
||||||
|
/** 配色说明项:色块 + 文案 */
|
||||||
|
const legendItems: Array<{ color: string; label: string }> = [
|
||||||
|
{ color: 'bg-[#95d475]', label: '白班' },
|
||||||
|
{ color: 'bg-[#f0a020]', label: '中班(三班倒)' },
|
||||||
|
{ color: 'bg-[#909399]', label: '中班(两班倒)/ 夜班' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 px-1 py-2 text-xs"
|
||||||
|
>
|
||||||
|
<span class="shrink-0">配色说明:</span>
|
||||||
|
<span
|
||||||
|
v-for="item in legendItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-block h-2.5 w-2.5 shrink-0 rounded-sm"
|
||||||
|
:class="item.color"
|
||||||
|
></span>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<span class="inline-block h-2.5 w-2.5 shrink-0 rounded-sm bg-[#f56c6c] opacity-60"></span>
|
||||||
|
<span class="text-red-500">红色日期</span>
|
||||||
|
= 周末
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<ElTag effect="dark" size="small" type="success"> 休 </ElTag>
|
||||||
|
= 节假日(不显示排班)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { MesCalCalendarApi } from '#/api/mes/cal/calendar';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ElCalendar } from 'element-plus';
|
||||||
|
|
||||||
|
import CalendarDateCell from './calendar-date-cell.vue';
|
||||||
|
import CalendarLegend from './calendar-legend.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
calendarDayMap: Map<string, MesCalCalendarApi.CalendarDay>;
|
||||||
|
holidaySet: Set<string>;
|
||||||
|
loading?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const currentDate = defineModel<Dayjs>('currentDate', { required: true });
|
||||||
|
|
||||||
|
/** ElCalendar v-model 桥接:内部用 Dayjs,组件需要 Date */
|
||||||
|
const calendarValue = computed({
|
||||||
|
get: () => currentDate.value.toDate(),
|
||||||
|
set: (val: Date) => {
|
||||||
|
currentDate.value = dayjs(val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CalendarLegend />
|
||||||
|
<div v-loading="loading" class="bg-card overflow-hidden rounded-md">
|
||||||
|
<ElCalendar v-model="calendarValue" class="mes-calendar-panel">
|
||||||
|
<template #date-cell="{ data }">
|
||||||
|
<CalendarDateCell
|
||||||
|
v-if="data.type === 'current-month'"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:day="data.day"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
/>
|
||||||
|
<div v-else class="text-muted-foreground/50 p-1 text-base">
|
||||||
|
{{ data.day.split('-')[2] }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElCalendar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 收紧 ElCalendar 默认日期单元高度,使用自定义单元 */
|
||||||
|
.mes-calendar-panel {
|
||||||
|
:deep(.el-calendar-table .el-calendar-day) {
|
||||||
|
height: 110px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { MesCalCalendarApi } from '#/api/mes/cal/calendar';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { getCalendarList } from '#/api/mes/cal/calendar';
|
||||||
|
import { getHolidayList } from '#/api/mes/cal/holiday';
|
||||||
|
import { HolidayType } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排班日历通用 composable
|
||||||
|
*
|
||||||
|
* 封装日历组件中通用的响应式状态、假期加载、排班查询、月份切换逻辑
|
||||||
|
*/
|
||||||
|
export function useCalendar() {
|
||||||
|
const loading = ref(false);
|
||||||
|
const currentDate = ref<Dayjs>(dayjs());
|
||||||
|
const calendarDayMap = ref<Map<string, MesCalCalendarApi.CalendarDay>>(
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
const holidaySet = ref(new Set<string>());
|
||||||
|
|
||||||
|
/** 计算当前月份的起止时间 */
|
||||||
|
function getMonthRange() {
|
||||||
|
const startDay = currentDate.value
|
||||||
|
.startOf('month')
|
||||||
|
.format('YYYY-MM-DD 00:00:00');
|
||||||
|
const endDay = currentDate.value
|
||||||
|
.endOf('month')
|
||||||
|
.format('YYYY-MM-DD 23:59:59');
|
||||||
|
return { endDay, startDay };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节假日列表,按当前月份范围加载
|
||||||
|
*
|
||||||
|
* 失败(无权限 / 接口异常)时不抛出,仅清空当月假期标记,
|
||||||
|
* 避免阻断使用方的主数据初始化
|
||||||
|
*/
|
||||||
|
async function loadHolidays() {
|
||||||
|
const { endDay, startDay } = getMonthRange();
|
||||||
|
const days = new Set<string>();
|
||||||
|
try {
|
||||||
|
const list = await getHolidayList({ endDay, startDay });
|
||||||
|
for (const item of list || []) {
|
||||||
|
const day = item.day ? dayjs(item.day).format('YYYY-MM-DD') : '';
|
||||||
|
if (day && item.type === HolidayType.HOLIDAY) {
|
||||||
|
days.add(day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 没有 mes:cal-holiday:query 权限或接口异常时,仅忽略假期标记
|
||||||
|
}
|
||||||
|
holidaySet.value = days;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询排班日历,params 由调用方提供 queryType 相关参数 */
|
||||||
|
async function fetchCalendar(params: Record<string, any>) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { endDay, startDay } = getMonthRange();
|
||||||
|
const list = await getCalendarList({ ...params, endDay, startDay });
|
||||||
|
const map = new Map<string, MesCalCalendarApi.CalendarDay>();
|
||||||
|
for (const item of list || []) {
|
||||||
|
const day = item.day ? dayjs(item.day).format('YYYY-MM-DD') : '';
|
||||||
|
if (day) {
|
||||||
|
map.set(day, { ...item, day });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calendarDayMap.value = map;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,调用回调刷新数据 */
|
||||||
|
function watchMonth(callback: () => void) {
|
||||||
|
watch(
|
||||||
|
() => currentDate.value.format('YYYY-MM'),
|
||||||
|
() => {
|
||||||
|
void loadHolidays();
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ElTabPane, ElTabs } from 'element-plus';
|
||||||
|
|
||||||
|
import TeamView from './modules/team-view.vue';
|
||||||
|
import TypeView from './modules/type-view.vue';
|
||||||
|
import UserView from './modules/user-view.vue';
|
||||||
|
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
const activeTab = ref<string>('type');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert
|
||||||
|
title="【排班】排班计划、排班日历"
|
||||||
|
url="https://doc.iocoder.cn/mes/cal/calendar/"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="bg-card rounded-md p-3">
|
||||||
|
<ElTabs v-model="activeTab" type="border-card">
|
||||||
|
<ElTabPane label="按分类" name="type" :force-render="true">
|
||||||
|
<TypeView />
|
||||||
|
</ElTabPane>
|
||||||
|
<ElTabPane label="按班组" name="team" :force-render="true">
|
||||||
|
<TeamView />
|
||||||
|
</ElTabPane>
|
||||||
|
<ElTabPane label="按个人" name="user" :force-render="true">
|
||||||
|
<UserView />
|
||||||
|
</ElTabPane>
|
||||||
|
</ElTabs>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MesCalTeamApi } from '#/api/mes/cal/team';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { getTeamList } from '#/api/mes/cal/team';
|
||||||
|
|
||||||
|
import CalendarPanel from '../components/calendar-panel.vue';
|
||||||
|
import { useCalendar } from '../components/use-calendar';
|
||||||
|
|
||||||
|
const {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
} = useCalendar();
|
||||||
|
|
||||||
|
const teamList = ref<MesCalTeamApi.Team[]>([]);
|
||||||
|
const selectedTeamId = ref<number>();
|
||||||
|
|
||||||
|
/** 查询当前月份的排班日历,按选中班组过滤 */
|
||||||
|
function doFetch() {
|
||||||
|
if (!selectedTeamId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchCalendar({ queryType: 'TEAM', teamId: selectedTeamId.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击左侧班组后切换并刷新日历 */
|
||||||
|
function onSelectTeam(id: number) {
|
||||||
|
selectedTeamId.value = id;
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,重新加载当月排班 */
|
||||||
|
watchMonth(() => {
|
||||||
|
if (selectedTeamId.value) {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 假期列表与班组列表并行加载,避免假期接口无权限或异常时阻断班组排班查询
|
||||||
|
void loadHolidays();
|
||||||
|
teamList.value = await getTeamList();
|
||||||
|
if (teamList.value.length > 0 && teamList.value[0]?.id) {
|
||||||
|
onSelectTeam(teamList.value[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<!-- 左侧:班组列表选择 -->
|
||||||
|
<div class="border-border mr-3 w-[150px] shrink-0 overflow-hidden rounded border">
|
||||||
|
<div
|
||||||
|
v-for="team in teamList"
|
||||||
|
:key="team.id"
|
||||||
|
class="text-foreground border-border hover:bg-muted/50 cursor-pointer border-b px-4 py-2.5 text-sm transition-colors last:border-b-0"
|
||||||
|
:class="
|
||||||
|
selectedTeamId === team.id
|
||||||
|
? 'bg-primary/10 text-primary font-medium'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="onSelectTeam(team.id!)"
|
||||||
|
>
|
||||||
|
{{ team.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:日历 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<CalendarPanel
|
||||||
|
v-model:current-date="currentDate"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DictDataType } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import CalendarPanel from '../components/calendar-panel.vue';
|
||||||
|
import { useCalendar } from '../components/use-calendar';
|
||||||
|
|
||||||
|
const {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
} = useCalendar();
|
||||||
|
|
||||||
|
const typeOptions = ref<DictDataType[]>([]);
|
||||||
|
const selectedType = ref<number>();
|
||||||
|
|
||||||
|
/** 查询当前月份的排班日历,按选中分类过滤 */
|
||||||
|
function doFetch() {
|
||||||
|
if (selectedType.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchCalendar({ calendarType: selectedType.value, queryType: 'TYPE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击左侧分类后切换并刷新日历 */
|
||||||
|
function onSelectType(value: number) {
|
||||||
|
selectedType.value = value;
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,重新加载当月排班 */
|
||||||
|
watchMonth(() => {
|
||||||
|
if (selectedType.value !== undefined) {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 假期列表与排班主数据并行加载,避免假期接口无权限或异常时阻断分类/排班加载
|
||||||
|
void loadHolidays();
|
||||||
|
typeOptions.value = getDictOptions(DICT_TYPE.MES_CAL_CALENDAR_TYPE, 'number');
|
||||||
|
if (typeOptions.value.length > 0) {
|
||||||
|
onSelectType(typeOptions.value[0]!.value as number);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<!-- 左侧:班组类型选择 -->
|
||||||
|
<div class="border-border mr-3 w-[150px] shrink-0 overflow-hidden rounded border">
|
||||||
|
<div
|
||||||
|
v-for="item in typeOptions"
|
||||||
|
:key="item.value as number"
|
||||||
|
class="text-foreground border-border hover:bg-muted/50 cursor-pointer border-b px-4 py-2.5 text-sm transition-colors last:border-b-0"
|
||||||
|
:class="
|
||||||
|
selectedType === item.value
|
||||||
|
? 'bg-primary/10 text-primary font-medium'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="onSelectType(item.value as number)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:日历 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<CalendarPanel
|
||||||
|
v-model:current-date="currentDate"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ElButton,
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
|
||||||
|
import CalendarPanel from '../components/calendar-panel.vue';
|
||||||
|
import { useCalendar } from '../components/use-calendar';
|
||||||
|
|
||||||
|
const {
|
||||||
|
calendarDayMap,
|
||||||
|
currentDate,
|
||||||
|
fetchCalendar,
|
||||||
|
holidaySet,
|
||||||
|
loadHolidays,
|
||||||
|
loading,
|
||||||
|
watchMonth,
|
||||||
|
} = useCalendar();
|
||||||
|
|
||||||
|
const userId = ref<number>();
|
||||||
|
const userOptions = ref<SystemUserApi.User[]>([]);
|
||||||
|
|
||||||
|
/** 查询当前月份的排班日历,按选中人员过滤 */
|
||||||
|
function doFetch() {
|
||||||
|
if (!userId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchCalendar({ queryType: 'USER', userId: userId.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询按钮 / 下拉选人后刷新日历 */
|
||||||
|
function onUserQuery() {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听月份切换,重新加载当月排班 */
|
||||||
|
watchMonth(() => {
|
||||||
|
if (userId.value) {
|
||||||
|
doFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 假期列表与人员列表并行加载,避免假期接口无权限或异常时阻断人员排班查询
|
||||||
|
void loadHolidays();
|
||||||
|
userOptions.value = await getSimpleUserList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 顶部:人员选择 -->
|
||||||
|
<ElForm :inline="true" class="mb-2.5">
|
||||||
|
<ElFormItem label="人员">
|
||||||
|
<ElSelect
|
||||||
|
v-model="userId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请输入人员姓名搜索"
|
||||||
|
class="!w-[200px]"
|
||||||
|
@change="onUserQuery"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="user in userOptions"
|
||||||
|
:key="user.id"
|
||||||
|
:label="user.nickname"
|
||||||
|
:value="user.id!"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem>
|
||||||
|
<ElButton type="primary" @click="onUserQuery">
|
||||||
|
<IconifyIcon icon="lucide:search" class="mr-1" />
|
||||||
|
查询
|
||||||
|
</ElButton>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<!-- 日历 -->
|
||||||
|
<CalendarPanel
|
||||||
|
v-model:current-date="currentDate"
|
||||||
|
:calendar-day-map="calendarDayMap"
|
||||||
|
:holiday-set="holidaySet"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue