feat(hrm): 请假批量导入、加班选申请人、批量排班与节假日区间联动
- 请假申请新增导入按钮与导入结果弹窗(下载模板、逐行失败原因)
- 加班申请申请人改为员工下拉选择,带出姓名/工号/部门/岗位
- 排班管理新增「批量排班(按人群)」,按部门/岗位/在职状态一次排班
- 节假日列表改为按连续区间展示(日期范围 + 天数),编辑/删除以区间为单位
- 排班日历标记节假日「休/班」,自动排班与批量排班优先遵循节假日配置
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmAttendanceHolidayApi { |
| | | /** è忥 */ |
| | | /** è忥ï¼éæ¥ï¼ */ |
| | | export interface Holiday { |
| | | id?: number; |
| | | name?: string; |
| | |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** èåæ¥ï¼æè¿ç»æ¥æåºé´åå¹¶åçè¡ï¼ */ |
| | | export interface HolidayGroup { |
| | | id?: number; |
| | | /** åºé´å
å
¨é¨è®°å½IDï¼ç¼è¾/å 餿¶æ´ä½æäº¤ */ |
| | | ids?: number[]; |
| | | name?: string; |
| | | /** åºé´å¼å§æ¥æ */ |
| | | date?: string; |
| | | /** åºé´ç»ææ¥æ */ |
| | | endDate?: string; |
| | | /** åºé´å¤©æ° */ |
| | | dayCount?: number; |
| | | type?: number; |
| | | isLegal?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** ææ¥æåºé´æ´æ°åæ° */ |
| | | export interface HolidayRangeUpdateReq { |
| | | ids: number[]; |
| | | name: string; |
| | | startDate: string; |
| | | endDate: string; |
| | | type: number; |
| | | isLegal?: boolean; |
| | | remark?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è忥å页å表 */ |
| | | /** æ¥è¯¢è忥å页å表ï¼éæ¥ï¼å¯¼åº/å
¼å®¹ç¨ï¼ */ |
| | | export function getAttendanceHolidayPage(params: PageParam) { |
| | | return requestClient.get<PageResult<HrmAttendanceHolidayApi.Holiday>>( |
| | | '/hrm/attendance/holiday/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è忥å页åè¡¨ï¼æè¿ç»æ¥æåºé´åå¹¶ï¼å表页使ç¨ï¼ */ |
| | | export function getAttendanceHolidayGroupPage(params: PageParam) { |
| | | return requestClient.get<PageResult<HrmAttendanceHolidayApi.HolidayGroup>>( |
| | | '/hrm/attendance/holiday/page-grouped', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æå®æ¥æèå´çè忥ï¼éæ¥ï¼æçæ¥å使ç¨ï¼ */ |
| | | export function getAttendanceHolidayList(params: { |
| | | startDate: string; |
| | | endDate: string; |
| | | }) { |
| | | return requestClient.get<HrmAttendanceHolidayApi.Holiday[]>( |
| | | '/hrm/attendance/holiday/list', |
| | | { params }, |
| | | ); |
| | | } |
| | |
| | | return requestClient.post('/hrm/attendance/holiday/create', data); |
| | | } |
| | | |
| | | /** ææ¥æèå´æ°å¢è忥ï¼åºé´å
æ¯ä¸ªèªç¶æ¥å䏿¡ï¼ */ |
| | | export function createAttendanceHolidayRange(data: { |
| | | name: string; |
| | | startDate: string; |
| | | endDate: string; |
| | | type: number; |
| | | isLegal?: boolean; |
| | | remark?: string; |
| | | }) { |
| | | return requestClient.post<number>( |
| | | '/hrm/attendance/holiday/create-range', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** æ´æ°è忥 */ |
| | | export function updateAttendanceHoliday(data: HrmAttendanceHolidayApi.Holiday) { |
| | | return requestClient.put('/hrm/attendance/holiday/update', data); |
| | | } |
| | | |
| | | /** ææ¥æåºé´æ´æ°è忥ï¼å é¤ååºé´è®°å½åææ°èµ·æ¢æ¥æéåï¼ */ |
| | | export function updateAttendanceHolidayRange( |
| | | data: HrmAttendanceHolidayApi.HolidayRangeUpdateReq, |
| | | ) { |
| | | return requestClient.put('/hrm/attendance/holiday/update-range', data); |
| | | } |
| | | |
| | | /** å é¤è忥 */ |
| | |
| | | return requestClient.delete(`/hrm/attendance/holiday/delete?id=${id}`); |
| | | } |
| | | |
| | | /** ææ¥æåºé´å é¤è忥ï¼åºé´å
å
¨é¨æ¥æï¼ */ |
| | | export function deleteAttendanceHolidayRange(ids: number[]) { |
| | | return requestClient.delete( |
| | | `/hrm/attendance/holiday/delete-range?ids=${ids.join(',')}`, |
| | | ); |
| | | } |
| | | |
| | | /** 导åºè忥 Excel */ |
| | | export function exportAttendanceHoliday(params: any) { |
| | | return requestClient.download('/hrm/attendance/holiday/export-excel', { params }); |
| | | } |
| | | return requestClient.download('/hrm/attendance/holiday/export-excel', { |
| | | params, |
| | | }); |
| | | } |
| | |
| | | name: string; |
| | | deptName?: string; |
| | | postName?: string; |
| | | /** åå·¥ç¶æï¼1-å¨èï¼2-è¯ç¨ï¼3-离è */ |
| | | employeeStatus?: number; |
| | | } |
| | | |
| | | /** å工导å
¥ç»æ */ |
| | |
| | | key: string; |
| | | name: string; |
| | | } |
| | | |
| | | /** 导å
¥ç»æ */ |
| | | export interface ImportResult { |
| | | /** å建æåçè®°å½ï¼ææºå· + æ¥æï¼ */ |
| | | createNames?: string[]; |
| | | /** 导å
¥å¤±è´¥éåï¼key 为ã第Nè¡ãï¼value 为失败åå */ |
| | | failureNames?: Record<string, string>; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è¯·åç³è¯·å页å表 */ |
| | |
| | | return requestClient.get<HrmLeaveApi.ApproveProcess[]>( |
| | | '/hrm/leave/approve-process-definition-list', |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½è¯·åç³è¯·å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importLeaveTemplate() { |
| | | return requestClient.download('/hrm/leave/get-import-template'); |
| | | } |
| | | |
| | | /** æ¹é导å
¥è¯·åç³è¯·ï¼çæèç¨¿ï¼ */ |
| | | export function importLeave(file: File) { |
| | | return requestClient.upload<HrmLeaveApi.ImportResult>('/hrm/leave/import', { |
| | | file, |
| | | }); |
| | | } |
| | |
| | | shiftId?: number | null; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** æç¾¤ä½æ¹éæçè¯·æ± */ |
| | | export interface BatchSaveReq { |
| | | /** é¨é¨IDï¼å«ä¸çº§é¨é¨ï¼ */ |
| | | deptId: number; |
| | | /** å²ä½IDï¼ä¸ºç©ºè¡¨ç¤ºä¸éå²ä½ */ |
| | | postId?: number; |
| | | /** åå·¥ç¶æï¼1-å¨èï¼2-è¯ç¨ï¼3-离èï¼ä¸ºç©ºè¡¨ç¤ºå¨èåè¯ç¨ */ |
| | | employeeStatus?: number; |
| | | /** å¼å§æ¥æ yyyy-MM-dd */ |
| | | startDate: string; |
| | | /** ç»ææ¥æ yyyy-MM-dd */ |
| | | endDate: string; |
| | | /** çæ¬¡IDï¼ä¸ºç©ºè¡¨ç¤ºä¼æ¯ï¼æ¸
餿çï¼ */ |
| | | shiftId?: number | null; |
| | | /** none-ä¸ä¼æ¯ï¼single-卿¥ä¼æ¯ï¼double-å¨å
æ¥ä¼æ¯ */ |
| | | restMode?: 'double' | 'none' | 'single'; |
| | | } |
| | | |
| | | /** æç¾¤ä½æ¹éæçç»æ */ |
| | | export interface BatchSaveResp { |
| | | employeeCount?: number; |
| | | itemCount?: number; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æææçå表 */ |
| | |
| | | export function saveEmployeeSchedule(data: HrmEmployeeScheduleApi.ScheduleItem[]) { |
| | | return requestClient.post('/hrm/employee-schedule/save', data); |
| | | } |
| | | |
| | | /** æé¨é¨+å²ä½æ¹éæçï¼ä¸ç±»äººä¸æ¬¡æå®ï¼ */ |
| | | export function batchSaveEmployeeSchedule(data: HrmEmployeeScheduleApi.BatchSaveReq) { |
| | | return requestClient.post<HrmEmployeeScheduleApi.BatchSaveResp>( |
| | | '/hrm/employee-schedule/batch-save', |
| | | data, |
| | | ); |
| | | } |
| | |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<HrmAttendanceHolidayApi.Holiday>['columns'] { |
| | | export function useGridColumns(): VxeTableGridOptions<HrmAttendanceHolidayApi.HolidayGroup>['columns'] { |
| | | return [ |
| | | { field: 'name', title: 'è忥åç§°', minWidth: 150 }, |
| | | { field: 'date', title: 'æ¥æ', width: 120 }, |
| | | { |
| | | field: 'date', |
| | | title: 'æ¥æ', |
| | | minWidth: 220, |
| | | slots: { |
| | | default: 'dateRange', |
| | | }, |
| | | }, |
| | | { |
| | | field: 'dayCount', |
| | | title: '天æ°', |
| | | width: 90, |
| | | formatter: ({ cellValue }) => (cellValue ? `${cellValue} 天` : '-'), |
| | | }, |
| | | { |
| | | field: 'type', |
| | | title: 'ç±»å', |
| | |
| | | import { message, Modal, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteAttendanceHoliday, exportAttendanceHoliday, getAttendanceHolidayPage } from '#/api/hrm/attendance/holiday'; |
| | | import { |
| | | deleteAttendanceHoliday, |
| | | deleteAttendanceHolidayRange, |
| | | exportAttendanceHoliday, |
| | | getAttendanceHolidayGroupPage, |
| | | } from '#/api/hrm/attendance/holiday'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾è忥 */ |
| | | function handleUpdate(row: HrmAttendanceHolidayApi.Holiday) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | | /** æ¥æåºé´æ é¢ï¼å¦ 2026-10-01 ~ 2026-10-03ï¼å
±3å¤©ï¼ */ |
| | | function rangeText(row: HrmAttendanceHolidayApi.HolidayGroup) { |
| | | if (!row.endDate || row.endDate === row.date) { |
| | | return row.date; |
| | | } |
| | | return `${row.date} ~ ${row.endDate}`; |
| | | } |
| | | |
| | | /** ç¼è¾èåæ¥ï¼æ´ä¸ªåºé´ä¸èµ·ç¼è¾ï¼ */ |
| | | function handleUpdate(row: HrmAttendanceHolidayApi.HolidayGroup) { |
| | | formModalApi.setData({ id: row.id, row, formType: 'update' }).open(); |
| | | } |
| | | |
| | | /** æ¥çè忥 */ |
| | | function handleDetail(row: HrmAttendanceHolidayApi.Holiday) { |
| | | formModalApi.setData({ id: row.id, formType: 'detail' }).open(); |
| | | function handleDetail(row: HrmAttendanceHolidayApi.HolidayGroup) { |
| | | formModalApi.setData({ id: row.id, row, formType: 'detail' }).open(); |
| | | } |
| | | |
| | | /** å é¤è忥 */ |
| | | async function handleDelete(id: number) { |
| | | /** å é¤èåæ¥ï¼æ´ä¸ªåºé´ä¸èµ·å é¤ï¼ */ |
| | | async function handleDelete(row: HrmAttendanceHolidayApi.HolidayGroup) { |
| | | const content = |
| | | row.ids && row.ids.length > 1 |
| | | ? `ç¡®å®è¦å é¤ã${row.name}ã${rangeText(row)}ï¼å
± ${row.dayCount} 天ï¼åï¼` |
| | | : 'ç¡®å®è¦å é¤è¯¥è忥åï¼'; |
| | | Modal.confirm({ |
| | | title: '确认å é¤', |
| | | content: 'ç¡®å®è¦å é¤è¯¥è忥åï¼', |
| | | content, |
| | | okText: 'ç¡®å®', |
| | | cancelText: 'åæ¶', |
| | | onOk: async () => { |
| | | await deleteAttendanceHoliday(id); |
| | | if (row.ids && row.ids.length > 0) { |
| | | await deleteAttendanceHolidayRange(row.ids); |
| | | } else if (row.id != null) { |
| | | await deleteAttendanceHoliday(row.id); |
| | | } |
| | | message.success('å 餿å'); |
| | | handleRefresh(); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | /** æç´¢æ¡ä»¶ï¼æ¥æåºé´å段 date 转æåç«¯æ¥æ¶ç startDate/endDate */ |
| | | function buildQueryParams(formValues: Record<string, any>) { |
| | | const { date, ...rest } = formValues; |
| | | const [startDate, endDate] = Array.isArray(date) ? date : []; |
| | | return { ...rest, startDate, endDate }; |
| | | } |
| | | |
| | | /** 导åºè忥 */ |
| | | async function handleExport() { |
| | | const data = await exportAttendanceHoliday(await gridApi.formApi.getValues()); |
| | | const params = buildQueryParams(await gridApi.formApi.getValues()); |
| | | const data = await exportAttendanceHoliday(params); |
| | | downloadFileFromBlobPart({ fileName: 'è忥é
ç½®.xls', source: data }); |
| | | } |
| | | |
| | |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => |
| | | await getAttendanceHolidayPage({ |
| | | await getAttendanceHolidayGroupPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | ...buildQueryParams(formValues), |
| | | }), |
| | | }, |
| | | }, |
| | |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<HrmAttendanceHolidayApi.Holiday>, |
| | | } as VxeTableGridOptions<HrmAttendanceHolidayApi.HolidayGroup>, |
| | | }); |
| | | </script> |
| | | |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #dateRange="{ row }"> |
| | | <span>{{ row.date }}</span> |
| | | <span v-if="row.endDate && row.endDate !== row.date"> |
| | | ~ {{ row.endDate }} |
| | | </span> |
| | | </template> |
| | | <template #isLegal="{ row }"> |
| | | <Tag v-if="row.isLegal === 1" color="red">æ³å®</Tag> |
| | | <span v-else>-</span> |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | auth: ['hrm:attendance-holiday:delete'], |
| | | onClick: handleDelete.bind(null, row.id!), |
| | | onClick: handleDelete.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createAttendanceHoliday, getAttendanceHoliday, updateAttendanceHoliday } from '#/api/hrm/attendance/holiday'; |
| | | import { |
| | | createAttendanceHolidayRange, |
| | | getAttendanceHoliday, |
| | | updateAttendanceHoliday, |
| | | updateAttendanceHolidayRange, |
| | | } from '#/api/hrm/attendance/holiday'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | /** å½åç¼è¾çåºé´å
å
¨é¨è®°å½IDï¼å表æè¿ç»åºé´åå¹¶å±ç¤ºï¼ç¼è¾æ¶æ´ä½éåï¼ */ |
| | | const rangeIds = ref<number[]>([]); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const getTitle = computed(() => { |
| | |
| | | date: string | string[]; |
| | | }; |
| | | try { |
| | | const dates = Array.isArray(data.date) ? data.date : [data.date]; |
| | | const startDate = dates[0]!; |
| | | const endDate = dates[1] || dates[0]!; |
| | | if (formType.value === 'update') { |
| | | data.date = Array.isArray(data.date) ? data.date[0] : data.date; |
| | | await updateAttendanceHoliday(data); |
| | | } else { |
| | | const dates = Array.isArray(data.date) ? data.date : [data.date]; |
| | | for (const date of dates) { |
| | | await createAttendanceHoliday({ ...data, date }); |
| | | if (rangeIds.value.length > 0) { |
| | | // ç¼è¾çæ¯åå¹¶åçåºé´ï¼æ´ä½å é¤åè®°å½ï¼ææ°èµ·æ¢æ¥æéå |
| | | await updateAttendanceHolidayRange({ |
| | | ids: rangeIds.value, |
| | | name: data.name!, |
| | | startDate, |
| | | endDate, |
| | | type: data.type!, |
| | | isLegal: Boolean(data.isLegal), |
| | | remark: data.remark, |
| | | }); |
| | | } else { |
| | | data.date = startDate; |
| | | await updateAttendanceHoliday(data); |
| | | } |
| | | } else { |
| | | // ææ¥æåºé´ä¸æ¬¡æ§åå
¥åºé´å
æ¯ä¸å¤©ï¼æ³å®è忥 10-01~10-03 éåæ»¡ 3 å¤©ï¼ |
| | | await createAttendanceHolidayRange({ |
| | | name: data.name!, |
| | | startDate, |
| | | endDate, |
| | | type: data.type!, |
| | | isLegal: Boolean(data.isLegal), |
| | | remark: data.remark, |
| | | }); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | const data = modalApi.getData<{ |
| | | formType: FormType; |
| | | id?: number; |
| | | row?: HrmAttendanceHolidayApi.HolidayGroup; |
| | | }>(); |
| | | formType.value = data.formType; |
| | | formApi.setState({ schema: useFormSchema(data.formType) }); |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | modalApi.setState({ showConfirmButton: data.formType !== 'detail' }); |
| | | // å表è¡å·²å¸¦åºé´ä¿¡æ¯ï¼id/ids/date/endDateï¼ï¼ç´æ¥å¸¦å
¥ï¼æ éåæ¥åæ¥è®°å½ |
| | | if (data?.row) { |
| | | rangeIds.value = data.row.ids ?? (data.row.id != null ? [data.row.id] : []); |
| | | await formApi.setValues({ |
| | | id: data.row.id, |
| | | name: data.row.name, |
| | | date: [data.row.date, data.row.endDate || data.row.date], |
| | | type: data.row.type, |
| | | isLegal: data.row.isLegal, |
| | | remark: data.row.remark, |
| | | }); |
| | | return; |
| | | } |
| | | rangeIds.value = []; |
| | | if (!data?.id) { |
| | | return; |
| | | } |
| | |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** æå¼æ¹é导å
¥ */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Grid table-title="请åç³è¯·å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | auth: ['hrm:leave:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: '导å
¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['hrm:leave:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { HrmLeaveApi } from '#/api/hrm/leave'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Upload } from 'ant-design-vue'; |
| | | |
| | | import { importLeave, importLeaveTemplate } from '#/api/hrm/leave'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const fileList = ref<FileType[]>([]); |
| | | const importResult = ref<HrmLeaveApi.ImportResult>(); |
| | | const importing = ref(false); |
| | | const progressPercent = ref(0); |
| | | let progressTimer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | let shouldClose = false; |
| | | if (fileList.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦å¯¼å
¥çæä»¶'); |
| | | return; |
| | | } |
| | | if (importing.value) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importLeave(fileList.value[0] as File); |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = failureCount.value; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导å
¥å®æï¼æå ${createCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, |
| | | ); |
| | | } else { |
| | | message.success('导å
¥å®æ'); |
| | | shouldClose = true; |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | if (shouldClose) { |
| | | await modalApi.close(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ |
| | | function startMockProgress() { |
| | | progressPercent.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progressPercent.value < 90) { |
| | | progressPercent.value = Math.min( |
| | | 90, |
| | | progressPercent.value + Math.random() * 12, |
| | | ); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function stopMockProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = undefined; |
| | | } |
| | | } |
| | | |
| | | function delay(ms: number) { |
| | | return new Promise((resolve) => setTimeout(resolve, ms)); |
| | | } |
| | | |
| | | function beforeUpload(file: FileType) { |
| | | fileList.value = [file]; |
| | | importResult.value = undefined; |
| | | return false; |
| | | } |
| | | |
| | | function handleRemove() { |
| | | fileList.value = []; |
| | | importResult.value = undefined; |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | const data = await importLeaveTemplate(); |
| | | downloadFileFromBlobPart({ fileName: '请åç³è¯·å¯¼å
¥æ¨¡æ¿.xls', source: data }); |
| | | } |
| | | |
| | | const createCount = computed(() => importResult.value?.createNames?.length ?? 0); |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureNames ?? {}).length, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="导å
¥è¯·åç³è¯·" class="w-[640px]"> |
| | | <div class="mx-4"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :file-list="fileList" |
| | | :before-upload="beforeUpload" |
| | | @remove="handleRemove" |
| | | > |
| | | <Button type="primary">éæ© Excel æä»¶</Button> |
| | | </Upload> |
| | | <div class="mt-2 text-gray-500"> |
| | | ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼ãç³è¯·äººææºå·ãé对åºç³»ç»å
åå·¥ï¼ã请åç±»åãå¡«ååå
¸åç§°ï¼å¦å¹´åãäºåãç
åï¼ |
| | | </div> |
| | | <div class="mt-2 text-gray-500"> |
| | | 导å
¥åçæçæ¯è稿ï¼éå¨å表ä¸éæ¡æäº¤å®¡æ¹ |
| | | </div> |
| | | <!-- 导å
¥è¿åº¦æ¡ --> |
| | | <div v-if="importing" class="mt-4"> |
| | | <div class="mb-2 text-sm text-gray-600"> |
| | | æ£å¨å¯¼å
¥è¯·åç³è¯·æ°æ®ï¼è¯·ç¨å... |
| | | </div> |
| | | <Progress :percent="progressPercent" status="active" /> |
| | | </div> |
| | | <!-- 导å
¥ç»æ --> |
| | | <div |
| | | v-if="importResult" |
| | | class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" |
| | | > |
| | | <div class="mb-2 font-medium">导å
¥ç»æ</div> |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">æåå建ï¼{{ createCount }} æ¡</span> |
| | | <span class="text-red-600">失败ï¼{{ failureCount }} æ¡</span> |
| | | </div> |
| | | <div v-if="createCount > 0" class="mb-2 max-h-24 overflow-y-auto"> |
| | | <div |
| | | v-for="name in importResult.createNames" |
| | | :key="name" |
| | | class="text-sm leading-6 text-green-600" |
| | | > |
| | | {{ name }} |
| | | </div> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, row) in importResult.failureNames" |
| | | :key="row" |
| | | class="text-sm leading-6 text-red-500" |
| | | > |
| | | {{ row }}ï¼{{ reason }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å
¥æ¨¡æ¿</Button> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmEmployeeApi } from '#/api/hrm/employee'; |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | |
| | | import { getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | |
| | | }, |
| | | { |
| | | fieldName: 'employeeId', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | label: 'ç³è¯·äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: async () => { |
| | | const list = await getEmployeeSimpleList(); |
| | | // 离èåå·¥ä¸å¯éä½å çç³è¯·äºº |
| | | return list.filter((item) => item.employeeStatus !== 3); |
| | | }, |
| | | labelField: 'name', |
| | | labelFn: (item: HrmEmployeeApi.EmployeeSimple) => |
| | | `${item.name}ï¼${item.employeeNo}ï¼`, |
| | | valueField: 'id', |
| | | showSearch: true, |
| | | filterOption: (input: string, option: any) => |
| | | String(option.label ?? '') |
| | | .toLowerCase() |
| | | .includes(input.toLowerCase()), |
| | | placeholder: 'è¯·éæ©ç³è¯·äºº', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'baseInfoDivider', |
| | |
| | | }, |
| | | { |
| | | fieldName: 'employeeName', |
| | | label: 'ç³è¯·äºº', |
| | | label: 'ç³è¯·äººå§å', |
| | | component: 'Input', |
| | | componentProps: { disabled: true, placeholder: 'ç³»ç»èªå¨å¸¦åº' }, |
| | | componentProps: { disabled: true, placeholder: 'éæ©ç³è¯·äººåèªå¨å¸¦åº' }, |
| | | }, |
| | | { |
| | | fieldName: 'employeeNo', |
| | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createOvertime, |
| | | getCurrentOvertimeApplicant, |
| | | getOvertime, |
| | | updateOvertime, |
| | | } from '#/api/hrm/overtime'; |
| | | import type { HrmEmployeeApi } from '#/api/hrm/employee'; |
| | | import { getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | import { createOvertime, getOvertime, updateOvertime } from '#/api/hrm/overtime'; |
| | | import { $t } from '#/locales'; |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | |
| | | const result = calculateDuration(values); |
| | | void formApi.setValues(result); |
| | | } |
| | | if (changedFields.includes('employeeId')) { |
| | | void fillEmployeeInfo(values.employeeId); |
| | | } |
| | | if (changedFields.includes('relatedType') && !values.relatedType) { |
| | | void formApi.setFieldValue('relatedCode', undefined); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | async function setCurrentEmployee() { |
| | | const employee = await getCurrentOvertimeApplicant(); |
| | | /** å工档æ¡ç¼åï¼éæ©ç³è¯·äººæ¶ç¨äºåå¡«å§å/å·¥å·/é¨é¨/å²ä½ */ |
| | | let employeeCache: HrmEmployeeApi.EmployeeSimple[] = []; |
| | | |
| | | async function loadEmployeeCache() { |
| | | if (employeeCache.length === 0) { |
| | | employeeCache = await getEmployeeSimpleList(); |
| | | } |
| | | return employeeCache; |
| | | } |
| | | |
| | | /** éæ©ç³è¯·äººååå¡«å
¶åºç¡ä¿¡æ¯ */ |
| | | async function fillEmployeeInfo(employeeId?: number) { |
| | | const list = await loadEmployeeCache(); |
| | | const employee = list.find((item) => item.id === employeeId); |
| | | await formApi.setValues({ |
| | | employeeId: employee.employeeId, |
| | | employeeName: employee.employeeName, |
| | | employeeNo: employee.employeeNo, |
| | | deptName: employee.deptName, |
| | | postName: employee.postName, |
| | | mealDeduction: false, |
| | | mealDeductionHours: 1, |
| | | crossDay: false, |
| | | duration: 0, |
| | | employeeName: employee?.name, |
| | | employeeNo: employee?.employeeNo, |
| | | deptName: employee?.deptName, |
| | | postName: employee?.postName, |
| | | }); |
| | | } |
| | | |
| | |
| | | |
| | | const data = (await formApi.getValues()) as HrmOvertimeApi.Overtime; |
| | | if (!data.employeeId) { |
| | | message.warning('å½åè´¦å·æªå
³èå工档æ¡ï¼ææ¶æ æ³æäº¤å çç³è¯·'); |
| | | message.warning('è¯·éæ©ç³è¯·äºº'); |
| | | return; |
| | | } |
| | | if (!data.duration || data.duration <= 0) { |
| | |
| | | modalApi.setState({ showConfirmButton: !isDetail.value }); |
| | | |
| | | if (!data.id) { |
| | | await setCurrentEmployee(); |
| | | // æ°å¢ï¼ä¸é»è®¤å½åè´¦å·ï¼ç±ç¨æ·éæ©ç³è¯·äºº |
| | | await loadEmployeeCache(); |
| | | await formApi.setValues({ |
| | | employeeId: undefined, |
| | | employeeName: undefined, |
| | | employeeNo: undefined, |
| | | deptName: undefined, |
| | | postName: undefined, |
| | | mealDeduction: false, |
| | | mealDeductionHours: 1, |
| | | crossDay: false, |
| | | duration: 0, |
| | | }); |
| | | return; |
| | | } |
| | | |
| | |
| | | Modal, |
| | | Pagination, |
| | | Radio, |
| | | RangePicker, |
| | | Segmented, |
| | | Select, |
| | | TreeSelect, |
| | |
| | | |
| | | import { Page } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { getAttendanceHolidayList } from '#/api/hrm/attendance/holiday'; |
| | | import { getEmployeePage } from '#/api/hrm/employee'; |
| | | import { getShiftList } from '#/api/hrm/shift'; |
| | | import { getEmployeeScheduleList, saveEmployeeSchedule } from '#/api/hrm/schedule'; |
| | | import { |
| | | batchSaveEmployeeSchedule, |
| | | getEmployeeScheduleList, |
| | | saveEmployeeSchedule, |
| | | } from '#/api/hrm/schedule'; |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { getSimplePostList } from '#/api/system/post'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | |
| | | import { |
| | |
| | | // ========== æ°æ® ========== |
| | | const employees = ref<HrmEmployeeApi.Employee[]>([]); |
| | | const shiftList = ref<HrmShiftApi.Shift[]>([]); |
| | | /** |
| | | * å½åè§å¾æ¥æèå´å
çè忥é
ç½®ï¼key = yyyy-MM-ddï¼value = 1-伿¯æ¥ / 2-工使¥ |
| | | */ |
| | | const holidayMap = ref(new Map<string, number>()); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | /** æ¯å¦å·²æ§è¡è¿æ¥è¯¢ */ |
| | |
| | | const week = new Date( |
| | | `${month.value}-${String(day).padStart(2, '0')}`, |
| | | ).getDay(); |
| | | // è忥é
ç½®ä¼å
ï¼ä¼æ¯æ¥å¼ºå¶ä¼æ¯ï¼å·¥ä½æ¥å¼ºå¶æçï¼å«è°ä¼ä¸çç卿«ï¼ |
| | | const holidayType = getHolidayType(date); |
| | | let rest = false; |
| | | if (autoRestMode.value === REST_MODE.DOUBLE) { |
| | | if (holidayType === 1) { |
| | | rest = true; |
| | | } else if (holidayType === 2) { |
| | | rest = false; |
| | | } else if (autoRestMode.value === REST_MODE.DOUBLE) { |
| | | rest = week === 0 || week === 6; |
| | | } else if (autoRestMode.value === REST_MODE.SINGLE) { |
| | | rest = week === 0; |
| | |
| | | scheduleMap.value = map; |
| | | autoOpen.value = false; |
| | | message.success('èªå¨æçå·²çæï¼å¯æå¨å¾®è°åä¿å'); |
| | | } |
| | | |
| | | // ========== æç¾¤ä½æ¹éæçï¼ä¸ç±»äººä¸æ¬¡æå®ï¼ ========== |
| | | const batchOpen = ref(false); |
| | | const batchDeptId = ref<number>(); |
| | | const batchPostId = ref<number>(); |
| | | /** 0-å¨èåè¯ç¨ï¼1-å¨èï¼2-è¯ç¨ï¼3-离è */ |
| | | const batchEmployeeStatus = ref<number>(0); |
| | | /** 0 è¡¨ç¤ºä¼æ¯ï¼æ¸
餿çï¼ */ |
| | | const batchShiftId = ref<number>(); |
| | | const batchRestMode = ref<RestMode>(REST_MODE.DOUBLE); |
| | | const batchDateRange = ref<[string, string]>(); |
| | | const batchSaving = ref(false); |
| | | const postList = ref<{ label: string; value: number }[]>([]); |
| | | |
| | | const batchEmployeeStatusOptions = [ |
| | | { label: 'å¨èåè¯ç¨', value: 0 }, |
| | | { label: 'å¨è', value: 1 }, |
| | | { label: 'è¯ç¨', value: 2 }, |
| | | { label: '离è', value: 3 }, |
| | | ]; |
| | | |
| | | const batchShiftOptions = computed(() => [ |
| | | { label: '伿¯ï¼æ¸
餿çï¼', value: 0 }, |
| | | ...shiftList.value.map((shift) => ({ |
| | | label: `${shift.name} (${shift.startTime}~${shift.endTime})`, |
| | | value: shift.id!, |
| | | })), |
| | | ]); |
| | | |
| | | async function loadPostList() { |
| | | if (postList.value.length > 0) { |
| | | return; |
| | | } |
| | | const posts = await getSimplePostList(); |
| | | postList.value = posts.map((post) => ({ |
| | | label: post.name, |
| | | value: post.id!, |
| | | })); |
| | | } |
| | | |
| | | async function openBatch() { |
| | | if (!month.value) { |
| | | message.warning('请å
éæ©æä»½'); |
| | | return; |
| | | } |
| | | if (shiftList.value.length === 0) { |
| | | shiftList.value = await getShiftList(); |
| | | } |
| | | batchDeptId.value = deptId.value; |
| | | batchPostId.value = undefined; |
| | | batchEmployeeStatus.value = 0; |
| | | batchShiftId.value = shiftList.value[0]?.id; |
| | | batchRestMode.value = REST_MODE.DOUBLE; |
| | | batchDateRange.value = [ |
| | | `${month.value}-01`, |
| | | dayjs(`${month.value}-01`).endOf('month').format('YYYY-MM-DD'), |
| | | ]; |
| | | batchOpen.value = true; |
| | | await loadPostList(); |
| | | } |
| | | |
| | | async function applyBatchSchedule() { |
| | | if (!batchDeptId.value) { |
| | | message.warning('è¯·éæ©é¨é¨'); |
| | | return; |
| | | } |
| | | if (!batchDateRange.value?.[0] || !batchDateRange.value?.[1]) { |
| | | message.warning('è¯·éæ©æçæ¥æèå´'); |
| | | return; |
| | | } |
| | | if (batchShiftId.value == null) { |
| | | message.warning('è¯·éæ©æççæ¬¡æä¼æ¯'); |
| | | return; |
| | | } |
| | | batchSaving.value = true; |
| | | try { |
| | | const result = await batchSaveEmployeeSchedule({ |
| | | deptId: batchDeptId.value, |
| | | postId: batchPostId.value, |
| | | employeeStatus: batchEmployeeStatus.value || undefined, |
| | | startDate: batchDateRange.value[0], |
| | | endDate: batchDateRange.value[1], |
| | | shiftId: batchShiftId.value === 0 ? null : batchShiftId.value, |
| | | restMode: batchRestMode.value, |
| | | }); |
| | | message.success( |
| | | `æ¹éæç宿ï¼å
± ${result.employeeCount ?? 0} ååå·¥ï¼åå
¥/æ¸
é¤ ${result.itemCount ?? 0} æ¡æç`, |
| | | ); |
| | | batchOpen.value = false; |
| | | await loadData(); |
| | | } finally { |
| | | batchSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | /** çæ¬¡ id â çæ¬¡ æ å° */ |
| | |
| | | }; |
| | | }); |
| | | }); |
| | | |
| | | /** ææ¥èåæ¥ç±»åï¼1-伿¯æ¥ï¼2-工使¥ï¼undefined-æªé
ç½® */ |
| | | function getHolidayType(date: string): number | undefined { |
| | | return holidayMap.value.get(date); |
| | | } |
| | | |
| | | /** èåæ¥è§æ ææ¡ï¼ä¼ / ç */ |
| | | function holidayLabel(date: string): string { |
| | | const type = getHolidayType(date); |
| | | if (type === 1) { |
| | | return 'ä¼'; |
| | | } |
| | | if (type === 2) { |
| | | return 'ç'; |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | /** å è½½å½åè§å¾æ¥æèå´å
çè忥é
ç½® */ |
| | | async function loadHolidays() { |
| | | const list = viewDays.value; |
| | | const first = list[0]; |
| | | const last = list[list.length - 1]; |
| | | if (!first || !last) { |
| | | holidayMap.value = new Map(); |
| | | return; |
| | | } |
| | | const holidays = await getAttendanceHolidayList({ |
| | | startDate: first.date, |
| | | endDate: last.date, |
| | | }); |
| | | const map = new Map<string, number>(); |
| | | for (const item of holidays) { |
| | | if (item.date) { |
| | | map.set(item.date, item.type ?? 1); |
| | | } |
| | | } |
| | | holidayMap.value = map; |
| | | } |
| | | |
| | | /** è§å¾æ¥æååï¼åæ/åå¨/åè§å¾ï¼æ¶éæ°æåè忥 */ |
| | | watch( |
| | | () => viewDays.value.map((vd) => vd.date).join(','), |
| | | (key) => { |
| | | if (key) { |
| | | loadHolidays(); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | /** è忥æ è®°çåå
æ ¼/表头é
è² */ |
| | | function holidayClass(date: string): string { |
| | | const type = getHolidayType(date); |
| | | if (type === 1) { |
| | | return 'holiday-rest'; |
| | | } |
| | | if (type === 2) { |
| | | return 'holiday-work'; |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | /** å¨è§å¾å½åæ¾ç¤ºèå´æ ç¾ */ |
| | | const weekRangeLabel = computed(() => { |
| | |
| | | <Button :disabled="!employees.length" @click="autoOpen = true"> |
| | | èªå¨æç |
| | | </Button> |
| | | <Button type="primary" ghost :disabled="!month" @click="openBatch"> |
| | | æ¹éæçï¼æäººç¾¤ï¼ |
| | | </Button> |
| | | <Button |
| | | type="primary" |
| | | :loading="saving" |
| | |
| | | :key="vd.date" |
| | | :class="[ |
| | | 'schedule-date-col text-center', |
| | | vd.isWeekend ? 'bg-red-50' : 'bg-gray-50', |
| | | holidayClass(vd.date) |
| | | ? holidayClass(vd.date) |
| | | : vd.isWeekend |
| | | ? 'bg-red-50' |
| | | : 'bg-gray-50', |
| | | ]" |
| | | > |
| | | <div class="font-medium">{{ vd.day }}</div> |
| | | <div class="text-xs text-gray-400">{{ vd.weekLabel }}</div> |
| | | <div class="text-xs text-gray-400"> |
| | | {{ vd.weekLabel }} |
| | | <span |
| | | v-if="holidayLabel(vd.date)" |
| | | class="holiday-tag" |
| | | >{{ holidayLabel(vd.date) }}</span |
| | | > |
| | | </div> |
| | | </th> |
| | | </tr> |
| | | </thead> |
| | |
| | | v-for="vd in viewDays" |
| | | :key="vd.date" |
| | | class="schedule-cell" |
| | | :class="[vd.isWeekend ? 'bg-red-50/40' : '']" |
| | | :class="[vd.isWeekend ? 'bg-red-50/40' : '', holidayClass(vd.date)]" |
| | | @click="openPicker(emp, vd.date)" |
| | | > |
| | | <span |
| | |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | |
| | | <!-- æäººç¾¤æ¹éæçå¼¹çªï¼éé¨é¨/å²ä½/å¨èç¶æ + æ¥æèå´ + çæ¬¡ï¼ä¸æ¬¡æå®ä¸ç±»äºº --> |
| | | <Modal |
| | | v-model:open="batchOpen" |
| | | title="æ¹éæçï¼æäººç¾¤ï¼" |
| | | :footer="null" |
| | | :width="520" |
| | | > |
| | | <div class="space-y-4"> |
| | | <div> |
| | | <div class="mb-1.5 text-sm text-gray-500"> |
| | | é¨é¨ï¼å«ä¸çº§é¨é¨ï¼<span class="text-red-500">*</span> |
| | | </div> |
| | | <TreeSelect |
| | | v-model:value="batchDeptId" |
| | | :tree-data="deptTree" |
| | | :field-names="{ label: 'name', value: 'id', children: 'children' }" |
| | | allow-clear |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | tree-default-expand-all |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | <div class="grid grid-cols-2 gap-3"> |
| | | <div> |
| | | <div class="mb-1.5 text-sm text-gray-500">å²ä½</div> |
| | | <Select |
| | | v-model:value="batchPostId" |
| | | :options="postList" |
| | | allow-clear |
| | | placeholder="ä¸éå²ä½" |
| | | show-search |
| | | option-filter-prop="label" |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1.5 text-sm text-gray-500">åå·¥ç¶æ</div> |
| | | <Select |
| | | v-model:value="batchEmployeeStatus" |
| | | :options="batchEmployeeStatusOptions" |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1.5 text-sm text-gray-500"> |
| | | æçæ¥æèå´<span class="text-red-500">*</span> |
| | | </div> |
| | | <RangePicker |
| | | v-model:value="batchDateRange" |
| | | value-format="YYYY-MM-DD" |
| | | :allow-clear="false" |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1.5 text-sm text-gray-500"> |
| | | æççæ¬¡<span class="text-red-500">*</span> |
| | | </div> |
| | | <Select |
| | | v-model:value="batchShiftId" |
| | | :options="batchShiftOptions" |
| | | placeholder="è¯·éæ©çæ¬¡ï¼æéæ©ä¼æ¯" |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | <div v-if="batchShiftId !== 0"> |
| | | <div class="mb-1.5 text-sm text-gray-500">伿¯æ¨¡å¼</div> |
| | | <Radio.Group |
| | | v-model:value="batchRestMode" |
| | | :options="REST_MODE_OPTIONS" |
| | | option-type="button" |
| | | /> |
| | | </div> |
| | | <div class="rounded bg-blue-50 px-3 py-2 text-xs text-blue-600"> |
| | | å°å¯¹æéé¨é¨/å²ä½/å¨èç¶æçå
¨é¨åå·¥ï¼å¨æ¥æèå´å
鿥æçï¼{{ |
| | | batchShiftId === 0 |
| | | ? 'éæ©ã伿¯ãå°æ¸
é¤è¯¥èå´å
å·²ææçã' |
| | | : 'æä¸æ¹ä¼æ¯æ¨¡å¼è·³è¿å¨æ«ï¼å
¶ä½æ¥æææéçæ¬¡ã' |
| | | }}å·²é
ç½®èåæ¥çæ¥æä»¥èåæ¥ä¸ºåï¼ä¼æ¯æ¥å¼ºå¶ä¼æ¯ï¼å·¥ä½æ¥å¼ºå¶æçï¼å«è°ä¼ä¸çç卿«ï¼ã |
| | | </div> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="batchOpen = false">åæ¶</Button> |
| | | <Button type="primary" :loading="batchSaving" @click="applyBatchSchedule"> |
| | | å¼å§æç |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /* è忥æ è®°ï¼ä¼æ¯æ¥æ©è²ãè°ä¼å·¥ä½æ¥ç»¿è²ï¼ä¼å
äºå¨æ«åºè² */ |
| | | .holiday-rest { |
| | | background-color: #fff1e6 !important; |
| | | } |
| | | |
| | | .holiday-work { |
| | | background-color: #e8f8ec !important; |
| | | } |
| | | |
| | | .holiday-tag { |
| | | display: inline-block; |
| | | padding: 0 3px; |
| | | margin-left: 2px; |
| | | color: #fff; |
| | | font-size: 10px; |
| | | line-height: 14px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .holiday-rest .holiday-tag { |
| | | background-color: #fa8c16; |
| | | } |
| | | |
| | | .holiday-work .holiday-tag { |
| | | background-color: #52c41a; |
| | | } |
| | | |
| | | .shift-chip { |
| | | display: inline-block; |
| | | max-width: 100%; |