fix(产品需求、项目需求、工作报告、我的绩效、加班申请): 1、修复搜索区域的下拉框,无法搜索的问题。2、修复工作报告内容溢出的问题。3、修复我的待办 - 待审批,里面的二级tabs没有加权限的问题。4、优化周报里工作日志展示时的分隔符。5、优化项目需求池、我的绩效请求重复发送、影响效率的问题。
feat(我的绩效): 1、增加我的绩效在工作台可以直接处理的功能。2、增加我的绩效全部导出时,合并多个excel的sheet为一个excel的功能。
This commit is contained in:
@@ -358,8 +358,10 @@ function resolveTaskItemTypeLabel(value?: string | null) {
|
||||
return getTaskItemTypeLabel(value, { fallback: value || '工作内容' });
|
||||
}
|
||||
|
||||
const STRUCTURED_TASK_PREFIX_RE = /^(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*/u;
|
||||
|
||||
function stripStructuredTaskPrefixV2(value: string) {
|
||||
return value.trim().replace(/^\d+[..、]\s*/u, '');
|
||||
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
|
||||
}
|
||||
|
||||
function stripStructuredTaskSuffixV2(value: string) {
|
||||
@@ -381,16 +383,13 @@ function formatStructuredTaskDisplayLine(task: StructuredTask, index: number, sh
|
||||
return `${index + 1}、${formatStructuredTaskLineV2(task, showHours)}`;
|
||||
}
|
||||
|
||||
// 周报工作日志弹层展示:后端用中文分号 ";" 拼接多条工作日志,
|
||||
// 在 popover 中按行展示,每条工作日志仍保留末尾的分号。
|
||||
function formatWorkLogDetail(detail: string): string {
|
||||
if (!detail) return '';
|
||||
// 仅按中文分号切分,避免误伤文本中的其他标点;每段保持原样展示。
|
||||
function getWorkLogEntries(detail: string): string[] {
|
||||
if (!detail) return [];
|
||||
// 仅按中文分号切分,避免误伤文本中的其他标点;每段作为单独一天的工作日志展示。
|
||||
return detail
|
||||
.split(';')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
.join(';\n');
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function createStructuredTextV2(sections: StructuredSection[], showHours = false) {
|
||||
@@ -493,7 +492,7 @@ function createStructuredTasksFromText(text: string, defaultTask?: Partial<Struc
|
||||
}
|
||||
|
||||
function stripStructuredTaskPrefix(value: string) {
|
||||
return value.trim().replace(/^\d+[..、]\s*/u, '');
|
||||
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
|
||||
}
|
||||
|
||||
function stripStructuredTaskSuffix(value: string) {
|
||||
@@ -524,6 +523,52 @@ function resolveTaskMetrics(metricsText: string, fallback?: Partial<StructuredTa
|
||||
};
|
||||
}
|
||||
|
||||
function isStructuredMetricsText(text: string) {
|
||||
return /(?:\bP\d+\b|进度\s*\d+(?:\.\d+)?%|\d+(?:\.\d+)?%|\d+(?:\.\d+)?h)/iu.test(text.trim());
|
||||
}
|
||||
|
||||
function extractStructuredTaskParts(normalizedText: string) {
|
||||
const colonIndex = normalizedText.search(/[::]/u);
|
||||
const mainText = colonIndex >= 0 ? normalizedText.slice(0, colonIndex).trim() : normalizedText.trim();
|
||||
const detailText = colonIndex >= 0 ? normalizedText.slice(colonIndex + 1).trim() : '';
|
||||
|
||||
const bracketMatches = Array.from(mainText.matchAll(/[((]([^()()]*)[))]/gu));
|
||||
const completeMetricsMatch = [...bracketMatches].reverse().find(match => isStructuredMetricsText(match[1] || ''));
|
||||
|
||||
if (completeMetricsMatch && completeMetricsMatch.index !== undefined) {
|
||||
const fullMatch = completeMetricsMatch[0] || '';
|
||||
const metricsText = completeMetricsMatch[1] || '';
|
||||
const startIndex = completeMetricsMatch.index;
|
||||
const endIndex = startIndex + fullMatch.length;
|
||||
|
||||
return {
|
||||
rawTitle: `${mainText.slice(0, startIndex)}${mainText.slice(endIndex)}`.trim(),
|
||||
metricsText,
|
||||
detail: detailText
|
||||
};
|
||||
}
|
||||
|
||||
const lastOpenIndex = Math.max(mainText.lastIndexOf('('), mainText.lastIndexOf('('));
|
||||
const lastCloseIndex = Math.max(mainText.lastIndexOf(')'), mainText.lastIndexOf(')'));
|
||||
|
||||
if (lastOpenIndex > lastCloseIndex) {
|
||||
const metricsText = mainText.slice(lastOpenIndex + 1).trim();
|
||||
if (isStructuredMetricsText(metricsText)) {
|
||||
return {
|
||||
rawTitle: mainText.slice(0, lastOpenIndex).trim(),
|
||||
metricsText,
|
||||
detail: detailText
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rawTitle: mainText,
|
||||
metricsText: '',
|
||||
detail: detailText
|
||||
};
|
||||
}
|
||||
|
||||
function parseStructuredSectionTaskText(
|
||||
text: string,
|
||||
fallback?: Partial<StructuredTask>,
|
||||
@@ -532,13 +577,7 @@ function parseStructuredSectionTaskText(
|
||||
const normalizedText = stripStructuredTaskPrefix(text);
|
||||
if (!normalizedText) return null;
|
||||
|
||||
const structuredMatch =
|
||||
normalizedText.match(/^(.+?)(?:[((]([^()()]*)[))])?(?:\s*[::]\s*(.*))?$/u) ||
|
||||
normalizedText.match(/^(.+?)(?:\(([^()]*)\))?(?::\s*(.*))?$/u);
|
||||
|
||||
if (!structuredMatch) return null;
|
||||
|
||||
const [, rawTitle, metricsText = '', detail = ''] = structuredMatch;
|
||||
const { rawTitle, metricsText, detail } = extractStructuredTaskParts(normalizedText);
|
||||
const title = stripStructuredTaskSuffix(rawTitle);
|
||||
if (!title) return null;
|
||||
|
||||
@@ -569,6 +608,16 @@ function createStructuredSectionsFromTextV2(
|
||||
};
|
||||
|
||||
let currentCategory = '';
|
||||
let previousTask: StructuredTask | null = null;
|
||||
|
||||
const shouldAppendToPreviousTaskDetail = (line: string) => {
|
||||
if (!previousTask?.detail) return false;
|
||||
if (line.startsWith('#')) return false;
|
||||
if (line.includes('(') || line.includes('(') || line.includes(':') || line.includes(':')) return false;
|
||||
return !/^(?!(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*)(.+?)\s*[--]\s*(.+)$/u.test(
|
||||
line
|
||||
);
|
||||
};
|
||||
|
||||
lines.forEach(line => {
|
||||
const trimmedLine = line.trim();
|
||||
@@ -577,12 +626,15 @@ function createStructuredSectionsFromTextV2(
|
||||
if (trimmedLine.startsWith('#')) {
|
||||
currentCategory = trimmedLine.replace(/^#\s*/u, '').trim();
|
||||
if (currentCategory) ensureSection(currentCategory);
|
||||
previousTask = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅当行首不是结构化任务前缀(如 "3、")时,才按旧式 "<分类> - <事项>" 解析;
|
||||
// 否则会把 "2026-06-12 - 2026-06-19" 这种含 " - " 的出差行误判为分类。
|
||||
const legacyMatch = trimmedLine.match(/^(?!\d+[、..]\s*)(.+?)\s*[--]\s*(.+)$/u);
|
||||
const legacyMatch = trimmedLine.match(
|
||||
/^(?!(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*)(.+?)\s*[--]\s*(.+)$/u
|
||||
);
|
||||
if (legacyMatch) {
|
||||
const [, rawCategory, rawTaskText] = legacyMatch;
|
||||
const category = rawCategory.trim();
|
||||
@@ -590,6 +642,14 @@ function createStructuredSectionsFromTextV2(
|
||||
if (!category || !task) return;
|
||||
ensureSection(category).tasks.push(task);
|
||||
currentCategory = category;
|
||||
previousTask = task;
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldAppendToPreviousTaskDetail(trimmedLine)) {
|
||||
const lastTask = previousTask;
|
||||
if (!lastTask) return;
|
||||
lastTask.detail = lastTask.detail ? `${lastTask.detail}\n${trimmedLine}` : trimmedLine;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -597,7 +657,7 @@ function createStructuredSectionsFromTextV2(
|
||||
if (!task) return;
|
||||
|
||||
const hasStructuredHint =
|
||||
/^(\d+[..、]\s*)/u.test(trimmedLine) ||
|
||||
STRUCTURED_TASK_PREFIX_RE.test(trimmedLine) ||
|
||||
trimmedLine.includes('(') ||
|
||||
trimmedLine.includes('(') ||
|
||||
trimmedLine.includes(':') ||
|
||||
@@ -606,6 +666,7 @@ function createStructuredSectionsFromTextV2(
|
||||
if (!hasStructuredHint && !currentCategory) return;
|
||||
|
||||
ensureSection(currentCategory || defaultCategory).tasks.push(task);
|
||||
previousTask = task;
|
||||
});
|
||||
|
||||
return mergeSectionsByCategory(sections);
|
||||
@@ -657,7 +718,7 @@ function parseStructuredSectionsFromEditorV2(
|
||||
...task,
|
||||
detail: fallbackTask?.detail || '',
|
||||
kind: fallbackTask?.kind,
|
||||
hours: task.hours ?? fallbackTask?.hours
|
||||
hours: task.hours
|
||||
};
|
||||
})
|
||||
};
|
||||
@@ -1065,6 +1126,11 @@ function blurEditField(key: string) {
|
||||
if (activeEditField.value === key) activeEditField.value = '';
|
||||
}
|
||||
|
||||
function isStructuredEditorUnchanged(text: string, sections: StructuredSection[] | undefined, showHours = false) {
|
||||
if (!sections?.length) return false;
|
||||
return normalizeEditorText(text) === createStructuredTextV2(sections, showHours);
|
||||
}
|
||||
|
||||
/** 编辑态下是否显示"具体工作内容"的结构化预览(含 ElPopover 工作日志) */
|
||||
function showContentStructuredView(index: number) {
|
||||
const item = reviewItems.value[index];
|
||||
@@ -1096,6 +1162,11 @@ function handleStructuredViewClick(fieldKey: string) {
|
||||
function syncRichContent(item: ReviewItem, event: Event) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (!item.source) return;
|
||||
if (isStructuredEditorUnchanged(target.innerText, item.contentSections, true)) {
|
||||
item.source.contentJson = createSectionsJson(item.contentSections || []);
|
||||
item.source.contentText = createStructuredTextV2(item.contentSections || [], true);
|
||||
return;
|
||||
}
|
||||
const sections = parseStructuredSectionsFromEditorV2(
|
||||
target,
|
||||
item.contentSections || [],
|
||||
@@ -1108,6 +1179,11 @@ function syncRichContent(item: ReviewItem, event: Event) {
|
||||
function syncRichTarget(item: PlanItem, event: Event) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (!item.source) return;
|
||||
if (isStructuredEditorUnchanged(target.innerText, item.targetSections)) {
|
||||
item.source.targetJson = createSectionsJson(item.targetSections || []);
|
||||
item.source.targetText = createStructuredTextV2(item.targetSections || []);
|
||||
return;
|
||||
}
|
||||
const sections = parseStructuredSectionsFromEditorV2(
|
||||
target,
|
||||
item.targetSections || [],
|
||||
@@ -1232,7 +1308,20 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
</div>
|
||||
</template>
|
||||
<div class="structured-preview__popover">
|
||||
{{ formatWorkLogDetail(task.detail) || '暂无内容' }}
|
||||
<template v-if="getWorkLogEntries(task.detail).length">
|
||||
<div
|
||||
v-for="(entry, entryIndex) in getWorkLogEntries(task.detail)"
|
||||
:key="`${index}-${sectionIndex}-${taskIndex}-${entryIndex}`"
|
||||
class="structured-preview__log-entry"
|
||||
>
|
||||
<div class="structured-preview__log-text">{{ entry }}</div>
|
||||
<div
|
||||
v-if="entryIndex < getWorkLogEntries(task.detail).length - 1"
|
||||
class="structured-preview__log-divider"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>暂无内容</template>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div v-else class="rich-task-line">
|
||||
@@ -1337,7 +1426,20 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
</div>
|
||||
</template>
|
||||
<div class="structured-preview__popover">
|
||||
{{ formatWorkLogDetail(task.detail) || '暂无内容' }}
|
||||
<template v-if="getWorkLogEntries(task.detail).length">
|
||||
<div
|
||||
v-for="(entry, entryIndex) in getWorkLogEntries(task.detail)"
|
||||
:key="`${index}-${sectionIndex}-${taskIndex}-${entryIndex}`"
|
||||
class="structured-preview__log-entry"
|
||||
>
|
||||
<div class="structured-preview__log-text">{{ entry }}</div>
|
||||
<div
|
||||
v-if="entryIndex < getWorkLogEntries(task.detail).length - 1"
|
||||
class="structured-preview__log-divider"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>暂无内容</template>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div v-else class="rich-task-line">
|
||||
@@ -2161,11 +2263,30 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.structured-preview__log-entry {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.structured-preview__log-text {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.structured-preview__log-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.rich-editor :deep(.rich-task) {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
Reference in New Issue
Block a user