feat(工作报告定时生成): 工作报告现在可以定时生成,并且可以刷新当前报告。

This commit is contained in:
dk
2026-06-13 22:13:44 +08:00
parent 80f028bcb9
commit 030dc737fc
10 changed files with 294 additions and 152 deletions

View File

@@ -499,6 +499,17 @@ export async function fetchPreviewWeeklyReportDefaultDraft(
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
}
export async function fetchRefreshWeeklyReportDraft(data: Api.WorkReport.Weekly.WeeklyReportRefreshDraftParams) {
const result = await request<WeeklyReportResponse>({
...safeJsonRequestConfig,
url: `${WEEKLY_PREFIX}/refresh-draft`,
method: 'post',
data: toWeeklySaveRequest(data)
});
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
}
export async function fetchCreateWeeklyReport(data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
const result = await request<StringIdResponse>({
...safeJsonRequestConfig,
@@ -636,6 +647,17 @@ export async function fetchPreviewMonthlyReportDefaultDraft(
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
}
export async function fetchRefreshMonthlyReportDraft(data: Api.WorkReport.Monthly.MonthlyReportRefreshDraftParams) {
const result = await request<MonthlyReportResponse>({
...safeJsonRequestConfig,
url: `${MONTHLY_PREFIX}/refresh-draft`,
method: 'post',
data: toMonthlySaveRequest(data)
});
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
}
export async function fetchCreateMonthlyReport(data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
const result = await request<StringIdResponse>({
...safeJsonRequestConfig,
@@ -787,6 +809,32 @@ export async function fetchPreviewProjectReportDefaultDraft(
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
}
export async function fetchRefreshProjectReportDraft(
projectId: string,
data: Api.WorkReport.Project.ProjectReportRefreshDraftParams
) {
const result = await request<ProjectReportResponse>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/${projectId}/refresh-draft`,
method: 'post',
data: {
periodKey: data.periodKey,
periodLabel: data.periodLabel,
periodStartDate: data.periodStartDate,
periodEndDate: data.periodEndDate,
flag: data.flag,
projectStatusDesc: data.projectStatusDesc?.trim() || '',
projectProgressPlan: data.projectProgressPlan?.trim() || '',
projectKeyPoints: data.projectKeyPoints?.trim() || '',
projectProblems: data.projectProblems?.trim() || '',
currentItems: toProjectItems(data.currentItems),
nextItems: toProjectItems(data.nextItems)
}
});
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
}
export async function fetchCreateProjectReport(data: Api.WorkReport.Project.ProjectReportSaveParams) {
const result = await request<StringIdResponse>({
...safeJsonRequestConfig,

View File

@@ -132,6 +132,8 @@ declare namespace Api {
WeeklyReportSaveParams,
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate'
>;
type WeeklyReportRefreshDraftParams = WeeklyReportSaveParams;
}
namespace Monthly {
@@ -178,6 +180,8 @@ declare namespace Api {
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate'
>;
type MonthlyReportRefreshDraftParams = MonthlyReportSaveParams;
interface MonthlyReportApproveParams extends Common.StatusActionParams {
meetingDate?: string | null;
strengthDesc?: string | null;
@@ -285,6 +289,8 @@ declare namespace Api {
ProjectReportSaveParams,
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate' | 'flag'
>;
type ProjectReportRefreshDraftParams = Omit<ProjectReportSaveParams, 'projectId'>;
}
}
}

View File

@@ -310,7 +310,9 @@ function createStructuredSectionsFromTextV2(text: string, defaultCategory: strin
return;
}
const legacyMatch = trimmedLine.match(/^(.+?)\s*-\s*(.+?)(?:[(]([^()]*)[)])?[。.!?]*$/u);
// 旧格式数据是“分类 - 事项(指标)”,这里需要把括号里的指标一并交给任务解析器,
// 否则月报默认稿中的优先级/进度/工时会在这一层被截掉。
const legacyMatch = trimmedLine.match(/^(?!\d+[、.]\s*)(.+?)\s*[-]\s*(.+)$/u);
if (legacyMatch) {
const [, rawCategory, rawTaskText] = legacyMatch;
const category = rawCategory.trim();
@@ -774,6 +776,14 @@ function syncRichSupport(item: PlanItem, event: Event) {
<div class="section">
<div class="section-title">
<span>基础信息</span>
<div v-if="mode === 'edit' && !isReadonly" class="section-title-right">
<ElButton size="small" plain type="primary" @click="emit('pullDefaultDraft')">
<template #icon>
<icon-mdi-refresh class="text-icon" />
</template>
刷新
</ElButton>
</div>
</div>
<div class="compose-grid">
<div class="field">

View File

@@ -299,6 +299,14 @@ function notifyTitleSaved(item: WorkItem) {
<div class="section">
<div class="section-title">
<span>基础信息</span>
<div v-if="mode === 'edit' && !isReadonly" class="section-title-right">
<ElButton size="small" plain type="primary" @click="emit('pullDefaultDraft')">
<template #icon>
<icon-mdi-refresh class="text-icon" />
</template>
刷新
</ElButton>
</div>
</div>
<div class="compose-grid">
<div class="field">

View File

@@ -14,6 +14,9 @@ import {
fetchPreviewMonthlyReportDefaultDraft,
fetchPreviewProjectReportDefaultDraft,
fetchPreviewWeeklyReportDefaultDraft,
fetchRefreshMonthlyReportDraft,
fetchRefreshProjectReportDraft,
fetchRefreshWeeklyReportDraft,
fetchUpdateMonthlyReport,
fetchUpdateProjectReport,
fetchUpdateWeeklyReport
@@ -146,6 +149,110 @@ function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
patchPeriod(projectModel);
}
function applyWeeklyEditableFields(draft: Api.WorkReport.Weekly.WeeklyReport) {
weeklyModel.reviewItems = normalizeReviewItems(draft.reviewItems);
weeklyModel.planItems = normalizePlanItems(draft.planItems);
weeklyModel.travelSegments = draft.travelSegments || [];
}
function applyMonthlyEditableFields(draft: Api.WorkReport.Monthly.MonthlyReport) {
monthlyModel.reviewItems = normalizeReviewItems(draft.reviewItems);
monthlyModel.planItems = normalizePlanItems(draft.planItems);
}
function applyProjectEditableFields(draft: Api.WorkReport.Project.ProjectReport) {
projectModel.projectStatusDesc = draft.projectStatusDesc || '';
projectModel.projectProgressPlan = draft.projectProgressPlan || '';
projectModel.projectKeyPoints = draft.projectKeyPoints || '';
projectModel.projectProblems = draft.projectProblems || '';
projectModel.currentItems = normalizeProjectItems(draft.currentItems);
projectModel.nextItems = normalizeProjectItems(draft.nextItems);
}
function applyEditableFieldsByReportType(
draft:
| Api.WorkReport.Weekly.WeeklyReport
| Api.WorkReport.Monthly.MonthlyReport
| Api.WorkReport.Project.ProjectReport
) {
if (props.reportType === 'weekly') {
applyWeeklyEditableFields(draft as Api.WorkReport.Weekly.WeeklyReport);
return;
}
if (props.reportType === 'monthly') {
applyMonthlyEditableFields(draft as Api.WorkReport.Monthly.MonthlyReport);
return;
}
applyProjectEditableFields(draft as Api.WorkReport.Project.ProjectReport);
}
function createCurrentPeriodPayload(): PeriodPayload {
return {
periodKey: activeModel.value.periodKey,
periodLabel: activeModel.value.periodLabel,
periodStartDate: activeModel.value.periodStartDate,
periodEndDate: activeModel.value.periodEndDate
};
}
async function confirmDraftOverwrite(confirmOverwrite: boolean) {
if (!confirmOverwrite || props.operateType === 'edit') return true;
try {
await ElMessageBox.confirm('重新拉取默认稿会覆盖当前已编辑内容,是否继续?', '覆盖确认', {
type: 'warning',
confirmButtonText: '继续',
cancelButtonText: '取消'
});
return true;
} catch {
return false;
}
}
async function fetchEditDraftRefresh() {
if (props.reportType === 'weekly') {
return fetchRefreshWeeklyReportDraft(weeklyModel);
}
if (props.reportType === 'monthly') {
return fetchRefreshMonthlyReportDraft(monthlyModel);
}
return fetchRefreshProjectReportDraft(projectModel.projectId, {
periodKey: projectModel.periodKey,
periodLabel: projectModel.periodLabel,
periodStartDate: projectModel.periodStartDate,
periodEndDate: projectModel.periodEndDate,
flag: projectModel.flag,
projectStatusDesc: projectModel.projectStatusDesc,
projectProgressPlan: projectModel.projectProgressPlan,
projectKeyPoints: projectModel.projectKeyPoints,
projectProblems: projectModel.projectProblems,
currentItems: projectModel.currentItems,
nextItems: projectModel.nextItems
});
}
async function fetchDefaultDraftPreview() {
const period = createCurrentPeriodPayload();
if (props.reportType === 'weekly') {
return fetchPreviewWeeklyReportDefaultDraft(period);
}
if (props.reportType === 'monthly') {
return fetchPreviewMonthlyReportDefaultDraft(period);
}
return fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
...period,
flag: projectModel.flag
});
}
async function loadDetail() {
if (!props.rowData?.id) return;
@@ -194,56 +301,25 @@ async function loadInitAndDraft() {
}
async function pullDefaultDraft(confirmOverwrite = true) {
if (confirmOverwrite) {
try {
await ElMessageBox.confirm('重新拉取默认稿会覆盖当前已编辑内容,是否继续?', '覆盖确认', {
type: 'warning',
confirmButtonText: '继续',
cancelButtonText: '取消'
});
} catch {
const confirmed = await confirmDraftOverwrite(confirmOverwrite);
if (!confirmed) return;
if (props.operateType === 'edit') {
const refreshResult = await fetchEditDraftRefresh();
if (refreshResult.error || !refreshResult.data) return;
applyEditableFieldsByReportType(refreshResult.data);
window.$message?.success('最新数据已刷新');
return;
}
}
const period = {
periodKey: activeModel.value.periodKey,
periodLabel: activeModel.value.periodLabel,
periodStartDate: activeModel.value.periodStartDate,
periodEndDate: activeModel.value.periodEndDate
};
let result;
if (props.reportType === 'weekly') {
result = await fetchPreviewWeeklyReportDefaultDraft(period);
} else if (props.reportType === 'monthly') {
result = await fetchPreviewMonthlyReportDefaultDraft(period);
} else {
result = await fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
...period,
flag: projectModel.flag
});
}
const result = await fetchDefaultDraftPreview();
if (result.error || !result.data) return;
if (props.reportType === 'weekly') {
weeklyModel.reviewItems = normalizeReviewItems((result.data as Api.WorkReport.Weekly.WeeklyReport).reviewItems);
weeklyModel.planItems = normalizePlanItems((result.data as Api.WorkReport.Weekly.WeeklyReport).planItems);
}
if (props.reportType === 'monthly') {
monthlyModel.reviewItems = normalizeReviewItems((result.data as Api.WorkReport.Monthly.MonthlyReport).reviewItems);
monthlyModel.planItems = normalizePlanItems((result.data as Api.WorkReport.Monthly.MonthlyReport).planItems);
}
if (props.reportType === 'project') {
projectModel.currentItems = normalizeProjectItems(
(result.data as Api.WorkReport.Project.ProjectReport).currentItems
);
projectModel.nextItems = normalizeProjectItems((result.data as Api.WorkReport.Project.ProjectReport).nextItems);
}
applyEditableFieldsByReportType(result.data);
}
watch(visible, isVisible => {
@@ -346,7 +422,7 @@ async function handleSubmit() {
<template #icon>
<icon-mdi-refresh class="text-icon" />
</template>
重新拉取默认稿
刷新
</ElButton>
</div>
</BusinessFormSection>

View File

@@ -19,6 +19,9 @@ import {
fetchPreviewMonthlyReportDefaultDraft,
fetchPreviewProjectReportDefaultDraft,
fetchPreviewWeeklyReportDefaultDraft,
fetchRefreshMonthlyReportDraft,
fetchRefreshProjectReportDraft,
fetchRefreshWeeklyReportDraft,
fetchRejectMonthlyReport,
fetchRejectProjectReport,
fetchRejectWeeklyReport,
@@ -243,6 +246,27 @@ function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
if (props.mode === 'add') patchPeriod(projectModel);
}
function applyWeeklyEditableFields(draft: Api.WorkReport.Weekly.WeeklyReport) {
weeklyModel.isBusinessTrip = draft.isBusinessTrip;
weeklyModel.reviewItems = draft.reviewItems?.length ? normalizeReviewItems(draft.reviewItems) : [];
weeklyModel.planItems = draft.planItems?.length ? normalizePlanItems(draft.planItems) : [];
weeklyModel.travelSegments = draft.travelSegments || [];
}
function applyMonthlyEditableFields(draft: Api.WorkReport.Monthly.MonthlyReport) {
monthlyModel.reviewItems = draft.reviewItems?.length ? normalizeReviewItems(draft.reviewItems) : [];
monthlyModel.planItems = draft.planItems?.length ? normalizePlanItems(draft.planItems) : [];
}
function applyProjectEditableFields(draft: Api.WorkReport.Project.ProjectReport) {
projectModel.projectStatusDesc = draft.projectStatusDesc || '';
projectModel.projectProgressPlan = draft.projectProgressPlan || '';
projectModel.projectKeyPoints = draft.projectKeyPoints || '';
projectModel.projectProblems = draft.projectProblems || '';
projectModel.currentItems = draft.currentItems?.length ? normalizeProjectItems(draft.currentItems) : [];
projectModel.nextItems = draft.nextItems?.length ? normalizeProjectItems(draft.nextItems) : [];
}
function firstMeaningfulValue<T>(...values: Array<T | null | undefined | ''>) {
return values.find(value => value !== null && value !== undefined && value !== '') as T | undefined;
}
@@ -346,26 +370,64 @@ async function pullDefaultDraft(confirmOverwrite = false) {
if (props.reportType === 'weekly') {
const data = result.data as Api.WorkReport.Weekly.WeeklyReport;
weeklyModel.reviewItems = data.reviewItems?.length ? normalizeReviewItems(data.reviewItems) : [];
weeklyModel.planItems = data.planItems?.length ? normalizePlanItems(data.planItems) : [];
applyWeeklyEditableFields(data);
if (confirmOverwrite) {
weeklyModel.travelSegments = data.travelSegments || [];
}
}
if (props.reportType === 'monthly') {
const data = result.data as Api.WorkReport.Monthly.MonthlyReport;
monthlyModel.reviewItems = data.reviewItems?.length ? normalizeReviewItems(data.reviewItems) : [];
monthlyModel.planItems = data.planItems?.length ? normalizePlanItems(data.planItems) : [];
applyMonthlyEditableFields(result.data as Api.WorkReport.Monthly.MonthlyReport);
}
if (props.reportType === 'project') {
const data = result.data as Api.WorkReport.Project.ProjectReport;
projectModel.currentItems = data.currentItems?.length ? normalizeProjectItems(data.currentItems) : [];
projectModel.nextItems = data.nextItems?.length ? normalizeProjectItems(data.nextItems) : [];
applyProjectEditableFields(result.data as Api.WorkReport.Project.ProjectReport);
}
}
async function refreshDraft() {
if (props.mode !== 'edit') {
await pullDefaultDraft(true);
return;
}
loading.value = true;
let result;
if (props.reportType === 'weekly') {
result = await fetchRefreshWeeklyReportDraft(weeklyModel);
} else if (props.reportType === 'monthly') {
result = await fetchRefreshMonthlyReportDraft(monthlyModel);
} else {
result = await fetchRefreshProjectReportDraft(projectModel.projectId, {
periodKey: projectModel.periodKey,
periodLabel: projectModel.periodLabel,
periodStartDate: projectModel.periodStartDate,
periodEndDate: projectModel.periodEndDate,
flag: projectModel.flag,
projectStatusDesc: projectModel.projectStatusDesc,
projectProgressPlan: projectModel.projectProgressPlan,
projectKeyPoints: projectModel.projectKeyPoints,
projectProblems: projectModel.projectProblems,
currentItems: projectModel.currentItems,
nextItems: projectModel.nextItems
});
}
loading.value = false;
if (result.error || !result.data) return;
if (props.reportType === 'weekly') {
applyWeeklyEditableFields(result.data as Api.WorkReport.Weekly.WeeklyReport);
} else if (props.reportType === 'monthly') {
applyMonthlyEditableFields(result.data as Api.WorkReport.Monthly.MonthlyReport);
} else {
applyProjectEditableFields(result.data as Api.WorkReport.Project.ProjectReport);
}
window.$message?.success('最新数据已刷新');
}
async function loadInitData() {
loading.value = true;
let result;
@@ -665,7 +727,7 @@ function handleRequestReject() {
}
function handlePullDefaultDraft() {
pullDefaultDraft(true);
refreshDraft();
}
function handleMonthlyApprovalChange(payload: Api.WorkReport.Monthly.MonthlyReportApproveParams) {

View File

@@ -1105,6 +1105,14 @@ function syncRichSupport(item: PlanItem, event: Event) {
<div class="section">
<div class="section-title">
<span>基础信息</span>
<div v-if="mode === 'edit' && !isReadonly" class="section-title-right">
<ElButton size="small" plain type="primary" @click="emit('pullDefaultDraft')">
<template #icon>
<icon-mdi-refresh class="text-icon" />
</template>
刷新
</ElButton>
</div>
</div>
<div class="compose-grid">
<div class="field">
@@ -1206,7 +1214,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
class="rich-editor"
:contenteditable="!isReadonly"
spellcheck="false"
:data-placeholder="isReadonly ? undefined : '璇疯緭鍏ュ伐浣滃唴瀹瑰強鎴愭灉鎻忚堪'"
:data-placeholder="isReadonly ? undefined : '请输入具体工作内容及成果描述'"
@focus="focusEditField(`content-${index}`)"
@blur="
syncRichContent(item, $event);
@@ -1305,7 +1313,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
class="rich-editor"
:contenteditable="!isReadonly"
spellcheck="false"
:data-placeholder="isReadonly ? undefined : '璇疯緭鍏ュ叿浣撶洰鏍?'"
:data-placeholder="isReadonly ? undefined : '请输入具体目标'"
@focus="focusEditField(`target-${index}`)"
@blur="
syncRichTarget(item, $event);
@@ -2099,81 +2107,6 @@ function syncRichSupport(item: PlanItem, event: Event) {
cursor: pointer;
}
.structured-preview--readonly {
display: grid;
gap: 8px;
cursor: default;
}
.structured-preview__section {
display: grid;
gap: 6px;
}
.structured-preview__section + .structured-preview__section {
padding-top: 8px;
border-top: 1px dashed #cbd5e1;
}
.structured-preview__category {
position: relative;
padding-left: 14px;
color: #0f766e;
font-size: 13px;
font-weight: 800;
line-height: 1.6;
}
.structured-preview__category::before {
content: '';
position: absolute;
left: 0;
top: 4px;
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
}
.structured-preview__tasks {
display: grid;
gap: 6px;
}
.structured-preview__task-line {
display: inline-flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: #334155;
line-height: 1.6;
cursor: pointer;
}
.structured-preview__task-title {
color: #334155;
overflow-wrap: anywhere;
word-break: break-word;
}
.structured-preview__task-metric {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 6px;
border-radius: 999px;
background: #f3f7f9;
color: #475467;
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.structured-preview__task-metric:first-of-type {
background: #fff7ed;
color: #c2410c;
}
.structured-preview__popover {
max-width: 100%;
color: #334155;

View File

@@ -560,7 +560,7 @@ const columns = computed(() => [
<div class="requirement-action-cell" onClick={event => event.stopPropagation()}>
{actions.length === 0 ? (
<ElButton link size="small" class="requirement-action-icon-btn" type="primary" disabled>
<IconMdiPencilOutline class="text-18px" />
<IconMdiPencilOutline class="text-15px" />
</ElButton>
) : (
actions.map(action => {
@@ -575,7 +575,7 @@ const columns = computed(() => [
disabled={action.disabled}
onClick={() => action.onClick()}
>
<IconComponent class="text-18px" />
<IconComponent class="text-15px" />
</ElButton>
</ElTooltip>
);
@@ -1068,13 +1068,18 @@ onMounted(async () => {
:deep(.requirement-action-cell) {
display: inline-flex;
align-items: center;
gap: 1px;
gap: 6px;
}
:deep(.requirement-action-cell .el-button + .el-button) {
margin-left: 0;
}
:deep(.requirement-action-icon-btn) {
padding: 1px;
padding: 3px;
height: auto;
min-width: auto;
line-height: 1;
}
:deep(.requirement-action-icon-btn:hover) {

View File

@@ -445,7 +445,7 @@ const columns = computed(() => [
<div class="requirement-action-cell" onClick={event => event.stopPropagation()}>
{actions.length === 0 ? (
<ElButton link size="small" class="requirement-action-icon-btn" type="primary" disabled>
<IconMdiPencilOutline class="text-18px" />
<IconMdiPencilOutline class="text-15px" />
</ElButton>
) : (
actions.map(action => {
@@ -461,7 +461,7 @@ const columns = computed(() => [
disabled={action.disabled}
onClick={() => action.onClick()}
>
<IconComponent class="text-18px" />
<IconComponent class="text-15px" />
</ElButton>
</ElTooltip>
);
@@ -1035,13 +1035,18 @@ Promise.all([loadStatusOptions()]);
:deep(.requirement-action-cell) {
display: inline-flex;
align-items: center;
gap: 1px;
gap: 6px;
}
:deep(.requirement-action-cell .el-button + .el-button) {
margin-left: 0;
}
:deep(.requirement-action-icon-btn) {
padding: 1px;
padding: 3px;
height: auto;
min-width: auto;
line-height: 1;
}
:deep(.requirement-action-icon-btn:hover) {

View File

@@ -1023,10 +1023,8 @@ onMounted(async () => {
:key="item.id"
class="workbench-todo__item"
:class="{
'workbench-todo__item--clickable': Boolean(item.routeKey || item.approvalBizType),
'workbench-todo__item--selected': isOvertimeItemSelected(item)
}"
@click="handleClickItem(item)"
>
<div class="workbench-todo__leading">
<!-- 加班申请待审批时显示复选框 -->
@@ -1496,15 +1494,6 @@ onMounted(async () => {
background-color 160ms ease;
}
.workbench-todo__item--clickable {
cursor: pointer;
}
.workbench-todo__item--clickable:hover {
border-color: rgb(14 116 144 / 60%);
background-color: rgb(240 253 250 / 84%);
}
.workbench-todo__item--selected {
border-color: rgb(14 116 144 / 60%);
background-color: rgb(240 253 250 / 90%);