feat(工作报告、加班申请团队视角): 工作报告、加班申请现在可以查看团队视角了(查看下属)。
fix(工作报告): 修复周报在新增/编辑时,不能展示工作日志。
This commit is contained in:
@@ -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>
|
||||
@@ -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: '待审批',
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user