2026-06-01 21:37:08 +08:00
|
|
|
<script setup lang="tsx">
|
|
|
|
|
import { computed, markRaw, reactive, ref } from 'vue';
|
|
|
|
|
import { ElButton, ElMessageBox, ElTag } from 'element-plus';
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import {
|
|
|
|
|
fetchCancelOvertimeApplication,
|
|
|
|
|
fetchDeleteOvertimeApplication,
|
|
|
|
|
fetchExportOvertimeApplications,
|
|
|
|
|
fetchGetOvertimeApplicationPage
|
|
|
|
|
} from '@/service/api';
|
|
|
|
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
|
|
|
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
|
|
|
|
import OvertimeApplicationActionDialog from './modules/overtime-application-action-dialog.vue';
|
|
|
|
|
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
|
|
|
|
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
|
|
|
|
import OvertimeApplicationSearch from './modules/overtime-application-search.vue';
|
|
|
|
|
import OvertimeApplicationStatusLogDialog from './modules/overtime-application-status-log-dialog.vue';
|
|
|
|
|
import {
|
|
|
|
|
downloadBlob,
|
|
|
|
|
formatEmptyText,
|
|
|
|
|
formatOvertimeDate,
|
|
|
|
|
formatOvertimeDateTime,
|
|
|
|
|
getOvertimeApplicationStatusLabel,
|
|
|
|
|
resolveOvertimeApplicationStatusTagType
|
|
|
|
|
} from './modules/overtime-application-shared';
|
|
|
|
|
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
|
|
|
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
|
|
|
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
|
|
|
|
import IconMdiHistory from '~icons/mdi/history';
|
|
|
|
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'OvertimeApplication' });
|
|
|
|
|
|
|
|
|
|
type OvertimeApplicationPageResponse = Awaited<ReturnType<typeof fetchGetOvertimeApplicationPage>>;
|
|
|
|
|
type ActionType = 'cancel';
|
|
|
|
|
|
|
|
|
|
function getInitSearchParams(): Api.OvertimeApplication.OvertimeApplicationSearchParams {
|
|
|
|
|
return {
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
keyword: undefined,
|
|
|
|
|
applicantName: undefined,
|
|
|
|
|
approverId: undefined,
|
|
|
|
|
approverName: undefined,
|
|
|
|
|
statusCode: undefined,
|
|
|
|
|
overtimeDate: undefined,
|
|
|
|
|
createTime: undefined
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function transformPageResult(response: OvertimeApplicationPageResponse, pageNo: number, pageSize: number) {
|
|
|
|
|
if (!response.error && response.data) {
|
|
|
|
|
return {
|
|
|
|
|
data: response.data.list,
|
|
|
|
|
pageNum: pageNo,
|
|
|
|
|
pageSize,
|
|
|
|
|
total: response.data.total
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: [],
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize,
|
|
|
|
|
total: 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const searchParams = reactive(getInitSearchParams());
|
|
|
|
|
const operateVisible = ref(false);
|
|
|
|
|
const detailVisible = ref(false);
|
|
|
|
|
const statusLogVisible = ref(false);
|
|
|
|
|
const actionVisible = ref(false);
|
|
|
|
|
const operateType = ref<'add' | 'edit'>('add');
|
|
|
|
|
const currentRow = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
|
|
|
|
const currentActionType = ref<ActionType>('cancel');
|
|
|
|
|
const actionSubmitting = ref(false);
|
|
|
|
|
const exporting = ref(false);
|
|
|
|
|
|
|
|
|
|
const ACTION_ICON_MAP = {
|
|
|
|
|
detail: markRaw(IconMdiEyeOutline),
|
|
|
|
|
statusLog: markRaw(IconMdiHistory),
|
|
|
|
|
edit: markRaw(IconMdiPencilOutline),
|
|
|
|
|
cancel: markRaw(IconMdiCloseCircleOutline),
|
|
|
|
|
delete: markRaw(IconMdiDeleteOutline)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
|
|
|
|
OvertimeApplicationPageResponse,
|
|
|
|
|
Api.OvertimeApplication.OvertimeApplication
|
|
|
|
|
>({
|
|
|
|
|
paginationProps: {
|
|
|
|
|
currentPage: searchParams.pageNo,
|
|
|
|
|
pageSize: searchParams.pageSize
|
|
|
|
|
},
|
|
|
|
|
api: () => fetchGetOvertimeApplicationPage(searchParams),
|
|
|
|
|
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
|
|
|
|
onPaginationParamsChange: params => {
|
|
|
|
|
searchParams.pageNo = params.currentPage ?? 1;
|
|
|
|
|
searchParams.pageSize = params.pageSize ?? 10;
|
|
|
|
|
},
|
|
|
|
|
columns: () => [
|
|
|
|
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
|
|
|
|
{ prop: 'applicantName', label: '申请人', minWidth: 120, showOverflowTooltip: true },
|
|
|
|
|
{
|
|
|
|
|
prop: 'overtimeDate',
|
|
|
|
|
label: '加班日期',
|
|
|
|
|
width: 120,
|
|
|
|
|
formatter: row => formatOvertimeDate(row.overtimeDate)
|
|
|
|
|
},
|
|
|
|
|
{ prop: 'overtimeDuration', label: '加班时长', width: 110, showOverflowTooltip: true },
|
|
|
|
|
{
|
|
|
|
|
prop: 'overtimeReason',
|
|
|
|
|
label: '加班原因',
|
|
|
|
|
minWidth: 180,
|
|
|
|
|
showOverflowTooltip: true,
|
|
|
|
|
formatter: row => formatEmptyText(row.overtimeReason)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'overtimeContent',
|
|
|
|
|
label: '加班内容',
|
|
|
|
|
minWidth: 200,
|
|
|
|
|
showOverflowTooltip: true,
|
|
|
|
|
formatter: row => formatEmptyText(row.overtimeContent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'statusCode',
|
|
|
|
|
label: '状态',
|
|
|
|
|
width: 110,
|
|
|
|
|
align: 'center',
|
|
|
|
|
formatter: row => (
|
|
|
|
|
<ElTag type={resolveOvertimeApplicationStatusTagType(row.statusCode)}>
|
|
|
|
|
{getOvertimeApplicationStatusLabel(row.statusCode, row.statusName)}
|
|
|
|
|
</ElTag>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{ prop: 'approverName', label: '审核人', minWidth: 120, showOverflowTooltip: true },
|
|
|
|
|
{
|
|
|
|
|
prop: 'submitTime',
|
|
|
|
|
label: '提交时间',
|
|
|
|
|
minWidth: 170,
|
|
|
|
|
formatter: row => formatOvertimeDateTime(row.submitTime)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'approvalTime',
|
|
|
|
|
label: '审核时间',
|
|
|
|
|
minWidth: 170,
|
|
|
|
|
formatter: row => formatOvertimeDateTime(row.approvalTime)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'operate',
|
|
|
|
|
label: '操作',
|
|
|
|
|
width: 170,
|
|
|
|
|
align: 'center',
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const totalCount = computed(() => mobilePagination.value.total || data.value.length);
|
|
|
|
|
|
|
|
|
|
function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): BusinessTableAction[] {
|
|
|
|
|
const actions: BusinessTableAction[] = [
|
|
|
|
|
{
|
|
|
|
|
key: 'detail',
|
|
|
|
|
label: '详情',
|
|
|
|
|
buttonType: 'primary',
|
|
|
|
|
icon: ACTION_ICON_MAP.detail,
|
|
|
|
|
onClick: () => openDetail(row)
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
2026-06-03 21:04:51 +08:00
|
|
|
if ((row.statusCode === 'rejected' || row.statusCode === 'cancelled') && row.allowEdit) {
|
2026-06-01 21:37:08 +08:00
|
|
|
actions.push({
|
|
|
|
|
key: 'edit',
|
|
|
|
|
label: '修改',
|
|
|
|
|
buttonType: 'primary',
|
|
|
|
|
icon: ACTION_ICON_MAP.edit,
|
|
|
|
|
onClick: () => openEdit(row)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actions.push({
|
|
|
|
|
key: 'status-log',
|
|
|
|
|
label: '状态日志',
|
|
|
|
|
buttonType: 'info',
|
|
|
|
|
icon: ACTION_ICON_MAP.statusLog,
|
|
|
|
|
onClick: () => openStatusLog(row)
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-03 21:04:51 +08:00
|
|
|
if (row.statusCode === 'pending') {
|
2026-06-01 21:37:08 +08:00
|
|
|
actions.push({
|
|
|
|
|
key: 'cancel',
|
|
|
|
|
label: '撤销',
|
|
|
|
|
buttonType: 'danger',
|
|
|
|
|
icon: ACTION_ICON_MAP.cancel,
|
|
|
|
|
onClick: () => openCancel(row)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (row.statusCode === 'cancelled') {
|
|
|
|
|
actions.push({
|
|
|
|
|
key: 'delete',
|
|
|
|
|
label: '删除',
|
|
|
|
|
buttonType: 'danger',
|
|
|
|
|
icon: ACTION_ICON_MAP.delete,
|
|
|
|
|
onClick: () => handleDelete(row)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return actions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openAdd() {
|
|
|
|
|
operateType.value = 'add';
|
|
|
|
|
currentRow.value = null;
|
|
|
|
|
operateVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEdit(row: Api.OvertimeApplication.OvertimeApplication) {
|
|
|
|
|
operateType.value = 'edit';
|
|
|
|
|
currentRow.value = row;
|
|
|
|
|
operateVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openDetail(row: Api.OvertimeApplication.OvertimeApplication) {
|
|
|
|
|
currentRow.value = row;
|
|
|
|
|
detailVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openStatusLog(row: Api.OvertimeApplication.OvertimeApplication) {
|
|
|
|
|
currentRow.value = row;
|
|
|
|
|
statusLogVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openCancel(row: Api.OvertimeApplication.OvertimeApplication) {
|
|
|
|
|
currentRow.value = row;
|
|
|
|
|
currentActionType.value = 'cancel';
|
|
|
|
|
actionVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function reloadTable(page = searchParams.pageNo ?? 1) {
|
|
|
|
|
await getDataByPage(page);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetSearchParams() {
|
|
|
|
|
const pageSize = searchParams.pageSize ?? 10;
|
|
|
|
|
Object.assign(searchParams, getInitSearchParams(), { pageSize });
|
|
|
|
|
reloadTable(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSearch() {
|
|
|
|
|
reloadTable(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSubmitted() {
|
|
|
|
|
operateVisible.value = false;
|
|
|
|
|
reloadTable(searchParams.pageNo ?? 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleActionSubmit(reason: string | null) {
|
|
|
|
|
if (!currentRow.value) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actionSubmitting.value = true;
|
|
|
|
|
const { error } = await fetchCancelOvertimeApplication(currentRow.value.id, { reason });
|
|
|
|
|
actionSubmitting.value = false;
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actionVisible.value = false;
|
|
|
|
|
window.$message?.success('加班申请已撤销');
|
|
|
|
|
await reloadTable(searchParams.pageNo ?? 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDelete(row: Api.OvertimeApplication.OvertimeApplication) {
|
|
|
|
|
try {
|
|
|
|
|
await ElMessageBox.confirm(
|
|
|
|
|
`确定删除 ${row.applicantName} ${formatOvertimeDate(row.overtimeDate)} 的加班申请吗?`,
|
|
|
|
|
'删除确认',
|
|
|
|
|
{
|
|
|
|
|
type: 'warning',
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
cancelButtonText: '取消'
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { error } = await fetchDeleteOvertimeApplication(row.id);
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.$message?.success('加班申请已删除');
|
|
|
|
|
await reloadTable(searchParams.pageNo ?? 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleExport() {
|
|
|
|
|
exporting.value = true;
|
|
|
|
|
const { error, data: blob } = await fetchExportOvertimeApplications(searchParams);
|
|
|
|
|
exporting.value = false;
|
|
|
|
|
|
|
|
|
|
if (error || !blob) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadBlob(blob, `加班申请_${dayjs().format('YYYY-MM-DD')}.xls`);
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
|
|
|
|
<OvertimeApplicationSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
|
|
|
|
|
|
|
|
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-12px">
|
|
|
|
|
<div class="flex items-center gap-10px">
|
|
|
|
|
<p class="text-16px font-600">加班申请</p>
|
|
|
|
|
<ElTag effect="plain">{{ totalCount }}</ElTag>
|
|
|
|
|
</div>
|
|
|
|
|
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadTable">
|
|
|
|
|
<template #default>
|
|
|
|
|
<ElButton plain :loading="exporting" @click="handleExport">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<icon-mdi-download class="text-icon" />
|
|
|
|
|
</template>
|
|
|
|
|
导出
|
|
|
|
|
</ElButton>
|
|
|
|
|
<ElButton plain type="primary" @click="openAdd">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<icon-ic-round-plus class="text-icon" />
|
|
|
|
|
</template>
|
|
|
|
|
新增
|
|
|
|
|
</ElButton>
|
|
|
|
|
</template>
|
|
|
|
|
</TableHeaderOperation>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
|
|
|
|
<template v-for="col in columns" :key="String(col.prop)">
|
|
|
|
|
<ElTableColumn v-bind="col" />
|
|
|
|
|
</template>
|
|
|
|
|
</ElTable>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mt-20px flex justify-end">
|
|
|
|
|
<ElPagination
|
|
|
|
|
v-if="mobilePagination.total"
|
|
|
|
|
layout="total,prev,pager,next,sizes"
|
|
|
|
|
v-bind="mobilePagination"
|
|
|
|
|
@current-change="mobilePagination['current-change']"
|
|
|
|
|
@size-change="mobilePagination['size-change']"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</ElCard>
|
|
|
|
|
|
|
|
|
|
<OvertimeApplicationOperateDialog
|
|
|
|
|
v-model:visible="operateVisible"
|
|
|
|
|
:operate-type="operateType"
|
|
|
|
|
:row-data="currentRow"
|
|
|
|
|
@submitted="handleSubmitted"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<OvertimeApplicationDetailDialog v-model:visible="detailVisible" :row-data="currentRow" />
|
|
|
|
|
|
|
|
|
|
<OvertimeApplicationStatusLogDialog v-model:visible="statusLogVisible" :row-data="currentRow" />
|
|
|
|
|
|
|
|
|
|
<OvertimeApplicationActionDialog
|
|
|
|
|
v-model:visible="actionVisible"
|
|
|
|
|
:action-type="currentActionType"
|
|
|
|
|
:loading="actionSubmitting"
|
|
|
|
|
@submit="handleActionSubmit"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
:deep(.overtime-application__reason-link) {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
padding: 0;
|
|
|
|
|
vertical-align: baseline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.overtime-application__reason-link > span) {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
</style>
|