fix(加班申请、工作报告、我的绩效): 重构页面样式、修复一系列bug、对不合理的地方进行调整。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { type ComputedRef, type Ref, computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { onBeforeRouteLeave } from 'vue-router';
|
||||
import { fetchGetMySubordinateTree, fetchGetProjectReportOwnerProjectOptions } from '@/service/api';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
collectSubordinateUserIds,
|
||||
findSubordinateNode
|
||||
} from '../shared/team-dashboard';
|
||||
import TeamReportSummary from './shared/components/team-report-summary.vue';
|
||||
import WorkReportCreateDialog from './shared/components/create-dialog.vue';
|
||||
import WorkReportPrototypePageDialog from './shared/components/prototype-page-dialog.vue';
|
||||
import WorkReportTabs from './shared/components/tabs.vue';
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
type WorkReportType,
|
||||
getWorkReportTypeDisplayLabel
|
||||
} from './shared/types';
|
||||
import { formatIsoWeekRangeLabel } from './shared/utils';
|
||||
import type { resolveWorkReportSummaryPeriod } from './shared/utils';
|
||||
import WeeklyReportIndex from './weekly/index.vue';
|
||||
import WeeklyReportApprovalRecordDialog from './weekly/modules/approval-record-dialog.vue';
|
||||
import MonthlyReportIndex from './monthly/index.vue';
|
||||
@@ -32,6 +35,12 @@ defineOptions({ name: 'PersonalCenterWorkReport' });
|
||||
type PageDialogMode = 'add' | 'edit' | 'detail';
|
||||
type ReportListExpose = {
|
||||
reload: (page?: number) => Promise<void>;
|
||||
teamSummary: Ref<Api.WorkReport.Common.TeamReportSummary | null>;
|
||||
teamSummaryLoading: Ref<boolean>;
|
||||
summaryPeriod: Ref<ReturnType<typeof resolveWorkReportSummaryPeriod>>;
|
||||
summaryPeriodKeys: ComputedRef<string[]>;
|
||||
hasSearchedDateRange: ComputedRef<boolean>;
|
||||
loadTeamSummary: () => Promise<void>;
|
||||
};
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
@@ -67,6 +76,19 @@ const allSubordinateUserIds = computed(() => collectSubordinateUserIds(subordina
|
||||
const selectedSubordinateNode = computed(() =>
|
||||
findSubordinateNode(subordinateTree.value, selectedSubordinateUserId.value)
|
||||
);
|
||||
const subordinateOptions = computed(() => {
|
||||
const options: Array<{ label: string; value: string }> = [];
|
||||
|
||||
const walk = (nodes?: Api.SystemManage.MySubordinateTreeNode[] | null) => {
|
||||
nodes?.forEach(node => {
|
||||
options.push({ label: node.userNickname, value: node.userId });
|
||||
walk(node.children ?? null);
|
||||
});
|
||||
};
|
||||
|
||||
walk(subordinateTree.value?.children ?? null);
|
||||
return options;
|
||||
});
|
||||
const isRootSelected = computed(() => Boolean(selectedSubordinateNode.value?.isRoot));
|
||||
const selectedTeamLabel = computed(() => {
|
||||
if (teamViewMode.value === 'self') return '我自己';
|
||||
@@ -118,6 +140,30 @@ function getListRef(reportType: WorkReportType) {
|
||||
return weeklyRef.value;
|
||||
}
|
||||
|
||||
const activeReportRef = computed(() => getListRef(activeTab.value));
|
||||
const activeTeamSummary = computed(() => activeReportRef.value?.teamSummary ?? null);
|
||||
const activeTeamSummaryLoading = computed(() => activeReportRef.value?.teamSummaryLoading ?? false);
|
||||
const activeSummaryPeriodKeys = computed(() => {
|
||||
const activeRef = activeReportRef.value;
|
||||
if (!activeRef) return [];
|
||||
return activeRef.summaryPeriodKeys ?? [];
|
||||
});
|
||||
const activeSummaryReportType = computed<Api.WorkReport.Common.ReportType>(() => {
|
||||
if (activeTab.value === 'monthly') return 'monthly';
|
||||
if (activeTab.value === 'project') return 'project';
|
||||
return 'weekly';
|
||||
});
|
||||
const activeSummaryPeriodLabel = computed(() => {
|
||||
const summaryPeriod = activeReportRef.value?.summaryPeriod;
|
||||
if (!summaryPeriod) return '';
|
||||
|
||||
if (activeTab.value === 'weekly') {
|
||||
return formatIsoWeekRangeLabel(summaryPeriod.periodStartDate, summaryPeriod.periodEndDate);
|
||||
}
|
||||
|
||||
return summaryPeriod.periodLabel ?? '';
|
||||
});
|
||||
|
||||
async function loadProjectOptions() {
|
||||
if (!canShowProjectTab.value) return;
|
||||
|
||||
@@ -198,6 +244,10 @@ function handleSubmitted() {
|
||||
reloadReport(currentReportType.value);
|
||||
}
|
||||
|
||||
async function handleSummaryRemind() {
|
||||
await activeReportRef.value?.loadTeamSummary();
|
||||
}
|
||||
|
||||
function closeFloatingPanels() {
|
||||
createVisible.value = false;
|
||||
pageDialogVisible.value = false;
|
||||
@@ -273,13 +323,24 @@ onBeforeRouteLeave(() => {
|
||||
:selected-label="selectedTeamLabel"
|
||||
:subordinate-count="subordinateTree?.subordinateCount || 0"
|
||||
@update:mode="handleTeamViewModeChange"
|
||||
/>
|
||||
>
|
||||
<TeamReportSummary
|
||||
v-if="isRootSelected && teamViewMode === 'team'"
|
||||
:report-type="activeSummaryReportType"
|
||||
:period-keys="activeSummaryPeriodKeys"
|
||||
:period-label="activeSummaryPeriodLabel"
|
||||
:loading="activeTeamSummaryLoading"
|
||||
:summary="activeTeamSummary"
|
||||
@reminded="handleSummaryRemind"
|
||||
/>
|
||||
</TeamContextPanel>
|
||||
|
||||
<WeeklyReportIndex
|
||||
v-show="activeTab === 'weekly'"
|
||||
ref="weeklyRef"
|
||||
class="flex-1-hidden"
|
||||
:team-context="teamContext"
|
||||
:subordinate-options="subordinateOptions"
|
||||
@create="openCreate('weekly')"
|
||||
@edit="openEdit('weekly', $event)"
|
||||
@detail="openDetail('weekly', $event)"
|
||||
@@ -291,6 +352,7 @@ onBeforeRouteLeave(() => {
|
||||
ref="monthlyRef"
|
||||
class="flex-1-hidden"
|
||||
:team-context="teamContext"
|
||||
:subordinate-options="subordinateOptions"
|
||||
@create="openCreate('monthly')"
|
||||
@edit="openEdit('monthly', $event)"
|
||||
@detail="openDetail('monthly', $event)"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="tsx">
|
||||
/* eslint-disable no-void */
|
||||
import { computed, markRaw, reactive, ref } from 'vue';
|
||||
import { computed, markRaw, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchDeleteMonthlyReport,
|
||||
fetchExportMonthlyReportContent,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import { type TeamViewContext, resolveTeamQueryUserIds } from '@/views/personal-center/shared/team-dashboard';
|
||||
import { type TeamViewContext } from '@/views/personal-center/shared/team-dashboard';
|
||||
import {
|
||||
type WorkReportRow,
|
||||
createMonthlySearchParams,
|
||||
@@ -27,8 +28,7 @@ import {
|
||||
resolveWorkReportStatusTagType,
|
||||
transformWorkReportPage
|
||||
} from '../shared/types';
|
||||
import { resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||
import TeamReportSummary from '../shared/components/team-report-summary.vue';
|
||||
import { buildMonthlyPeriodFromMonth, resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||
import MonthlyReportSearch from './modules/search-panel.vue';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
@@ -41,6 +41,7 @@ defineOptions({ name: 'MonthlyWorkReportIndex' });
|
||||
|
||||
const props = defineProps<{
|
||||
teamContext?: TeamViewContext | null;
|
||||
subordinateOptions?: Array<{ label: string; value: string }>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -68,8 +69,45 @@ const ACTION_ICON_MAP = {
|
||||
|
||||
const isTeamMode = computed(() => props.teamContext?.mode === 'team');
|
||||
const isTeamRootSelected = computed(() => Boolean(isTeamMode.value && props.teamContext?.isRootSelected));
|
||||
const currentTeamReporterIds = computed(() => resolveTeamQueryUserIds(props.teamContext));
|
||||
const currentTeamReporterIds = computed(() => {
|
||||
if (!isTeamMode.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isTeamRootSelected.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return props.teamContext?.selectedUserIds ?? [];
|
||||
});
|
||||
const resolvedTeamReporterIds = computed(() => {
|
||||
if (!isTeamMode.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (searchParams.reporterIds?.length) {
|
||||
return searchParams.reporterIds;
|
||||
}
|
||||
|
||||
return currentTeamReporterIds.value;
|
||||
});
|
||||
const reportTitle = computed(() => getWorkReportTypeDisplayLabel('monthly', isTeamMode.value));
|
||||
const normalizedPeriodRange = computed(() => {
|
||||
const periodRange = searchParams.periodStartDate;
|
||||
if (!periodRange?.length) {
|
||||
return periodRange;
|
||||
}
|
||||
|
||||
const [startDate, endDate] = periodRange;
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate || startDate);
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return periodRange;
|
||||
}
|
||||
|
||||
return [start.startOf('month').format('YYYY-MM-DD'), end.endOf('month').format('YYYY-MM-DD')];
|
||||
});
|
||||
|
||||
const table = useUIPaginatedTable<
|
||||
Awaited<ReturnType<typeof fetchGetMonthlyReportPage>>,
|
||||
@@ -79,52 +117,104 @@ const table = useUIPaginatedTable<
|
||||
api: () =>
|
||||
fetchGetMonthlyReportPage({
|
||||
...searchParams,
|
||||
reporterIds: currentTeamReporterIds.value
|
||||
periodStartDate: normalizedPeriodRange.value,
|
||||
reporterIds: resolvedTeamReporterIds.value
|
||||
}),
|
||||
transform: response => transformWorkReportPage(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 },
|
||||
...(isTeamMode.value ? [{ prop: 'reporterName', label: '提交人', minWidth: 100, showOverflowTooltip: true }] : []),
|
||||
{ prop: 'periodLabel', label: '月份', minWidth: 80, formatter: row => formatPeriod(row) },
|
||||
{
|
||||
prop: 'reporterDeptName',
|
||||
label: '部门',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.reporterDeptName || '--'
|
||||
},
|
||||
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||
{ prop: 'totalWorkHours', label: '总工时', minWidth: 80, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: isTeamMode.value ? 140 : 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
columns: () => {
|
||||
const cols: UI.TableColumn<Api.WorkReport.Monthly.MonthlyReport>[] = [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'periodLabel', label: '月份', minWidth: 80, formatter: row => formatPeriod(row) }
|
||||
];
|
||||
|
||||
if (isTeamMode.value) {
|
||||
cols.push({ prop: 'reporterName', label: '提交人', minWidth: 100, showOverflowTooltip: true });
|
||||
}
|
||||
]
|
||||
|
||||
cols.push(
|
||||
{
|
||||
prop: 'reporterDeptName',
|
||||
label: '部门',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.reporterDeptName || '--'
|
||||
},
|
||||
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||
{ prop: 'totalWorkHours', label: '总工时', minWidth: 80, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: isTeamMode.value ? 140 : 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
}
|
||||
);
|
||||
|
||||
return cols;
|
||||
}
|
||||
});
|
||||
|
||||
// 团队统计始终使用当前周期(本月),不跟随列表第一条数据的周期
|
||||
const summaryPeriod = computed(() => resolveWorkReportSummaryPeriod('monthly'));
|
||||
const summaryPeriod = computed(() =>
|
||||
resolveWorkReportSummaryPeriod('monthly', {
|
||||
periodRange: normalizedPeriodRange.value
|
||||
})
|
||||
);
|
||||
const summaryPeriodKeys = computed(() => {
|
||||
const dateRange = normalizedPeriodRange.value;
|
||||
const fallbackKey = summaryPeriod.value.periodKey;
|
||||
|
||||
if (!dateRange?.length) {
|
||||
return fallbackKey ? [fallbackKey] : [];
|
||||
}
|
||||
|
||||
const [startDate, endDate] = dateRange;
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate || startDate);
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return fallbackKey ? [fallbackKey] : [];
|
||||
}
|
||||
|
||||
const keys: string[] = [];
|
||||
const endBoundary = end.endOf('month');
|
||||
|
||||
for (
|
||||
let cursor = start.startOf('month');
|
||||
cursor.isBefore(endBoundary, 'month') || cursor.isSame(endBoundary, 'month');
|
||||
cursor = cursor.add(1, 'month')
|
||||
) {
|
||||
keys.push(buildMonthlyPeriodFromMonth(cursor).periodKey);
|
||||
}
|
||||
|
||||
return keys;
|
||||
});
|
||||
const hasSearchedDateRange = computed(() => searchParams.periodStartDate?.length === 2);
|
||||
|
||||
watch(
|
||||
() => isTeamMode.value,
|
||||
() => {
|
||||
table.reloadColumns();
|
||||
}
|
||||
);
|
||||
|
||||
function getRowActions(row: Api.WorkReport.Monthly.MonthlyReport): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
@@ -266,7 +356,8 @@ function createExportSearchParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return {
|
||||
...params,
|
||||
reporterIds: currentTeamReporterIds.value
|
||||
periodStartDate: normalizedPeriodRange.value,
|
||||
reporterIds: resolvedTeamReporterIds.value
|
||||
};
|
||||
}
|
||||
|
||||
@@ -332,31 +423,42 @@ async function loadTeamSummary() {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateRange = normalizedPeriodRange.value;
|
||||
const summaryParams: Api.WorkReport.Common.TeamReportSummaryParams = { reportType: 'monthly' };
|
||||
|
||||
if (dateRange?.length === 2) {
|
||||
summaryParams.periodStartDate = dateRange[0];
|
||||
summaryParams.periodEndDate = dateRange[1];
|
||||
} else {
|
||||
summaryParams.periodKey = summaryPeriod.value.periodKey;
|
||||
}
|
||||
|
||||
teamSummaryLoading.value = true;
|
||||
const { error, data } = await fetchGetTeamReportSummary({
|
||||
reportType: 'monthly',
|
||||
periodKey: summaryPeriod.value.periodKey
|
||||
});
|
||||
const { error, data } = await fetchGetTeamReportSummary(summaryParams);
|
||||
teamSummaryLoading.value = false;
|
||||
|
||||
teamSummary.value = error || !data ? null : data;
|
||||
}
|
||||
|
||||
defineExpose({ reload });
|
||||
defineExpose({
|
||||
reload,
|
||||
teamSummary,
|
||||
teamSummaryLoading,
|
||||
summaryPeriod,
|
||||
summaryPeriodKeys,
|
||||
hasSearchedDateRange,
|
||||
loadTeamSummary
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<MonthlyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<TeamReportSummary
|
||||
v-if="isTeamRootSelected"
|
||||
report-type="monthly"
|
||||
:period-key="summaryPeriod.periodKey"
|
||||
:period-label="formatPeriod(summaryPeriod)"
|
||||
:loading="teamSummaryLoading"
|
||||
:summary="teamSummary"
|
||||
@reminded="loadTeamSummary"
|
||||
<MonthlyReportSearch
|
||||
v-model:model="searchParams"
|
||||
:team-mode="isTeamMode"
|
||||
:subordinate-options="props.subordinateOptions"
|
||||
@reset="resetSearchParams"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
|
||||
@@ -4,6 +4,8 @@ import SharedWorkReportSearch from '../../shared/components/search-panel.vue';
|
||||
defineOptions({ name: 'MonthlyReportSearch' });
|
||||
|
||||
defineProps<{
|
||||
teamMode?: boolean;
|
||||
subordinateOptions?: Array<{ label: string; value: string }>;
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}>();
|
||||
|
||||
@@ -19,6 +21,8 @@ const emit = defineEmits<{
|
||||
<SharedWorkReportSearch
|
||||
v-model:model="model"
|
||||
report-type="monthly"
|
||||
:team-mode="teamMode"
|
||||
:subordinate-options="subordinateOptions"
|
||||
:project-options="projectOptions"
|
||||
@reset="emit('reset')"
|
||||
@search="emit('search')"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* eslint-disable no-void */
|
||||
import { computed, markRaw, reactive, ref } from 'vue';
|
||||
import { ElMessageBox, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchDeleteProjectReport,
|
||||
fetchExportProjectReportContent,
|
||||
@@ -27,8 +28,7 @@ import {
|
||||
resolveWorkReportStatusTagType,
|
||||
transformWorkReportPage
|
||||
} from '../shared/types';
|
||||
import { resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||
import TeamReportSummary from '../shared/components/team-report-summary.vue';
|
||||
import { buildProjectPeriodFromMonth, resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||
import ProjectReportSearch from './modules/search-panel.vue';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
@@ -72,6 +72,22 @@ const isTeamMode = computed(() => props.teamContext?.mode === 'team');
|
||||
const isTeamRootSelected = computed(() => Boolean(isTeamMode.value && props.teamContext?.isRootSelected));
|
||||
const currentProjectOwnerIds = computed(() => resolveTeamQueryUserIds(props.teamContext));
|
||||
const reportTitle = computed(() => getWorkReportTypeDisplayLabel('project', isTeamMode.value));
|
||||
const normalizedPeriodRange = computed(() => {
|
||||
const periodRange = searchParams.periodStartDate;
|
||||
if (!periodRange?.length) {
|
||||
return periodRange;
|
||||
}
|
||||
|
||||
const [startDate, endDate] = periodRange;
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate || startDate);
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return periodRange;
|
||||
}
|
||||
|
||||
return [start.startOf('month').format('YYYY-MM-DD'), end.endOf('month').format('YYYY-MM-DD')];
|
||||
});
|
||||
|
||||
const table = useUIPaginatedTable<
|
||||
Awaited<ReturnType<typeof fetchGetProjectReportPage>>,
|
||||
@@ -81,6 +97,7 @@ const table = useUIPaginatedTable<
|
||||
api: () =>
|
||||
fetchGetProjectReportPage({
|
||||
...searchParams,
|
||||
periodStartDate: normalizedPeriodRange.value,
|
||||
projectOwnerIds: currentProjectOwnerIds.value
|
||||
}),
|
||||
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
@@ -129,7 +146,42 @@ const table = useUIPaginatedTable<
|
||||
});
|
||||
|
||||
// 团队统计始终使用当前周期(当前半月),不跟随列表第一条数据的周期
|
||||
const summaryPeriod = computed(() => resolveWorkReportSummaryPeriod('project'));
|
||||
const summaryPeriod = computed(() =>
|
||||
resolveWorkReportSummaryPeriod('project', {
|
||||
periodRange: normalizedPeriodRange.value
|
||||
})
|
||||
);
|
||||
const summaryPeriodKeys = computed(() => {
|
||||
const dateRange = normalizedPeriodRange.value;
|
||||
const fallbackKey = summaryPeriod.value.periodKey;
|
||||
|
||||
if (!dateRange?.length) {
|
||||
return fallbackKey ? [fallbackKey] : [];
|
||||
}
|
||||
|
||||
const [startDate, endDate] = dateRange;
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate || startDate);
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return fallbackKey ? [fallbackKey] : [];
|
||||
}
|
||||
|
||||
const keys: string[] = [];
|
||||
const endBoundary = end.endOf('month');
|
||||
|
||||
for (
|
||||
let cursor = start.startOf('month');
|
||||
cursor.isBefore(endBoundary, 'month') || cursor.isSame(endBoundary, 'month');
|
||||
cursor = cursor.add(1, 'month')
|
||||
) {
|
||||
keys.push(buildProjectPeriodFromMonth(cursor, 1).periodKey);
|
||||
keys.push(buildProjectPeriodFromMonth(cursor, 2).periodKey);
|
||||
}
|
||||
|
||||
return keys;
|
||||
});
|
||||
const hasSearchedDateRange = computed(() => searchParams.periodStartDate?.length === 2);
|
||||
|
||||
function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
@@ -271,6 +323,7 @@ function createExportSearchParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return {
|
||||
...params,
|
||||
periodStartDate: normalizedPeriodRange.value,
|
||||
projectOwnerIds: currentProjectOwnerIds.value
|
||||
};
|
||||
}
|
||||
@@ -337,17 +390,32 @@ async function loadTeamSummary() {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateRange = normalizedPeriodRange.value;
|
||||
const summaryParams: Api.WorkReport.Common.TeamReportSummaryParams = { reportType: 'project' };
|
||||
|
||||
if (dateRange?.length === 2) {
|
||||
summaryParams.periodStartDate = dateRange[0];
|
||||
summaryParams.periodEndDate = dateRange[1];
|
||||
} else {
|
||||
summaryParams.periodKey = summaryPeriod.value.periodKey;
|
||||
}
|
||||
|
||||
teamSummaryLoading.value = true;
|
||||
const { error, data } = await fetchGetTeamReportSummary({
|
||||
reportType: 'project',
|
||||
periodKey: summaryPeriod.value.periodKey
|
||||
});
|
||||
const { error, data } = await fetchGetTeamReportSummary(summaryParams);
|
||||
teamSummaryLoading.value = false;
|
||||
|
||||
teamSummary.value = error || !data ? null : data;
|
||||
}
|
||||
|
||||
defineExpose({ reload });
|
||||
defineExpose({
|
||||
reload,
|
||||
teamSummary,
|
||||
teamSummaryLoading,
|
||||
summaryPeriod,
|
||||
summaryPeriodKeys,
|
||||
hasSearchedDateRange,
|
||||
loadTeamSummary
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -364,16 +432,6 @@ defineExpose({ reload });
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<TeamReportSummary
|
||||
v-if="isTeamRootSelected"
|
||||
report-type="project"
|
||||
:period-key="summaryPeriod.periodKey"
|
||||
:period-label="formatPeriod(summaryPeriod)"
|
||||
:loading="teamSummaryLoading"
|
||||
:summary="teamSummary"
|
||||
@reminded="loadTeamSummary"
|
||||
/>
|
||||
|
||||
<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">
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable no-void */
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import { fetchGetWorkReportStatusDict } from '@/service/api';
|
||||
import type { SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||
import { BOOLEAN_TRUE_FALSE_OPTIONS, type WorkReportSearchParams, type WorkReportType } from '../types';
|
||||
import { formatIsoWeekRangeLabel, normalizeWeeklySearchRange } from '../utils';
|
||||
|
||||
dayjs.extend(isoWeek);
|
||||
|
||||
defineOptions({ name: 'WorkReportSearch' });
|
||||
|
||||
interface Props {
|
||||
reportType: WorkReportType;
|
||||
teamMode?: boolean;
|
||||
subordinateOptions?: Array<{ label: string; value: string }>;
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
teamMode: false,
|
||||
subordinateOptions: () => [],
|
||||
projectOptions: () => []
|
||||
});
|
||||
|
||||
@@ -35,6 +44,12 @@ const statusOptions = computed(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const weeklyPeriodPlaceholder = computed(() => {
|
||||
const range = normalizeWeeklySearchRange(model.value.periodStartDate as string[] | undefined);
|
||||
if (!range?.length) return '请选择周报周期';
|
||||
return formatIsoWeekRangeLabel(range[0], range[1]) || '请选择周报周期';
|
||||
});
|
||||
|
||||
const fields = computed<SearchField[]>(() => {
|
||||
const baseFields: SearchField[] = [
|
||||
{ key: 'statusCode', label: '状态', type: 'select', options: statusOptions.value, placeholder: '请选择状态' },
|
||||
@@ -51,8 +66,33 @@ const fields = computed<SearchField[]>(() => {
|
||||
};
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
const weeklyPeriodField: SearchField = {
|
||||
key: 'periodStartDate',
|
||||
label: '周期',
|
||||
type: 'dateRange',
|
||||
placeholder: weeklyPeriodPlaceholder.value,
|
||||
format: 'YYYY[年第]ww[周]',
|
||||
rangeSeparator: '至'
|
||||
};
|
||||
|
||||
const teamReporterField: SearchField[] = props.teamMode
|
||||
? [
|
||||
{
|
||||
key: 'reporterIds',
|
||||
label: '提交人',
|
||||
type: 'select',
|
||||
options: props.subordinateOptions,
|
||||
placeholder: '请选择提交人',
|
||||
transformValue: value => (value ? [value] : undefined),
|
||||
resolveValue: value => (Array.isArray(value) ? value[0] : value)
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
return [
|
||||
...baseFields,
|
||||
baseFields[0],
|
||||
weeklyPeriodField,
|
||||
...teamReporterField,
|
||||
{
|
||||
key: 'isBusinessTrip',
|
||||
label: '是否出差',
|
||||
@@ -81,7 +121,21 @@ const fields = computed<SearchField[]>(() => {
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly') {
|
||||
return [baseFields[0], monthPeriodField];
|
||||
const teamReporterField: SearchField[] = props.teamMode
|
||||
? [
|
||||
{
|
||||
key: 'reporterIds',
|
||||
label: '提交人',
|
||||
type: 'select',
|
||||
options: props.subordinateOptions,
|
||||
placeholder: '请选择提交人',
|
||||
transformValue: value => (value ? [value] : undefined),
|
||||
resolveValue: value => (Array.isArray(value) ? value[0] : value)
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
return [baseFields[0], monthPeriodField, ...teamReporterField];
|
||||
}
|
||||
|
||||
return baseFields;
|
||||
@@ -95,6 +149,34 @@ async function loadStatusDict() {
|
||||
onMounted(() => {
|
||||
loadStatusDict();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.reportType,
|
||||
type => {
|
||||
if (type !== 'weekly') return;
|
||||
model.value.periodStartDate = normalizeWeeklySearchRange(model.value.periodStartDate as string[] | undefined);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => model.value.periodStartDate,
|
||||
value => {
|
||||
if (props.reportType !== 'weekly') return;
|
||||
|
||||
const normalizedValue = normalizeWeeklySearchRange(value as string[] | undefined);
|
||||
const currentValue = Array.isArray(value) ? value : [];
|
||||
|
||||
if (
|
||||
normalizedValue?.length === currentValue.length &&
|
||||
normalizedValue?.every((item, index) => item === currentValue[index])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value.periodStartDate = normalizedValue;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchRemindTeamReport } from '@/service/api';
|
||||
import { formatIsoWeekRangeLabel } from '../utils';
|
||||
|
||||
defineOptions({ name: 'TeamReportSummary' });
|
||||
|
||||
interface Props {
|
||||
reportType: Api.WorkReport.Common.ReportType;
|
||||
periodKey: string;
|
||||
periodKeys?: string[];
|
||||
periodLabel?: string;
|
||||
loading?: boolean;
|
||||
summary?: Api.WorkReport.Common.TeamReportSummary | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
periodKeys: () => [],
|
||||
periodLabel: '',
|
||||
loading: false,
|
||||
summary: null
|
||||
@@ -24,6 +27,34 @@ const emit = defineEmits<{
|
||||
|
||||
const remindingAll = ref(false);
|
||||
const remindingUserId = ref('');
|
||||
const canRemind = computed(() => props.periodKeys.length > 0);
|
||||
|
||||
function formatSummaryPeriodLabel() {
|
||||
if (!props.summary?.periodStartDate || !props.summary?.periodEndDate) {
|
||||
return props.periodLabel;
|
||||
}
|
||||
|
||||
const start = dayjs(props.summary.periodStartDate);
|
||||
const end = dayjs(props.summary.periodEndDate);
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return `${props.summary.periodStartDate} 至 ${props.summary.periodEndDate}`;
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly' || props.reportType === 'project') {
|
||||
const startMonth = start.format('YYYY-MM');
|
||||
const endMonth = end.format('YYYY-MM');
|
||||
return startMonth === endMonth ? startMonth : `${startMonth} 至 ${endMonth}`;
|
||||
}
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
return formatIsoWeekRangeLabel(props.summary.periodStartDate, props.summary.periodEndDate);
|
||||
}
|
||||
|
||||
return `${start.format('YYYY-MM-DD')} 至 ${end.format('YYYY-MM-DD')}`;
|
||||
}
|
||||
|
||||
const displayPeriodLabel = computed(() => formatSummaryPeriodLabel());
|
||||
|
||||
const cards = computed(() => [
|
||||
{ label: '应填人数', value: props.summary?.totalShouldSubmit ?? 0 },
|
||||
@@ -33,6 +64,8 @@ const cards = computed(() => [
|
||||
]);
|
||||
|
||||
async function handleRemind(userIds?: string[]) {
|
||||
if (!props.periodKeys.length) return;
|
||||
|
||||
const targetUserId = userIds?.length === 1 ? userIds[0] : '';
|
||||
|
||||
if (targetUserId) {
|
||||
@@ -41,27 +74,36 @@ async function handleRemind(userIds?: string[]) {
|
||||
remindingAll.value = true;
|
||||
}
|
||||
|
||||
const { error, data } = await fetchRemindTeamReport({
|
||||
reportType: props.reportType,
|
||||
periodKey: props.periodKey,
|
||||
userIds
|
||||
});
|
||||
const results = await Promise.all(
|
||||
props.periodKeys.map(periodKey =>
|
||||
fetchRemindTeamReport({
|
||||
reportType: props.reportType,
|
||||
periodKey,
|
||||
userIds
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (!targetUserId) {
|
||||
remindingAll.value = false;
|
||||
}
|
||||
remindingUserId.value = '';
|
||||
|
||||
if (error) return;
|
||||
const remindedCount = results.reduce((total, result) => {
|
||||
if (result.error) return total;
|
||||
return total + (result.data?.remindedCount ?? 0);
|
||||
}, 0);
|
||||
|
||||
window.$message?.success(`已催办 ${data?.remindedCount ?? 0} 人`);
|
||||
if (!remindedCount) return;
|
||||
|
||||
window.$message?.success(props.periodKeys.length > 1 ? '已按所选区间发送催办提醒' : `已催办 ${remindedCount} 人`);
|
||||
emit('reminded');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="props.loading" class="team-report-summary">
|
||||
<div v-if="props.periodLabel" class="team-report-summary__period">{{ props.periodLabel }}</div>
|
||||
<div v-if="displayPeriodLabel" class="team-report-summary__period">{{ displayPeriodLabel }}</div>
|
||||
|
||||
<div class="team-report-summary__grid">
|
||||
<div v-for="card in cards" :key="card.label" class="team-report-summary__item">
|
||||
@@ -83,6 +125,7 @@ async function handleRemind(userIds?: string[]) {
|
||||
>
|
||||
<span class="team-report-summary__user-name">{{ user.userNickname }}</span>
|
||||
<ElButton
|
||||
v-if="canRemind"
|
||||
link
|
||||
type="primary"
|
||||
:loading="remindingUserId === user.userId"
|
||||
@@ -94,7 +137,7 @@ async function handleRemind(userIds?: string[]) {
|
||||
</div>
|
||||
<ElEmpty v-else :image-size="60" description="暂无待提交人员" />
|
||||
|
||||
<div class="team-report-summary__popover-footer">
|
||||
<div v-if="canRemind" class="team-report-summary__popover-footer">
|
||||
<ElButton
|
||||
size="small"
|
||||
type="primary"
|
||||
|
||||
@@ -155,12 +155,16 @@ export function createInitBaseSearchParams() {
|
||||
export function createWeeklySearchParams(): Api.WorkReport.Weekly.WeeklyReportSearchParams {
|
||||
return {
|
||||
...createInitBaseSearchParams(),
|
||||
reporterIds: undefined,
|
||||
isBusinessTrip: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function createMonthlySearchParams(): Api.WorkReport.Monthly.MonthlyReportSearchParams {
|
||||
return createInitBaseSearchParams();
|
||||
return {
|
||||
...createInitBaseSearchParams(),
|
||||
reporterIds: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function createProjectSearchParams(): Api.WorkReport.Project.ProjectReportSearchParams {
|
||||
|
||||
@@ -46,6 +46,29 @@ export function getIsoWeekDisplay(date: string | dayjs.Dayjs) {
|
||||
return `${selectedDate.format('GGGG')} 第${String(selectedDate.isoWeek()).padStart(2, '0')} 周`;
|
||||
}
|
||||
|
||||
export function formatIsoWeekCompactLabel(date: string | dayjs.Dayjs) {
|
||||
const selectedDate = dayjs(date);
|
||||
if (!selectedDate.isValid()) return '';
|
||||
|
||||
const weekDate = selectedDate.startOf('isoWeek');
|
||||
const weekYear = weekDate.add(3, 'day').format('YYYY');
|
||||
return `${weekYear}-${String(weekDate.isoWeek()).padStart(2, '0')}周`;
|
||||
}
|
||||
|
||||
export function formatIsoWeekRangeLabel(
|
||||
startDate?: string | dayjs.Dayjs | null,
|
||||
endDate?: string | dayjs.Dayjs | null
|
||||
) {
|
||||
const startLabel = startDate ? formatIsoWeekCompactLabel(startDate) : '';
|
||||
const endLabel = endDate ? formatIsoWeekCompactLabel(endDate) : '';
|
||||
|
||||
if (!startLabel && !endLabel) return '';
|
||||
if (!endLabel || startLabel === endLabel) return startLabel;
|
||||
if (!startLabel) return endLabel;
|
||||
|
||||
return `${startLabel} 至 ${endLabel}`;
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line max-params */
|
||||
function buildPeriod(reportType: WorkReportType, start: dayjs.Dayjs, end: dayjs.Dayjs, label: string, flag?: number) {
|
||||
const startText = start.format('YYYY-MM-DD');
|
||||
@@ -67,6 +90,20 @@ export function buildWeeklyPeriodFromDate(date: string | dayjs.Dayjs) {
|
||||
return buildPeriod('weekly', start, end, formatRangeLabel(start, end));
|
||||
}
|
||||
|
||||
export function normalizeWeeklySearchRange(periodRange?: string[] | null): string[] | undefined {
|
||||
if (!periodRange?.length) return undefined;
|
||||
|
||||
const [startDate, endDate] = periodRange;
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate || startDate);
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return periodRange ?? undefined;
|
||||
}
|
||||
|
||||
return [start.startOf('isoWeek').format('YYYY-MM-DD'), end.startOf('isoWeek').format('YYYY-MM-DD')];
|
||||
}
|
||||
|
||||
export function buildMonthlyPeriodFromMonth(month: string | dayjs.Dayjs) {
|
||||
const selectedMonth = dayjs(month);
|
||||
const start = selectedMonth.startOf('month');
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="tsx">
|
||||
/* eslint-disable no-void */
|
||||
import { computed, markRaw, reactive, ref } from 'vue';
|
||||
import { computed, markRaw, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox, ElTag, ElTooltip } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchDeleteWeeklyReport,
|
||||
fetchExportWeeklyReportContent,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import { type TeamViewContext, resolveTeamQueryUserIds } from '@/views/personal-center/shared/team-dashboard';
|
||||
import { type TeamViewContext } from '@/views/personal-center/shared/team-dashboard';
|
||||
import {
|
||||
type WorkReportRow,
|
||||
createWeeklySearchParams,
|
||||
@@ -29,8 +30,7 @@ import {
|
||||
resolveWorkReportStatusTagType,
|
||||
transformWorkReportPage
|
||||
} from '../shared/types';
|
||||
import { resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||
import TeamReportSummary from '../shared/components/team-report-summary.vue';
|
||||
import { buildWeeklyPeriodFromDate, normalizeWeeklySearchRange, resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||
import WeeklyReportSearch from './modules/search-panel.vue';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
@@ -43,6 +43,7 @@ defineOptions({ name: 'WeeklyWorkReportIndex' });
|
||||
|
||||
const props = defineProps<{
|
||||
teamContext?: TeamViewContext | null;
|
||||
subordinateOptions?: Array<{ label: string; value: string }>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -70,9 +71,30 @@ const ACTION_ICON_MAP = {
|
||||
|
||||
const isTeamMode = computed(() => props.teamContext?.mode === 'team');
|
||||
const isTeamRootSelected = computed(() => Boolean(isTeamMode.value && props.teamContext?.isRootSelected));
|
||||
const currentTeamReporterIds = computed(() => resolveTeamQueryUserIds(props.teamContext));
|
||||
const reportTitle = computed(() => getWorkReportTypeDisplayLabel('weekly', isTeamMode.value));
|
||||
const currentTeamReporterIds = computed(() => {
|
||||
if (!isTeamMode.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isTeamRootSelected.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return props.teamContext?.selectedUserIds ?? [];
|
||||
});
|
||||
const resolvedTeamReporterIds = computed(() => {
|
||||
if (!isTeamMode.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (searchParams.reporterIds?.length) {
|
||||
return searchParams.reporterIds;
|
||||
}
|
||||
|
||||
return currentTeamReporterIds.value;
|
||||
});
|
||||
const reportTitle = computed(() => getWorkReportTypeDisplayLabel('weekly', isTeamMode.value));
|
||||
const normalizedPeriodRange = computed(() => normalizeWeeklySearchRange(searchParams.periodStartDate));
|
||||
const table = useUIPaginatedTable<
|
||||
Awaited<ReturnType<typeof fetchGetWeeklyReportPage>>,
|
||||
Api.WorkReport.Weekly.WeeklyReport
|
||||
@@ -81,79 +103,134 @@ const table = useUIPaginatedTable<
|
||||
api: () =>
|
||||
fetchGetWeeklyReportPage({
|
||||
...searchParams,
|
||||
reporterIds: currentTeamReporterIds.value
|
||||
periodStartDate: normalizedPeriodRange.value,
|
||||
reporterIds: resolvedTeamReporterIds.value
|
||||
}),
|
||||
transform: response => transformWorkReportPage(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 },
|
||||
...(isTeamMode.value ? [{ prop: 'reporterName', label: '提交人', minWidth: 100, showOverflowTooltip: true }] : []),
|
||||
{
|
||||
prop: 'periodLabel',
|
||||
label: '周期',
|
||||
minWidth: 150,
|
||||
formatter: row => {
|
||||
const periodText = formatWeeklyPeriodLabel(row);
|
||||
const weekLabel = formatPeriodDateRange(row);
|
||||
if (!weekLabel || weekLabel === '--') return periodText;
|
||||
return (
|
||||
<ElTooltip content={weekLabel} placement="top">
|
||||
<span>{periodText}</span>
|
||||
</ElTooltip>
|
||||
);
|
||||
columns: () => {
|
||||
const cols: UI.TableColumn<Api.WorkReport.Weekly.WeeklyReport>[] = [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{
|
||||
prop: 'periodLabel',
|
||||
label: '周期',
|
||||
minWidth: 150,
|
||||
formatter: row => {
|
||||
const periodText = formatWeeklyPeriodLabel(row);
|
||||
const weekLabel = formatPeriodDateRange(row);
|
||||
if (!weekLabel || weekLabel === '--') return periodText;
|
||||
return (
|
||||
<ElTooltip content={weekLabel} placement="top">
|
||||
<span>{periodText}</span>
|
||||
</ElTooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'reporterDeptName',
|
||||
label: '部门',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.reporterDeptName || '--'
|
||||
},
|
||||
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||
{ prop: 'totalWorkHours', label: '总工时', minWidth: 80, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||
{
|
||||
prop: 'isBusinessTrip',
|
||||
label: '出差',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (row.isBusinessTrip ? '是' : '否')
|
||||
},
|
||||
{
|
||||
prop: 'totalTravelDays',
|
||||
label: '出差天数',
|
||||
minWidth: 90,
|
||||
formatter: row => formatEmptyText(row.totalTravelDays)
|
||||
},
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: isTeamMode.value ? 140 : 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
];
|
||||
|
||||
if (isTeamMode.value) {
|
||||
cols.push({ prop: 'reporterName', label: '提交人', minWidth: 100, showOverflowTooltip: true });
|
||||
}
|
||||
]
|
||||
|
||||
cols.push(
|
||||
{
|
||||
prop: 'reporterDeptName',
|
||||
label: '部门',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.reporterDeptName || '--'
|
||||
},
|
||||
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||
{ prop: 'totalWorkHours', label: '总工时', minWidth: 80, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||
{
|
||||
prop: 'isBusinessTrip',
|
||||
label: '出差',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (row.isBusinessTrip ? '是' : '否')
|
||||
},
|
||||
{
|
||||
prop: 'totalTravelDays',
|
||||
label: '出差天数',
|
||||
minWidth: 90,
|
||||
formatter: row => formatEmptyText(row.totalTravelDays)
|
||||
},
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: isTeamMode.value ? 140 : 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
}
|
||||
);
|
||||
|
||||
return cols;
|
||||
}
|
||||
});
|
||||
|
||||
// 团队统计始终使用当前周期(本周),不跟随列表第一条数据的周期
|
||||
const summaryPeriod = computed(() => resolveWorkReportSummaryPeriod('weekly'));
|
||||
const summaryPeriod = computed(() =>
|
||||
resolveWorkReportSummaryPeriod('weekly', {
|
||||
periodRange: normalizedPeriodRange.value
|
||||
})
|
||||
);
|
||||
const summaryPeriodKeys = computed(() => {
|
||||
const dateRange = normalizedPeriodRange.value;
|
||||
const fallbackKey = summaryPeriod.value.periodKey;
|
||||
|
||||
if (!dateRange?.length) {
|
||||
return fallbackKey ? [fallbackKey] : [];
|
||||
}
|
||||
|
||||
const [startDate, endDate] = dateRange;
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate || startDate);
|
||||
|
||||
if (!start.isValid() || !end.isValid()) {
|
||||
return fallbackKey ? [fallbackKey] : [];
|
||||
}
|
||||
|
||||
const keys: string[] = [];
|
||||
const startBoundary = start.startOf('day');
|
||||
const endBoundary = end.endOf('day');
|
||||
|
||||
for (
|
||||
let cursor = start.startOf('isoWeek');
|
||||
cursor.isBefore(endBoundary, 'day') || cursor.isSame(endBoundary, 'day');
|
||||
cursor = cursor.add(1, 'week')
|
||||
) {
|
||||
if (!cursor.isBefore(startBoundary, 'day')) {
|
||||
keys.push(buildWeeklyPeriodFromDate(cursor).periodKey);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
});
|
||||
const hasSearchedDateRange = computed(() => searchParams.periodStartDate?.length === 2);
|
||||
|
||||
watch(
|
||||
() => isTeamMode.value,
|
||||
() => {
|
||||
table.reloadColumns();
|
||||
}
|
||||
);
|
||||
|
||||
function getRowActions(row: Api.WorkReport.Weekly.WeeklyReport): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
@@ -295,7 +372,8 @@ function createExportSearchParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return {
|
||||
...params,
|
||||
reporterIds: currentTeamReporterIds.value
|
||||
periodStartDate: normalizedPeriodRange.value,
|
||||
reporterIds: resolvedTeamReporterIds.value
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,31 +439,42 @@ async function loadTeamSummary() {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateRange = normalizedPeriodRange.value;
|
||||
const summaryParams: Api.WorkReport.Common.TeamReportSummaryParams = { reportType: 'weekly' };
|
||||
|
||||
if (dateRange?.length === 2) {
|
||||
summaryParams.periodStartDate = dateRange[0];
|
||||
summaryParams.periodEndDate = dateRange[1];
|
||||
} else {
|
||||
summaryParams.periodKey = summaryPeriod.value.periodKey;
|
||||
}
|
||||
|
||||
teamSummaryLoading.value = true;
|
||||
const { error, data } = await fetchGetTeamReportSummary({
|
||||
reportType: 'weekly',
|
||||
periodKey: summaryPeriod.value.periodKey
|
||||
});
|
||||
const { error, data } = await fetchGetTeamReportSummary(summaryParams);
|
||||
teamSummaryLoading.value = false;
|
||||
|
||||
teamSummary.value = error || !data ? null : data;
|
||||
}
|
||||
|
||||
defineExpose({ reload });
|
||||
defineExpose({
|
||||
reload,
|
||||
teamSummary,
|
||||
teamSummaryLoading,
|
||||
summaryPeriod,
|
||||
summaryPeriodKeys,
|
||||
hasSearchedDateRange,
|
||||
loadTeamSummary
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<WeeklyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<TeamReportSummary
|
||||
v-if="isTeamRootSelected"
|
||||
report-type="weekly"
|
||||
:period-key="summaryPeriod.periodKey"
|
||||
:period-label="formatWeeklyPeriodLabel(summaryPeriod)"
|
||||
:loading="teamSummaryLoading"
|
||||
:summary="teamSummary"
|
||||
@reminded="loadTeamSummary"
|
||||
<WeeklyReportSearch
|
||||
v-model:model="searchParams"
|
||||
:team-mode="isTeamMode"
|
||||
:subordinate-options="props.subordinateOptions"
|
||||
@reset="resetSearchParams"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
|
||||
@@ -4,6 +4,8 @@ import SharedWorkReportSearch from '../../shared/components/search-panel.vue';
|
||||
defineOptions({ name: 'WeeklyReportSearch' });
|
||||
|
||||
defineProps<{
|
||||
teamMode?: boolean;
|
||||
subordinateOptions?: Array<{ label: string; value: string }>;
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}>();
|
||||
|
||||
@@ -19,6 +21,8 @@ const emit = defineEmits<{
|
||||
<SharedWorkReportSearch
|
||||
v-model:model="model"
|
||||
report-type="weekly"
|
||||
:team-mode="teamMode"
|
||||
:subordinate-options="subordinateOptions"
|
||||
:project-options="projectOptions"
|
||||
@reset="emit('reset')"
|
||||
@search="emit('search')"
|
||||
|
||||
Reference in New Issue
Block a user