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