feat(新增加班申请功能): 新增申请功能,可在工作台进行审核。

fix(dict_data): 在字典数据新增、编辑时可以操作颜色类型字段(color_type)。
This commit is contained in:
dk
2026-06-01 21:37:08 +08:00
parent b2da882b31
commit d3d0830820
29 changed files with 1966 additions and 23 deletions

View File

@@ -1,8 +1,16 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { computed, markRaw, onMounted, ref, watch } from 'vue';
import type { RouteKey } from '@elegant-router/types';
import {
fetchApproveOvertimeApplication,
fetchGetOvertimeApplicationApprovalPage,
fetchRejectOvertimeApplication
} from '@/service/api';
import { useRouterPush } from '@/hooks/common/router';
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
import OvertimeApplicationActionDialog from '@/views/personal-center/overtime-application/modules/overtime-application-action-dialog.vue';
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
import OvertimeApplicationStatusLogDialog from '@/views/personal-center/overtime-application/modules/overtime-application-status-log-dialog.vue';
import {
type WorkbenchTodoDeadlineFilter,
type WorkbenchTodoItem,
@@ -15,8 +23,14 @@ import {
} from '../homepage';
import { workbenchTodoMock } from '../mock';
import WorkbenchModuleCard from './workbench-module-card.vue';
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
import IconMdiHistory from '~icons/mdi/history';
type SortKey = 'created' | 'priority' | 'deadline';
type OvertimeApprovalActionType = 'approve' | 'reject';
type ApprovalBizType = 'overtime_application';
defineOptions({ name: 'WorkbenchTodoPanel' });
@@ -38,6 +52,7 @@ const PAGE_SIZE = 5;
const activeTab = ref<WorkbenchTodoMainTab>('all');
const activeDeadlineFilter = ref<WorkbenchTodoDeadlineFilter>(null);
const activeApprovalBizType = ref<ApprovalBizType>('overtime_application');
const activeSort = ref<SortKey>('deadline');
const currentPage = ref(1);
@@ -66,9 +81,33 @@ const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>;
{ key: 'week', label: '本周到期' }
];
const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
{ key: 'overtime_application', label: '加班申请' }
];
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
const mergedItems = computed(() => {
const mockItems = allItems.value.filter(item => item.category !== 'approval');
return [...mockItems, ...overtimeApprovalItems.value];
});
const addDialogVisible = ref(false);
const overtimeDetailVisible = ref(false);
const overtimeStatusLogVisible = ref(false);
const overtimeActionVisible = ref(false);
const overtimeActionSubmitting = ref(false);
const currentOvertimeApplication = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
const currentOvertimeActionType = ref<OvertimeApprovalActionType>('approve');
const OVERTIME_APPROVAL_ACTION_ICONS = {
detail: markRaw(IconMdiEyeOutline),
approve: markRaw(IconMdiCheckCircleOutline),
reject: markRaw(IconMdiCloseCircleOutline),
statusLog: markRaw(IconMdiHistory)
};
function handleOpenAdd() {
addDialogVisible.value = true;
@@ -81,13 +120,13 @@ function handleAddSubmitted() {
const tabCounts = computed(() => {
const counts: Record<WorkbenchTodoMainTab, number> = {
all: allItems.value.length,
all: mergedItems.value.length,
task: 0,
ticket: 0,
personal: 0,
approval: 0
};
allItems.value.forEach(item => {
mergedItems.value.forEach(item => {
counts[item.category] += 1;
});
return counts;
@@ -101,7 +140,7 @@ const tabOverdueCount = computed(() => {
personal: 0,
approval: 0
};
allItems.value.forEach(item => {
mergedItems.value.forEach(item => {
if (!isWorkbenchTodoOverdue(item)) return;
map.all += 1;
map[item.category] += 1;
@@ -109,9 +148,29 @@ const tabOverdueCount = computed(() => {
return map;
});
const itemsInTab = computed(() => filterWorkbenchTodoItemsByCategory(allItems.value, activeTab.value));
const itemsInTab = computed(() => filterWorkbenchTodoItemsByCategory(mergedItems.value, activeTab.value));
const filteredItems = computed(() => filterWorkbenchTodoItemsByDeadline(itemsInTab.value, activeDeadlineFilter.value));
const filteredItems = computed(() => {
if (activeTab.value === 'approval') {
return itemsInTab.value.filter(item => item.approvalBizType === activeApprovalBizType.value);
}
return filterWorkbenchTodoItemsByDeadline(itemsInTab.value, activeDeadlineFilter.value);
});
const approvalBizTabCounts = computed(() => {
const counts: Record<ApprovalBizType, number> = {
overtime_application: 0
};
itemsInTab.value.forEach(item => {
if (item.approvalBizType === 'overtime_application') {
counts.overtime_application += 1;
}
});
return counts;
});
const sortedItems = computed(() => {
const base = filteredItems.value;
@@ -155,20 +214,121 @@ function handleSelectDeadlineFilter(key: Exclude<WorkbenchTodoDeadlineFilter, nu
activeDeadlineFilter.value = activeDeadlineFilter.value === key ? null : key;
}
function handleSelectApprovalBizType(key: ApprovalBizType) {
activeApprovalBizType.value = key;
}
function handleSelectSort(key: SortKey) {
activeSort.value = key;
}
function handleClickItem(item: WorkbenchTodoItem) {
if (item.approvalBizType === 'overtime_application') {
openOvertimeDetail(item);
return;
}
if (!item.routeKey) return;
routerPushByKey(item.routeKey as RouteKey);
}
function findOvertimeApprovalRow(item: WorkbenchTodoItem) {
if (!item.approvalBizId) {
return null;
}
return overtimeApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
}
function openOvertimeDetail(item: WorkbenchTodoItem) {
const row = findOvertimeApprovalRow(item);
if (!row) return;
currentOvertimeApplication.value = row;
overtimeDetailVisible.value = true;
}
function openOvertimeStatusLog(item: WorkbenchTodoItem) {
const row = findOvertimeApprovalRow(item);
if (!row) return;
currentOvertimeApplication.value = row;
overtimeStatusLogVisible.value = true;
}
function openOvertimeAction(item: WorkbenchTodoItem, actionType: OvertimeApprovalActionType) {
const row = findOvertimeApprovalRow(item);
if (!row) return;
currentOvertimeApplication.value = row;
currentOvertimeActionType.value = actionType;
overtimeActionVisible.value = true;
}
async function handleOvertimeActionSubmit(reason: string | null) {
if (!currentOvertimeApplication.value) {
return;
}
overtimeActionSubmitting.value = true;
const result =
currentOvertimeActionType.value === 'approve'
? await fetchApproveOvertimeApplication(currentOvertimeApplication.value.id, { reason })
: await fetchRejectOvertimeApplication(currentOvertimeApplication.value.id, { reason });
overtimeActionSubmitting.value = false;
if (result.error) {
return;
}
overtimeActionVisible.value = false;
overtimeDetailVisible.value = false;
window.$message?.success(currentOvertimeActionType.value === 'approve' ? '加班申请已通过' : '加班申请已退回');
await loadOvertimeApprovalItems();
}
async function loadOvertimeApprovalItems() {
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
pageNo: 1,
pageSize: 20,
statusCode: 'pending',
keyword: undefined,
applicantName: undefined,
approverId: undefined,
approverName: undefined,
overtimeDate: undefined,
createTime: undefined
});
if (error || !data) {
overtimeApprovalRows.value = [];
overtimeApprovalItems.value = [];
return;
}
overtimeApprovalRows.value = data.list;
overtimeApprovalItems.value = buildWorkbenchTodoItems(
data.list.map(item => ({
id: `overtime-application-${item.id}`,
category: 'approval',
title: `${item.applicantName} · ${item.overtimeDate.slice(5, 7)} 月加班 ${item.overtimeDuration} 申请待审批`,
createdTime: item.submitTime || item.createTime,
deadline: item.submitTime || item.createTime,
source: `加班申请 · ${item.applicantName}`,
priority: 'mid',
approvalBizType: 'overtime_application',
approvalBizId: item.id
}))
);
}
function getDeadlineToneClass(item: WorkbenchTodoItem) {
if (isWorkbenchTodoOverdue(item)) return 'workbench-todo__deadline--rose';
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
return 'workbench-todo__deadline--slate';
}
onMounted(loadOvertimeApprovalItems);
</script>
<template>
@@ -222,7 +382,19 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
{{ filter.label }}
</button>
</div>
<div v-else></div>
<div v-else class="workbench-todo__filters-left">
<button
v-for="tab in approvalBizTabs"
:key="tab.key"
type="button"
class="workbench-todo__filter"
:class="{ 'workbench-todo__filter--active': activeApprovalBizType === tab.key }"
@click="handleSelectApprovalBizType(tab.key)"
>
{{ tab.label }}
<span class="workbench-todo__filter-count">{{ approvalBizTabCounts[tab.key] }}</span>
</button>
</div>
<ElDropdown trigger="click" placement="bottom-end" @command="handleSelectSort">
<span class="workbench-todo__sort">
@@ -250,7 +422,7 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
v-for="item in pagedItems"
:key="item.id"
class="workbench-todo__item"
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey) }"
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey || item.approvalBizType) }"
@click="handleClickItem(item)"
>
<div class="workbench-todo__leading">
@@ -268,6 +440,38 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
</div>
<div class="workbench-todo__trailing">
<div v-if="item.approvalBizType === 'overtime_application'" class="workbench-todo__actions" @click.stop>
<ElTooltip content="详情">
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openOvertimeDetail(item)">
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.detail" class="text-15px" />
</ElButton>
</ElTooltip>
<ElTooltip content="通过">
<ElButton
link
type="success"
class="workbench-todo__action-btn"
@click="openOvertimeAction(item, 'approve')"
>
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.approve" class="text-15px" />
</ElButton>
</ElTooltip>
<ElTooltip content="退回">
<ElButton
link
type="danger"
class="workbench-todo__action-btn"
@click="openOvertimeAction(item, 'reject')"
>
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.reject" class="text-15px" />
</ElButton>
</ElTooltip>
<ElTooltip content="状态日志">
<ElButton link type="info" class="workbench-todo__action-btn" @click="openOvertimeStatusLog(item)">
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.statusLog" class="text-15px" />
</ElButton>
</ElTooltip>
</div>
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
{{ item.deadlineLabel }}
</span>
@@ -295,6 +499,18 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
:row-data="null"
@submitted="handleAddSubmitted"
/>
<OvertimeApplicationDetailDialog v-model:visible="overtimeDetailVisible" :row-data="currentOvertimeApplication" />
<OvertimeApplicationStatusLogDialog
v-model:visible="overtimeStatusLogVisible"
:row-data="currentOvertimeApplication"
/>
<OvertimeApplicationActionDialog
v-model:visible="overtimeActionVisible"
:action-type="currentOvertimeActionType"
:loading="overtimeActionSubmitting"
@submit="handleOvertimeActionSubmit"
/>
</WorkbenchModuleCard>
</template>
@@ -468,6 +684,12 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
color: rgb(190 18 60 / 96%);
}
.workbench-todo__filter-count {
margin-left: 4px;
font-size: 11px;
font-weight: 700;
}
.workbench-todo__content {
min-height: 400px;
display: flex;
@@ -596,6 +818,25 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
.workbench-todo__trailing {
display: flex;
align-items: center;
gap: 12px;
}
.workbench-todo__actions {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.workbench-todo__actions :deep(.el-button + .el-button) {
margin-left: 0;
}
:deep(.workbench-todo__action-btn) {
min-width: auto;
height: auto;
padding: 3px;
line-height: 1;
}
.workbench-todo__deadline {