feat(工作报告、加班申请团队视角): 工作报告、加班申请现在可以查看团队视角了(查看下属)。
fix(工作报告): 修复周报在新增/编辑时,不能展示工作日志。
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, markRaw, reactive, ref } from 'vue';
|
||||
import { computed, markRaw, reactive, ref, watch } from 'vue';
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchExportOvertimeApplications, fetchGetOvertimeApplicationPage } from '@/service/api';
|
||||
import {
|
||||
fetchExportOvertimeApplications,
|
||||
fetchGetMySubordinateTree,
|
||||
fetchGetOvertimeApplicationPage,
|
||||
fetchGetTeamOvertimeSummary
|
||||
} from '@/service/api';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import SubordinateSelector from '@/components/custom/subordinate-selector.vue';
|
||||
import TeamContextPanel from '@/components/custom/team-context-panel.vue';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import {
|
||||
type TeamViewContext,
|
||||
type TeamViewMode,
|
||||
collectSubordinateUserIds,
|
||||
findSubordinateNode
|
||||
} from '../shared/team-dashboard';
|
||||
import OvertimeApplicationApprovalRecordDialog from './modules/overtime-application-approval-record-dialog.vue';
|
||||
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
||||
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
||||
@@ -20,6 +34,7 @@ import {
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
import IconMdiDownloadOutline from '~icons/mdi/download-outline';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplication' });
|
||||
|
||||
@@ -58,6 +73,13 @@ function transformPageResult(response: OvertimeApplicationPageResponse, pageNo:
|
||||
}
|
||||
|
||||
const searchParams = reactive(getInitSearchParams());
|
||||
const { hasAuth } = useAuth();
|
||||
const teamViewMode = ref<TeamViewMode>('self');
|
||||
const subordinateTreeLoading = ref(false);
|
||||
const subordinateTree = ref<Api.SystemManage.MySubordinateTreeNode | null>(null);
|
||||
const selectedSubordinateUserId = ref<string | null>(null);
|
||||
const teamSummaryLoading = ref(false);
|
||||
const teamSummary = ref<Api.OvertimeApplication.TeamOvertimeSummary | null>(null);
|
||||
const operateVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const approvalRecordVisible = ref(false);
|
||||
@@ -71,6 +93,39 @@ const ACTION_ICON_MAP = {
|
||||
edit: markRaw(IconMdiPencilOutline)
|
||||
};
|
||||
|
||||
const canUseTeamDashboard = computed(() => hasAuth('project:work-report:team-dashboard'));
|
||||
const allSubordinateUserIds = computed(() => collectSubordinateUserIds(subordinateTree.value));
|
||||
const selectedSubordinateNode = computed(() =>
|
||||
findSubordinateNode(subordinateTree.value, selectedSubordinateUserId.value)
|
||||
);
|
||||
const isTeamMode = computed(() => teamViewMode.value === 'team');
|
||||
const isRootSelected = computed(() => Boolean(isTeamMode.value && selectedSubordinateNode.value?.isRoot));
|
||||
const selectedTeamLabel = computed(() => {
|
||||
if (!isTeamMode.value) return '我自己';
|
||||
if (!selectedSubordinateNode.value) return '--';
|
||||
return selectedSubordinateNode.value.isRoot ? '全部下属' : selectedSubordinateNode.value.userNickname;
|
||||
});
|
||||
const teamContext = computed<TeamViewContext | null>(() => {
|
||||
if (!canUseTeamDashboard.value) return null;
|
||||
|
||||
return {
|
||||
mode: teamViewMode.value,
|
||||
selectedUserId: selectedSubordinateUserId.value,
|
||||
selectedUserIds:
|
||||
isTeamMode.value && selectedSubordinateUserId.value && !isRootSelected.value
|
||||
? [selectedSubordinateUserId.value]
|
||||
: [],
|
||||
isRootSelected: isRootSelected.value,
|
||||
allSubordinateUserIds: allSubordinateUserIds.value,
|
||||
selectedLabel: selectedTeamLabel.value
|
||||
};
|
||||
});
|
||||
const currentApplicantIds = computed(() => {
|
||||
if (!isTeamMode.value) return null;
|
||||
if (isRootSelected.value) return [];
|
||||
return teamContext.value?.selectedUserIds ?? [];
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||
OvertimeApplicationPageResponse,
|
||||
Api.OvertimeApplication.OvertimeApplication
|
||||
@@ -79,7 +134,11 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
||||
currentPage: searchParams.pageNo,
|
||||
pageSize: searchParams.pageSize
|
||||
},
|
||||
api: () => fetchGetOvertimeApplicationPage(searchParams),
|
||||
api: () =>
|
||||
fetchGetOvertimeApplicationPage({
|
||||
...searchParams,
|
||||
applicantIds: currentApplicantIds.value
|
||||
}),
|
||||
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
@@ -87,7 +146,7 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'applicantName', label: '申请人', minWidth: 120, showOverflowTooltip: true },
|
||||
...(isTeamMode.value ? [{ prop: 'applicantName', label: '申请人', minWidth: 120, showOverflowTooltip: true }] : []),
|
||||
{
|
||||
prop: 'overtimeDate',
|
||||
label: '加班日期',
|
||||
@@ -136,7 +195,7 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 170,
|
||||
width: isTeamMode.value ? 140 : 170,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
@@ -150,13 +209,27 @@ function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): Busine
|
||||
const actions: BusinessTableAction[] = [
|
||||
{
|
||||
key: 'detail',
|
||||
label: '详情',
|
||||
label: '查看',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.detail,
|
||||
onClick: () => openDetail(row)
|
||||
}
|
||||
];
|
||||
|
||||
if (isTeamMode.value) {
|
||||
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||
actions.push({
|
||||
key: 'approval-record',
|
||||
label: '审批记录',
|
||||
buttonType: 'info',
|
||||
icon: ACTION_ICON_MAP.approvalRecord,
|
||||
onClick: () => openApprovalRecord(row)
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
if (row.statusCode === 'rejected' && row.allowEdit) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
@@ -221,9 +294,20 @@ function handleSubmitted() {
|
||||
reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
function createExportParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return {
|
||||
...params,
|
||||
applicantIds: currentApplicantIds.value
|
||||
};
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
exporting.value = true;
|
||||
const { error, data: blob } = await fetchExportOvertimeApplications(searchParams);
|
||||
const { error, data: blob } = await fetchExportOvertimeApplications({
|
||||
...createExportParams(),
|
||||
applicantIds: currentApplicantIds.value
|
||||
});
|
||||
exporting.value = false;
|
||||
|
||||
if (error || !blob) {
|
||||
@@ -232,56 +316,150 @@ async function handleExport() {
|
||||
|
||||
downloadBlob(blob, `加班申请_${dayjs().format('YYYY-MM-DD')}.xls`);
|
||||
}
|
||||
|
||||
async function loadSubordinateTree() {
|
||||
if (!canUseTeamDashboard.value) return;
|
||||
|
||||
subordinateTreeLoading.value = true;
|
||||
const { error, data: treeData } = await fetchGetMySubordinateTree();
|
||||
subordinateTreeLoading.value = false;
|
||||
|
||||
subordinateTree.value = error || !treeData ? null : treeData;
|
||||
selectedSubordinateUserId.value = treeData?.userId || null;
|
||||
}
|
||||
|
||||
async function loadTeamSummary() {
|
||||
if (!isRootSelected.value) {
|
||||
teamSummary.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
teamSummaryLoading.value = true;
|
||||
const { error, data: summaryData } = await fetchGetTeamOvertimeSummary();
|
||||
teamSummaryLoading.value = false;
|
||||
|
||||
teamSummary.value = error || !summaryData ? null : summaryData;
|
||||
}
|
||||
|
||||
async function handleTeamViewModeChange(mode: TeamViewMode) {
|
||||
teamViewMode.value = mode;
|
||||
|
||||
if (mode === 'team') {
|
||||
if (!subordinateTree.value) {
|
||||
await loadSubordinateTree();
|
||||
}
|
||||
if (!selectedSubordinateUserId.value) {
|
||||
selectedSubordinateUserId.value = subordinateTree.value?.userId || null;
|
||||
}
|
||||
}
|
||||
|
||||
await reloadTable(1);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [teamViewMode.value, selectedSubordinateUserId.value],
|
||||
async () => {
|
||||
if (!isTeamMode.value) return;
|
||||
await reloadTable(1);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => isRootSelected.value,
|
||||
() => {
|
||||
loadTeamSummary();
|
||||
}
|
||||
);
|
||||
</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 class="overtime-application-page">
|
||||
<TeamContextPanel
|
||||
v-if="canUseTeamDashboard"
|
||||
v-model:mode="teamViewMode"
|
||||
:loading="subordinateTreeLoading"
|
||||
:selected-label="selectedTeamLabel"
|
||||
:subordinate-count="subordinateTree?.subordinateCount || 0"
|
||||
@update:mode="handleTeamViewModeChange"
|
||||
>
|
||||
<div v-if="isRootSelected" v-loading="teamSummaryLoading" class="team-overtime-summary">
|
||||
<div class="team-overtime-summary__item">
|
||||
<span class="team-overtime-summary__label">本月申请单数</span>
|
||||
<strong class="team-overtime-summary__value">{{ teamSummary?.totalApplicationCount ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="team-overtime-summary__item">
|
||||
<span class="team-overtime-summary__label">本月待审批</span>
|
||||
<strong class="team-overtime-summary__value">{{ teamSummary?.pendingCount ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="team-overtime-summary__item">
|
||||
<span class="team-overtime-summary__label">本月已通过</span>
|
||||
<strong class="team-overtime-summary__value">{{ teamSummary?.approvedCount ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="team-overtime-summary__item">
|
||||
<span class="team-overtime-summary__label">本月已退回</span>
|
||||
<strong class="team-overtime-summary__value">{{ teamSummary?.rejectedCount ?? 0 }}</strong>
|
||||
</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>
|
||||
</TeamContextPanel>
|
||||
|
||||
<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 class="overtime-application-page__content" :class="{ 'overtime-application-page__content--team': isTeamMode }">
|
||||
<div v-if="canUseTeamDashboard && isTeamMode" class="overtime-application-page__sidebar">
|
||||
<SubordinateSelector
|
||||
v-model:selected-user-id="selectedSubordinateUserId"
|
||||
:loading="subordinateTreeLoading"
|
||||
:data="subordinateTree"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<div class="overtime-application-page__main">
|
||||
<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 v-if="!isTeamMode" 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OvertimeApplicationOperateDialog
|
||||
v-model:visible="operateVisible"
|
||||
@@ -297,6 +475,42 @@ async function handleExport() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overtime-application-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overtime-application-page__content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.overtime-application-page__main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.overtime-application-page__content--team {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.overtime-application-page__sidebar {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.overtime-application__reason-link) {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
@@ -318,4 +532,31 @@ async function handleExport() {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team-overtime-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.team-overtime-summary__item {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.team-overtime-summary__label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.team-overtime-summary__value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user