feat(工作报告、加班申请团队视角): 工作报告、加班申请现在可以查看团队视角了(查看下属)。

fix(工作报告): 修复周报在新增/编辑时,不能展示工作日志。
This commit is contained in:
dk
2026-06-14 23:57:42 +08:00
parent 17690283f6
commit 3c1cf6c7fa
19 changed files with 1618 additions and 94 deletions

View File

@@ -0,0 +1,217 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { fetchRemindTeamReport } from '@/service/api';
defineOptions({ name: 'TeamReportSummary' });
interface Props {
reportType: Api.WorkReport.Common.ReportType;
periodKey: string;
periodLabel?: string;
loading?: boolean;
summary?: Api.WorkReport.Common.TeamReportSummary | null;
}
const props = withDefaults(defineProps<Props>(), {
periodLabel: '',
loading: false,
summary: null
});
const emit = defineEmits<{
reminded: [];
}>();
const remindingAll = ref(false);
const remindingUserId = ref('');
const cards = computed(() => [
{ label: '应填人数', value: props.summary?.totalShouldSubmit ?? 0 },
{ label: '已提交', value: props.summary?.submittedCount ?? 0 },
{ label: '待提交', value: props.summary?.unsubmittedCount ?? 0, key: 'unsubmitted' as const },
{ label: '待审批', value: props.summary?.pendingApprovalCount ?? 0 }
]);
async function handleRemind(userIds?: string[]) {
const targetUserId = userIds?.length === 1 ? userIds[0] : '';
if (targetUserId) {
remindingUserId.value = targetUserId;
} else {
remindingAll.value = true;
}
const { error, data } = await fetchRemindTeamReport({
reportType: props.reportType,
periodKey: props.periodKey,
userIds
});
if (!targetUserId) {
remindingAll.value = false;
}
remindingUserId.value = '';
if (error) return;
window.$message?.success(`已催办 ${data?.remindedCount ?? 0}`);
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 class="team-report-summary__grid">
<div v-for="card in cards" :key="card.label" class="team-report-summary__item">
<div class="team-report-summary__label">{{ card.label }}</div>
<div class="team-report-summary__value">
<template v-if="card.key === 'unsubmitted'">
<ElPopover placement="bottom" :width="300" trigger="hover">
<template #reference>
<button type="button" class="team-report-summary__link-button">{{ card.value }}</button>
</template>
<div class="team-report-summary__popover">
<div class="team-report-summary__popover-title">未提交人员</div>
<div v-if="props.summary?.unsubmittedUsers?.length" class="team-report-summary__user-list">
<div
v-for="user in props.summary.unsubmittedUsers"
:key="user.userId"
class="team-report-summary__user-item"
>
<span class="team-report-summary__user-name">{{ user.userNickname }}</span>
<ElButton
link
type="primary"
:loading="remindingUserId === user.userId"
@click="handleRemind([user.userId])"
>
催办
</ElButton>
</div>
</div>
<ElEmpty v-else :image-size="60" description="暂无待提交人员" />
<div class="team-report-summary__popover-footer">
<ElButton
size="small"
type="primary"
plain
:loading="remindingAll"
:disabled="!props.summary?.unsubmittedUsers?.length"
@click="handleRemind()"
>
一键催办全部
</ElButton>
</div>
</div>
</ElPopover>
</template>
<template v-else>
{{ card.value }}
</template>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.team-report-summary {
display: grid;
gap: 12px;
}
.team-report-summary__period {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.team-report-summary__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.team-report-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-report-summary__label {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.team-report-summary__value {
color: var(--el-text-color-primary);
font-size: 22px;
font-weight: 600;
line-height: 1.2;
}
.team-report-summary__link-button {
padding: 0;
border: none;
background: transparent;
color: var(--el-color-primary);
font: inherit;
cursor: pointer;
}
.team-report-summary__popover {
display: grid;
gap: 10px;
}
.team-report-summary__popover-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.team-report-summary__user-list {
display: grid;
gap: 8px;
max-height: 240px;
overflow: auto;
}
.team-report-summary__user-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
border-radius: 8px;
background: var(--el-fill-color-light);
}
.team-report-summary__user-name {
min-width: 0;
color: var(--el-text-color-regular);
}
.team-report-summary__popover-footer {
display: flex;
justify-content: flex-end;
}
@media (width <= 1200px) {
.team-report-summary__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (width <= 768px) {
.team-report-summary__grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -41,6 +41,16 @@ export const WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
project: '项目半月报'
};
export const TEAM_WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
weekly: '团队周报',
monthly: '团队月报',
project: '团队项目半月报'
};
export function getWorkReportTypeDisplayLabel(reportType: WorkReportType, isTeamMode = false) {
return isTeamMode ? TEAM_WORK_REPORT_TYPE_LABEL[reportType] : WORK_REPORT_TYPE_LABEL[reportType];
}
export const WORK_REPORT_STATUS_LABEL: Record<string, string> = {
draft: '待提交',
pending_approval: '待审批',

View File

@@ -20,6 +20,14 @@ export interface WorkReportPeriodOption {
};
}
export interface WorkReportResolvedPeriod {
periodKey: string;
periodLabel: string;
periodStartDate: string;
periodEndDate: string;
flag?: number;
}
function formatRangeLabel(start: dayjs.Dayjs, end: dayjs.Dayjs) {
return `${start.format('YYYY-MM-DD')}${end.format('YYYY-MM-DD')}`;
}
@@ -192,3 +200,72 @@ export function getReportTypePeriodOptions(now = dayjs()) {
project: getProjectPeriodOptions(now)
} as const;
}
type PeriodRange = string[] | null | undefined;
interface ResolveWorkReportSummaryPeriodOptions {
currentRow?: Partial<WorkReportResolvedPeriod> | null;
periodRange?: PeriodRange;
flag?: number | null;
}
function getRangeReferenceDate(periodRange?: PeriodRange) {
const [, endDate] = periodRange || [];
return endDate || periodRange?.[0] || '';
}
function inferProjectSummaryFlag(referenceDate: string, explicitFlag?: number | null) {
if (explicitFlag === 1 || explicitFlag === 2) {
return explicitFlag;
}
const selectedDate = dayjs(referenceDate);
if (selectedDate.isValid()) {
return selectedDate.date() > 15 ? 2 : 1;
}
return getProjectPeriodOptions()[0]?.flag || 1;
}
export function resolveWorkReportSummaryPeriod(
reportType: WorkReportType,
options: ResolveWorkReportSummaryPeriodOptions = {}
): WorkReportResolvedPeriod {
const { currentRow, periodRange, flag } = options;
if (currentRow?.periodKey && currentRow.periodStartDate && currentRow.periodEndDate) {
return {
periodKey: currentRow.periodKey,
periodLabel: currentRow.periodLabel || '',
periodStartDate: currentRow.periodStartDate,
periodEndDate: currentRow.periodEndDate,
flag: currentRow.flag
};
}
const referenceDate = getRangeReferenceDate(periodRange);
const fallbackNow = dayjs();
if (reportType === 'weekly') {
return referenceDate ? buildWeeklyPeriodFromDate(referenceDate) : buildWeeklyPeriodFromDate(fallbackNow);
}
if (reportType === 'monthly') {
return referenceDate ? buildMonthlyPeriodFromMonth(referenceDate) : buildMonthlyPeriodFromMonth(fallbackNow);
}
if (referenceDate) {
const resolvedFlag = inferProjectSummaryFlag(referenceDate, flag);
return {
...buildProjectPeriodFromMonth(referenceDate, resolvedFlag),
flag: resolvedFlag
};
}
const fallbackOption = getProjectPeriodOptions(fallbackNow)[0];
return {
...fallbackOption.period,
flag: fallbackOption.flag
};
}