| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref } from 'vue' |
| | | import { computed, nextTick, ref, watch } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | |
| | | interface FormModel { |
| | |
| | | }) |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const showRestTimeSection = ref(false) |
| | | const formModel = ref<FormModel>({ |
| | | shiftName: '', |
| | | startTime: null, |
| | |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | [() => show.value, () => formModel.value.isRest], |
| | | async ([modalVisible, isRest]) => { |
| | | if (!modalVisible || !isRest) { |
| | | showRestTimeSection.value = false |
| | | if (!isRest) { |
| | | // 关闭中途休息时清空时间,避免旧值残留 |
| | | formModel.value.restStartTime = null |
| | | formModel.value.restEndTime = null |
| | | } |
| | | return |
| | | } |
| | | |
| | | // 等弹窗可见后再挂载时间组件,避免动态渲染异常 |
| | | await nextTick() |
| | | showRestTimeSection.value = true |
| | | }, |
| | | { immediate: true }, |
| | | ) |
| | | |
| | | function openEdit(data: any) { |
| | | formModel.value = { |
| | | ...formModel.value, |
| | |
| | | <n-switch v-model:value="formModel.isRest" /> |
| | | </n-form-item> |
| | | |
| | | <div v-if="formModel.isRest" class="grid grid-cols-3 gap-x-24px"> |
| | | <div v-if="showRestTimeSection" class="grid grid-cols-3 gap-x-24px"> |
| | | <n-form-item label="开始休息时间" path="restStartTime"> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.restStartTime" |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { ref, computed } from 'vue' |
| | | import { computed, ref } from 'vue' |
| | | import { useBoolean } from '@/hooks' |
| | | import { useRoute } from 'vue-router' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NTag } from 'naive-ui' |
| | | import { getClockInStatisticsDetailList, exportClockInStatisticsDetails } from '@/service/api/attendance' |
| | | import { exportClockInStatisticsDetails, getClockInStatisticsDetailList } from '@/service/api/attendance' |
| | | import { downloadFile } from '@/utils' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import dayjs from 'dayjs' |
| | |
| | | |
| | | const searchModel = ref({ |
| | | appUserId: null as any, |
| | | statisticDate: null as [number, number] | null |
| | | statisticDate: null as [number, number] | null, |
| | | }) |
| | | |
| | | // 统计数据 |
| | |
| | | shouldClockInDays: '', |
| | | clockInDays: '', |
| | | notClockInDays: '', |
| | | lateEarly: '' |
| | | lateEarly: '', |
| | | }) |
| | | |
| | | const detailList = ref<any[]>([]) |
| | |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | const handleReset = () => { |
| | | function handleReset() { |
| | | searchModel.value = { |
| | | appUserId: route.params.id, |
| | | statisticDate: null |
| | | statisticDate: null, |
| | | } |
| | | handleSearch() |
| | | } |
| | |
| | | getList() |
| | | } |
| | | |
| | | const handleExport = async () => { |
| | | async function handleExport() { |
| | | const params = { |
| | | ...pageParams.value, |
| | | appUserId: searchModel.value.appUserId, |
| | |
| | | { |
| | | label: '打卡日期', |
| | | path: 'statisticDate', |
| | | render: () => <n-date-picker style="width: 280px" actions={[]} close-on-select={true} v-model:value={searchModel.value.statisticDate} format="yyyy/MM/dd" type="daterange" clearable start-placeholder="开始时间" end-placeholder="结束时间" /> |
| | | } |
| | | render: () => <n-date-picker style="width: 280px" actions={[]} close-on-select={true} v-model:value={searchModel.value.statisticDate} format="yyyy/MM/dd" type="daterange" clearable start-placeholder="开始时间" end-placeholder="结束时间" />, |
| | | }, |
| | | ]) |
| | | |
| | | const getResultTagType = (result: string) => { |
| | | if (result === '正常') return 'primary' |
| | | if (result === '迟到') return 'warning' |
| | | if (result === '缺卡') return 'error' |
| | | if (result === '早退') return 'warning' |
| | | function getResultTagType(result: string) { |
| | | if (result === '正常') |
| | | return 'primary' |
| | | if (result === '迟到') |
| | | return 'warning' |
| | | if (result === '缺卡') |
| | | return 'error' |
| | | if (result === '早退') |
| | | return 'warning' |
| | | return 'default' |
| | | } |
| | | |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'serialNumber', width: 60, align: 'left', resizable: true, fixed: 'left' }, |
| | | { title: '操作人', key: 'name', width: 120, align: 'left', resizable: true, fixed: 'left' }, |
| | | { title: '角色', key: 'roleName', width: 120, align: 'left', resizable: true, |
| | | render: row => <span style="padding:2px 8px;background-color: #E6F4FF;color:#0068FF;border-radius: 4px;">{row.roleName}</span> |
| | | }, |
| | | { title: '所属队伍', key: 'teamName', width: 120, align: 'left', resizable: true, }, |
| | | { title: '操作人', key: 'name', width: 120, align: 'left', resizable: true, fixed: 'left' }, |
| | | { |
| | | title: '角色', |
| | | key: 'roleName', |
| | | width: 140, |
| | | align: 'left', |
| | | resizable: true, |
| | | render: (row) => { |
| | | const roleNames = String(row.roleName || '') |
| | | .split(';') |
| | | .map((item: string) => item.trim()) |
| | | .filter(Boolean) |
| | | |
| | | if (!roleNames.length) |
| | | return '-' |
| | | |
| | | return ( |
| | | <div class="flex flex-wrap gap-6px"> |
| | | {roleNames.map((roleName: string) => ( |
| | | <NTag type="primary" bordered={false} size="small" style={{ borderRadius: '4px' }}> |
| | | {roleName} |
| | | </NTag> |
| | | ))} |
| | | </div> |
| | | ) |
| | | }, |
| | | }, |
| | | { title: '所属队伍', key: 'teamName', width: 120, align: 'left', resizable: true }, |
| | | { |
| | | title: '日期', |
| | | key: 'clockTime', |
| | | width: 100, |
| | | align: 'left', |
| | | resizable: true, |
| | | render: row => row.clockTime ? dayjs(row.clockTime).format('YYYY/MM/DD ') : '' |
| | | render: row => row.clockTime ? dayjs(row.clockTime).format('YYYY/MM/DD ') : '', |
| | | }, |
| | | { title: '班次', key: 'shiftName', width: 190, align: 'left', resizable: true, |
| | | render: (row) => { |
| | | return h('span', { |
| | | style: { |
| | | display: 'inline-block', |
| | | padding: '2px 12px', |
| | | borderRadius: '4px', |
| | | fontSize: '12px', |
| | | backgroundColor: row.shiftColor, |
| | | color: 'rgba(23, 31, 38, 0.8)', |
| | | }, |
| | | }, row.shiftName) |
| | | }, |
| | | }, |
| | | { title: '班次', key: 'shiftName', width: 190, align: 'left', resizable: true, render: (row) => { |
| | | return h('span', { |
| | | style: { |
| | | display: 'inline-block', |
| | | padding: '2px 12px', |
| | | borderRadius: '4px', |
| | | fontSize: '12px', |
| | | backgroundColor: row.shiftColor, |
| | | color: 'rgba(23, 31, 38, 0.8)', |
| | | }, |
| | | }, row.shiftName) |
| | | } }, |
| | | { |
| | | title: '上班打卡时间', |
| | | key: 'clockInTime', |
| | | width: 170, |
| | | align: 'left', |
| | | resizable: true, |
| | | render: row => row.clockInTime ? dayjs(row.clockInTime).format('YYYY/MM/DD HH:mm') : '' |
| | | render: row => row.clockInTime ? dayjs(row.clockInTime).format('YYYY/MM/DD HH:mm') : '', |
| | | }, |
| | | { |
| | | title: '下班打卡时间', |
| | |
| | | width: 170, |
| | | align: 'left', |
| | | resizable: true, |
| | | render: row => row.clockOutTime ? dayjs(row.clockOutTime).format('YYYY/MM/DD HH:mm') : '' |
| | | render: row => row.clockOutTime ? dayjs(row.clockOutTime).format('YYYY/MM/DD HH:mm') : '', |
| | | }, |
| | | { |
| | | title: '上班打卡结果', |
| | |
| | | align: 'left', |
| | | resizable: true, |
| | | render: (row) => { |
| | | if (!row.clockInResult) return '' |
| | | if (!row.clockInResult) |
| | | return '' |
| | | return ( |
| | | <NTag type={getResultTagType(row.clockInResult)} size="small" bordered={false}> |
| | | {row.clockInResult} |
| | | </NTag> |
| | | ) |
| | | } |
| | | }, |
| | | }, |
| | | |
| | | |
| | | { |
| | | title: '下班打卡结果', |
| | | key: 'clockOutResult', |
| | |
| | | align: 'left', |
| | | resizable: true, |
| | | render: (row) => { |
| | | if (!row.clockOutResult) return '' |
| | | if (!row.clockOutResult) |
| | | return '' |
| | | return ( |
| | | <NTag type={getResultTagType(row.clockOutResult)} size="small" bordered={false}> |
| | | {row.clockOutResult} |
| | | </NTag> |
| | | ) |
| | | } |
| | | }, |
| | | }, |
| | | { title: '打卡地址', key: 'clockInLocation', width: 320, align: 'left', ellipsis: { tooltip: true }} |
| | | { title: '打卡地址', key: 'clockInLocation', width: 320, align: 'left', ellipsis: { tooltip: true } }, |
| | | ]) |
| | | |
| | | async function getList() { |
| | |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset"> |
| | | 重置 |
| | | </n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch"> |
| | | 查询 |
| | | </n-button> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | |
| | | <div class="flex gap-2"> |
| | | <n-button |
| | | style="width: 100px; height: 32px;" |
| | | @click="handleExport"> |
| | | @click="handleExport" |
| | | > |
| | | 导出打卡明细 |
| | | </n-button> |
| | | </div> |
| | | </n-flex> |
| | | </template> |
| | | <div class="flex-1 flex flex-col h-full"> |
| | | <n-data-table :columns="columns" :data="detailList" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" :scroll-x="1510" class="flex-1"> |
| | | <n-data-table |
| | | :columns="columns" :data="detailList" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" :scroll-x="1510" class="flex-1" flex-height |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img :src="noDataImg" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }"> |
| | |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onActivated, onMounted, ref } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | import { NButton, NTag, useDialog } from 'naive-ui' |
| | | import { NButton, NSwitch, NTag, useDialog } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { useRouter } from 'vue-router' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | |
| | | |
| | | async function handleToggleStatus(row: SafetyOfficerItem) { |
| | | const nextStatus = row.status === 1 ? 0 : 1 |
| | | const actionText = nextStatus === 1 ? '停用' : '启用' |
| | | const actionText = nextStatus === 1 ? '启用' : '停用' |
| | | dialog.warning({ |
| | | title: `是否确认${actionText}该安全员?`, |
| | | content: ``, |
| | |
| | | <NButton text v-permission="2026061540" type="primary" onClick={() => handleEdit(row)}> |
| | | 编辑 |
| | | </NButton> |
| | | <NButton text v-permission="2026061541" type="primary" onClick={() => handleToggleStatus(row)}> |
| | | {row.status === 1 ? '停用' : '启用'} |
| | | </NButton> |
| | | <NSwitch |
| | | v-permission="2026061541" |
| | | value={row.status === 1} |
| | | onUpdateValue={() => handleToggleStatus(row)} |
| | | > |
| | | {{ |
| | | checked: () => '停用', |
| | | unchecked: () => '启用', |
| | | }} |
| | | </NSwitch> |
| | | </n-space> |
| | | ), |
| | | }, |
| | |
| | | width: 100, |
| | | resizable: true, |
| | | render(row) { |
| | | if (!row.taskUrgencyLevel) { |
| | | return '-' |
| | | } |
| | | const option = taskUrgencyLevelOptions.find(item => item.value === row.taskUrgencyLevel) |
| | | return ( |
| | | <n-tag type={option?.type || 'default'} bordered={false}> |
| | |
| | | v-model:value="model.basicPricingJson1.waitFeeEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | v-model:value="model.basicPricingJson1.longDistanceFeeEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | v-model:value="model.basicPricingJson1.nightFeeEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | v-model:value="model.basicPricingJson1.peakFeeEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | v-model:value="model.basicPricingJson1.holidayFeeEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | v-model:value="model.dynamicPricingJson.supplyDemandEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | v-model:value="model.dynamicPricingJson.congestionEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | v-model:value="model.dynamicPricingJson.weatherEnabled" |
| | | :checked-value="1" |
| | | :unchecked-value="0" |
| | | class="ml-8px" |
| | | class="ml-8px font-400! text-13px!" |
| | | > |
| | | <template #checked> |
| | | 启用 |
| | |
| | | box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05); |
| | | padding: 16px 24px; |
| | | } |
| | | |
| | | :deep(.n-switch:focus .n-switch__rail) { |
| | | box-shadow: none; |
| | | } |
| | | </style> |