refactor(work-report): 将项目报告周期从月份半月改为自由的日期范围

- 移除项目报告中的 flag 字段必填限制,改为可选
- 将项目报告创建对话框中的月份和半月选择改为日期范围选择器
- 更新项目报告详情对话框,移除半月周期显示字段
- 修改项目报告搜索面板,调整周期字段标签和类型
- 更新团队报告汇总组件中的项目报告催办逻辑
- 调整项目报告表格列宽和标签显示
- 重构项目报告周期构建逻辑,支持灵活的日期范围输入
- 更新相关类型定义和 API 接口中的 flag 字段为可选
- 统一将"项目半月报"更名为"研发简报"以反映新的日期范围特性
This commit is contained in:
dk
2026-07-24 10:55:14 +08:00
parent aa683f892b
commit a9c6ddccba
12 changed files with 175 additions and 205 deletions

View File

@@ -172,7 +172,7 @@ const local: App.I18n.Schema = {
'personal-center_work-report': '工作报告', 'personal-center_work-report': '工作报告',
'personal-center_work-report_weekly': '个人周报', 'personal-center_work-report_weekly': '个人周报',
'personal-center_work-report_monthly': '个人月报', 'personal-center_work-report_monthly': '个人月报',
'personal-center_work-report_project': '项目半月报', 'personal-center_work-report_project': '研发简报',
'personal-center_my-performance': '绩效管理', 'personal-center_my-performance': '绩效管理',
'personal-center_my-application': '我的申请', 'personal-center_my-application': '我的申请',
'personal-center_overtime-application': '业务学习', 'personal-center_overtime-application': '业务学习',

View File

@@ -300,7 +300,7 @@ declare namespace Api {
periodLabel: string; periodLabel: string;
periodStartDate: string; periodStartDate: string;
periodEndDate: string; periodEndDate: string;
flag: number; flag?: number | null;
statusCode: Common.WorkReportStatusCode | string; statusCode: Common.WorkReportStatusCode | string;
statusName: string; statusName: string;
allowEdit: boolean; allowEdit: boolean;
@@ -332,7 +332,7 @@ declare namespace Api {
periodLabel: string; periodLabel: string;
periodStartDate: string; periodStartDate: string;
periodEndDate: string; periodEndDate: string;
flag: number; flag?: number | null;
projectStatusDesc?: string | null; projectStatusDesc?: string | null;
projectProgressPlan?: string | null; projectProgressPlan?: string | null;
projectKeyPoints?: string | null; projectKeyPoints?: string | null;

View File

@@ -111,7 +111,7 @@ const teamContext = computed<TeamViewContext | null>(() => {
}; };
}); });
/** 项目选项是否加载成功(用于项目半月报列表内部判断) */ /** 项目选项是否加载成功(用于项目报列表内部判断) */
const projectOptionsLoaded = ref(false); const projectOptionsLoaded = ref(false);
const visibleTabs = computed<Array<{ label: string; name: WorkReportType }>>(() => { const visibleTabs = computed<Array<{ label: string; name: WorkReportType }>>(() => {
@@ -194,7 +194,7 @@ function handleCreateConfirm(
| { | {
reportType: 'project'; reportType: 'project';
projectId: string; projectId: string;
flag: number; flag?: number;
period: typeof initialPeriod.value extends infer T ? T : never; period: typeof initialPeriod.value extends infer T ? T : never;
} }
) { ) {
@@ -203,7 +203,7 @@ function handleCreateConfirm(
currentRow.value = null; currentRow.value = null;
initialPeriod.value = payload.period as typeof initialPeriod.value; initialPeriod.value = payload.period as typeof initialPeriod.value;
initialProjectId.value = 'projectId' in payload ? payload.projectId : ''; initialProjectId.value = 'projectId' in payload ? payload.projectId : '';
initialFlag.value = 'flag' in payload ? payload.flag : 1; initialFlag.value = 'flag' in payload ? (payload.flag ?? 1) : 1;
pageDialogVisible.value = true; pageDialogVisible.value = true;
} }

View File

@@ -28,7 +28,7 @@ import {
resolveWorkReportStatusTagType, resolveWorkReportStatusTagType,
transformWorkReportPage transformWorkReportPage
} from '../shared/types'; } from '../shared/types';
import { buildProjectPeriodFromMonth, resolveWorkReportSummaryPeriod } from '../shared/utils'; import { resolveWorkReportSummaryPeriod } from '../shared/utils';
import ProjectReportSearch from './modules/search-panel.vue'; import ProjectReportSearch from './modules/search-panel.vue';
import IconMdiDeleteOutline from '~icons/mdi/delete-outline'; import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
import IconMdiEyeOutline from '~icons/mdi/eye-outline'; import IconMdiEyeOutline from '~icons/mdi/eye-outline';
@@ -86,7 +86,7 @@ const normalizedPeriodRange = computed(() => {
return periodRange; return periodRange;
} }
return [start.startOf('month').format('YYYY-MM-DD'), end.endOf('month').format('YYYY-MM-DD')]; return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')];
}); });
const table = useUIPaginatedTable< const table = useUIPaginatedTable<
@@ -110,8 +110,8 @@ const table = useUIPaginatedTable<
...(isTeamMode.value ...(isTeamMode.value
? [{ prop: 'projectOwnerName', label: '提交人', minWidth: 100, showOverflowTooltip: true }] ? [{ prop: 'projectOwnerName', label: '提交人', minWidth: 100, showOverflowTooltip: true }]
: []), : []),
{ prop: 'projectName', label: '项目名称', minWidth: 200, showOverflowTooltip: true }, { prop: 'projectName', label: '项目名称', minWidth: 210, showOverflowTooltip: true },
{ prop: 'periodLabel', label: '半月周期', minWidth: 120, formatter: row => formatPeriod(row) }, { prop: 'periodLabel', label: '周期', minWidth: 150, formatter: row => formatPeriod(row) },
{ prop: 'projectOwnerName', label: '项目负责人', minWidth: 80 }, { prop: 'projectOwnerName', label: '项目负责人', minWidth: 80 },
{ {
prop: 'technicalOwnerName', prop: 'technicalOwnerName',
@@ -145,42 +145,12 @@ const table = useUIPaginatedTable<
] ]
}); });
// 团队统计始终使用当前周期(当前半月),不跟随列表第一条数据的周期
const summaryPeriod = computed(() => const summaryPeriod = computed(() =>
resolveWorkReportSummaryPeriod('project', { resolveWorkReportSummaryPeriod('project', {
periodRange: normalizedPeriodRange.value periodRange: normalizedPeriodRange.value
}) })
); );
const summaryPeriodKeys = computed(() => { 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); const hasSearchedDateRange = computed(() => searchParams.periodStartDate?.length === 2);
function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] { function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] {

View File

@@ -7,7 +7,7 @@ import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { import {
type WorkReportPeriodOption, type WorkReportPeriodOption,
buildMonthlyPeriodFromMonth, buildMonthlyPeriodFromMonth,
buildProjectPeriodFromMonth, buildProjectPeriodFromDateRange,
buildWeeklyPeriodFromDate, buildWeeklyPeriodFromDate,
formatPeriodDisplayLabel, formatPeriodDisplayLabel,
getReportTypePeriodOptions getReportTypePeriodOptions
@@ -40,7 +40,7 @@ const emit = defineEmits<{
| { | {
reportType: 'project'; reportType: 'project';
projectId: string; projectId: string;
flag: number; flag?: number;
period: WorkReportPeriodOption['period']; period: WorkReportPeriodOption['period'];
} }
): void; ): void;
@@ -50,8 +50,7 @@ const selectedPeriodKey = ref('');
const selectedProjectId = ref(''); const selectedProjectId = ref('');
const rawCustomWeekDate = ref(''); const rawCustomWeekDate = ref('');
const customMonth = ref(''); const customMonth = ref('');
const customProjectMonth = ref(''); const customProjectDateRange = ref<string[]>([]);
const customProjectFlag = ref(1);
// 自定义周报周期:无论用户点哪一天,都归一到该 ISO 周的周一, // 自定义周报周期:无论用户点哪一天,都归一到该 ISO 周的周一,
// 这样 ElDatePicker 使用内置 dayjs 的 wwlocale week也能正确显示 ISO 周数。 // 这样 ElDatePicker 使用内置 dayjs 的 wwlocale week也能正确显示 ISO 周数。
@@ -74,10 +73,7 @@ const selectedReportType = computed<WorkReportType>(() => {
const periodOptionMap = computed(() => getReportTypePeriodOptions()); const periodOptionMap = computed(() => getReportTypePeriodOptions());
const activePeriodOptions = computed(() => periodOptionMap.value[selectedReportType.value]); const activePeriodOptions = computed(() => periodOptionMap.value[selectedReportType.value]);
const dialogTitle = computed(() => `新增${WORK_REPORT_TYPE_LABEL[selectedReportType.value]}`); const dialogTitle = computed(() => `新增${WORK_REPORT_TYPE_LABEL[selectedReportType.value]}`);
const projectHalfOptions = [ const isProjectReport = computed(() => selectedReportType.value === 'project');
{ label: '上半月', value: 1 },
{ label: '下半月', value: 2 }
];
const defaultCustomMonth = computed(() => { const defaultCustomMonth = computed(() => {
const period = activePeriodOptions.value[0]?.period; const period = activePeriodOptions.value[0]?.period;
@@ -85,7 +81,7 @@ const defaultCustomMonth = computed(() => {
}); });
const customPeriod = computed<WorkReportPeriodOption['period'] | null>(() => { const customPeriod = computed<WorkReportPeriodOption['period'] | null>(() => {
if (selectedPeriodKey.value !== 'custom') return null; if (!isProjectReport.value && selectedPeriodKey.value !== 'custom') return null;
if (selectedReportType.value === 'weekly') { if (selectedReportType.value === 'weekly') {
if (!customWeekDate.value) return null; if (!customWeekDate.value) return null;
@@ -97,16 +93,15 @@ const customPeriod = computed<WorkReportPeriodOption['period'] | null>(() => {
return buildMonthlyPeriodFromMonth(customMonth.value); return buildMonthlyPeriodFromMonth(customMonth.value);
} }
if (!customProjectMonth.value) return null; if (customProjectDateRange.value.length !== 2) return null;
return buildProjectPeriodFromMonth(customProjectMonth.value, customProjectFlag.value); return buildProjectPeriodFromDateRange(customProjectDateRange.value[0], customProjectDateRange.value[1]);
}); });
const selectedPeriod = computed( const selectedPeriod = computed(
() => activePeriodOptions.value.find(item => item.key === selectedPeriodKey.value) ?? activePeriodOptions.value[0] () => activePeriodOptions.value.find(item => item.key === selectedPeriodKey.value) ?? activePeriodOptions.value[0]
); );
const selectedPeriodValue = computed(() => const selectedPeriodValue = computed(() =>
selectedPeriodKey.value === 'custom' ? customPeriod.value : selectedPeriod.value?.period isProjectReport.value || selectedPeriodKey.value === 'custom' ? customPeriod.value : selectedPeriod.value?.period
); );
const customPeriodPreviewLabel = computed(() => const customPeriodPreviewLabel = computed(() =>
customPeriod.value ? formatPeriodDisplayLabel(customPeriod.value.periodLabel) : '' customPeriod.value ? formatPeriodDisplayLabel(customPeriod.value.periodLabel) : ''
@@ -114,14 +109,14 @@ const customPeriodPreviewLabel = computed(() =>
const confirmDisabled = computed(() => { const confirmDisabled = computed(() => {
if (!selectedPeriodValue.value) return true; if (!selectedPeriodValue.value) return true;
if (selectedReportType.value === 'project' && !selectedProjectId.value) return true; if (isProjectReport.value && !selectedProjectId.value) return true;
return false; return false;
}); });
watch( watch(
selectedReportType, selectedReportType,
type => { type => {
selectedPeriodKey.value = periodOptionMap.value[type][0]?.key || ''; selectedPeriodKey.value = type === 'project' ? 'custom' : periodOptionMap.value[type][0]?.key || '';
if (type === 'project' && !selectedProjectId.value) { if (type === 'project' && !selectedProjectId.value) {
selectedProjectId.value = props.projectOptions[0]?.id || ''; selectedProjectId.value = props.projectOptions[0]?.id || '';
@@ -134,27 +129,34 @@ watch(visible, isVisible => {
if (!isVisible) return; if (!isVisible) return;
selectedProjectId.value = props.projectOptions[0]?.id || ''; selectedProjectId.value = props.projectOptions[0]?.id || '';
selectedPeriodKey.value = periodOptionMap.value[selectedReportType.value][0]?.key || ''; selectedPeriodKey.value = isProjectReport.value
? 'custom'
: periodOptionMap.value[selectedReportType.value][0]?.key || '';
customWeekDate.value = activePeriodOptions.value[0]?.period.periodStartDate || ''; customWeekDate.value = activePeriodOptions.value[0]?.period.periodStartDate || '';
customMonth.value = defaultCustomMonth.value; customMonth.value = defaultCustomMonth.value;
customProjectMonth.value = defaultCustomMonth.value; customProjectDateRange.value = [
customProjectFlag.value = activePeriodOptions.value[0]?.flag || 1; activePeriodOptions.value[0]?.period.periodStartDate || '',
activePeriodOptions.value[0]?.period.periodEndDate || ''
].filter(Boolean);
}); });
function handleConfirm() { function handleConfirm() {
const period = selectedPeriodValue.value; const period = selectedPeriodValue.value;
if (!period) return; if (!period) return;
if (selectedReportType.value === 'project') { if (isProjectReport.value) {
emit('confirm', { emit('confirm', {
reportType: 'project', reportType: 'project',
projectId: selectedProjectId.value, projectId: selectedProjectId.value,
flag: selectedPeriodKey.value === 'custom' ? customProjectFlag.value : selectedPeriod.value.flag || 1, flag: period.flag,
period period
}); });
} else { } else {
const reportType = selectedReportType.value;
if (reportType !== 'weekly' && reportType !== 'monthly') return;
emit('confirm', { emit('confirm', {
reportType: selectedReportType.value, reportType,
period period
}); });
} }
@@ -194,7 +196,7 @@ function handleConfirm() {
</div> </div>
<div class="work-report-create-dialog__section"> <div class="work-report-create-dialog__section">
<div class="work-report-create-dialog__grid is-period"> <div v-if="!isProjectReport" class="work-report-create-dialog__grid is-period">
<button <button
v-for="item in activePeriodOptions" v-for="item in activePeriodOptions"
:key="item.key" :key="item.key"
@@ -214,18 +216,16 @@ function handleConfirm() {
> >
<div class="work-report-create-dialog__choice-title">自定义周期</div> <div class="work-report-create-dialog__choice-title">自定义周期</div>
<div class="work-report-create-dialog__choice-desc"> <div class="work-report-create-dialog__choice-desc">
{{ {{ selectedReportType === 'weekly' ? '选择某一周作为周报周期。' : '选择某一月作为月报周期。' }}
selectedReportType === 'weekly'
? '选择某一周作为周报周期。'
: selectedReportType === 'monthly'
? '选择某一月作为月报周期。'
: '选择某个月的上半月或下半月。'
}}
</div> </div>
</button> </button>
</div> </div>
<div v-if="selectedPeriodKey === 'custom'" class="work-report-create-dialog__custom-period"> <div
v-if="isProjectReport || selectedPeriodKey === 'custom'"
class="work-report-create-dialog__custom-period"
:class="{ 'work-report-create-dialog__custom-period--project': isProjectReport }"
>
<div v-if="selectedReportType === 'weekly'" class="work-report-create-dialog__custom-row"> <div v-if="selectedReportType === 'weekly'" class="work-report-create-dialog__custom-row">
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline"> <div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
<label class="work-report-create-dialog__label">周报周期</label> <label class="work-report-create-dialog__label">周报周期</label>
@@ -260,27 +260,19 @@ function handleConfirm() {
</div> </div>
<div v-else class="work-report-create-dialog__custom-project"> <div v-else class="work-report-create-dialog__custom-project">
<div class="work-report-create-dialog__custom-project-grid">
<div class="work-report-create-dialog__custom-project-item"> <div class="work-report-create-dialog__custom-project-item">
<div class="work-report-create-dialog__custom-project-item-label">选择月份</div> <div class="work-report-create-dialog__custom-project-item-label">选择周期</div>
<ElDatePicker <ElDatePicker
v-model="customProjectMonth" v-model="customProjectDateRange"
class="w-full" class="w-full"
type="month" type="daterange"
value-format="YYYY-MM" value-format="YYYY-MM-DD"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
popper-class="work-report-create-date-popper" popper-class="work-report-create-date-popper"
placeholder="请选择月份"
/> />
</div> </div>
<div class="work-report-create-dialog__custom-project-item">
<div class="work-report-create-dialog__custom-project-item-label">选择半月</div>
<ElSegmented
v-model="customProjectFlag"
:options="projectHalfOptions"
class="work-report-create-dialog__half-segmented"
/>
</div>
</div>
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview"> <div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
<ElIcon class="work-report-create-dialog__period-preview-icon"><Calendar /></ElIcon> <ElIcon class="work-report-create-dialog__period-preview-icon"><Calendar /></ElIcon>
<span class="work-report-create-dialog__period-preview-text">已选周期</span> <span class="work-report-create-dialog__period-preview-text">已选周期</span>
@@ -422,6 +414,10 @@ function handleConfirm() {
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
} }
.work-report-create-dialog__custom-period--project {
margin-top: 0;
}
.work-report-create-dialog__custom-row { .work-report-create-dialog__custom-row {
display: flex; display: flex;
gap: 12px; gap: 12px;
@@ -438,13 +434,6 @@ function handleConfirm() {
gap: 14px; gap: 14px;
} }
.work-report-create-dialog__custom-project-grid {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 14px;
align-items: stretch;
}
.work-report-create-dialog__custom-project-item { .work-report-create-dialog__custom-project-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -471,24 +460,6 @@ function handleConfirm() {
width: 100%; width: 100%;
} }
.work-report-create-dialog__half-segmented {
width: 100%;
display: flex;
}
.work-report-create-dialog__half-segmented :deep(.el-segmented__group) {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
gap: 0;
}
.work-report-create-dialog__half-segmented :deep(.el-segmented__item) {
flex: 1;
min-width: 0;
justify-content: center;
}
.work-report-create-dialog__period-preview { .work-report-create-dialog__period-preview {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -532,10 +503,8 @@ function handleConfirm() {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.work-report-create-dialog__custom-row, .work-report-create-dialog__custom-row {
.work-report-create-dialog__custom-project-grid {
flex-direction: column; flex-direction: column;
grid-template-columns: 1fr;
} }
.work-report-create-dialog__field--inline { .work-report-create-dialog__field--inline {

View File

@@ -12,7 +12,6 @@ import {
formatPeriod, formatPeriod,
formatPeriodDateRange, formatPeriodDateRange,
formatWeeklyPeriodLabel, formatWeeklyPeriodLabel,
getProjectReportFlagLabel,
getWorkReportStatusLabel getWorkReportStatusLabel
} from '../types'; } from '../types';
@@ -100,9 +99,6 @@ function getPersonalDetail() {
<BusinessFormSection title="项目信息"> <BusinessFormSection title="项目信息">
<ElDescriptions :column="2" border size="small"> <ElDescriptions :column="2" border size="small">
<ElDescriptionsItem label="项目名称">{{ getProjectDetail()?.projectName }}</ElDescriptionsItem> <ElDescriptionsItem label="项目名称">{{ getProjectDetail()?.projectName }}</ElDescriptionsItem>
<ElDescriptionsItem label="半月周期">
{{ getProjectReportFlagLabel(getProjectDetail()?.flag) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="项目负责人">{{ getProjectDetail()?.projectOwnerName }}</ElDescriptionsItem> <ElDescriptionsItem label="项目负责人">{{ getProjectDetail()?.projectOwnerName }}</ElDescriptionsItem>
<ElDescriptionsItem label="技术负责人"> <ElDescriptionsItem label="技术负责人">
{{ formatEmptyText(getProjectDetail()?.technicalOwnerName) }} {{ formatEmptyText(getProjectDetail()?.technicalOwnerName) }}

View File

@@ -91,7 +91,7 @@ const emit = defineEmits<{
const REPORT_TYPE_TEXT: Record<WorkReportType, string> = { const REPORT_TYPE_TEXT: Record<WorkReportType, string> = {
weekly: '个人周报', weekly: '个人周报',
monthly: '个人月报', monthly: '个人月报',
project: '项目半月报' project: '研发简报'
}; };
const loading = ref(false); const loading = ref(false);

View File

@@ -58,7 +58,7 @@ const fields = computed<SearchField[]>(() => {
const monthPeriodField: SearchField = { const monthPeriodField: SearchField = {
key: 'periodStartDate', key: 'periodStartDate',
label: props.reportType === 'project' ? '月份' : '月份', label: '月份',
type: 'dateRange', type: 'dateRange',
dateRangeType: 'monthrange', dateRangeType: 'monthrange',
valueFormat: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD',
@@ -107,7 +107,7 @@ const fields = computed<SearchField[]>(() => {
if (props.reportType === 'project') { if (props.reportType === 'project') {
return [ return [
baseFields[0], baseFields[0],
monthPeriodField, { key: 'periodStartDate', label: '周期', type: 'dateRange', placeholder: '请选择周期' },
{ {
key: 'projectId', key: 'projectId',
label: '项目', label: '项目',

View File

@@ -28,7 +28,10 @@ const emit = defineEmits<{
const remindingAll = ref(false); const remindingAll = ref(false);
const remindingItemKey = ref(''); const remindingItemKey = ref('');
const canRemind = computed(() => props.periodKeys.length > 0); const canProjectRemind = computed(() => Boolean(props.summary?.unsubmittedReports?.length));
const canRemind = computed(() =>
props.reportType === 'project' ? canProjectRemind.value : props.periodKeys.length > 0
);
function formatSummaryPeriodLabel() { function formatSummaryPeriodLabel() {
if (!props.summary?.periodStartDate || !props.summary?.periodEndDate) { if (!props.summary?.periodStartDate || !props.summary?.periodEndDate) {
@@ -42,12 +45,16 @@ function formatSummaryPeriodLabel() {
return `${props.summary.periodStartDate}${props.summary.periodEndDate}`; return `${props.summary.periodStartDate}${props.summary.periodEndDate}`;
} }
if (props.reportType === 'monthly' || props.reportType === 'project') { if (props.reportType === 'monthly') {
const startMonth = start.format('YYYY-MM'); const startMonth = start.format('YYYY-MM');
const endMonth = end.format('YYYY-MM'); const endMonth = end.format('YYYY-MM');
return startMonth === endMonth ? startMonth : `${startMonth}${endMonth}`; return startMonth === endMonth ? startMonth : `${startMonth}${endMonth}`;
} }
if (props.reportType === 'project') {
return `${start.format('YYYY-MM-DD')}${end.format('YYYY-MM-DD')}`;
}
if (props.reportType === 'weekly') { if (props.reportType === 'weekly') {
return formatIsoWeekRangeLabel(props.summary.periodStartDate, props.summary.periodEndDate); return formatIsoWeekRangeLabel(props.summary.periodStartDate, props.summary.periodEndDate);
} }
@@ -101,9 +108,36 @@ function getSingleRemindSuccessText(remindedCount: number, preciseProjectReminde
} }
async function handleRemind(userIds?: string[]) { async function handleRemind(userIds?: string[]) {
if (!props.periodKeys.length) return; if (props.reportType === 'project') {
const unsubmittedReports = props.summary?.unsubmittedReports || [];
if (!unsubmittedReports.length) return;
const isSingleUserReminder = userIds?.length === 1; remindingAll.value = true;
const results = await Promise.all(
unsubmittedReports.map(item =>
fetchRemindTeamReport({
reportType: 'project',
periodKey: item.periodKey,
userIds: userIds?.length ? userIds : [item.userId],
projectId: item.projectId || undefined
})
)
);
remindingAll.value = false;
const remindedCount = results.reduce((total, result) => {
if (result.error) return total;
return total + (result.data?.remindedCount ?? 0);
}, 0);
if (!remindedCount) return;
window.$message?.success('已按当前未提交明细发送催办提醒');
emit('reminded');
return;
}
if (!props.periodKeys.length) return;
remindingAll.value = true; remindingAll.value = true;
@@ -127,9 +161,7 @@ async function handleRemind(userIds?: string[]) {
if (!remindedCount) return; if (!remindedCount) return;
window.$message?.success( window.$message?.success(
props.periodKeys.length > 1 props.periodKeys.length > 1 ? '已按所选区间发送催办提醒' : getSingleRemindSuccessText(remindedCount, false)
? '已按所选区间发送催办提醒'
: getSingleRemindSuccessText(remindedCount, props.reportType === 'project' && isSingleUserReminder)
); );
emit('reminded'); emit('reminded');
} }

View File

@@ -38,13 +38,13 @@ export const WORK_REPORT_PROJECT_OWNER_PERMISSION = 'project:work-report:project
export const WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = { export const WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
weekly: '个人周报', weekly: '个人周报',
monthly: '个人月报', monthly: '个人月报',
project: '项目半月报' project: '研发简报'
}; };
export const TEAM_WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = { export const TEAM_WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
weekly: '团队周报', weekly: '团队周报',
monthly: '团队月报', monthly: '团队月报',
project: '团队项目半月报' project: '团队研发简报'
}; };
export function getWorkReportTypeDisplayLabel(reportType: WorkReportType, isTeamMode = false) { export function getWorkReportTypeDisplayLabel(reportType: WorkReportType, isTeamMode = false) {
@@ -145,7 +145,7 @@ export function formatWeeklyPeriodLabel(
export function formatPeriodLabel(value?: string | null) { export function formatPeriodLabel(value?: string | null) {
return String(value || '') return String(value || '')
.trim() .trim()
.replace(/\s*(|||)\s*$/u, ''); .replace(/\s*(|||||)\s*$/u, '');
} }
export function formatPeriod(row: Pick<WorkReportRow, 'periodLabel' | 'periodStartDate' | 'periodEndDate'>) { export function formatPeriod(row: Pick<WorkReportRow, 'periodLabel' | 'periodStartDate' | 'periodEndDate'>) {
@@ -472,7 +472,7 @@ export function createProjectSaveParams(
periodLabel: '', periodLabel: '',
periodStartDate: '', periodStartDate: '',
periodEndDate: '', periodEndDate: '',
flag: 1, flag: undefined,
projectStatusDesc: '', projectStatusDesc: '',
projectProgressPlan: '', projectProgressPlan: '',
projectKeyPoints: '', projectKeyPoints: '',

View File

@@ -17,6 +17,7 @@ export interface WorkReportPeriodOption {
periodLabel: string; periodLabel: string;
periodStartDate: string; periodStartDate: string;
periodEndDate: string; periodEndDate: string;
flag?: number;
}; };
} }
@@ -35,7 +36,7 @@ function formatRangeLabel(start: dayjs.Dayjs, end: dayjs.Dayjs) {
export function formatPeriodDisplayLabel(label?: string | null) { export function formatPeriodDisplayLabel(label?: string | null) {
return String(label || '') return String(label || '')
.trim() .trim()
.replace(/\s*(|||)\s*$/u, ''); .replace(/\s*(|||||)\s*$/u, '');
} }
// 使用 ISO 周数,确保与 buildWeeklyPeriodFromDate 的计算一致 // 使用 ISO 周数,确保与 buildWeeklyPeriodFromDate 的计算一致
@@ -119,12 +120,43 @@ export function buildProjectPeriodFromMonth(month: string | dayjs.Dayjs, flag: n
if (flag === 2) { if (flag === 2) {
const start = monthStart.date(16); const start = monthStart.date(16);
const end = selectedMonth.endOf('month'); const end = selectedMonth.endOf('month');
return buildPeriod('project', start, end, `${selectedMonth.format('YYYY-MM')} 下半月`, 2); return buildProjectPeriodFromDateRange(start, end);
} }
const start = monthStart.startOf('month'); const start = monthStart.startOf('month');
const end = monthStart.date(15); const end = monthStart.date(15);
return buildPeriod('project', start, end, `${selectedMonth.format('YYYY-MM')} 上半月`, 1); return buildProjectPeriodFromDateRange(start, end);
}
export function buildProjectPeriodFromDateRange(startDate: string | dayjs.Dayjs, endDate: string | dayjs.Dayjs) {
const start = dayjs(startDate).startOf('day');
const end = dayjs(endDate).startOf('day');
if (!start.isValid() || !end.isValid()) {
return {
periodKey: '',
periodLabel: '',
periodStartDate: '',
periodEndDate: '',
flag: undefined
};
}
let flag: number | undefined;
let label = formatRangeLabel(start, end);
if (start.isSame(end, 'month') && start.date() === 1 && end.date() === 15) {
flag = 1;
label = `${start.format('YYYY-MM')} 上半月`;
} else if (start.isSame(end, 'month') && start.date() === 16 && end.date() === end.daysInMonth()) {
flag = 2;
label = `${start.format('YYYY-MM')} 下半月`;
}
return {
...buildPeriod('project', start, end, label, flag),
flag
};
} }
export function getWeeklyPeriodOptions(now = dayjs()): WorkReportPeriodOption[] { export function getWeeklyPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
@@ -177,59 +209,30 @@ export function getMonthlyPeriodOptions(now = dayjs()): WorkReportPeriodOption[]
} }
export function getProjectPeriodOptions(now = dayjs()): WorkReportPeriodOption[] { export function getProjectPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
const currentMonthStart = now.startOf('month'); const recent15Start = now.subtract(14, 'day').startOf('day');
const currentMonthEnd = now.endOf('month'); const recent30Start = now.subtract(29, 'day').startOf('day');
const currentFirstHalfEnd = currentMonthStart.date(15); const today = now.startOf('day');
const currentSecondHalfStart = currentMonthStart.date(16); const recent15Period = buildProjectPeriodFromDateRange(recent15Start, today);
const previousMonth = now.subtract(1, 'month'); const recent30Period = buildProjectPeriodFromDateRange(recent30Start, today);
const previousMonthStart = previousMonth.startOf('month');
const previousMonthEnd = previousMonth.endOf('month');
const previousSecondHalfStart = previousMonthStart.date(16);
const isCurrentFirstHalf = now.date() <= 15;
const currentOption: WorkReportPeriodOption = isCurrentFirstHalf return [
? { {
key: 'current-first-half', key: 'recent-15-days',
label: '上半月', label: '最近15天',
description: `${now.format('YYYY-MM')} 上半月`, description: formatRangeLabel(recent15Start, today),
reportType: 'project', reportType: 'project',
flag: 1, flag: recent15Period.flag,
period: buildPeriod('project', currentMonthStart, currentFirstHalfEnd, `${now.format('YYYY-MM')} 上半月`, 1) period: recent15Period
},
{
key: 'recent-30-days',
label: '最近30天',
description: formatRangeLabel(recent30Start, today),
reportType: 'project',
flag: recent30Period.flag,
period: recent30Period
} }
: { ];
key: 'current-second-half',
label: '下半月',
description: `${now.format('YYYY-MM')} 下半月`,
reportType: 'project',
flag: 2,
period: buildPeriod('project', currentSecondHalfStart, currentMonthEnd, `${now.format('YYYY-MM')} 下半月`, 2)
};
const previousOption: WorkReportPeriodOption = isCurrentFirstHalf
? {
key: 'previous-second-half',
label: '下半月',
description: `${previousMonth.format('YYYY-MM')} 下半月`,
reportType: 'project',
flag: 2,
period: buildPeriod(
'project',
previousSecondHalfStart,
previousMonthEnd,
`${previousMonth.format('YYYY-MM')} 下半月`,
2
)
}
: {
key: 'previous-first-half',
label: '上半月',
description: `${now.format('YYYY-MM')} 上半月`,
reportType: 'project',
flag: 1,
period: buildPeriod('project', currentMonthStart, currentFirstHalfEnd, `${now.format('YYYY-MM')} 上半月`, 1)
};
return [currentOption, previousOption];
} }
export function getReportTypePeriodOptions(now = dayjs()) { export function getReportTypePeriodOptions(now = dayjs()) {
@@ -293,18 +296,18 @@ export function resolveWorkReportSummaryPeriod(
return referenceDate ? buildMonthlyPeriodFromMonth(referenceDate) : buildMonthlyPeriodFromMonth(fallbackNow); return referenceDate ? buildMonthlyPeriodFromMonth(referenceDate) : buildMonthlyPeriodFromMonth(fallbackNow);
} }
if (periodRange?.length) {
const [startDate, endDate] = periodRange;
const fallbackEndDate = endDate || startDate;
if (startDate && fallbackEndDate) {
return buildProjectPeriodFromDateRange(startDate, fallbackEndDate);
}
}
if (referenceDate) { if (referenceDate) {
const resolvedFlag = inferProjectSummaryFlag(referenceDate, flag); const resolvedFlag = inferProjectSummaryFlag(referenceDate, flag);
return { return buildProjectPeriodFromMonth(referenceDate, resolvedFlag);
...buildProjectPeriodFromMonth(referenceDate, resolvedFlag),
flag: resolvedFlag
};
} }
const fallbackOption = getProjectPeriodOptions(fallbackNow)[0]; return buildProjectPeriodFromDateRange(fallbackNow.subtract(29, 'day'), fallbackNow);
return {
...fallbackOption.period,
flag: fallbackOption.flag
};
} }

View File

@@ -147,7 +147,7 @@ const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>;
const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [ const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
{ key: 'weekly', label: '周报' }, { key: 'weekly', label: '周报' },
{ key: 'monthly', label: '月报' }, { key: 'monthly', label: '月报' },
{ key: 'project', label: '项目半月报' }, { key: 'project', label: '研发简报' },
{ key: 'overtime_application', label: '业务学习' } { key: 'overtime_application', label: '业务学习' }
]; ];
const confirmBizTabs: Array<{ key: Extract<ApprovalBizType, 'performance' | 'monthly'>; label: string }> = [ const confirmBizTabs: Array<{ key: Extract<ApprovalBizType, 'performance' | 'monthly'>; label: string }> = [
@@ -261,7 +261,7 @@ const APPROVAL_ENTRY_ICON = markRaw(IconMdiClipboardCheckOutline);
function getApprovalCategoryLabel(bizType: ApprovalBizType) { function getApprovalCategoryLabel(bizType: ApprovalBizType) {
if (bizType === 'weekly') return '周报'; if (bizType === 'weekly') return '周报';
if (bizType === 'monthly') return '月报'; if (bizType === 'monthly') return '月报';
if (bizType === 'project') return '项目半月报'; if (bizType === 'project') return '研发简报';
if (bizType === 'performance') return '绩效表'; if (bizType === 'performance') return '绩效表';
if (bizType === 'overtime_application') return '业务学习'; if (bizType === 'overtime_application') return '业务学习';
return '待审批'; return '待审批';