feat(hrm,bi): 移植长辉电力的考勤请假展示与 BI 大屏
参考 dev_pro_长辉电力 分支移植:
- 考勤记录页展示请假状态、请假工时;打卡面板提示当天已请假
- 新增员工下拉模糊选择器 searchEmployeeDropdown
- BI 大屏支持新图表(出勤指标、库存采购)与全屏自适应
文件:
- src/api/hrm/attendance/record/index.ts:新增请假/我的状态相关接口
- src/views/hrm/attendance/record/{data.ts,index.vue,modules/clock-panel.vue}
- src/views/hrm/employee/components/select-dropdown.vue(新增)
- src/views/bi/dashboard/index.vue:图表渲染与自适应
- src/views/hrm/{resignation/data.ts,salary/calculation/data.ts}:日期展示
Co-Authored-By: Claude <noreply@anthropic.com>
| | |
| | | clockOutAddress?: string; |
| | | dataSource?: number; |
| | | dataSourceName?: string; |
| | | /** å½å¤©æ¯å¦è¯·åï¼åå¨å®¡æ¹éè¿ä¸ä¸å·¥ä½æ¶æ®µéå ç请ååï¼ */ |
| | | leaveFlag?: boolean; |
| | | /** å½å¤©è¯·åå·¥æ¶ï¼å°æ¶ï¼ï¼æªè¯·å为 0 */ |
| | | leaveHours?: number; |
| | | /** å½å¤©è¯·åç±»åï¼éå·åéç HrmLeaveTypeEnum.typeï¼å¦ "1,2" */ |
| | | leaveTypes?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** æçèå¤ç¶æï¼/hrm/attendance/record/my-statusï¼ */ |
| | | export interface AttendanceMyStatus { |
| | | /** åå·¥å§å */ |
| | | employeeName?: string; |
| | | userName?: string; |
| | | deptName?: string; |
| | | /** è夿¥æ */ |
| | | date?: string; |
| | | /** æå¡å¨æ¶é´ */ |
| | | serverTime?: string; |
| | | /** æ¯å¦å·¥ä½æ¥ */ |
| | | workDay?: boolean; |
| | | /** 伿¯æ¥åå ï¼å¦ï¼å½åºèãå¨å
*/ |
| | | restDayReason?: string; |
| | | /** æå¡å°ç¹åç§° */ |
| | | locationName?: string; |
| | | locationAddress?: string; |
| | | /** æ¯å¦å¯ç¨å®ä½èå´æ ¡éª */ |
| | | locationRequired?: boolean; |
| | | /** å
许æå¡åå¾ï¼åä½ç±³ */ |
| | | radiusMeters?: number; |
| | | /** 彿¥èå¤è®°å½ï¼æªæå¡æ¶ä¸º null */ |
| | | record?: AttendanceRecord; |
| | | /** 仿¥è®°å½ID */ |
| | | recordId?: number; |
| | | clockInTime?: string; |
| | | clockOutTime?: string; |
| | | clockInType?: number; |
| | | clockOutType?: number; |
| | | clockInDone?: boolean; |
| | | clockOutDone?: boolean; |
| | | workHours?: number; |
| | | overtimeHours?: number; |
| | | location?: string; |
| | | /** å½å¤©æ¯å¦è¯·å */ |
| | | onLeave?: boolean; |
| | | /** å½å¤©è¯·åå·¥æ¶ï¼å°æ¶ï¼ï¼æªè¯·å为 0 */ |
| | | leaveHours?: number; |
| | | /** å½å¤©è¯·åç±»ååç§°ï¼å¤æ®µç¨é¡¿å·åéï¼å¦ "äºåãç
å" */ |
| | | leaveTypeNames?: string; |
| | | /** å½å坿å¡ç¶æ */ |
| | | clockable?: { |
| | | clockIn?: boolean; |
| | | clockOut?: boolean; |
| | | /** ä¸ä¸æ¥å¨ä½ï¼in-ä¸çæå¡ï¼out-ä¸çæå¡ï¼none-æ éæå¡ */ |
| | | nextAction?: 'in' | 'none' | 'out' | string; |
| | | /** æç¤ºææ¡ */ |
| | | message?: string; |
| | | }; |
| | | } |
| | | |
| | | /** èå¤ç»è®¡ */ |
| | |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * æ¥è¯¢å½åç»å½äººçèå¤ç¶æ |
| | | * |
| | | * @param date è夿¥æï¼æ ¼å¼ yyyy-MM-ddï¼ä¸ä¼ åæå¡å¨å½å¤© |
| | | */ |
| | | export function getAttendanceMyStatus(date?: string) { |
| | | return requestClient.get<HrmAttendanceRecordApi.AttendanceMyStatus>( |
| | | '/hrm/attendance/record/my-status', |
| | | date ? { params: { date } } : undefined, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢èå¤ç»è®¡ï¼ææï¼ */ |
| | | export function getAttendanceStatistics(params: { |
| | | userId?: number; |
| | |
| | | loading.value = false; |
| | | await nextTick(); |
| | | if (!disposed && sequence === requestSequence) { |
| | | layoutFill(); |
| | | resizeAllCharts(); |
| | | if (!background) { |
| | | const intervals = charts.value.map((chart) => chart.refreshInterval).filter((interval) => interval > 0); |
| | |
| | | const dashboardRef = ref<HTMLElement>(); |
| | | let resizeObserver: ResizeObserver | null = null; |
| | | |
| | | /** |
| | | * 精确计ç®å¾è¡¨åºå¯ç¨é«åº¦ï¼åå
¥ CSS åé --fill-h |
| | | * |
| | | * ä»
é flex + 1fr å¨é¨ååºæ¯ä¸çæï¼è¿éç´æ¥éæµå¤´é¨ãKPI è¡ã |
| | | * è¡¨æ ¼å¡çå ç¨é«åº¦ï¼æå©ä½é«åº¦ç®åºæ¥ç»å¾è¡¨åºã |
| | | */ |
| | | function layoutFill() { |
| | | const el = dashboardRef.value; |
| | | if (!el) return; |
| | | if (!isFullscreen.value) { |
| | | el.style.removeProperty('--fill-h'); |
| | | return; |
| | | } |
| | | const header = el.querySelector('.dash-header') as HTMLElement | null; |
| | | const kpi = el.querySelector('.kpi-row') as HTMLElement | null; |
| | | const tables = el.querySelectorAll<HTMLElement>('.table-card'); |
| | | const cs = getComputedStyle(el); |
| | | let used = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0); |
| | | if (header) used += header.offsetHeight + 24; |
| | | if (kpi) used += kpi.offsetHeight + 16; |
| | | tables.forEach((t) => { used += t.offsetHeight + 16; }); |
| | | const avail = Math.max(240, window.innerHeight - used); |
| | | el.style.setProperty('--fill-h', `${avail}px`); |
| | | } |
| | | |
| | | function resizeAllCharts() { |
| | | setTimeout(() => window.dispatchEvent(new Event('resize')), 100); |
| | | } |
| | |
| | | await document.exitFullscreen(); |
| | | isFullscreen.value = false; |
| | | } |
| | | setTimeout(resizeAllCharts, 400); |
| | | setTimeout(() => { layoutFill(); resizeAllCharts(); }, 400); |
| | | } |
| | | function onFullscreenChange() { |
| | | isFullscreen.value = !!document.fullscreenElement; |
| | | setTimeout(resizeAllCharts, 300); |
| | | setTimeout(() => { layoutFill(); resizeAllCharts(); }, 300); |
| | | } |
| | | |
| | | // ======== KPI æ°åå¨ç» ======== |
| | |
| | | onMounted(() => { |
| | | document.addEventListener('fullscreenchange', onFullscreenChange); |
| | | setupResizeObserver(); |
| | | window.addEventListener('resize', layoutFill); |
| | | loadData(); |
| | | }); |
| | | onBeforeUnmount(() => { |
| | |
| | | requestSequence++; |
| | | if (dataRefreshTimer) clearInterval(dataRefreshTimer); |
| | | document.removeEventListener('fullscreenchange', onFullscreenChange); |
| | | window.removeEventListener('resize', layoutFill); |
| | | resizeObserver?.disconnect(); |
| | | }); |
| | | </script> |
| | |
| | | .kpi-row { |
| | | position: relative; z-index: 1; |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); |
| | | grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); |
| | | gap: 12px; margin-bottom: 16px; |
| | | } |
| | | |
| | |
| | | .chart-row { |
| | | position: relative; z-index: 1; |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); |
| | | grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); |
| | | gap: 12px; |
| | | margin-bottom: 16px; |
| | | } |
| | |
| | | |
| | | /* ======== å
¨å± ======== */ |
| | | .is-fullscreen { |
| | | max-height: none; min-height: 100vh; padding: 24px 28px; border-radius: 0; overflow-y: auto; |
| | | height: 100vh; |
| | | min-height: 100vh; |
| | | max-height: 100vh; |
| | | padding: 24px 28px; |
| | | border-radius: 0; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | /* 头é¨ä¸ KPI è¡ä¿æèªç¶é«åº¦ */ |
| | | .is-fullscreen > .dash-header, |
| | | .is-fullscreen > .kpi-row { |
| | | flex: 0 0 auto; |
| | | } |
| | | |
| | | /* å¾è¡¨åºåæå©ä½é«åº¦ï¼è¡é«ååï¼é¿å
åºé¨å¤§ççç½ */ |
| | | .is-fullscreen > .chart-row { |
| | | flex: 0 0 auto; |
| | | /* é«åº¦ç± layoutFill() éæµååå
¥ï¼ä¿è¯æ£å¥½å¡«æ»¡è§å£ */ |
| | | height: var(--fill-h, auto); |
| | | grid-auto-rows: minmax(200px, 1fr); |
| | | align-content: stretch; |
| | | } |
| | | |
| | | /* è¡¨æ ¼å¡ç䏿¢é«åº¦ï¼å
容è¶
åºæ¶å®¹å¨æ»å¨ */ |
| | | .is-fullscreen > .table-card { |
| | | flex: 0 1 auto; |
| | | } |
| | | .is-fullscreen::before, |
| | | .is-fullscreen::after { |
| | |
| | | :deep(.ant-empty-description) { color: var(--text-muted); } |
| | | |
| | | /* ======== ååºå¼ ======== */ |
| | | |
| | | /* å¤§å± / 4Kï¼æ¾å¤§é´è·ä¸åå·ï¼å¾è¡¨å宽å 大 */ |
| | | @media (min-width: 1921px) { |
| | | .bi-dashboard { padding: 24px 32px; } |
| | | .kpi-row { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } |
| | | .chart-row { grid-template-columns: repeat(auto-fit, minmax(460px, 1fr)); } |
| | | .kpi-value { font-size: 28px; } |
| | | } |
| | | |
| | | /* å®½å± */ |
| | | @media (min-width: 1600px) and (max-width: 1920px) { |
| | | .bi-dashboard { padding: 20px 26px; } |
| | | } |
| | | |
| | | /* ç¬è®°æ¬ / å¹³æ¿æ¨ªå±ï¼å¤´é¨ç©ºé´ç´§å¼ æ¶å缩 */ |
| | | @media (max-width: 1280px) { |
| | | .dash-status-tags { display: none; } |
| | | .dash-title-group p { display: none; } |
| | | } |
| | | |
| | | /* å¹³æ¿ç«å± */ |
| | | @media (max-width: 1024px) { |
| | | .bi-dashboard { padding: 14px; } |
| | | .dash-header { flex-wrap: wrap; gap: 10px; } |
| | | .dash-header-divider { display: none; } |
| | | .kpi-row { grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); } |
| | | .chart-row { grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); } |
| | | } |
| | | |
| | | /* ææº */ |
| | | @media (max-width: 768px) { |
| | | .bi-dashboard { padding: 10px; } |
| | | .dash-header { flex-wrap: wrap; } |
| | | .dash-header { flex-wrap: wrap; gap: 8px; } |
| | | .dash-header-divider { display: none; } |
| | | .kpi-row { grid-template-columns: 1fr 1fr; } |
| | | .chart-row { grid-template-columns: 1fr; } |
| | | .dash-header-right { width: 100%; justify-content: space-between; } |
| | | .dash-live-badge { display: none; } |
| | | .kpi-row { grid-template-columns: 1fr 1fr; gap: 8px; } |
| | | .kpi-card { padding: 10px 12px; gap: 8px; } |
| | | .kpi-icon { width: 32px; height: 32px; font-size: 15px; } |
| | | .kpi-value { font-size: 18px; } |
| | | .kpi-label { font-size: 10px; } |
| | | .chart-row { grid-template-columns: 1fr; gap: 10px; } |
| | | .chart-row > div { grid-column: 1 / -1 !important; } |
| | | .kpi-value { font-size: 20px; } |
| | | } |
| | | |
| | | /* è¶
çªå±ï¼å°å±ææºï¼ */ |
| | | @media (max-width: 400px) { |
| | | .kpi-row { grid-template-columns: 1fr; } |
| | | } |
| | | |
| | | </style> |
| | |
| | | import type { HrmAttendanceRecordApi } from '#/api/hrm/attendance/record'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | import { getDictLabel, getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | |
| | | /** 请åç±»åçå
åºåç§°ï¼ä¸å端 HrmLeaveTypeEnum ä¿æä¸è´ */ |
| | | const LEAVE_TYPE_FALLBACK_LABELS: Record<number, string> = { |
| | | 1: 'äºå', |
| | | 2: 'ç
å', |
| | | 3: 'å¹´å', |
| | | 4: 'å©å', |
| | | 5: '产å', |
| | | 6: '丧å', |
| | | }; |
| | | |
| | | /** |
| | | * 请åç±»åçåå
¸æ ç¾ |
| | | * |
| | | * åå
¸ hrm_leave_type æªåæ¥æ¶è¿å空串ï¼ç±è°ç¨æ¹å³å®å
åºï¼ |
| | | * é¿å
è¿ç±»ç¯å¢æè¯·åç±»åæ¾ç¤ºæ "-" è让人误以为没æè¯·åã |
| | | */ |
| | | export function getLeaveTypeLabel(type: number | string) { |
| | | const label = getDictLabel(DICT_TYPE.HRM_LEAVE_TYPE, String(type)); |
| | | return label || LEAVE_TYPE_FALLBACK_LABELS[Number(type)] || ''; |
| | | } |
| | | |
| | | /** æ leaveTypesï¼å¦ "1,2"ï¼ç¿»è¯æè¯·åç±»ååç§°ï¼å¤æ®µç¨é¡¿å·åé */ |
| | | export function formatLeaveTypeNames(leaveTypes?: string) { |
| | | if (!leaveTypes) { |
| | | return ''; |
| | | } |
| | | return leaveTypes |
| | | .split(',') |
| | | .map((type) => type.trim()) |
| | | .filter(Boolean) |
| | | .map((type) => getLeaveTypeLabel(type) || type) |
| | | .join('ã'); |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | |
| | | placeholder: 'è¯·éæ©ä¸çæå¡ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'leaveFlag', |
| | | label: 'æ¯å¦è¯·å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: [ |
| | | { label: '已请å', value: true }, |
| | | { label: 'æªè¯·å', value: false }, |
| | | ], |
| | | placeholder: 'è¯·éæ©æ¯å¦è¯·å', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | props: { type: DICT_TYPE.HRM_CLOCK_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'leaveFlag', |
| | | title: '请å', |
| | | width: 160, |
| | | slots: { |
| | | default: 'leave', |
| | | }, |
| | | }, |
| | | { field: 'workHours', title: '工使¶é¿(h)', width: 100 }, |
| | | { field: 'overtimeHours', title: 'å çæ¶é¿(h)', width: 100 }, |
| | | { field: 'location', title: 'æå¡å°ç¹', minWidth: 150 }, |
| | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { exportAttendanceRecord, getAttendanceRecordPage } from '#/api/hrm/attendance/record'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import { formatLeaveTypeNames, useGridColumns, useGridFormSchema } from './data'; |
| | | import ClockPanel from './modules/clock-panel.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #leave="{ row }"> |
| | | <Tag v-if="row.leaveFlag" color="blue"> |
| | | {{ formatLeaveTypeNames(row.leaveTypes) || '请å' }} |
| | | <span v-if="Number(row.leaveHours) > 0"> |
| | | {{ Number(row.leaveHours).toFixed(2) }}h |
| | | </span> |
| | | </Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { AttendanceClockLocation, AttendanceClockResponse } from '#/api/hrm/attendance/record'; |
| | | import type { AttendanceClockLocation, AttendanceClockResponse, HrmAttendanceRecordApi } from '#/api/hrm/attendance/record'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { computed, 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'; |
| | | import { clockIn, clockOut, getAttendanceMyStatus } from '#/api/hrm/attendance/record'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | |
| | | const location = ref(DEFAULT_LOCATION); |
| | | const loading = ref(false); |
| | | const locating = ref(false); |
| | | /** 仿¥èå¤ç¶æï¼å«è¯·åä¿¡æ¯ï¼ï¼å 载失败æ¶ä¸º undefined */ |
| | | const myStatus = ref<HrmAttendanceRecordApi.AttendanceMyStatus>(); |
| | | const statusLoading = ref(false); |
| | | /** å½åæå¡è¿åçå½å¤©è®°å½ï¼ç¨äºæå¡å峿¶å·æ°è¯·åä¿¡æ¯ */ |
| | | const lastClockRecord = ref<HrmAttendanceRecordApi.AttendanceRecord>(); |
| | | |
| | | /** å½å¤©æ¯å¦è¯·åï¼ä¼å
ç¨ my-status ç onLeaveï¼å
¶æ¬¡ç¨å½å¤©è®°å½ä¸ç leaveFlag */ |
| | | const onLeave = computed( |
| | | () => |
| | | myStatus.value?.onLeave === true || |
| | | myStatus.value?.record?.leaveFlag === true || |
| | | lastClockRecord.value?.leaveFlag === true, |
| | | ); |
| | | |
| | | /** 请åç±»ååç§°ï¼å¦ "äºåãç
å" */ |
| | | const leaveTypeNames = computed( |
| | | () => |
| | | myStatus.value?.leaveTypeNames || |
| | | formatLeaveTypes( |
| | | myStatus.value?.record?.leaveTypes || |
| | | lastClockRecord.value?.leaveTypes, |
| | | ), |
| | | ); |
| | | |
| | | /** 请åå·¥æ¶ï¼å°æ¶ï¼ */ |
| | | const leaveHours = computed( |
| | | () => |
| | | Number( |
| | | myStatus.value?.leaveHours ?? |
| | | myStatus.value?.record?.leaveHours ?? |
| | | lastClockRecord.value?.leaveHours ?? |
| | | 0, |
| | | ) || 0, |
| | | ); |
| | | |
| | | /** 请åç±»åå
åºåç§°ï¼ä¸å端 HrmLeaveTypeEnum ä¿æä¸è´ */ |
| | | const LEAVE_TYPE_NAMES: Record<number, string> = { |
| | | 1: 'äºå', |
| | | 2: 'ç
å', |
| | | 3: 'å¹´å', |
| | | 4: 'å©å', |
| | | 5: '产å', |
| | | 6: '丧å', |
| | | }; |
| | | |
| | | /** æ "1,2" ç¿»è¯æ "äºåãç
å" */ |
| | | function formatLeaveTypes(leaveTypes?: string) { |
| | | if (!leaveTypes) { |
| | | return ''; |
| | | } |
| | | return leaveTypes |
| | | .split(',') |
| | | .map((type) => type.trim()) |
| | | .filter(Boolean) |
| | | .map((type) => LEAVE_TYPE_NAMES[Number(type)] ?? '') |
| | | .filter(Boolean) |
| | | .join('ã'); |
| | | } |
| | | |
| | | /** å è½½å½åç»å½äººçèå¤ç¶æï¼å¤±è´¥æ¶ä¸é»ææå¡ */ |
| | | async function loadMyStatus() { |
| | | statusLoading.value = true; |
| | | try { |
| | | myStatus.value = await getAttendanceMyStatus(); |
| | | } catch { |
| | | // æ¥å£é误已ç±è¯·æ±æ¦æªå¨ç»ä¸æç¤ºï¼è¿ééé»é级ï¼ä¸å½±åæå¡ä¸»æµç¨ |
| | | myStatus.value = undefined; |
| | | } finally { |
| | | statusLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * è·åæµè§å¨å®ä½å¹¶ç»ä¸ä¸º WGS84 交ç»åç«¯æ ¡éªã |
| | |
| | | action === 'in' |
| | | ? await clockIn(position, locationText) |
| | | : await clockOut(position, locationText); |
| | | lastClockRecord.value = result?.record; |
| | | message.success( |
| | | buildSuccessMessage(action === 'in' ? 'ä¸çæå¡' : 'ä¸çæå¡', result), |
| | | ); |
| | | // æå¡åå·æ°ä»æ¥ç¶æï¼å«è¯·åä¿¡æ¯ï¼ï¼ä¿è¯é¢æ¿ä¸çæç¤ºä¸å端ä¸è´ |
| | | await loadMyStatus(); |
| | | emit('success'); |
| | | } catch (error: any) { |
| | | // æ¥å£é误已ç±è¯·æ±æ¦æªå¨ç»ä¸æç¤ºï¼è¿éåªå
åºæ¬å°å¼å¸¸ï¼é¿å
éå¤å¼¹åº |
| | |
| | | |
| | | const [Modal] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | if (isOpen) { |
| | | // æ¯æ¬¡æå¼é½æä¸æ¬¡ä»æ¥ç¶æï¼é¿å
å±ç¤ºè¿æç请åä¿¡æ¯ |
| | | myStatus.value = undefined; |
| | | lastClockRecord.value = undefined; |
| | | void loadMyStatus(); |
| | | } else { |
| | | location.value = DEFAULT_LOCATION; |
| | | } |
| | | }, |
| | |
| | | <template> |
| | | <Modal title="æå¡" class="w-[420px]"> |
| | | <div class="p-4"> |
| | | <a-alert |
| | | v-if="onLeave" |
| | | class="mb-4" |
| | | type="info" |
| | | show-icon |
| | | message="仿¥å·²è¯·åï¼å¦éä¸ç坿£å¸¸æå¡" |
| | | > |
| | | <template #description> |
| | | <div class="text-xs"> |
| | | <span v-if="leaveTypeNames">请åç±»åï¼{{ leaveTypeNames }}</span> |
| | | <span v-if="leaveHours > 0" class="ml-2"> |
| | | 请å {{ leaveHours.toFixed(2) }} å°æ¶ |
| | | </span> |
| | | <div class="mt-1 text-gray-500"> |
| | | 请åå½å¤©æå¡ä¸è®¡è¿å°/æ©éï¼ä¹ä¸è®¡ç¼ºå¡ã |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </a-alert> |
| | | <div v-else-if="statusLoading" class="mb-4 text-xs text-gray-400"> |
| | | æ£å¨å è½½ä»æ¥èå¤ç¶æâ¦ |
| | | </div> |
| | | <div class="mb-4"> |
| | | <label class="mr-2">æå¡å°ç¹ï¼</label> |
| | | <a-input |
| | |
| | | @press-enter="handleSearch" |
| | | /> |
| | | <Button :loading="loading" @click="handleSearch">æç´¢å°å</Button> |
| | | <Button @click="handleCurrentLocation">å®ä½å°å½åä½ç½®</Button> |
| | | </div> |
| | | <div |
| | | ref="mapContainer" |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmEmployeeApi } from '#/api/hrm/employee'; |
| | | |
| | | import { computed, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Select } from 'ant-design-vue'; |
| | | |
| | | import { getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | |
| | | defineOptions({ name: 'EmployeeSelect', inheritAttrs: false }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | allowClear?: boolean; |
| | | disabled?: boolean; |
| | | /** é¨é¨ IDï¼ä¼ å
¥ååªæ¾ç¤ºè¯¥é¨é¨çåå·¥ */ |
| | | deptId?: number; |
| | | /** å ä½ææ¡ */ |
| | | placeholder?: string; |
| | | modelValue?: number; |
| | | }>(), |
| | | { |
| | | allowClear: true, |
| | | disabled: false, |
| | | deptId: undefined, |
| | | placeholder: 'è¾å
¥å§åæå·¥å·æç´¢', |
| | | modelValue: undefined, |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: [item: HrmEmployeeApi.EmployeeSimple | undefined]; |
| | | 'update:modelValue': [value: number | undefined]; |
| | | }>(); |
| | | |
| | | const loading = ref(false); |
| | | const allOptions = ref<HrmEmployeeApi.EmployeeSimple[]>([]); |
| | | |
| | | /** 䏿é项ï¼å§åï¼é¨é¨ï¼ */ |
| | | const options = computed(() => |
| | | allOptions.value |
| | | .filter((e) => !props.deptId || (e as any).deptId === props.deptId) |
| | | .map((e) => ({ |
| | | label: e.deptName ? `${e.name}ï¼${e.deptName}ï¼` : e.name, |
| | | value: e.id, |
| | | raw: e, |
| | | })), |
| | | ); |
| | | |
| | | onMounted(async () => { |
| | | loading.value = true; |
| | | try { |
| | | allOptions.value = (await getEmployeeSimpleList()) || []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | |
| | | /** 模ç³å¹é
ï¼å§å / å·¥å· / é¨é¨ */ |
| | | function filterOption(input: string, option: any) { |
| | | const kw = String(input ?? '').toLowerCase().trim(); |
| | | if (!kw) { |
| | | return true; |
| | | } |
| | | const raw = option?.raw || {}; |
| | | const hay = [raw.name, raw.employeeNo, raw.deptName, option?.label] |
| | | .filter(Boolean) |
| | | .join(' ') |
| | | .toLowerCase(); |
| | | return hay.includes(kw); |
| | | } |
| | | |
| | | function handleChange(val: any) { |
| | | const id = typeof val === 'number' ? val : undefined; |
| | | emit('update:modelValue', id); |
| | | emit('change', allOptions.value.find((e) => e.id === id)); |
| | | } |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | () => { |
| | | // ä»
ç¨äºè§¦åè§å¾æ´æ° |
| | | }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Select |
| | | v-bind="$attrs" |
| | | :allow-clear="allowClear" |
| | | :disabled="disabled" |
| | | :filter-option="filterOption" |
| | | :loading="loading" |
| | | :options="options" |
| | | :placeholder="placeholder" |
| | | :value="modelValue" |
| | | class="w-full" |
| | | option-filter-prop="label" |
| | | show-search |
| | | @change="handleChange" |
| | | /> |
| | | </template> |
| | |
| | | label: '交æ¥äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | api: getEmployeeSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | |
| | | label: 'åå·¥', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | api: getEmployeeSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |