fix(工作报告): 修复工作报告存在的若干问题。
feat(加班申请): 支持批量审批。
This commit is contained in:
@@ -70,7 +70,7 @@ const table = useUIPaginatedTable<
|
||||
{ prop: 'periodLabel', label: '月份', minWidth: 80, formatter: row => formatPeriod(row) },
|
||||
{
|
||||
prop: 'reporterDeptName',
|
||||
label: '部门/方向',
|
||||
label: '部门',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.reporterDeptName || '--'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-mutating-props, unicorn/prefer-dom-node-text-content, no-useless-escape, no-nested-ternary, no-plusplus, @typescript-eslint/no-shadow */
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchGetMyParticipatedProjectPage } from '@/service/api';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
@@ -59,12 +60,14 @@ interface StructuredTask {
|
||||
priority?: string;
|
||||
progress?: number;
|
||||
hours?: number;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface PlanTaskDraft {
|
||||
title: string;
|
||||
priority?: StructuredTask['priority'];
|
||||
progress: number;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface StructuredSection {
|
||||
@@ -106,6 +109,37 @@ const planForm = ref({
|
||||
|
||||
const activePlanSectionIndex = ref(-1);
|
||||
|
||||
/** 工作事项下拉里的"我的事项"固定项,用于区分纯个人事务。 */
|
||||
const MY_AFFAIRS_TITLE = '我的事项';
|
||||
|
||||
/** 「我参与的项目」项目名清单,下拉框数据源。 */
|
||||
const participatedProjectNames = ref<string[]>([]);
|
||||
|
||||
async function loadParticipatedProjectNames() {
|
||||
// pageSize=-1 一次拉全部(不分页),由后端按"进行中 + 创建时间升序"过滤排序。
|
||||
const { data, error } = await fetchGetMyParticipatedProjectPage({ pageNo: 1, pageSize: -1 });
|
||||
if (error || !data) {
|
||||
participatedProjectNames.value = [];
|
||||
return;
|
||||
}
|
||||
participatedProjectNames.value = data.list
|
||||
.map(project => project.name?.trim())
|
||||
.filter((name): name is string => Boolean(name));
|
||||
}
|
||||
|
||||
onMounted(loadParticipatedProjectNames);
|
||||
|
||||
const reviewWorkItemOptions = computed(() => {
|
||||
// 下拉数据来自「我参与的项目」API 拉取的项目名 + 「我的事项」固定项,
|
||||
// 与周报共用同一份数据源(fetchGetMyParticipatedProjectPage, pageSize=-1)。
|
||||
const names = new Set<string>(participatedProjectNames.value);
|
||||
return [MY_AFFAIRS_TITLE, ...Array.from(names).filter(name => name !== MY_AFFAIRS_TITLE)];
|
||||
});
|
||||
|
||||
/** 选中"我的事项"时,事项不参与项目优先级,需要隐藏优先级相关展示。 */
|
||||
const isMyAffairsPlanItem = (workItem: string) => workItem.trim() === MY_AFFAIRS_TITLE;
|
||||
const DEFAULT_SECTION_CATEGORY = '工作内容';
|
||||
|
||||
function createPlanTaskDraft(): PlanTaskDraft {
|
||||
return { title: '', priority: '2', progress: 0 };
|
||||
}
|
||||
@@ -115,7 +149,8 @@ function normalizeTask(task: WorkReportStructuredTask): StructuredTask {
|
||||
title: task.title || '',
|
||||
priority: normalizePriorityCode(task.priority),
|
||||
progress: typeof task.progress === 'number' ? task.progress : undefined,
|
||||
hours: typeof task.hours === 'number' ? task.hours : undefined
|
||||
hours: typeof task.hours === 'number' ? task.hours : undefined,
|
||||
detail: task.detail || ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,16 +182,14 @@ function resolveTaskItemTypeLabel(value: string) {
|
||||
return getTaskItemTypeLabel(value, { fallback: value || '工作内容' });
|
||||
}
|
||||
|
||||
function normalizeSection(section: WorkReportStructuredSection): StructuredSection {
|
||||
return {
|
||||
category: resolveTaskItemTypeLabel(section.category || '工作内容'),
|
||||
tasks: section.tasks.map(normalizeTask)
|
||||
};
|
||||
function normalizeSectionCategory(value?: string | null, fallback = DEFAULT_SECTION_CATEGORY) {
|
||||
const category = resolveTaskItemTypeLabel(value || '').trim();
|
||||
return category || fallback;
|
||||
}
|
||||
|
||||
function normalizeSectionForMerge(section: WorkReportStructuredSection): StructuredSection {
|
||||
function normalizeSection(section: WorkReportStructuredSection): StructuredSection {
|
||||
return {
|
||||
category: section.category || '工作内容',
|
||||
category: normalizeSectionCategory(section.category),
|
||||
tasks: section.tasks.map(normalizeTask)
|
||||
};
|
||||
}
|
||||
@@ -165,7 +198,7 @@ function mergeSectionsByCategory(sections: StructuredSection[]) {
|
||||
const sectionMap = new Map<string, StructuredSection>();
|
||||
|
||||
sections.forEach(section => {
|
||||
const category = section.category || '未分类';
|
||||
const category = normalizeSectionCategory(section.category, '未分类');
|
||||
const existing = sectionMap.get(category);
|
||||
if (existing) {
|
||||
existing.tasks.push(...section.tasks);
|
||||
@@ -191,7 +224,10 @@ function resolveTaskMetricsV2(metricsText: string) {
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
const priority = normalizePriorityCode(parts.find(item => /^P?\d+$/iu.test(item)));
|
||||
const progressText = metricsText.match(/进度\s*(\d+(?:\.\d+)?)%/u)?.[1];
|
||||
// 同时支持 "进度 XX%" 与裸 "XX%",避免失焦后丢失进度。
|
||||
const progressText =
|
||||
metricsText.match(/进度\s*(\d+(?:\.\d+)?)%/u)?.[1] ||
|
||||
parts.find(item => /^\d+(?:\.\d+)?%$/u.test(item))?.replace(/%$/u, '');
|
||||
const hoursText = metricsText.match(/(\d+(?:\.\d+)?)h/u)?.[1];
|
||||
|
||||
return {
|
||||
@@ -215,7 +251,7 @@ function formatStructuredTaskLineV2(task: StructuredTask, showHours = false) {
|
||||
function createStructuredTextV2(sections: StructuredSection[], showHours = false) {
|
||||
return sections
|
||||
.map(section => {
|
||||
const categoryLabel = resolveTaskItemTypeLabel(section.category).trim();
|
||||
const categoryLabel = normalizeSectionCategory(section.category);
|
||||
return [
|
||||
`#${categoryLabel}`,
|
||||
...section.tasks.map((task, index) => `${index + 1}、${formatStructuredTaskLineV2(task, showHours)}`)
|
||||
@@ -229,7 +265,7 @@ function createStructuredTextV2(sections: StructuredSection[], showHours = false
|
||||
function createStructuredHtmlV2(sections: StructuredSection[], showHours = false) {
|
||||
return sections
|
||||
.map(section => {
|
||||
const categoryLabel = resolveTaskItemTypeLabel(section.category.trim());
|
||||
const categoryLabel = normalizeSectionCategory(section.category);
|
||||
const tasks = section.tasks
|
||||
.map(
|
||||
(task, index) =>
|
||||
@@ -253,7 +289,7 @@ function createStructuredSectionsFromTextV2(text: string, defaultCategory: strin
|
||||
|
||||
const sections: StructuredSection[] = [];
|
||||
const ensureSection = (categoryText: string) => {
|
||||
const category = categoryText.trim() || defaultCategory;
|
||||
const category = normalizeSectionCategory(categoryText, defaultCategory);
|
||||
let section = sections.find(item => item.category === category);
|
||||
if (!section) {
|
||||
section = { category, tasks: [] };
|
||||
@@ -276,63 +312,110 @@ function createStructuredSectionsFromTextV2(text: string, defaultCategory: strin
|
||||
|
||||
const legacyMatch = trimmedLine.match(/^(.+?)\s*-\s*(.+?)(?:[((]([^()()]*)[))])?[。.!!??]*$/u);
|
||||
if (legacyMatch) {
|
||||
const [, rawCategory, rawTitle, metricsText = ''] = legacyMatch;
|
||||
const [, rawCategory, rawTaskText] = legacyMatch;
|
||||
const category = rawCategory.trim();
|
||||
const title = rawTitle.trim();
|
||||
if (!category || !title) return;
|
||||
const task = parseStructuredSectionTaskText(rawTaskText);
|
||||
if (!category || !task) return;
|
||||
|
||||
ensureSection(category).tasks.push({
|
||||
title,
|
||||
...resolveTaskMetricsV2(metricsText)
|
||||
});
|
||||
ensureSection(category).tasks.push(task);
|
||||
currentCategory = category;
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedLine = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(trimmedLine));
|
||||
if (!normalizedLine) return;
|
||||
const inlineMatch = normalizedLine.match(/^(.*?)(?:[((]([^()()]*)[))])?$/u);
|
||||
const title = inlineMatch?.[1]?.trim() || '';
|
||||
const metricsText = inlineMatch?.[2]?.trim() || '';
|
||||
if (!title) return;
|
||||
const task = parseStructuredSectionTaskText(trimmedLine);
|
||||
if (!task) return;
|
||||
|
||||
ensureSection(currentCategory || defaultCategory).tasks.push({
|
||||
title,
|
||||
...resolveTaskMetricsV2(metricsText)
|
||||
});
|
||||
const hasStructuredHint =
|
||||
/^(\d+[..、]\s*)/u.test(trimmedLine) ||
|
||||
trimmedLine.includes('(') ||
|
||||
trimmedLine.includes('(') ||
|
||||
trimmedLine.includes(':') ||
|
||||
trimmedLine.includes(':');
|
||||
|
||||
if (!hasStructuredHint && !currentCategory) return;
|
||||
|
||||
ensureSection(currentCategory || defaultCategory).tasks.push(task);
|
||||
});
|
||||
|
||||
return sections.filter(section => section.tasks.length);
|
||||
}
|
||||
|
||||
function parseStructuredSectionsFromEditorV2(editor: HTMLElement, defaultCategory = '工作内容'): StructuredSection[] {
|
||||
return createStructuredSectionsFromTextV2(editor.innerText, defaultCategory);
|
||||
}
|
||||
function parseStructuredSectionTaskText(
|
||||
text: string,
|
||||
fallback?: Partial<StructuredTask>,
|
||||
useFallback = false
|
||||
): StructuredTask | null {
|
||||
const normalizedText = stripStructuredTaskPrefixV2(text);
|
||||
if (!normalizedText) return null;
|
||||
|
||||
function createStructuredText(sections: StructuredSection[], showHours = false) {
|
||||
const stripOrderPrefix = (text: string) => text.trim().replace(/^\d+[..、]\s*/, '');
|
||||
const formatTaskText = (task: StructuredTask, taskIndex: number, showHours: boolean) => {
|
||||
const rawTitle = stripOrderPrefix(task.title);
|
||||
const punctuation = rawTitle.match(/[。.!!??]$/)?.[0] || '';
|
||||
const title = punctuation ? rawTitle.slice(0, -1) : rawTitle;
|
||||
const metrics = [
|
||||
task.priority ? resolvePriorityLabel(task.priority) : '',
|
||||
typeof task.progress === 'number' ? `进度${task.progress}%` : '',
|
||||
showHours && typeof task.hours === 'number' ? `${task.hours}h` : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
return `${taskIndex + 1}.${title}${metrics ? `(${metrics})` : ''}${punctuation}`;
|
||||
const structuredMatch =
|
||||
normalizedText.match(/^(.+?)(?:[((]([^()()]*)[))])?(?:\s*[::]\s*(.*))?$/u) ||
|
||||
normalizedText.match(/^(.+?)(?:\(([^()]*)\))?(?::\s*(.*))?$/u);
|
||||
|
||||
if (!structuredMatch) return null;
|
||||
|
||||
const [, rawTitle, metricsText = '', detail = ''] = structuredMatch;
|
||||
const title = stripStructuredTaskSuffixV2(rawTitle);
|
||||
if (!title) return null;
|
||||
|
||||
return {
|
||||
title,
|
||||
detail: detail.trim(),
|
||||
...resolveTaskMetrics(metricsText, fallback, useFallback)
|
||||
};
|
||||
|
||||
return sections
|
||||
.map(section => {
|
||||
const categoryLabel = resolveTaskItemTypeLabel(section.category);
|
||||
const tasksText = section.tasks.map((task, taskIndex) => formatTaskText(task, taskIndex, showHours)).join('');
|
||||
return [categoryLabel, tasksText ? ` ${tasksText}` : ''].filter(Boolean).join('\n');
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function createFallbackTaskLookup(sections: StructuredSection[]) {
|
||||
const lookup = new Map<string, StructuredTask[]>();
|
||||
|
||||
sections.forEach(section => {
|
||||
const categoryKey = normalizeSectionCategory(section.category);
|
||||
section.tasks.forEach(task => {
|
||||
const titleKey = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(task.title));
|
||||
const key = `${categoryKey}||${titleKey}`;
|
||||
const list = lookup.get(key);
|
||||
if (list) {
|
||||
list.push(task);
|
||||
} else {
|
||||
lookup.set(key, [task]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function parseStructuredSectionsFromEditorV2(
|
||||
editor: HTMLElement,
|
||||
fallbackSections: StructuredSection[] = [],
|
||||
defaultCategory = DEFAULT_SECTION_CATEGORY
|
||||
): StructuredSection[] {
|
||||
const parsedSections = createStructuredSectionsFromTextV2(editor.innerText, defaultCategory);
|
||||
if (!parsedSections.length) return [];
|
||||
|
||||
const fallbackLookup = createFallbackTaskLookup(fallbackSections);
|
||||
|
||||
return parsedSections.map((section, sectionIndex) => {
|
||||
const fallbackSection = fallbackSections[sectionIndex];
|
||||
const categoryKey = normalizeSectionCategory(section.category);
|
||||
|
||||
return {
|
||||
category: section.category,
|
||||
tasks: section.tasks.map((task, taskIndex) => {
|
||||
const titleKey = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(task.title));
|
||||
const matchedByKey = fallbackLookup.get(`${categoryKey}||${titleKey}`)?.shift();
|
||||
const matchedByIndex = fallbackSection?.tasks[taskIndex];
|
||||
const fallbackTask = matchedByKey || matchedByIndex;
|
||||
|
||||
return {
|
||||
...task,
|
||||
detail: fallbackTask?.detail || task.detail || '',
|
||||
hours: task.hours ?? fallbackTask?.hours
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
@@ -482,63 +565,19 @@ function resolveTaskMetrics(metricsText: string, fallback?: Partial<StructuredTa
|
||||
hours: hoursText === undefined ? (useFallback ? fallback?.hours : undefined) : Number(hoursText)
|
||||
};
|
||||
}
|
||||
|
||||
function parseStructuredSectionsFromEditor(
|
||||
editor: HTMLElement,
|
||||
fallbackSections: StructuredSection[] = [],
|
||||
defaultCategory = '工作内容'
|
||||
) {
|
||||
const sectionElements = Array.from(editor.querySelectorAll<HTMLElement>('.rich-section'));
|
||||
|
||||
if (!sectionElements.length) {
|
||||
return createStructuredSectionsFromText(editor.innerText, defaultCategory);
|
||||
}
|
||||
|
||||
return sectionElements
|
||||
.map((sectionElement, sectionIndex) => {
|
||||
const fallbackSection = fallbackSections[sectionIndex];
|
||||
const category =
|
||||
getElementText(sectionElement.querySelector('.rich-section-title')) ||
|
||||
fallbackSection?.category ||
|
||||
defaultCategory;
|
||||
const taskElements = Array.from(sectionElement.querySelectorAll<HTMLElement>('.rich-section-task'));
|
||||
const tasks = taskElements
|
||||
.map((taskElement, taskIndex) => {
|
||||
const fallbackTask = fallbackSection?.tasks[taskIndex];
|
||||
const title = stripTaskOrderPrefix(getElementText(taskElement.querySelector('.rich-section-task-title')));
|
||||
const metricsElement = taskElement.querySelector('.rich-inline-metrics');
|
||||
const metrics = metricsElement
|
||||
? resolveTaskMetrics(getElementText(metricsElement), fallbackTask, false)
|
||||
: { priority: undefined, progress: undefined, hours: undefined };
|
||||
|
||||
if (!title) return null;
|
||||
|
||||
return {
|
||||
title,
|
||||
...metrics
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as StructuredTask[];
|
||||
|
||||
if (!category && !tasks.length) return null;
|
||||
|
||||
return {
|
||||
category,
|
||||
tasks
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as StructuredSection[];
|
||||
}
|
||||
|
||||
function createSectionsJson(sections: StructuredSection[]) {
|
||||
return sections.length ? JSON.stringify({ sections }) : null;
|
||||
}
|
||||
|
||||
function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): ReviewItem {
|
||||
function getReviewItemSections(item: Api.WorkReport.Common.PersonalReportReviewItem) {
|
||||
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.contentJson).map(normalizeSection));
|
||||
const contentSections = structuredSections.length
|
||||
? structuredSections
|
||||
: createStructuredSectionsFromTextV2(item.contentText || '', item.itemTitle || '未分类');
|
||||
if (structuredSections.length) return structuredSections;
|
||||
|
||||
return createStructuredSectionsFromTextV2(item.contentText || '', item.itemTitle || DEFAULT_SECTION_CATEGORY);
|
||||
}
|
||||
|
||||
function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): ReviewItem {
|
||||
const contentSections = getReviewItemSections(item);
|
||||
|
||||
return {
|
||||
workItem: item.itemTitle || '未命名工作',
|
||||
@@ -555,11 +594,15 @@ function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): Rev
|
||||
};
|
||||
}
|
||||
|
||||
function toPlanItem(item: Api.WorkReport.Common.PersonalReportPlanItem, index: number): PlanItem {
|
||||
function getPlanItemSections(item: Api.WorkReport.Common.PersonalReportPlanItem) {
|
||||
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.targetJson).map(normalizeSection));
|
||||
const targetSections = structuredSections.length
|
||||
? structuredSections
|
||||
: createStructuredSectionsFromTextV2(item.targetText || '', item.itemTitle || '未分类');
|
||||
if (structuredSections.length) return structuredSections;
|
||||
|
||||
return createStructuredSectionsFromTextV2(item.targetText || '', item.itemTitle || DEFAULT_SECTION_CATEGORY);
|
||||
}
|
||||
|
||||
function toPlanItem(item: Api.WorkReport.Common.PersonalReportPlanItem, index: number): PlanItem {
|
||||
const targetSections = getPlanItemSections(item);
|
||||
|
||||
return {
|
||||
workItem: item.itemTitle || '未命名计划',
|
||||
@@ -569,7 +612,6 @@ function toPlanItem(item: Api.WorkReport.Common.PersonalReportPlanItem, index: n
|
||||
: escapeHtml(item.targetText || '').replace(/\n/g, '<br>') || EMPTY_TEXT,
|
||||
targetSections,
|
||||
supportNeed: item.supportNeed || '',
|
||||
/* 只有用户新增的计划才可删除,默认带入的不可删除 */
|
||||
removable: true,
|
||||
source: item,
|
||||
sourceIndex: index
|
||||
@@ -628,11 +670,16 @@ function removePlanSection(index: number) {
|
||||
function addPlanTask(sectionIndex: number) {
|
||||
const section = planForm.value.sections[sectionIndex];
|
||||
if (!section?.draft.title.trim()) return;
|
||||
section.tasks.push({
|
||||
const task: StructuredTask = {
|
||||
title: section.draft.title.trim(),
|
||||
priority: section.draft.priority,
|
||||
progress: section.draft.progress
|
||||
});
|
||||
progress: section.draft.progress,
|
||||
detail: section.draft.detail?.trim() || undefined
|
||||
};
|
||||
// 我的事项不参与项目优先级,存进任务前显式剔除,结构化展示括号里就不会出现优先级。
|
||||
if (!isMyAffairsPlanItem(planForm.value.workItem)) {
|
||||
task.priority = section.draft.priority;
|
||||
}
|
||||
section.tasks.push(task);
|
||||
section.draft = createPlanTaskDraft();
|
||||
}
|
||||
|
||||
@@ -644,25 +691,17 @@ function submitInlinePlan() {
|
||||
const sections = planForm.value.sections
|
||||
.filter(section => section.category.trim() && section.tasks.length)
|
||||
.map(section => ({
|
||||
category: section.category.trim(),
|
||||
tasks: section.tasks
|
||||
category: normalizeSectionCategory(section.category),
|
||||
tasks: section.tasks.map(task => ({ ...task }))
|
||||
}));
|
||||
if (!sections.length) return;
|
||||
const target = createStructuredTextV2(sections);
|
||||
const workItem = planForm.value.workItem.trim();
|
||||
const sameWorkItem = props.model.planItems.find(item => item.itemTitle.trim() === workItem);
|
||||
if (sameWorkItem) {
|
||||
const mergedSections = getStructuredSections(sameWorkItem.targetJson).map(normalizeSectionForMerge);
|
||||
sections.forEach(section => {
|
||||
const sameCategory = mergedSections.find(item => item.category === section.category);
|
||||
if (sameCategory) {
|
||||
sameCategory.tasks.push(...section.tasks);
|
||||
} else {
|
||||
mergedSections.push(section);
|
||||
}
|
||||
});
|
||||
const mergedSections = mergeSectionsByCategory([...getPlanItemSections(sameWorkItem), ...sections]);
|
||||
sameWorkItem.targetText = createStructuredTextV2(mergedSections);
|
||||
sameWorkItem.targetJson = JSON.stringify({ sections: mergedSections });
|
||||
sameWorkItem.targetJson = createSectionsJson(mergedSections);
|
||||
if (planForm.value.supportNeed.trim()) {
|
||||
sameWorkItem.supportNeed = [sameWorkItem.supportNeed, planForm.value.supportNeed.trim()]
|
||||
.filter(Boolean)
|
||||
@@ -673,7 +712,7 @@ function submitInlinePlan() {
|
||||
itemNumber: props.model.planItems.length + 1,
|
||||
itemTitle: workItem,
|
||||
targetText: target,
|
||||
targetJson: JSON.stringify({ sections }),
|
||||
targetJson: createSectionsJson(sections),
|
||||
supportNeed: planForm.value.supportNeed,
|
||||
_isNew: true
|
||||
} as Api.WorkReport.Common.PersonalReportPlanItem & { _isNew: boolean });
|
||||
@@ -698,15 +737,25 @@ function blurEditField(key: string) {
|
||||
function syncRichContent(item: ReviewItem, event: Event) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (!item.source) return;
|
||||
const sections = parseStructuredSectionsFromEditorV2(target, item.workItem || '未分类');
|
||||
const sections = parseStructuredSectionsFromEditorV2(
|
||||
target,
|
||||
item.contentSections || [],
|
||||
item.contentSections?.[0]?.category || DEFAULT_SECTION_CATEGORY
|
||||
);
|
||||
item.source.contentJson = createSectionsJson(sections);
|
||||
item.source.contentText = createStructuredTextV2(sections, true);
|
||||
}
|
||||
|
||||
function syncRichTarget(item: PlanItem, event: Event) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (!item.source) return;
|
||||
const sections = parseStructuredSectionsFromEditorV2(target, item.workItem || '未分类');
|
||||
const sections = parseStructuredSectionsFromEditorV2(
|
||||
target,
|
||||
item.targetSections || [],
|
||||
item.targetSections?.[0]?.category || DEFAULT_SECTION_CATEGORY
|
||||
);
|
||||
item.source.targetJson = createSectionsJson(sections);
|
||||
item.source.targetText = createStructuredTextV2(sections);
|
||||
}
|
||||
|
||||
function syncRichReflection(item: ReviewItem, event: Event) {
|
||||
@@ -732,7 +781,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
<ElInput v-model="mainForm.reporter" disabled />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>部门/方向</label>
|
||||
<label>部门</label>
|
||||
<ElInput v-model="mainForm.deptName" disabled />
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -1051,14 +1100,18 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
>
|
||||
<div class="plan-dialog-form">
|
||||
<div class="field">
|
||||
<label>项目名/事项名</label>
|
||||
<ElInput
|
||||
<label>工作事项</label>
|
||||
<ElSelect
|
||||
v-model="planForm.workItem"
|
||||
class="inline-plan-name-input"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入项目名或我的事项"
|
||||
/>
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="请选择项目名/我的事项"
|
||||
>
|
||||
<ElOption v-for="item in reviewWorkItemOptions" :key="item" :label="item" :value="item" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>具体目标</label>
|
||||
@@ -1070,15 +1123,18 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
:class="{ active: activePlanSectionIndex === sIdx }"
|
||||
>
|
||||
<div class="plan-section-head">
|
||||
<DictSelect
|
||||
v-model="section.category"
|
||||
:dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE"
|
||||
size="small"
|
||||
placeholder="选择类别"
|
||||
style="width: 100%"
|
||||
@focus="activePlanSectionIndex = sIdx"
|
||||
@change="activePlanSectionIndex = sIdx"
|
||||
/>
|
||||
<div class="field plan-section-head-field">
|
||||
<label>类别</label>
|
||||
<DictSelect
|
||||
v-model="section.category"
|
||||
:dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE"
|
||||
size="small"
|
||||
placeholder="选择类别"
|
||||
style="width: 100%"
|
||||
@focus="activePlanSectionIndex = sIdx"
|
||||
@change="activePlanSectionIndex = sIdx"
|
||||
/>
|
||||
</div>
|
||||
<ElButton
|
||||
v-if="planForm.sections.length > 1"
|
||||
link
|
||||
@@ -1093,14 +1149,20 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
<div class="plan-task-head">
|
||||
<span>{{ task.title }}</span>
|
||||
<div class="plan-task-metas">
|
||||
<em>{{ resolvePriorityLabel(task.priority) }}</em>
|
||||
<template v-if="!isMyAffairsPlanItem(planForm.workItem)">
|
||||
<em>{{ resolvePriorityLabel(task.priority) }}</em>
|
||||
</template>
|
||||
<em>进度 {{ task.progress }}%</em>
|
||||
</div>
|
||||
<ElButton link type="danger" size="small" @click="removePlanTask(sIdx, tIdx)">删除</ElButton>
|
||||
</div>
|
||||
<div v-if="task.detail" class="plan-task-detail">{{ task.detail }}</div>
|
||||
</div>
|
||||
<div class="plan-task-form">
|
||||
<div class="inline-task-row">
|
||||
<div
|
||||
class="inline-task-row"
|
||||
:class="{ 'inline-task-row--my-affairs': isMyAffairsPlanItem(planForm.workItem) }"
|
||||
>
|
||||
<div class="field">
|
||||
<label>任务名/事项名</label>
|
||||
<ElInput
|
||||
@@ -1110,7 +1172,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
@focus="activePlanSectionIndex = sIdx"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div v-if="!isMyAffairsPlanItem(planForm.workItem)" class="field">
|
||||
<label>优先级</label>
|
||||
<DictSelect
|
||||
v-model="section.draft.priority"
|
||||
@@ -1135,6 +1197,17 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>详细内容</label>
|
||||
<ElInput
|
||||
v-model="section.draft.detail"
|
||||
size="small"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="详细内容(可选)"
|
||||
@focus="activePlanSectionIndex = sIdx"
|
||||
/>
|
||||
</div>
|
||||
<ElButton
|
||||
class="dialog-inline-action"
|
||||
type="primary"
|
||||
@@ -1150,7 +1223,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
</div>
|
||||
<ElButton
|
||||
v-if="!planForm.sections.length || planForm.sections.every(s => s.category.trim())"
|
||||
class="dialog-inline-action dialog-inline-action--secondary"
|
||||
class="dialog-inline-action"
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
@@ -1885,6 +1958,10 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.inline-task-row--my-affairs {
|
||||
grid-template-columns: minmax(0, 1fr) 142px;
|
||||
}
|
||||
|
||||
.plan-sections {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -1910,7 +1987,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.plan-section-head :deep(.el-select__wrapper) {
|
||||
@@ -1932,6 +2009,14 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.plan-task-detail {
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.plan-task-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -2042,10 +2127,6 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.dialog-inline-action--secondary {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.dialog-inline-action :deep(.el-icon) {
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -2130,12 +2211,20 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 0;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid #d8e0e8;
|
||||
background: #fff;
|
||||
border-bottom-left-radius: 18px;
|
||||
border-bottom-right-radius: 18px;
|
||||
background: #f5f7fa;
|
||||
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
|
||||
Reference in New Issue
Block a user