From 4ffc61bbe8dd6ec89965a8e2448725f514075b53 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 08 七月 2026 13:29:32 +0800
Subject: [PATCH] 银川 1.考勤管理页面开发、联调
---
src/views/hrm/attendance/rule/modules/form.vue | 96 +++
src/api/hrm/attendance/holiday/index.ts | 52 ++
src/views/hrm/attendance/holiday/index.vue | 147 +++++
src/views/hrm/attendance/summary/index.vue | 106 ++++
src/views/hrm/attendance/holiday/modules/form.vue | 96 +++
src/api/hrm/attendance/summary/index.ts | 57 ++
src/views/hrm/attendance/summary/modules/generate-form.vue | 63 ++
src/views/hrm/attendance/summary/data.ts | 163 ++++++
src/views/hrm/attendance/record/index.vue | 19
src/api/hrm/attendance/exception/index.ts | 19
src/api/hrm/attendance/record/index.ts | 18
src/views/hrm/attendance/holiday/data.ts | 163 ++++++
src/views/hrm/attendance/exception/index.vue | 65 ++
src/views/hrm/attendance/record/modules/clock-panel.vue | 82 +++
src/views/hrm/attendance/rule/data.ts | 206 +++++++
src/views/hrm/attendance/rule/index.vue | 143 +++++
src/packages/constants/src/dict-enum.ts | 2
src/api/hrm/attendance/rule/index.ts | 63 ++
18 files changed, 1,553 insertions(+), 7 deletions(-)
diff --git a/src/api/hrm/attendance/exception/index.ts b/src/api/hrm/attendance/exception/index.ts
index 5bb1014..1e822e3 100644
--- a/src/api/hrm/attendance/exception/index.ts
+++ b/src/api/hrm/attendance/exception/index.ts
@@ -19,6 +19,12 @@
remark?: string;
createTime?: string;
}
+
+ /** 瀹℃壒娴佺▼ */
+ export interface ApproveProcess {
+ key: string;
+ name: string;
+ }
}
/** 鏌ヨ鑰冨嫟寮傚父鐢宠鍒嗛〉鍒楄〃 */
@@ -44,6 +50,15 @@
}
/** 鎻愪氦鑰冨嫟寮傚父鐢宠 */
-export function submitAttendanceException(id: number) {
- return requestClient.post(`/hrm/attendance/exception/submit?id=${id}`);
+export function submitAttendanceException(id: number, processDefinitionKey: string) {
+ return requestClient.post('/hrm/attendance/exception/submit', null, {
+ params: { id, processDefinitionKey },
+ });
+}
+
+/** 鑾峰彇鑰冨嫟寮傚父瀹℃壒娴佺▼鍒楄〃 */
+export function getAttendanceExceptionApproveProcessList() {
+ return requestClient.get<HrmAttendanceExceptionApi.ApproveProcess[]>(
+ '/hrm/attendance/exception/approve-process-list',
+ );
}
\ No newline at end of file
diff --git a/src/api/hrm/attendance/holiday/index.ts b/src/api/hrm/attendance/holiday/index.ts
new file mode 100644
index 0000000..89da4ec
--- /dev/null
+++ b/src/api/hrm/attendance/holiday/index.ts
@@ -0,0 +1,52 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmAttendanceHolidayApi {
+ /** 鑺傚亣鏃� */
+ export interface Holiday {
+ id?: number;
+ name?: string;
+ date?: string;
+ type?: number;
+ typeName?: string;
+ isLegal?: number;
+ remark?: string;
+ createTime?: string;
+ }
+}
+
+/** 鏌ヨ鑺傚亣鏃ュ垎椤靛垪琛� */
+export function getAttendanceHolidayPage(params: PageParam) {
+ return requestClient.get<PageResult<HrmAttendanceHolidayApi.Holiday>>(
+ '/hrm/attendance/holiday/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鑺傚亣鏃ヨ鎯� */
+export function getAttendanceHoliday(id: number) {
+ return requestClient.get<HrmAttendanceHolidayApi.Holiday>(
+ `/hrm/attendance/holiday/get?id=${id}`,
+ );
+}
+
+/** 鏂板鑺傚亣鏃� */
+export function createAttendanceHoliday(data: HrmAttendanceHolidayApi.Holiday) {
+ return requestClient.post('/hrm/attendance/holiday/create', data);
+}
+
+/** 鏇存柊鑺傚亣鏃� */
+export function updateAttendanceHoliday(data: HrmAttendanceHolidayApi.Holiday) {
+ return requestClient.put('/hrm/attendance/holiday/update', data);
+}
+
+/** 鍒犻櫎鑺傚亣鏃� */
+export function deleteAttendanceHoliday(id: number) {
+ return requestClient.delete(`/hrm/attendance/holiday/delete?id=${id}`);
+}
+
+/** 瀵煎嚭鑺傚亣鏃� Excel */
+export function exportAttendanceHoliday(params: any) {
+ return requestClient.download('/hrm/attendance/holiday/export-excel', { params });
+}
\ No newline at end of file
diff --git a/src/api/hrm/attendance/record/index.ts b/src/api/hrm/attendance/record/index.ts
index 7852fa8..584cb2e 100644
--- a/src/api/hrm/attendance/record/index.ts
+++ b/src/api/hrm/attendance/record/index.ts
@@ -77,4 +77,22 @@
/** 瀵煎嚭鑰冨嫟璁板綍 */
export function exportAttendanceRecord(params: any) {
return requestClient.download('/hrm/attendance/record/export-excel', { params });
+}
+
+/** 涓婄彮鎵撳崱 */
+export function clockIn(params: { location: string }) {
+ return requestClient.post<HrmAttendanceRecordApi.AttendanceRecord>(
+ '/hrm/attendance/record/clock-in',
+ null,
+ { params },
+ );
+}
+
+/** 涓嬬彮鎵撳崱 */
+export function clockOut(params: { location: string }) {
+ return requestClient.post<HrmAttendanceRecordApi.AttendanceRecord>(
+ '/hrm/attendance/record/clock-out',
+ null,
+ { params },
+ );
}
\ No newline at end of file
diff --git a/src/api/hrm/attendance/rule/index.ts b/src/api/hrm/attendance/rule/index.ts
new file mode 100644
index 0000000..2ba8920
--- /dev/null
+++ b/src/api/hrm/attendance/rule/index.ts
@@ -0,0 +1,63 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmAttendanceRuleApi {
+ /** 鑰冨嫟瑙勫垯 */
+ export interface AttendanceRule {
+ id?: number;
+ name?: string;
+ deptId?: number;
+ deptName?: string;
+ workStartTime?: string;
+ workEndTime?: string;
+ lateAllowMinutes?: number;
+ earlyAllowMinutes?: number;
+ overtimeThreshold?: number;
+ status?: number;
+ remark?: string;
+ createTime?: string;
+ }
+}
+
+/** 鏌ヨ鑰冨嫟瑙勫垯鍒嗛〉鍒楄〃 */
+export function getAttendanceRulePage(params: PageParam) {
+ return requestClient.get<PageResult<HrmAttendanceRuleApi.AttendanceRule>>(
+ '/hrm/attendance/rule/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鑰冨嫟瑙勫垯鍒楄〃锛堝惎鐢ㄧ姸鎬侊級 */
+export function getAttendanceRuleList() {
+ return requestClient.get<HrmAttendanceRuleApi.AttendanceRule[]>(
+ '/hrm/attendance/rule/list',
+ );
+}
+
+/** 鏌ヨ鑰冨嫟瑙勫垯璇︽儏 */
+export function getAttendanceRule(id: number) {
+ return requestClient.get<HrmAttendanceRuleApi.AttendanceRule>(
+ `/hrm/attendance/rule/get?id=${id}`,
+ );
+}
+
+/** 鏂板鑰冨嫟瑙勫垯 */
+export function createAttendanceRule(data: HrmAttendanceRuleApi.AttendanceRule) {
+ return requestClient.post('/hrm/attendance/rule/create', data);
+}
+
+/** 鏇存柊鑰冨嫟瑙勫垯 */
+export function updateAttendanceRule(data: HrmAttendanceRuleApi.AttendanceRule) {
+ return requestClient.put('/hrm/attendance/rule/update', data);
+}
+
+/** 鍒犻櫎鑰冨嫟瑙勫垯 */
+export function deleteAttendanceRule(id: number) {
+ return requestClient.delete(`/hrm/attendance/rule/delete?id=${id}`);
+}
+
+/** 瀵煎嚭鑰冨嫟瑙勫垯 Excel */
+export function exportAttendanceRule(params: any) {
+ return requestClient.download('/hrm/attendance/rule/export-excel', { params });
+}
\ No newline at end of file
diff --git a/src/api/hrm/attendance/summary/index.ts b/src/api/hrm/attendance/summary/index.ts
new file mode 100644
index 0000000..ec25193
--- /dev/null
+++ b/src/api/hrm/attendance/summary/index.ts
@@ -0,0 +1,57 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmAttendanceSummaryApi {
+ /** 鑰冨嫟缁熻 */
+ export interface AttendanceSummary {
+ id?: number;
+ userId?: number;
+ userName?: string;
+ deptId?: number;
+ deptName?: string;
+ yearMonth?: string;
+ workDays?: number;
+ actualDays?: number;
+ lateCount?: number;
+ earlyCount?: number;
+ absentCount?: number;
+ overtimeHours?: number;
+ leaveHours?: number;
+ remark?: string;
+ createTime?: string;
+ }
+
+ /** 鐢熸垚缁熻鍙傛暟 */
+ export interface GenerateParams {
+ yearMonth: string;
+ deptId?: number;
+ userId?: number;
+ }
+}
+
+/** 鏌ヨ鑰冨嫟缁熻鍒嗛〉鍒楄〃 */
+export function getAttendanceSummaryPage(params: PageParam) {
+ return requestClient.get<PageResult<HrmAttendanceSummaryApi.AttendanceSummary>>(
+ '/hrm/attendance/summary/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鍛樺伐鏈堝害姹囨�� */
+export function getAttendanceSummary(params: { userId: number; yearMonth: string }) {
+ return requestClient.get<HrmAttendanceSummaryApi.AttendanceSummary>(
+ '/hrm/attendance/summary/get',
+ { params },
+ );
+}
+
+/** 鐢熸垚鏈堝害鑰冨嫟姹囨�� */
+export function generateAttendanceSummary(params: HrmAttendanceSummaryApi.GenerateParams) {
+ return requestClient.post('/hrm/attendance/summary/generate', null, { params });
+}
+
+/** 瀵煎嚭鑰冨嫟缁熻 Excel */
+export function exportAttendanceSummary(params: any) {
+ return requestClient.download('/hrm/attendance/summary/export-excel', { params });
+}
\ No newline at end of file
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 8162670..ea20442 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -304,6 +304,8 @@
HRM_TRANSFER_TYPE: 'hrm_transfer_type', // HRM 璋冨矖绫诲瀷
HRM_RESIGNATION_TYPE: 'hrm_resignation_type', // HRM 绂昏亴绫诲瀷
HRM_CLOCK_TYPE: 'hrm_clock_type', // HRM 鎵撳崱绫诲瀷
+ HRM_HOLIDAY_TYPE: 'hrm_holiday_type', // HRM 鑺傚亣鏃ョ被鍨�
+ HRM_DATA_SOURCE: 'hrm_data_source', // HRM 鏁版嵁鏉ユ簮
HRM_SALARY_STATUS: 'hrm_salary_status', // HRM 钖祫鐘舵��
HRM_HANDOVER_STATUS: 'hrm_handover_status', // HRM 浜ゆ帴鐘舵��
HRM_PAYMENT_METHOD: 'hrm_payment_method', // HRM 鍙戞斁鏂瑰紡
diff --git a/src/views/hrm/attendance/exception/index.vue b/src/views/hrm/attendance/exception/index.vue
index ea9bbee..acd5d7a 100644
--- a/src/views/hrm/attendance/exception/index.vue
+++ b/src/views/hrm/attendance/exception/index.vue
@@ -2,12 +2,18 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
+import { ref } from 'vue';
+
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
-import { message } from 'ant-design-vue';
+import { message, Modal, Select } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getAttendanceExceptionPage, submitAttendanceException } from '#/api/hrm/attendance/exception';
+import {
+ getAttendanceExceptionApproveProcessList,
+ getAttendanceExceptionPage,
+ submitAttendanceException,
+} from '#/api/hrm/attendance/exception';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -33,11 +39,37 @@
formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
+/** 鎻愪氦瀹℃壒寮圭獥 */
+const submitModalVisible = ref(false);
+const currentRow = ref<HrmAttendanceExceptionApi.AttendanceException>();
+const processList = ref<HrmAttendanceExceptionApi.ApproveProcess[]>([]);
+const selectedProcessKey = ref<string>();
+const submitLoading = ref(false);
+
/** 鎻愪氦鑰冨嫟寮傚父鐢宠 */
async function handleSubmit(row: HrmAttendanceExceptionApi.AttendanceException) {
- await submitAttendanceException(row.id!);
- message.success('鎻愪氦鎴愬姛');
- handleRefresh();
+ const res = await getAttendanceExceptionApproveProcessList();
+ processList.value = res;
+ currentRow.value = row;
+ selectedProcessKey.value = undefined;
+ submitModalVisible.value = true;
+}
+
+/** 纭鎻愪氦 */
+async function confirmSubmit() {
+ if (!selectedProcessKey.value) {
+ message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+ return;
+ }
+ submitLoading.value = true;
+ try {
+ await submitAttendanceException(currentRow.value!.id!, selectedProcessKey.value);
+ message.success('鎻愪氦瀹℃壒鎴愬姛');
+ submitModalVisible.value = false;
+ handleRefresh();
+ } finally {
+ submitLoading.value = false;
+ }
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -106,5 +138,28 @@
/>
</template>
</Grid>
+
+ <!-- 鎻愪氦瀹℃壒寮圭獥 -->
+ <Modal
+ v-model:open="submitModalVisible"
+ title="閫夋嫨瀹℃壒娴佺▼"
+ width="400px"
+ @ok="confirmSubmit"
+ :confirmLoading="submitLoading"
+ >
+ <Select
+ v-model:value="selectedProcessKey"
+ placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+ class="w-full"
+ >
+ <Select.Option
+ v-for="item in processList"
+ :key="item.key"
+ :value="item.key"
+ >
+ {{ item.name }}
+ </Select.Option>
+ </Select>
+ </Modal>
</Page>
</template>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/holiday/data.ts b/src/views/hrm/attendance/holiday/data.ts
new file mode 100644
index 0000000..822451b
--- /dev/null
+++ b/src/views/hrm/attendance/holiday/data.ts
@@ -0,0 +1,163 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmAttendanceHolidayApi } from '#/api/hrm/attendance/holiday';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'update' | 'detail';
+
+/** 鏂板/缂栬緫鑺傚亣鏃ョ殑琛ㄥ崟 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鑺傚亣鏃ュ悕绉�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傦細鏄ヨ妭銆佸浗搴嗚妭',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'date',
+ label: '鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '閫夋嫨鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'type',
+ label: '绫诲瀷',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.HRM_HOLIDAY_TYPE, 'number'),
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'isLegal',
+ label: '娉曞畾鑺傚亣鏃�',
+ component: 'Checkbox',
+ componentProps: {
+ // @ts-ignore
+ trueValue: 1,
+ falseValue: 0,
+ },
+ help: '鍕鹃�夎〃绀轰负娉曞畾鑺傚亣鏃�',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ maxlength: 500,
+ showCount: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '鑺傚亣鏃ュ悕绉�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヨ妭鍋囨棩鍚嶇О',
+ },
+ },
+ {
+ fieldName: 'date',
+ label: '鏃ユ湡',
+ component: 'RangePicker',
+ componentProps: {
+ allowClear: true,
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_HOLIDAY_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'isLegal',
+ label: '娉曞畾鑺傚亣鏃�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鏄�', value: 1 },
+ { label: '鍚�', value: 0 },
+ ],
+ placeholder: '璇烽�夋嫨',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<HrmAttendanceHolidayApi.Holiday>['columns'] {
+ return [
+ { field: 'name', title: '鑺傚亣鏃ュ悕绉�', minWidth: 150 },
+ { field: 'date', title: '鏃ユ湡', width: 120 },
+ {
+ field: 'type',
+ title: '绫诲瀷',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_HOLIDAY_TYPE },
+ },
+ },
+ {
+ field: 'isLegal',
+ title: '娉曞畾鑺傚亣鏃�',
+ width: 100,
+ slots: {
+ default: 'isLegal',
+ },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 200 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 150,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/attendance/holiday/index.vue b/src/views/hrm/attendance/holiday/index.vue
new file mode 100644
index 0000000..8a33871
--- /dev/null
+++ b/src/views/hrm/attendance/holiday/index.vue
@@ -0,0 +1,147 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmAttendanceHolidayApi } from '#/api/hrm/attendance/holiday';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+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 { $t } from '#/locales';
+
+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({ formType: 'create' }).open();
+}
+
+/** 缂栬緫鑺傚亣鏃� */
+function handleUpdate(row: HrmAttendanceHolidayApi.Holiday) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 鏌ョ湅鑺傚亣鏃� */
+function handleDetail(row: HrmAttendanceHolidayApi.Holiday) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎鑺傚亣鏃� */
+async function handleDelete(id: number) {
+ Modal.confirm({
+ title: '纭鍒犻櫎',
+ content: '纭畾瑕佸垹闄よ鑺傚亣鏃ュ悧锛�',
+ okText: '纭畾',
+ cancelText: '鍙栨秷',
+ onOk: async () => {
+ await deleteAttendanceHoliday(id);
+ message.success('鍒犻櫎鎴愬姛');
+ handleRefresh();
+ },
+ });
+}
+
+/** 瀵煎嚭鑺傚亣鏃� */
+async function handleExport() {
+ const data = await exportAttendanceHoliday(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 getAttendanceHolidayPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<HrmAttendanceHolidayApi.Holiday>,
+});
+</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: ['hrm:attendance-holiday:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['hrm:attendance-holiday:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #isLegal="{ row }">
+ <Tag v-if="row.isLegal === 1" color="red">娉曞畾</Tag>
+ <span v-else>-</span>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鏌ョ湅',
+ type: 'link',
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('ui.actionTitle.edit'),
+ type: 'link',
+ auth: ['hrm:attendance-holiday:update'],
+ onClick: handleUpdate.bind(null, row),
+ },
+ {
+ label: $t('ui.actionTitle.delete'),
+ type: 'link',
+ danger: true,
+ auth: ['hrm:attendance-holiday:delete'],
+ onClick: handleDelete.bind(null, row.id!),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/holiday/modules/form.vue b/src/views/hrm/attendance/holiday/modules/form.vue
new file mode 100644
index 0000000..2ae9c5d
--- /dev/null
+++ b/src/views/hrm/attendance/holiday/modules/form.vue
@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { HrmAttendanceHolidayApi } from '#/api/hrm/attendance/holiday';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createAttendanceHoliday, getAttendanceHoliday, updateAttendanceHoliday } from '#/api/hrm/attendance/holiday';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅鑺傚亣鏃�';
+ }
+ if (formType.value === 'update') {
+ return '缂栬緫鑺傚亣鏃�';
+ }
+ return '鏂板鑺傚亣鏃�';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+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 HrmAttendanceHolidayApi.Holiday;
+ try {
+ if (formType.value === 'update') {
+ await updateAttendanceHoliday(data);
+ } else {
+ await createAttendanceHoliday(data);
+ }
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema(data.formType) });
+ formApi.setDisabled(data.formType === 'detail');
+ modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ await formApi.setValues(await getAttendanceHoliday(data.id));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-[600px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/record/index.vue b/src/views/hrm/attendance/record/index.vue
index 50f3b9e..6aabd6c 100644
--- a/src/views/hrm/attendance/record/index.vue
+++ b/src/views/hrm/attendance/record/index.vue
@@ -12,6 +12,7 @@
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
+import ClockPanel from './modules/clock-panel.vue';
import ImportForm from './modules/import-form.vue';
const [ImportModal, importModalApi] = useVbenModal({
@@ -19,9 +20,19 @@
destroyOnClose: true,
});
+const [ClockModal, clockModalApi] = useVbenModal({
+ connectedComponent: ClockPanel,
+ destroyOnClose: true,
+});
+
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
gridApi.query();
+}
+
+/** 鎵撳崱 */
+function handleClock() {
+ clockModalApi.open();
}
/** 瀵煎叆鑰冨嫟璁板綍 */
@@ -68,11 +79,19 @@
<template>
<Page auto-content-height>
<ImportModal @success="handleRefresh" />
+ <ClockModal @success="handleRefresh" />
<Grid table-title="鑰冨嫟璁板綍鍒楄〃">
<template #toolbar-tools>
<TableAction
:actions="[
{
+ label: '鎵撳崱',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:attendance-record:clock'],
+ onClick: handleClock,
+ },
+ {
label: '瀵煎叆',
type: 'primary',
icon: ACTION_ICON.UPLOAD,
diff --git a/src/views/hrm/attendance/record/modules/clock-panel.vue b/src/views/hrm/attendance/record/modules/clock-panel.vue
new file mode 100644
index 0000000..7f41eed
--- /dev/null
+++ b/src/views/hrm/attendance/record/modules/clock-panel.vue
@@ -0,0 +1,82 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { clockIn, clockOut } from '#/api/hrm/attendance/record';
+
+const emit = defineEmits(['success']);
+
+const location = ref('鍏徃鍓嶅彴');
+const loading = ref(false);
+
+/** 涓婄彮鎵撳崱 */
+async function handleClockIn() {
+ loading.value = true;
+ try {
+ await clockIn({ location: location.value });
+ message.success('涓婄彮鎵撳崱鎴愬姛');
+ emit('success');
+ } catch (e: any) {
+ message.error(e.message || '鎵撳崱澶辫触');
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 涓嬬彮鎵撳崱 */
+async function handleClockOut() {
+ loading.value = true;
+ try {
+ await clockOut({ location: location.value });
+ message.success('涓嬬彮鎵撳崱鎴愬姛');
+ emit('success');
+ } catch (e: any) {
+ message.error(e.message || '鎵撳崱澶辫触');
+ } finally {
+ loading.value = false;
+ }
+}
+
+const [Modal, modalApi] = useVbenModal({
+ onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ location.value = '鍏徃鍓嶅彴';
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="鎵撳崱" class="w-[400px]">
+ <div class="p-4">
+ <div class="mb-4">
+ <label class="mr-2">鎵撳崱鍦扮偣锛�</label>
+ <a-input
+ v-model:value="location"
+ placeholder="璇疯緭鍏ユ墦鍗″湴鐐�"
+ style="width: 200px"
+ />
+ </div>
+ <div class="flex gap-4">
+ <a-button
+ type="primary"
+ size="large"
+ :loading="loading"
+ @click="handleClockIn"
+ >
+ 涓婄彮鎵撳崱
+ </a-button>
+ <a-button
+ size="large"
+ :loading="loading"
+ @click="handleClockOut"
+ >
+ 涓嬬彮鎵撳崱
+ </a-button>
+ </div>
+ </div>
+ </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/rule/data.ts b/src/views/hrm/attendance/rule/data.ts
new file mode 100644
index 0000000..50bca13
--- /dev/null
+++ b/src/views/hrm/attendance/rule/data.ts
@@ -0,0 +1,206 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmAttendanceRuleApi } from '#/api/hrm/attendance/rule';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+import { getDeptList } from '#/api/system/dept';
+import { handleTree } from '#/packages/utils/src';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'update' | 'detail';
+
+/** 鏂板/缂栬緫鑰冨嫟瑙勫垯鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '瑙勫垯鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍒欏悕绉�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'deptId',
+ label: '閫傜敤閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ allowClear: true,
+ placeholder: '璇烽�夋嫨閮ㄩ棬锛堜笉閫夎〃绀哄叏灞�锛�',
+ treeDefaultExpandAll: true,
+ },
+ },
+ {
+ fieldName: 'workStartTime',
+ label: '涓婄彮鏃堕棿',
+ component: 'TimePicker',
+ componentProps: {
+ placeholder: '閫夋嫨涓婄彮鏃堕棿',
+ valueFormat: 'HH:mm:ss',
+ format: 'HH:mm:ss',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'workEndTime',
+ label: '涓嬬彮鏃堕棿',
+ component: 'TimePicker',
+ componentProps: {
+ placeholder: '閫夋嫨涓嬬彮鏃堕棿',
+ valueFormat: 'HH:mm:ss',
+ format: 'HH:mm:ss',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'lateAllowMinutes',
+ label: '杩熷埌鍏佽(鍒嗛挓)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '鍒嗛挓',
+ min: 0,
+ max: 60,
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'earlyAllowMinutes',
+ label: '鏃╅��鍏佽(鍒嗛挓)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '鍒嗛挓',
+ min: 0,
+ max: 60,
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'overtimeThreshold',
+ label: '鍔犵彮璧风畻(灏忔椂)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '灏忔椂',
+ min: 0,
+ precision: 1,
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ maxlength: 500,
+ showCount: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '瑙勫垯鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヨ鍒欏悕绉�',
+ },
+ },
+ {
+ fieldName: 'deptId',
+ label: '閫傜敤閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ allowClear: true,
+ placeholder: '璇烽�夋嫨閮ㄩ棬',
+ treeDefaultExpandAll: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<HrmAttendanceRuleApi.AttendanceRule>['columns'] {
+ return [
+ { field: 'name', title: '瑙勫垯鍚嶇О', minWidth: 150 },
+ {
+ field: 'deptName',
+ title: '閫傜敤閮ㄩ棬',
+ minWidth: 120,
+ formatter: ({ cellValue }) => cellValue || '鍏ㄥ眬',
+ },
+ { field: 'workStartTime', title: '涓婄彮鏃堕棿', width: 100 },
+ { field: 'workEndTime', title: '涓嬬彮鏃堕棿', width: 100 },
+ { field: 'lateAllowMinutes', title: '杩熷埌鍏佽(鍒嗛挓)', width: 120 },
+ { field: 'earlyAllowMinutes', title: '鏃╅��鍏佽(鍒嗛挓)', width: 120 },
+ { field: 'overtimeThreshold', title: '鍔犵彮璧风畻(灏忔椂)', width: 120 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 150,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/attendance/rule/index.vue b/src/views/hrm/attendance/rule/index.vue
new file mode 100644
index 0000000..903d9fe
--- /dev/null
+++ b/src/views/hrm/attendance/rule/index.vue
@@ -0,0 +1,143 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmAttendanceRuleApi } from '#/api/hrm/attendance/rule';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { message, Modal } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deleteAttendanceRule, exportAttendanceRule, getAttendanceRulePage } from '#/api/hrm/attendance/rule';
+import { $t } from '#/locales';
+
+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({ formType: 'create' }).open();
+}
+
+/** 缂栬緫鑰冨嫟瑙勫垯 */
+function handleUpdate(row: HrmAttendanceRuleApi.AttendanceRule) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 鏌ョ湅鑰冨嫟瑙勫垯 */
+function handleDetail(row: HrmAttendanceRuleApi.AttendanceRule) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎鑰冨嫟瑙勫垯 */
+async function handleDelete(id: number) {
+ Modal.confirm({
+ title: '纭鍒犻櫎',
+ content: '纭畾瑕佸垹闄よ鑰冨嫟瑙勫垯鍚楋紵',
+ okText: '纭畾',
+ cancelText: '鍙栨秷',
+ onOk: async () => {
+ await deleteAttendanceRule(id);
+ message.success('鍒犻櫎鎴愬姛');
+ handleRefresh();
+ },
+ });
+}
+
+/** 瀵煎嚭鑰冨嫟瑙勫垯 */
+async function handleExport() {
+ const data = await exportAttendanceRule(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 getAttendanceRulePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<HrmAttendanceRuleApi.AttendanceRule>,
+});
+</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: ['hrm:attendance-rule:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['hrm:attendance-rule:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鏌ョ湅',
+ type: 'link',
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('ui.actionTitle.edit'),
+ type: 'link',
+ auth: ['hrm:attendance-rule:update'],
+ onClick: handleUpdate.bind(null, row),
+ },
+ {
+ label: $t('ui.actionTitle.delete'),
+ type: 'link',
+ danger: true,
+ auth: ['hrm:attendance-rule:delete'],
+ onClick: handleDelete.bind(null, row.id!),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/rule/modules/form.vue b/src/views/hrm/attendance/rule/modules/form.vue
new file mode 100644
index 0000000..60bff96
--- /dev/null
+++ b/src/views/hrm/attendance/rule/modules/form.vue
@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { HrmAttendanceRuleApi } from '#/api/hrm/attendance/rule';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createAttendanceRule, getAttendanceRule, updateAttendanceRule } from '#/api/hrm/attendance/rule';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅鑰冨嫟瑙勫垯';
+ }
+ if (formType.value === 'update') {
+ return '缂栬緫鑰冨嫟瑙勫垯';
+ }
+ return '鏂板鑰冨嫟瑙勫垯';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+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 HrmAttendanceRuleApi.AttendanceRule;
+ try {
+ if (formType.value === 'update') {
+ await updateAttendanceRule(data);
+ } else {
+ await createAttendanceRule(data);
+ }
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema(data.formType) });
+ formApi.setDisabled(data.formType === 'detail');
+ modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ await formApi.setValues(await getAttendanceRule(data.id));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-[800px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/summary/data.ts b/src/views/hrm/attendance/summary/data.ts
new file mode 100644
index 0000000..02c53a2
--- /dev/null
+++ b/src/views/hrm/attendance/summary/data.ts
@@ -0,0 +1,163 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmAttendanceSummaryApi } from '#/api/hrm/attendance/summary';
+
+import { getDeptList } from '#/api/system/dept';
+import { handleTree } from '#/packages/utils/src';
+import { getEmployeeSimpleList } from '#/api/hrm/employee';
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'yearMonth',
+ label: '骞存湀',
+ component: 'DatePicker',
+ componentProps: {
+ picker: 'month',
+ allowClear: true,
+ valueFormat: 'YYYY-MM',
+ format: 'YYYY-MM',
+ style: { width: '100%' },
+ placeholder: '閫夋嫨骞存湀',
+ },
+ },
+ {
+ fieldName: 'deptId',
+ label: '閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ allowClear: true,
+ placeholder: '璇烽�夋嫨閮ㄩ棬',
+ treeDefaultExpandAll: true,
+ },
+ },
+ {
+ fieldName: 'userName',
+ label: '鍛樺伐濮撳悕',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ憳宸ュ鍚�',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<HrmAttendanceSummaryApi.AttendanceSummary>['columns'] {
+ return [
+ { field: 'userName', title: '鍛樺伐濮撳悕', minWidth: 100 },
+ { field: 'deptName', title: '閮ㄩ棬', minWidth: 120 },
+ { field: 'yearMonth', title: '骞存湀', width: 100 },
+ { field: 'workDays', title: '搴斿嚭鍕ゅぉ鏁�', width: 100 },
+ { field: 'actualDays', title: '瀹為檯鍑哄嫟澶╂暟', width: 110 },
+ {
+ field: 'lateCount',
+ title: '杩熷埌娆℃暟',
+ width: 90,
+ slots: {
+ default: 'lateCount',
+ },
+ },
+ {
+ field: 'earlyCount',
+ title: '鏃╅��娆℃暟',
+ width: 90,
+ slots: {
+ default: 'earlyCount',
+ },
+ },
+ {
+ field: 'absentCount',
+ title: '缂哄崱娆℃暟',
+ width: 90,
+ slots: {
+ default: 'absentCount',
+ },
+ },
+ { field: 'overtimeHours', title: '鍔犵彮鏃堕暱(h)', width: 100 },
+ { field: 'leaveHours', title: '璇峰亣鏃堕暱(h)', width: 100 },
+ ];
+}
+
+/** 鐢熸垚缁熻寮圭獥鐨勮〃鍗� */
+export function useGenerateFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'yearMonth',
+ label: '骞存湀',
+ component: 'DatePicker',
+ componentProps: {
+ picker: 'month',
+ valueFormat: 'YYYY-MM',
+ format: 'YYYY-MM',
+ style: { width: '100%' },
+ placeholder: '閫夋嫨骞存湀',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'rangeType',
+ label: '鐢熸垚鑼冨洿',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '鍏ㄥ叕鍙�', value: 'all' },
+ { label: '鎸囧畾閮ㄩ棬', value: 'dept' },
+ { label: '鎸囧畾鍛樺伐', value: 'user' },
+ ],
+ },
+ defaultValue: 'all',
+ },
+ {
+ fieldName: 'deptId',
+ label: '閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ dependencies: {
+ triggerFields: ['rangeType'],
+ show: (values) => values.rangeType === 'dept',
+ required: (values) => values.rangeType === 'dept',
+ },
+ componentProps: {
+ api: async () => {
+ const data = await getDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '閫夋嫨閮ㄩ棬',
+ treeDefaultExpandAll: true,
+ },
+ },
+ {
+ fieldName: 'userId',
+ label: '鍛樺伐',
+ component: 'ApiSelect',
+ dependencies: {
+ triggerFields: ['rangeType'],
+ show: (values) => values.rangeType === 'user',
+ required: (values) => values.rangeType === 'user',
+ },
+ componentProps: {
+ api: async () => {
+ const data = await getEmployeeSimpleList();
+ return data;
+ },
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '閫夋嫨鍛樺伐',
+ showSearch: true,
+ filterOption: true,
+ },
+ },
+ ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/attendance/summary/index.vue b/src/views/hrm/attendance/summary/index.vue
new file mode 100644
index 0000000..f3a2552
--- /dev/null
+++ b/src/views/hrm/attendance/summary/index.vue
@@ -0,0 +1,106 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmAttendanceSummaryApi } from '#/api/hrm/attendance/summary';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { exportAttendanceSummary, getAttendanceSummaryPage } from '#/api/hrm/attendance/summary';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import GenerateForm from './modules/generate-form.vue';
+
+const [GenerateModal, generateModalApi] = useVbenModal({
+ connectedComponent: GenerateForm,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鐢熸垚缁熻 */
+function handleGenerate() {
+ generateModalApi.open();
+}
+
+/** 瀵煎嚭鑰冨嫟缁熻 */
+async function handleExport() {
+ const data = await exportAttendanceSummary(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 getAttendanceSummaryPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<HrmAttendanceSummaryApi.AttendanceSummary>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <GenerateModal @success="handleRefresh" />
+ <Grid table-title="鑰冨嫟缁熻鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鐢熸垚缁熻',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:attendance-summary:generate'],
+ onClick: handleGenerate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['hrm:attendance-summary:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #lateCount="{ row }">
+ <Tag v-if="row.lateCount && row.lateCount > 0" color="warning">{{ row.lateCount }}</Tag>
+ <span v-else>{{ row.lateCount || 0 }}</span>
+ </template>
+ <template #earlyCount="{ row }">
+ <Tag v-if="row.earlyCount && row.earlyCount > 0" color="warning">{{ row.earlyCount }}</Tag>
+ <span v-else>{{ row.earlyCount || 0 }}</span>
+ </template>
+ <template #absentCount="{ row }">
+ <Tag v-if="row.absentCount && row.absentCount > 0" color="error">{{ row.absentCount }}</Tag>
+ <span v-else>{{ row.absentCount || 0 }}</span>
+ </template>
+ </Grid>
+ </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/summary/modules/generate-form.vue b/src/views/hrm/attendance/summary/modules/generate-form.vue
new file mode 100644
index 0000000..b509a85
--- /dev/null
+++ b/src/views/hrm/attendance/summary/modules/generate-form.vue
@@ -0,0 +1,63 @@
+<script lang="ts" setup>
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { generateAttendanceSummary } from '#/api/hrm/attendance/summary';
+
+import { useGenerateFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: useGenerateFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = await formApi.getValues();
+ try {
+ const params: any = {
+ yearMonth: data.yearMonth,
+ };
+ if (data.rangeType === 'dept') {
+ params.deptId = data.deptId;
+ } else if (data.rangeType === 'user') {
+ params.userId = data.userId;
+ }
+ await generateAttendanceSummary(params);
+ await modalApi.close();
+ emit('success');
+ message.success('鐢熸垚鑰冨嫟缁熻鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formApi.resetForm();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="鐢熸垚鑰冨嫟缁熻" class="w-[500px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
\ No newline at end of file
--
Gitblit v1.9.3