feat(mes):完成 cal plan 【排班计划】的迁移
parent
2bcd81dc94
commit
53a00f6e15
|
|
@ -0,0 +1,280 @@
|
||||||
|
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MesCalPlanApi } from '#/api/mes/cal/plan';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { generateAutoCode } from '#/api/mes/md/autocode/record';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
import {
|
||||||
|
MesAutoCodeRuleCode,
|
||||||
|
MesCalPlanStatusEnum,
|
||||||
|
MesCalShiftMethodEnum,
|
||||||
|
MesCalShiftTypeEnum,
|
||||||
|
} from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
/** 新增/修改排班计划的表单 */
|
||||||
|
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
defaultValue: MesCalPlanStatusEnum.PREPARE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '计划编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入计划编码',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
suffix: () =>
|
||||||
|
h(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
type: 'default',
|
||||||
|
onClick: async () => {
|
||||||
|
try {
|
||||||
|
const code = await generateAutoCode(MesAutoCodeRuleCode.CAL_PLAN_CODE);
|
||||||
|
await formApi?.setFieldValue('code', code);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ default: () => '生成' },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '计划名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入计划名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'calendarType',
|
||||||
|
label: '班组类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.MES_CAL_CALENDAR_TYPE, 'number'),
|
||||||
|
placeholder: '请选择班组类型',
|
||||||
|
},
|
||||||
|
rules: 'selectRequired',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'startDate',
|
||||||
|
label: '开始日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
placeholder: '请选择开始日期',
|
||||||
|
valueFormat: 'x',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'endDate',
|
||||||
|
label: '结束日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
placeholder: '请选择结束日期',
|
||||||
|
valueFormat: 'x',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'shiftType',
|
||||||
|
label: '轮班方式',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.MES_CAL_SHIFT_TYPE, 'number'),
|
||||||
|
placeholder: '请选择轮班方式',
|
||||||
|
},
|
||||||
|
rules: 'selectRequired',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'shiftMethod',
|
||||||
|
label: '倒班方式',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.MES_CAL_SHIFT_METHOD, 'number'),
|
||||||
|
placeholder: '请选择倒班方式',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['shiftType'],
|
||||||
|
show: (values) => !!values.shiftType && values.shiftType !== MesCalShiftTypeEnum.SINGLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'shiftCount',
|
||||||
|
label: '倒班天数',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
class: '!w-full',
|
||||||
|
min: 1,
|
||||||
|
precision: 0,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['shiftMethod'],
|
||||||
|
show: (values) => values.shiftMethod === MesCalShiftMethodEnum.DAY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
formItemClass: 'col-span-3',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '计划编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '请输入计划编码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '计划名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '请输入计划名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'startDate',
|
||||||
|
label: '开始日期',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'endDate',
|
||||||
|
label: '结束日期',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'shiftType',
|
||||||
|
label: '轮班方式',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.MES_CAL_SHIFT_TYPE, 'number'),
|
||||||
|
placeholder: '请选择轮班方式',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.MES_CAL_PLAN_STATUS, 'number'),
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<MesCalPlanApi.Plan>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
title: '计划编码',
|
||||||
|
minWidth: 140,
|
||||||
|
slots: {
|
||||||
|
default: 'code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'name', title: '计划名称', minWidth: 150 },
|
||||||
|
{
|
||||||
|
field: 'calendarType',
|
||||||
|
title: '班组类型',
|
||||||
|
width: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.MES_CAL_CALENDAR_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'startDate', title: '开始日期', width: 150, formatter: 'formatDate' },
|
||||||
|
{ field: 'endDate', title: '结束日期', width: 150, formatter: 'formatDate' },
|
||||||
|
{
|
||||||
|
field: 'shiftType',
|
||||||
|
title: '轮班方式',
|
||||||
|
width: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.MES_CAL_SHIFT_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'shiftMethod',
|
||||||
|
title: '倒班方式',
|
||||||
|
width: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.MES_CAL_SHIFT_METHOD },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '单据状态',
|
||||||
|
width: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.MES_CAL_PLAN_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'createTime', title: '创建时间', width: 180, formatter: 'formatDateTime' },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 160,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: {
|
||||||
|
default: 'actions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MesCalPlanApi } from '#/api/mes/cal/plan';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { deletePlan, exportPlan, getPlanPage } from '#/api/mes/cal/plan';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { MesCalPlanStatusEnum } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建排班计划 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData({ type: 'create' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看排班计划 */
|
||||||
|
function handleDetail(row: MesCalPlanApi.Plan) {
|
||||||
|
formModalApi.setData({ id: row.id, type: 'detail' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑排班计划 */
|
||||||
|
function handleEdit(row: MesCalPlanApi.Plan) {
|
||||||
|
formModalApi.setData({ id: row.id, type: 'update' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除排班计划 */
|
||||||
|
async function handleDelete(row: MesCalPlanApi.Plan) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deletePlan(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出排班计划 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportPlan(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '排班计划.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
await getPlanPage({ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<MesCalPlanApi.Plan>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="排班计划列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['排班计划']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['mes:cal-plan:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.export'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
auth: ['mes:cal-plan:export'],
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #code="{ row }">
|
||||||
|
<Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
v-if="row.status === MesCalPlanStatusEnum.PREPARE"
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['mes:cal-plan:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['mes:cal-plan:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MesCalPlanApi } from '#/api/mes/cal/plan';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message, Popconfirm, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { confirmPlan, createPlan, getPlan, updatePlan } from '#/api/mes/cal/plan';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { MesCalPlanStatusEnum } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
import ShiftList from './shift-list.vue';
|
||||||
|
import PlanTeamList from './team-list.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formMode = ref<'create' | 'detail' | 'update'>('create'); // 表单模式
|
||||||
|
const subTabsName = ref('shift'); // 当前资源页签
|
||||||
|
const formData = ref<MesCalPlanApi.Plan>();
|
||||||
|
const isDetail = computed(() => formMode.value === 'detail'); // 是否查看模式
|
||||||
|
const canConfirm = computed(
|
||||||
|
() => formMode.value === 'update' && formData.value?.status === MesCalPlanStatusEnum.PREPARE,
|
||||||
|
); // 是否可确认计划
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
if (formMode.value === 'detail') {
|
||||||
|
return $t('ui.actionTitle.view', ['排班计划']);
|
||||||
|
}
|
||||||
|
return formMode.value === 'update'
|
||||||
|
? $t('ui.actionTitle.edit', ['排班计划'])
|
||||||
|
: $t('ui.actionTitle.create', ['排班计划']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-1',
|
||||||
|
labelWidth: 100,
|
||||||
|
},
|
||||||
|
wrapperClass: 'grid-cols-3',
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 表单 schema 需要 formApi 引用,所以通过 setState 设置 schema */
|
||||||
|
formApi.setState({ schema: useFormSchema(formApi) });
|
||||||
|
|
||||||
|
/** 确认排班计划 */
|
||||||
|
async function handleConfirmPlan() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid || !formData.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const data = (await formApi.getValues()) as MesCalPlanApi.Plan;
|
||||||
|
await updatePlan(data);
|
||||||
|
await confirmPlan(formData.value.id);
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success('确认成功');
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
if (isDetail.value) {
|
||||||
|
await modalApi.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as MesCalPlanApi.Plan;
|
||||||
|
try {
|
||||||
|
if (formMode.value === 'create') {
|
||||||
|
const id = await createPlan(data);
|
||||||
|
formData.value = { ...data, id: id as number, status: MesCalPlanStatusEnum.PREPARE };
|
||||||
|
await formApi.setFieldValue('id', id);
|
||||||
|
formMode.value = 'update';
|
||||||
|
} else {
|
||||||
|
await updatePlan(data);
|
||||||
|
formData.value = { ...formData.value, ...data };
|
||||||
|
}
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await formApi.resetForm();
|
||||||
|
subTabsName.value = 'shift';
|
||||||
|
const data = modalApi.getData<{ id?: number; type?: 'create' | 'detail' | 'update' }>();
|
||||||
|
formMode.value = data?.type || 'create';
|
||||||
|
formApi.setDisabled(formMode.value === 'detail');
|
||||||
|
modalApi.setState({ showConfirmButton: formMode.value !== 'detail' });
|
||||||
|
if (!data?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getPlan(data.id);
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-4/5">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
<Tabs
|
||||||
|
v-if="formMode !== 'create' && formData?.id"
|
||||||
|
v-model:active-key="subTabsName"
|
||||||
|
class="mx-4 mt-4"
|
||||||
|
>
|
||||||
|
<Tabs.TabPane key="shift" tab="班次">
|
||||||
|
<ShiftList :form-type="formMode" :plan-id="formData.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="team" tab="班组">
|
||||||
|
<PlanTeamList :form-type="formMode" :plan-id="formData.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
<template #prepend-footer>
|
||||||
|
<div class="flex flex-auto items-center">
|
||||||
|
<Popconfirm
|
||||||
|
v-if="canConfirm"
|
||||||
|
title="确认该排班计划?确认后将不可修改或删除。"
|
||||||
|
@confirm="handleConfirmPlan"
|
||||||
|
>
|
||||||
|
<Button type="primary">确认计划</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MesCalPlanShiftApi } from '#/api/mes/cal/plan/shift';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm, z } from '#/adapter/form';
|
||||||
|
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
createPlanShift,
|
||||||
|
deletePlanShift,
|
||||||
|
getPlanShiftListByPlan,
|
||||||
|
updatePlanShift,
|
||||||
|
} from '#/api/mes/cal/plan/shift';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ formType?: string; planId: number }>(), {
|
||||||
|
formType: 'update',
|
||||||
|
});
|
||||||
|
const isEditable = computed(() => props.formType !== 'detail'); // 是否可编辑
|
||||||
|
const formOpen = ref(false); // 班次表单是否打开
|
||||||
|
const formLoading = ref(false); // 班次表单提交中
|
||||||
|
const shiftFormType = ref<'create' | 'update'>('create'); // 班次表单模式
|
||||||
|
const formTitle = computed(() => (shiftFormType.value === 'create' ? '添加班次' : '修改班次'));
|
||||||
|
const list = ref<MesCalPlanShiftApi.PlanShift[]>([]); // 班次列表
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'planId',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sort',
|
||||||
|
label: '顺序',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
class: '!w-full',
|
||||||
|
min: 1,
|
||||||
|
precision: 0,
|
||||||
|
},
|
||||||
|
rules: z.number().default(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '班次名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入班次名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'startTime',
|
||||||
|
label: '开始时间',
|
||||||
|
component: 'TimePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'HH:mm',
|
||||||
|
placeholder: '请选择开始时间',
|
||||||
|
valueFormat: 'HH:mm',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'endTime',
|
||||||
|
label: '结束时间',
|
||||||
|
component: 'TimePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'HH:mm',
|
||||||
|
placeholder: '请选择结束时间',
|
||||||
|
valueFormat: 'HH:mm',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
autoResize: true,
|
||||||
|
border: true,
|
||||||
|
columns: [
|
||||||
|
{ field: 'sort', title: '顺序', width: 80 },
|
||||||
|
{ field: 'name', title: '班次名称', minWidth: 120 },
|
||||||
|
{ field: 'startTime', title: '开始时间', width: 100 },
|
||||||
|
{ field: 'endTime', title: '结束时间', width: 100 },
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 150 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 130,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: {
|
||||||
|
default: 'actions',
|
||||||
|
},
|
||||||
|
visible: isEditable.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: list.value,
|
||||||
|
minHeight: 240,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
isHover: true,
|
||||||
|
keyField: 'id',
|
||||||
|
},
|
||||||
|
showOverflow: true,
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<MesCalPlanShiftApi.PlanShift>,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 加载班次列表 */
|
||||||
|
async function getList() {
|
||||||
|
gridApi.setLoading(true);
|
||||||
|
try {
|
||||||
|
list.value = await getPlanShiftListByPlan(props.planId);
|
||||||
|
gridApi.setGridOptions({ data: list.value });
|
||||||
|
} finally {
|
||||||
|
gridApi.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开班次表单 */
|
||||||
|
async function openForm(type: 'create' | 'update', row?: MesCalPlanShiftApi.PlanShift) {
|
||||||
|
formOpen.value = true;
|
||||||
|
shiftFormType.value = type;
|
||||||
|
await formApi.resetForm();
|
||||||
|
await formApi.setValues(row ? { ...row } : { planId: props.planId, sort: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交班次表单 */
|
||||||
|
async function submitForm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = (await formApi.getValues()) as MesCalPlanShiftApi.PlanShift;
|
||||||
|
await (shiftFormType.value === 'create' ? createPlanShift(data) : updatePlanShift(data));
|
||||||
|
formOpen.value = false;
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
await getList();
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除班次 */
|
||||||
|
async function handleDelete(id: number) {
|
||||||
|
await deletePlanShift(id);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', ['班次']));
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.planId,
|
||||||
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="isEditable" class="mb-3 flex items-center justify-start">
|
||||||
|
<TableAction
|
||||||
|
:actions="[{ label: '添加班次', type: 'primary', onClick: openForm.bind(null, 'create') }]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Grid class="w-full">
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '编辑', type: 'link', onClick: openForm.bind(null, 'update', row) },
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除该班次吗?',
|
||||||
|
confirm: handleDelete.bind(null, row.id!),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
<Modal
|
||||||
|
v-model:open="formOpen"
|
||||||
|
:title="formTitle"
|
||||||
|
width="520px"
|
||||||
|
:confirm-loading="formLoading"
|
||||||
|
@ok="submitForm"
|
||||||
|
>
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MesCalPlanTeamApi } from '#/api/mes/cal/plan/team';
|
||||||
|
import type { MesCalTeamApi } from '#/api/mes/cal/team';
|
||||||
|
import type { MesCalTeamMemberApi } from '#/api/mes/cal/team/member';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Card, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { createPlanTeam, deletePlanTeam, getPlanTeamListByPlan } from '#/api/mes/cal/plan/team';
|
||||||
|
import { getTeamMemberListByTeam } from '#/api/mes/cal/team/member';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { CalTeamSelectDialog } from '#/views/mes/cal/team/components';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ formType?: string; planId: number }>(), {
|
||||||
|
formType: 'update',
|
||||||
|
});
|
||||||
|
const isEditable = computed(() => props.formType !== 'detail'); // 是否可编辑
|
||||||
|
const list = ref<MesCalPlanTeamApi.PlanTeam[]>([]); // 计划班组列表
|
||||||
|
const memberList = ref<MesCalTeamMemberApi.TeamMember[]>([]); // 班组成员列表
|
||||||
|
const selectedTeamId = ref<number>(); // 选中班组编号
|
||||||
|
const selectedTeamName = ref(''); // 选中班组名称
|
||||||
|
const teamDialogRef = ref<InstanceType<typeof CalTeamSelectDialog>>(); // 班组选择弹窗
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
autoResize: true,
|
||||||
|
border: true,
|
||||||
|
columns: [
|
||||||
|
{ field: 'teamId', title: '班组编号', width: 100 },
|
||||||
|
{ field: 'teamCode', title: '班组编码', minWidth: 120 },
|
||||||
|
{ field: 'teamName', title: '班组名称', minWidth: 120 },
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 150 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 90,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: {
|
||||||
|
default: 'actions',
|
||||||
|
},
|
||||||
|
visible: isEditable.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: list.value,
|
||||||
|
minHeight: 260,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
isCurrent: true,
|
||||||
|
isHover: true,
|
||||||
|
keyField: 'id',
|
||||||
|
},
|
||||||
|
showOverflow: true,
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<MesCalPlanTeamApi.PlanTeam>,
|
||||||
|
gridEvents: {
|
||||||
|
cellClick: ({ row }: { row: MesCalPlanTeamApi.PlanTeam }) => handleTeamSelect(row),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [MemberGrid, memberGridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
autoResize: true,
|
||||||
|
border: true,
|
||||||
|
columns: [
|
||||||
|
{ field: 'nickname', title: '用户昵称', minWidth: 100 },
|
||||||
|
{ field: 'telephone', title: '手机号', minWidth: 120 },
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 120 },
|
||||||
|
],
|
||||||
|
data: memberList.value,
|
||||||
|
minHeight: 260,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
isHover: true,
|
||||||
|
keyField: 'id',
|
||||||
|
},
|
||||||
|
showOverflow: true,
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<MesCalTeamMemberApi.TeamMember>,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 加载计划班组列表 */
|
||||||
|
async function getList() {
|
||||||
|
gridApi.setLoading(true);
|
||||||
|
try {
|
||||||
|
list.value = await getPlanTeamListByPlan(props.planId);
|
||||||
|
gridApi.setGridOptions({ data: list.value });
|
||||||
|
} finally {
|
||||||
|
gridApi.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择班组后加载成员 */
|
||||||
|
async function handleTeamSelect(row?: MesCalPlanTeamApi.PlanTeam) {
|
||||||
|
if (!row?.teamId) {
|
||||||
|
selectedTeamId.value = undefined;
|
||||||
|
selectedTeamName.value = '';
|
||||||
|
memberList.value = [];
|
||||||
|
memberGridApi.setGridOptions({ data: memberList.value });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedTeamId.value = row.teamId;
|
||||||
|
selectedTeamName.value = row.teamName || '';
|
||||||
|
memberGridApi.setLoading(true);
|
||||||
|
try {
|
||||||
|
memberList.value = await getTeamMemberListByTeam(row.teamId);
|
||||||
|
memberGridApi.setGridOptions({ data: memberList.value });
|
||||||
|
} finally {
|
||||||
|
memberGridApi.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开班组选择弹窗 */
|
||||||
|
function openTeamSelect() {
|
||||||
|
teamDialogRef.value?.open(list.value.map((item) => item.teamId!).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理班组选择 */
|
||||||
|
async function handleTeamsSelected(rows: MesCalTeamApi.Team[]) {
|
||||||
|
const existingTeamIds = new Set(list.value.map((item) => item.teamId));
|
||||||
|
const newTeams = rows.filter((team) => team.id && !existingTeamIds.has(team.id));
|
||||||
|
if (newTeams.length === 0) {
|
||||||
|
message.warning('所选班组已全部添加过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gridApi.setLoading(true);
|
||||||
|
try {
|
||||||
|
for (const team of newTeams) {
|
||||||
|
await createPlanTeam({ planId: props.planId, teamId: team.id });
|
||||||
|
}
|
||||||
|
message.success('成功添加 ' + newTeams.length + ' 个班组');
|
||||||
|
await getList();
|
||||||
|
} finally {
|
||||||
|
gridApi.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除计划班组 */
|
||||||
|
async function handleDelete(row: MesCalPlanTeamApi.PlanTeam) {
|
||||||
|
await deletePlanTeam(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.teamName]));
|
||||||
|
if (row.teamId === selectedTeamId.value) {
|
||||||
|
await handleTeamSelect(undefined);
|
||||||
|
}
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.planId,
|
||||||
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="isEditable" class="mb-3 flex items-center justify-start">
|
||||||
|
<TableAction :actions="[{ label: '添加班组', type: 'primary', onClick: openTeamSelect }]" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-5 gap-4">
|
||||||
|
<div class="col-span-3">
|
||||||
|
<Grid class="w-full">
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除该班组吗?',
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<Card class="h-full" size="small">
|
||||||
|
<template #title>
|
||||||
|
{{ selectedTeamName ? `「${selectedTeamName}」班组成员` : '班组成员' }}
|
||||||
|
</template>
|
||||||
|
<div v-if="!selectedTeamId">
|
||||||
|
<div class="py-8 text-center text-gray-400">请点击左侧班组查看成员</div>
|
||||||
|
</div>
|
||||||
|
<MemberGrid v-else class="w-full" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CalTeamSelectDialog ref="teamDialogRef" :multiple="true" @selected="handleTeamsSelected" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { HolidayType } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
/** 假期设置表单 */
|
||||||
|
export function useHolidayFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'day',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'dayDisplay',
|
||||||
|
label: '日期',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '类型',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.MES_CAL_HOLIDAY_TYPE, 'number'),
|
||||||
|
},
|
||||||
|
rules: z.number().default(HolidayType.WORKDAY),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ElButton, ElMessage, ElTag } from 'element-plus';
|
||||||
|
import { SolarDay } from 'tyme4ts';
|
||||||
|
|
||||||
|
import { getHolidayList } from '#/api/mes/cal/holiday';
|
||||||
|
import { HolidayType } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
import HolidayForm from './modules/form.vue';
|
||||||
|
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
const currentDate = ref(dayjs()); // 当前日历月份
|
||||||
|
const holidaySet = ref(new Set<string>()); // 节假日日期集合
|
||||||
|
const lastFetchedMonth = ref(''); // 上次加载月份
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
const [HolidayFormModal, holidayFormModalApi] = useVbenModal({
|
||||||
|
connectedComponent: HolidayForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const weekLabels = ['日', '一', '二', '三', '四', '五', '六']; // 星期标题
|
||||||
|
const calendarDays = computed(() => {
|
||||||
|
const monthStart = currentDate.value.startOf('month');
|
||||||
|
const start = monthStart.subtract(monthStart.day(), 'day');
|
||||||
|
return Array.from({ length: 42 }, (_, index) => {
|
||||||
|
const date = start.add(index, 'day');
|
||||||
|
return { day: date.format('YYYY-MM-DD'), inMonth: date.month() === currentDate.value.month() };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const currentMonthTitle = computed(() => currentDate.value.format('YYYY 年 MM 月'));
|
||||||
|
|
||||||
|
/** 加载假期列表 */
|
||||||
|
async function getList() {
|
||||||
|
holidaySet.value = new Set<string>();
|
||||||
|
const current = currentDate.value;
|
||||||
|
const startDay = current.subtract(1, 'month').startOf('month').format('YYYY-MM-DD 00:00:00');
|
||||||
|
const endDay = current.add(1, 'month').endOf('month').format('YYYY-MM-DD 23:59:59');
|
||||||
|
const list = await getHolidayList({ startDay, endDay });
|
||||||
|
const days = new Set<string>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holidaySet.value = days;
|
||||||
|
lastFetchedMonth.value = current.format('YYYY-MM');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开假期设置表单 */
|
||||||
|
function handleDayClick(item: { day: string; inMonth: boolean }) {
|
||||||
|
if (!item.inMonth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasAccessByCodes(['mes:cal-holiday:create'])) {
|
||||||
|
ElMessage.warning('没有假期设置权限');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
holidayFormModalApi.setData({ day: item.day }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到上月 */
|
||||||
|
function handlePrevMonth() {
|
||||||
|
currentDate.value = currentDate.value.subtract(1, 'month');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到下月 */
|
||||||
|
function handleNextMonth() {
|
||||||
|
currentDate.value = currentDate.value.add(1, 'month');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到今天 */
|
||||||
|
function handleToday() {
|
||||||
|
currentDate.value = dayjs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否周末 */
|
||||||
|
function isWeekend(day: string) {
|
||||||
|
const weekday = dayjs(day).day();
|
||||||
|
return weekday === 0 || weekday === 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取农历信息 */
|
||||||
|
function getLunarInfo(day: string) {
|
||||||
|
const [year, month, date] = 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() : '';
|
||||||
|
return {
|
||||||
|
solarFestival: solarFestival ? solarFestival.getName() : '',
|
||||||
|
lunarFestival: lunarFestival ? lunarFestival.getName() : '',
|
||||||
|
termName,
|
||||||
|
lunarText: lunarDay.getLunarMonth().getName() + lunarDay.getName(),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { solarFestival: '', lunarFestival: '', termName: '', lunarText: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取农历显示文本 */
|
||||||
|
function getLunarDisplay(day: string) {
|
||||||
|
const info = getLunarInfo(day);
|
||||||
|
return info.solarFestival || info.lunarFestival || info.termName || info.lunarText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否有节日 */
|
||||||
|
function hasFestival(day: string) {
|
||||||
|
const info = getLunarInfo(day);
|
||||||
|
return !!(info.solarFestival || info.lunarFestival || info.termName);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentDate, (newDate) => {
|
||||||
|
const newMonth = newDate.format('YYYY-MM');
|
||||||
|
if (newMonth !== lastFetchedMonth.value) {
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(getList);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="【排班】班组设置、节假日设置" url="https://doc.iocoder.cn/mes/cal/team/" />
|
||||||
|
</template>
|
||||||
|
<HolidayFormModal @success="getList" />
|
||||||
|
<div class="flex h-full flex-col overflow-hidden rounded border bg-background">
|
||||||
|
<div class="flex items-center justify-between border-b p-4">
|
||||||
|
<div class="text-lg font-semibold">{{ currentMonthTitle }}</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton @click="handlePrevMonth">上月</ElButton>
|
||||||
|
<ElButton @click="handleToday">今天</ElButton>
|
||||||
|
<ElButton @click="handleNextMonth">下月</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 border-b text-center text-sm text-muted-foreground">
|
||||||
|
<div v-for="label in weekLabels" :key="label" class="py-2">{{ label }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid flex-1 grid-cols-7 grid-rows-6">
|
||||||
|
<button
|
||||||
|
v-for="item in calendarDays"
|
||||||
|
:key="item.day"
|
||||||
|
class="min-h-[104px] border-b border-r p-2 text-left transition hover:bg-muted/50"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer': item.inMonth,
|
||||||
|
'bg-muted/30 text-muted-foreground': !item.inMonth,
|
||||||
|
}"
|
||||||
|
type="button"
|
||||||
|
@click="handleDayClick(item)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span
|
||||||
|
class="text-base font-medium"
|
||||||
|
:class="{ 'text-red-500': isWeekend(item.day) && item.inMonth }"
|
||||||
|
>
|
||||||
|
{{ item.day.slice(8) }}
|
||||||
|
</span>
|
||||||
|
<template v-if="item.inMonth">
|
||||||
|
<span v-if="holidaySet.has(item.day)">
|
||||||
|
<ElTag effect="dark" type="success">休</ElTag>
|
||||||
|
</span>
|
||||||
|
<span v-else><ElTag effect="dark">班</ElTag></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-1 text-xs"
|
||||||
|
:class="{
|
||||||
|
'text-green-600': hasFestival(item.day),
|
||||||
|
'text-muted-foreground': !hasFestival(item.day),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ getLunarDisplay(item.day) }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MesCalHolidayApi } from '#/api/mes/cal/holiday';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { getHolidayByDay, saveHoliday } from '#/api/mes/cal/holiday';
|
||||||
|
import { HolidayType } from '#/views/mes/utils/constants';
|
||||||
|
|
||||||
|
import { useHolidayFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useHolidayFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as MesCalHolidayApi.Holiday & { dayDisplay?: string };
|
||||||
|
try {
|
||||||
|
await saveHoliday({ day: data.day, type: data.type, remark: data.remark });
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
ElMessage.success('设置成功');
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await formApi.resetForm();
|
||||||
|
const data = modalApi.getData<{ day: string }>();
|
||||||
|
if (!data?.day) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timestamp = dayjs(data.day + ' 00:00:00').valueOf();
|
||||||
|
await formApi.setValues({
|
||||||
|
day: timestamp,
|
||||||
|
dayDisplay: data.day,
|
||||||
|
type: HolidayType.WORKDAY,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const holiday = await getHolidayByDay(dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
if (holiday) {
|
||||||
|
await formApi.setValues({
|
||||||
|
type: holiday.type ?? HolidayType.WORKDAY,
|
||||||
|
remark: holiday.remark ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="假期设置" class="w-1/3">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue