Files
cn-rdms-web/src/views/personal-center/work-report/monthly/modules/fill-page.vue

2196 lines
59 KiB
Vue
Raw Normal View History

<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 { Plus } from '@element-plus/icons-vue';
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
import { useDict } from '@/hooks/business/dict';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import DictSelect from '@/components/custom/dict-select.vue';
import {
type WorkReportStructuredSection,
type WorkReportStructuredTask,
formatPeriodLabel,
getStructuredSections
} from '../../shared/types';
const props = defineProps<{
reportType: string;
period: string;
mode?: 'add' | 'edit' | 'detail';
scene?: 'fill' | 'detail' | 'approval';
baseInfo?: Api.WorkReport.Monthly.MonthlyReport | null;
model: Api.WorkReport.Monthly.MonthlyReportSaveParams;
}>();
const emit = defineEmits<{
back: [];
save: [];
submit: [];
viewAudit: [];
viewApproval: [];
pullDefaultDraft: [];
}>();
interface ReviewItem {
workItem: string;
days: number;
hours?: number;
content: string;
contentHtml: string;
contentSections?: StructuredSection[];
reflection: string;
removable?: boolean;
source?: Api.WorkReport.Common.PersonalReportReviewItem;
}
interface PlanItem {
workItem: string;
target: string;
targetHtml: string;
targetSections?: StructuredSection[];
supportNeed: string;
removable?: boolean;
source?: Api.WorkReport.Common.PersonalReportPlanItem;
sourceIndex?: number;
}
interface StructuredTask {
title: string;
priority?: string;
progress?: number;
hours?: number;
}
interface PlanTaskDraft {
title: string;
priority?: StructuredTask['priority'];
progress: number;
}
interface StructuredSection {
category: string;
tasks: StructuredTask[];
}
interface PlanSectionDraft extends StructuredSection {
draft: PlanTaskDraft;
}
const planDialogVisible = ref(false);
const activeEditField = ref('');
const EMPTY_TEXT = '本周期内暂无数据';
const isReadonly = computed(() => props.mode === 'detail' || props.scene === 'approval');
const { getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE);
const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
const mainForm = reactive({
get reporter() {
return props.baseInfo?.reporterName || '--';
},
get deptName() {
return props.baseInfo?.reporterDeptName || '--';
},
get postName() {
return props.baseInfo?.reporterPostName || '--';
},
get supervisor() {
return props.baseInfo?.supervisorName || '--';
}
});
const planForm = ref({
workItem: '',
supportNeed: '',
sections: [] as PlanSectionDraft[]
});
const activePlanSectionIndex = ref(-1);
function createPlanTaskDraft(): PlanTaskDraft {
return { title: '', priority: '2', progress: 0 };
}
function normalizeTask(task: WorkReportStructuredTask): StructuredTask {
return {
title: task.title || '',
priority: normalizePriorityCode(task.priority),
progress: typeof task.progress === 'number' ? task.progress : undefined,
hours: typeof task.hours === 'number' ? task.hours : undefined
};
}
function normalizePriorityCode(value?: string | number | null) {
const text = String(value ?? '').trim();
if (!text) return undefined;
const matchedByValue = priorityDictData.value.find(item => String(item.value) === text);
if (matchedByValue) return String(matchedByValue.value);
const matchedByLabel = priorityDictData.value.find(item => item.label === text);
if (matchedByLabel) return String(matchedByLabel.value);
const priorityLabelMatch = text.match(/^P(\d+)$/iu);
if (priorityLabelMatch) return priorityLabelMatch[1];
return text;
}
function resolvePriorityLabel(value?: string | number | null) {
const priorityCode = normalizePriorityCode(value);
if (!priorityCode) return '';
return getPriorityLabel(priorityCode, {
fallback: /^P\d+$/iu.test(priorityCode) ? priorityCode : `P${priorityCode}`
});
}
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 normalizeSectionForMerge(section: WorkReportStructuredSection): StructuredSection {
return {
category: section.category || '工作内容',
tasks: section.tasks.map(normalizeTask)
};
}
function mergeSectionsByCategory(sections: StructuredSection[]) {
const sectionMap = new Map<string, StructuredSection>();
sections.forEach(section => {
const category = section.category || '未分类';
const existing = sectionMap.get(category);
if (existing) {
existing.tasks.push(...section.tasks);
} else {
sectionMap.set(category, { category, tasks: [...section.tasks] });
}
});
return Array.from(sectionMap.values());
}
function stripStructuredTaskPrefixV2(value: string) {
return value.trim().replace(/^\d+[..、]\s*/u, '');
}
function stripStructuredTaskSuffixV2(value: string) {
return value.trim().replace(/[。.!?]+$/u, '');
}
function resolveTaskMetricsV2(metricsText: string) {
const parts = metricsText
.split('/')
.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];
const hoursText = metricsText.match(/(\d+(?:\.\d+)?)h/u)?.[1];
return {
priority,
progress: progressText === undefined ? undefined : Number(progressText),
hours: hoursText === undefined ? undefined : Number(hoursText)
};
}
function formatStructuredTaskLineV2(task: StructuredTask, showHours = false) {
const title = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(task.title));
const metrics = [
task.priority ? resolvePriorityLabel(task.priority) : '',
typeof task.progress === 'number' ? `${task.progress}%` : '',
showHours && typeof task.hours === 'number' ? `${task.hours}h` : ''
].filter(Boolean);
return `${title}${metrics.length ? `${metrics.join(' / ')}` : ''}`;
}
function createStructuredTextV2(sections: StructuredSection[], showHours = false) {
return sections
.map(section => {
const categoryLabel = resolveTaskItemTypeLabel(section.category).trim();
return [
`#${categoryLabel}`,
...section.tasks.map((task, index) => `${index + 1}${formatStructuredTaskLineV2(task, showHours)}`)
]
.filter(Boolean)
.join('\n');
})
.join('\n');
}
function createStructuredHtmlV2(sections: StructuredSection[], showHours = false) {
return sections
.map(section => {
const categoryLabel = resolveTaskItemTypeLabel(section.category.trim());
const tasks = section.tasks
.map(
(task, index) =>
`<div class="rich-task-line">${escapeHtml(`${index + 1}${formatStructuredTaskLineV2(task, showHours)}`)}</div>`
)
.join('');
return `
<div class="rich-section">
<div class="rich-category-line"># ${escapeHtml(categoryLabel)}</div>
${tasks ? `<div class="rich-section-tasks">${tasks}</div>` : ''}
</div>
`;
})
.join('');
}
function createStructuredSectionsFromTextV2(text: string, defaultCategory: string): StructuredSection[] {
const lines = normalizeEditorText(text).split('\n').filter(Boolean);
if (!lines.length || lines.join('\n') === EMPTY_TEXT) return [];
const sections: StructuredSection[] = [];
const ensureSection = (categoryText: string) => {
const category = categoryText.trim() || defaultCategory;
let section = sections.find(item => item.category === category);
if (!section) {
section = { category, tasks: [] };
sections.push(section);
}
return section;
};
let currentCategory = '';
lines.forEach(line => {
const trimmedLine = line.trim();
if (!trimmedLine) return;
if (trimmedLine.startsWith('#')) {
currentCategory = trimmedLine.replace(/^#\s*/u, '').trim();
if (currentCategory) ensureSection(currentCategory);
return;
}
const legacyMatch = trimmedLine.match(/^(.+?)\s*-\s*(.+?)(?:[(]([^()]*)[)])?[。.!?]*$/u);
if (legacyMatch) {
const [, rawCategory, rawTitle, metricsText = ''] = legacyMatch;
const category = rawCategory.trim();
const title = rawTitle.trim();
if (!category || !title) return;
ensureSection(category).tasks.push({
title,
...resolveTaskMetricsV2(metricsText)
});
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;
ensureSection(currentCategory || defaultCategory).tasks.push({
title,
...resolveTaskMetricsV2(metricsText)
});
});
return sections.filter(section => section.tasks.length);
}
function parseStructuredSectionsFromEditorV2(editor: HTMLElement, defaultCategory = '工作内容'): StructuredSection[] {
return createStructuredSectionsFromTextV2(editor.innerText, defaultCategory);
}
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}`;
};
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 escapeHtml(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function createTaskMetrics(task: StructuredTask, showHours: boolean) {
const metrics = [
task.priority ? escapeHtml(resolvePriorityLabel(task.priority)) : '',
typeof task.progress === 'number' ? `进度${task.progress}%` : '',
showHours && typeof task.hours === 'number' ? `${task.hours}h` : ''
].filter(Boolean);
return metrics.length ? `<span class="rich-inline-metrics">${metrics.join(' • ')}</span>` : '';
}
function createStructuredHtml(sections: StructuredSection[], showHours = false) {
const stripOrderPrefix = (text: string) => text.trim().replace(/^\d+[..、]\s*/, '');
const splitPunctuation = (text: string) => {
const rawTitle = stripOrderPrefix(text);
const punctuation = rawTitle.match(/[。.!?]$/)?.[0] || '';
return {
title: punctuation ? rawTitle.slice(0, -1) : rawTitle,
punctuation
};
};
return sections
.map(section => {
const tasks = section.tasks
.map((task, taskIndex) => {
const { title, punctuation } = splitPunctuation(task.title);
return `<div class="rich-section-task"><span class="rich-section-task-title">${taskIndex + 1}.${escapeHtml(title)}</span>${createTaskMetrics(task, showHours)}${escapeHtml(punctuation)}</div>`;
})
.join('');
return `
<div class="rich-section">
<div class="rich-section-title">${escapeHtml(resolveTaskItemTypeLabel(section.category.trim()))}</div>
${tasks ? `<div class="rich-section-tasks">${tasks}</div>` : ''}
</div>
`;
})
.join('');
}
function normalizeEditorText(value: string) {
return value
.replace(/\u00A0/g, ' ')
.split('\n')
.map(line => line.trim())
.filter(Boolean)
.join('\n');
}
function createStructuredSectionsFromText(
text: string,
category: string,
defaultTask?: Partial<StructuredTask>
): StructuredSection[] {
const lines = normalizeEditorText(text).split('\n').filter(Boolean);
if (!lines.length || lines.join('\n') === EMPTY_TEXT) return [];
const groupedSections: StructuredSection[] = [];
lines.forEach(line => {
const structuredMatch = line.match(/^(.+?)\s*[-]\s*(.+?)[(]([^)]*)[)]$/u);
if (!structuredMatch) return;
const [, rawCategory, rawTitle, metricsText] = structuredMatch;
const categoryText = rawCategory.trim();
const title = rawTitle.trim();
if (!categoryText || !title) return;
let section = groupedSections.find(item => item.category === categoryText);
if (!section) {
section = { category: categoryText, tasks: [] };
groupedSections.push(section);
}
section.tasks.push({
...defaultTask,
title,
...resolveTaskMetrics(metricsText, defaultTask)
});
});
if (groupedSections.length) return groupedSections;
const looksLikeTaskLine = (value: string) => /^\d+[.]\s*/u.test(value);
const hasNumberedTask = lines.some(looksLikeTaskLine);
const sectionCategory = hasNumberedTask && !looksLikeTaskLine(lines[0]) ? lines[0] : category;
const taskSourceText = (sectionCategory === lines[0] ? lines.slice(1) : lines).join(' ');
const taskParts = hasNumberedTask
? taskSourceText
.split(/(?=\d+[..、]\s*)/u)
.map(item => item.trim())
.filter(Boolean)
: lines;
const tasks = taskParts
.map((item, index) => {
const normalizedTitle = stripTaskOrderPrefix(item);
const metricsText = normalizedTitle.match(/[(]([^)]*)[)]/u)?.[1] || '';
const title = normalizedTitle.replace(/[(][^)]*[)]/u, '').trim();
if (!title) return null;
return {
...(index === 0 ? defaultTask : undefined),
title,
...resolveTaskMetrics(metricsText, index === 0 ? defaultTask : undefined)
};
})
.filter(Boolean) as StructuredTask[];
if (!tasks.length) return [];
return [
{
category: sectionCategory,
tasks
}
];
}
function getElementText(element: Element | null) {
return normalizeEditorText((element as HTMLElement | null)?.innerText || '');
}
function stripTaskOrderPrefix(value: string) {
return value.trim().replace(/^\d+[..、]\s*/, '');
}
function resolveTaskMetrics(metricsText: string, fallback?: Partial<StructuredTask>, useFallback = true) {
const metricParts = metricsText
.split(/[\/、•]/u)
.map(item => item.trim())
.filter(Boolean);
const priorityText = metricsText.match(/\bP\d+\b/iu)?.[0] || metricParts.find(item => /^P?\d+$/iu.test(item));
const priority = normalizePriorityCode(priorityText || (useFallback ? fallback?.priority : undefined));
const progressText = metricsText.match(/进度\s*(\d+(?:\.\d+)?)%/u)?.[1];
const hoursText = metricsText.match(/(\d+(?:\.\d+)?)h/u)?.[1];
return {
priority,
progress: progressText === undefined ? (useFallback ? fallback?.progress : undefined) : Number(progressText),
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 {
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.contentJson).map(normalizeSection));
const contentSections = structuredSections.length
? structuredSections
: createStructuredSectionsFromTextV2(item.contentText || '', item.itemTitle || '未分类');
return {
workItem: item.itemTitle || '未命名工作',
days: 0,
hours: item.workHours || 0,
content: item.contentText || '',
contentHtml: contentSections.length
? createStructuredHtmlV2(contentSections, true)
: escapeHtml(item.contentText || '').replace(/\n/g, '<br>') || EMPTY_TEXT,
contentSections,
reflection: item.reflectionText || '',
removable: false,
source: item
};
}
function toPlanItem(item: Api.WorkReport.Common.PersonalReportPlanItem, index: number): PlanItem {
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.targetJson).map(normalizeSection));
const targetSections = structuredSections.length
? structuredSections
: createStructuredSectionsFromTextV2(item.targetText || '', item.itemTitle || '未分类');
return {
workItem: item.itemTitle || '未命名计划',
target: item.targetText || '',
targetHtml: targetSections.length
? createStructuredHtmlV2(targetSections)
: escapeHtml(item.targetText || '').replace(/\n/g, '<br>') || EMPTY_TEXT,
targetSections,
supportNeed: item.supportNeed || '',
/* 只有用户新增的计划才可删除,默认带入的不可删除 */
removable: true,
source: item,
sourceIndex: index
};
}
const reviewItems = computed<ReviewItem[]>(() => {
return (props.model.reviewItems || []).map(toReviewItem);
});
const nextPlans = computed<PlanItem[]>(() => {
return (props.model.planItems || []).map(toPlanItem);
});
const totalHours = computed(() => {
const baseTotalWorkHours = Number(props.baseInfo?.totalWorkHours ?? 0);
if (Number.isFinite(baseTotalWorkHours) && baseTotalWorkHours > 0) return baseTotalWorkHours;
return (props.model.reviewItems || []).reduce((sum, item) => sum + Number(item.workHours || 0), 0);
});
const periodText = computed(() => formatPeriodLabel(props.model.periodLabel || props.period));
function resetPlanForm() {
planForm.value = {
workItem: '',
supportNeed: '',
sections: [{ category: '', tasks: [], draft: createPlanTaskDraft() }]
};
activePlanSectionIndex.value = 0;
}
function showInlinePlanForm() {
planDialogVisible.value = true;
resetPlanForm();
}
function cancelInlinePlan() {
planDialogVisible.value = false;
resetPlanForm();
}
function addPlanSection() {
planForm.value.sections.push({ category: '', tasks: [], draft: createPlanTaskDraft() });
activePlanSectionIndex.value = planForm.value.sections.length - 1;
}
function removePlanSection(index: number) {
planForm.value.sections.splice(index, 1);
if (activePlanSectionIndex.value === index) {
activePlanSectionIndex.value = -1;
} else if (activePlanSectionIndex.value > index) {
activePlanSectionIndex.value--;
}
}
function addPlanTask(sectionIndex: number) {
const section = planForm.value.sections[sectionIndex];
if (!section?.draft.title.trim()) return;
section.tasks.push({
title: section.draft.title.trim(),
priority: section.draft.priority,
progress: section.draft.progress
});
section.draft = createPlanTaskDraft();
}
function removePlanTask(sectionIndex: number, taskIndex: number) {
planForm.value.sections[sectionIndex].tasks.splice(taskIndex, 1);
}
function submitInlinePlan() {
const sections = planForm.value.sections
.filter(section => section.category.trim() && section.tasks.length)
.map(section => ({
category: section.category.trim(),
tasks: section.tasks
}));
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);
}
});
sameWorkItem.targetText = createStructuredTextV2(mergedSections);
sameWorkItem.targetJson = JSON.stringify({ sections: mergedSections });
if (planForm.value.supportNeed.trim()) {
sameWorkItem.supportNeed = [sameWorkItem.supportNeed, planForm.value.supportNeed.trim()]
.filter(Boolean)
.join('\n');
}
} else {
props.model.planItems.push({
itemNumber: props.model.planItems.length + 1,
itemTitle: workItem,
targetText: target,
targetJson: JSON.stringify({ sections }),
supportNeed: planForm.value.supportNeed,
_isNew: true
} as Api.WorkReport.Common.PersonalReportPlanItem & { _isNew: boolean });
}
resetPlanForm();
planDialogVisible.value = false;
}
function removePlanItem(index: number) {
const item = nextPlans.value[index];
if (item?.sourceIndex !== undefined) props.model.planItems.splice(item.sourceIndex, 1);
}
function focusEditField(key: string) {
activeEditField.value = key;
}
function blurEditField(key: string) {
if (activeEditField.value === key) activeEditField.value = '';
}
function syncRichContent(item: ReviewItem, event: Event) {
const target = event.currentTarget as HTMLElement;
if (!item.source) return;
const sections = parseStructuredSectionsFromEditorV2(target, item.workItem || '未分类');
item.source.contentJson = createSectionsJson(sections);
}
function syncRichTarget(item: PlanItem, event: Event) {
const target = event.currentTarget as HTMLElement;
if (!item.source) return;
const sections = parseStructuredSectionsFromEditorV2(target, item.workItem || '未分类');
item.source.targetJson = createSectionsJson(sections);
}
function syncRichReflection(item: ReviewItem, event: Event) {
const target = event.currentTarget as HTMLElement;
if (item.source) item.source.reflectionText = target.innerText.trim();
}
function syncRichSupport(item: PlanItem, event: Event) {
const target = event.currentTarget as HTMLElement;
if (item.source) item.source.supportNeed = target.innerText.trim();
}
</script>
<template>
<div class="card form-page">
<div class="section">
<div class="section-title">
<span>基础信息</span>
</div>
<div class="compose-grid">
<div class="field">
<label>姓名</label>
<ElInput v-model="mainForm.reporter" disabled />
</div>
<div class="field">
<label>部门/方向</label>
<ElInput v-model="mainForm.deptName" disabled />
</div>
<div class="field">
<label>岗位</label>
<ElInput v-model="mainForm.postName" disabled />
</div>
<div class="field">
<label>周期</label>
<ElInput :model-value="periodText" disabled />
</div>
<div class="field">
<label>直接上级</label>
<ElInput v-model="mainForm.supervisor" disabled />
</div>
</div>
</div>
<div class="section">
<div class="section-title">
<div class="section-title-left">
<span>当期重点工作回顾</span>
<span class="source-chip">{{ reviewItems.length }} 项工作</span>
<span class="source-chip"> {{ totalHours }}h</span>
</div>
</div>
<div class="review-grid layout-row">
<div v-if="!reviewItems.length">{{ EMPTY_TEXT }}</div>
<div v-for="(item, index) in reviewItems" :key="index" class="review-card">
<div class="review-index-cell">
<span class="row-index">{{ index + 1 }}</span>
</div>
<div class="review-name-cell">
<div class="work-title-line">
<strong>{{ item.workItem }}</strong>
<span> {{ item.hours }}h</span>
</div>
</div>
<div class="review-action-cell">
<ElPopconfirm
v-if="item.removable !== false && !isReadonly"
title="确认删除这条回顾吗?"
confirm-button-text="删除"
cancel-button-text="取消"
width="220"
@confirm="reviewItems.splice(index, 1)"
>
<template #reference>
<button class="item-remove-btn" aria-label="删除回顾">×</button>
</template>
</ElPopconfirm>
</div>
<div class="review-editor-grid">
<div class="field">
<label>具体工作内容及成果描述</label>
<div
class="rich-editor"
:contenteditable="!isReadonly"
spellcheck="false"
:data-placeholder="isReadonly ? undefined : '请输入工作内容及成果描述'"
@focus="focusEditField(`content-${index}`)"
@blur="
syncRichContent(item, $event);
blurEditField(`content-${index}`);
"
v-html="item.contentHtml"
></div>
</div>
<div class="field">
<label>工作感悟</label>
<div
class="rich-editor"
:contenteditable="!isReadonly"
spellcheck="false"
:data-placeholder="isReadonly ? undefined : '请输入工作感悟'"
@focus="focusEditField(`reflection-${index}`)"
@blur="
syncRichReflection(item, $event);
blurEditField(`reflection-${index}`);
"
>
{{ item.reflection }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">
<div class="section-title-left">
<span>下周期重点工作计划</span>
<span class="source-chip">{{ nextPlans.length }} 项计划</span>
</div>
<div class="section-title-right">
<ElButton v-if="!isReadonly" size="small" :disabled="planDialogVisible" @click="showInlinePlanForm">
新增计划
</ElButton>
</div>
</div>
<div class="plan-list layout-row">
<div v-if="!nextPlans.length">{{ EMPTY_TEXT }}</div>
<div v-for="(item, index) in nextPlans" :key="index" class="plan-card">
<div class="plan-index-cell">
<span class="row-index">{{ index + 1 }}</span>
</div>
<div class="plan-name-cell">
<strong>{{ item.workItem }}</strong>
</div>
<div class="plan-action-cell">
<ElPopconfirm
v-if="item.removable !== false && !isReadonly"
title="确认删除这条计划吗?"
confirm-button-text="删除"
cancel-button-text="取消"
width="220"
@confirm="removePlanItem(index)"
>
<template #reference>
<button class="item-remove-btn" aria-label="删除计划">×</button>
</template>
</ElPopconfirm>
</div>
<div class="plan-editor-grid">
<div class="field">
<label>具体目标</label>
<div
class="rich-editor"
:contenteditable="!isReadonly"
spellcheck="false"
:data-placeholder="isReadonly ? undefined : '请输入具体目标'"
@focus="focusEditField(`target-${index}`)"
@blur="
syncRichTarget(item, $event);
blurEditField(`target-${index}`);
"
v-html="item.targetHtml"
></div>
</div>
<div class="field">
<label>对他人协助的需求</label>
<div
class="rich-editor"
:contenteditable="!isReadonly"
spellcheck="false"
:data-placeholder="isReadonly ? undefined : '请输入协助需求,没有可留空'"
@focus="focusEditField(`support-${index}`)"
@blur="
syncRichSupport(item, $event);
blurEditField(`support-${index}`);
"
>
{{ item.supportNeed }}
</div>
</div>
</div>
</div>
<div v-if="false" class="plan-card inline-plan-card">
<div class="plan-index-cell">
<span class="row-index">{{ nextPlans.length + 1 }}</span>
</div>
<div class="plan-name-cell">
<ElInput
v-model="planForm.workItem"
class="inline-plan-name-input"
type="textarea"
:rows="2"
placeholder="请输入项目名或我的事项"
/>
</div>
<div class="plan-action-cell">
<button class="item-remove-btn" aria-label="取消新增计划" @click="cancelInlinePlan">×</button>
</div>
<div class="plan-editor-grid">
<div class="field">
<label>具体目标</label>
<div class="plan-sections">
<div
v-for="(section, sIdx) in planForm.sections"
:key="sIdx"
class="plan-section"
: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"
/>
<ElButton
v-if="planForm.sections.length > 1"
link
type="danger"
size="small"
@click="removePlanSection(sIdx)"
>
删除类别
</ElButton>
</div>
<div v-for="(task, tIdx) in section.tasks" :key="tIdx" class="plan-task">
<div class="plan-task-head">
<span>{{ task.title }}</span>
<div class="plan-task-metas">
<em>{{ resolvePriorityLabel(task.priority) }}</em>
<em>进度 {{ task.progress }}%</em>
</div>
<ElButton link type="danger" size="small" @click="removePlanTask(sIdx, tIdx)">删除</ElButton>
</div>
</div>
<div class="plan-task-form">
<div class="inline-task-row">
<ElInput
v-model="section.draft.title"
size="small"
placeholder="名称"
@focus="activePlanSectionIndex = sIdx"
/>
<div class="field">
<label>优先级</label>
<DictSelect
v-model="section.draft.priority"
:dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
:clearable="false"
style="width: 100%"
@focus="activePlanSectionIndex = sIdx"
/>
</div>
<div class="field">
<label>进度</label>
<ElInputNumber
v-model="section.draft.progress"
:min="0"
:max="100"
:step="5"
:precision="1"
controls-position="right"
size="small"
style="width: 100%"
@focus="activePlanSectionIndex = sIdx"
/>
</div>
</div>
<ElButton
type="primary"
plain
size="small"
:disabled="!section.draft.title.trim()"
@click="addPlanTask(sIdx)"
>
添加
</ElButton>
</div>
</div>
<ElButton
v-if="!planForm.sections.length || planForm.sections.every(s => s.category.trim())"
type="primary"
plain
size="small"
@click="addPlanSection"
>
+ 新增类别
</ElButton>
</div>
</div>
<div class="field">
<label>对他人协助的需求</label>
<ElInput
v-model="planForm.supportNeed"
class="styled-textarea"
type="textarea"
:autosize="{ minRows: 3, maxRows: 5 }"
placeholder="请输入协助需求,没有可留空"
/>
</div>
</div>
<div class="inline-plan-actions">
<ElButton size="small" @click="cancelInlinePlan">取消</ElButton>
<ElButton
size="small"
type="primary"
class="btn-submit"
:disabled="
!planForm.workItem.trim() ||
!planForm.sections.some(section => section.category.trim() && section.tasks.length)
"
@click="submitInlinePlan"
>
确认新增
</ElButton>
</div>
</div>
</div>
</div>
<div v-if="!isReadonly" class="form-actions">
<!-- <ElButton>重置表单</ElButton>-->
<ElButton @click="emit('save')">保存草稿</ElButton>
<ElButton type="primary" class="btn-submit" @click="emit('submit')">提交审批</ElButton>
</div>
<BusinessFormDialog
v-model="planDialogVisible"
title="新增下周期重点工作计划"
preset="md"
confirm-text="确认新增"
:confirm-disabled="
!planForm.workItem.trim() || !planForm.sections.some(section => section.category.trim() && section.tasks.length)
"
@confirm="submitInlinePlan"
@cancel="cancelInlinePlan"
>
<div class="plan-dialog-form">
<div class="field">
<label>项目名/事项名</label>
<ElInput
v-model="planForm.workItem"
class="inline-plan-name-input"
type="textarea"
:rows="2"
placeholder="请输入项目名或我的事项"
/>
</div>
<div class="field">
<label>具体目标</label>
<div class="plan-sections">
<div
v-for="(section, sIdx) in planForm.sections"
:key="sIdx"
class="plan-section"
: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"
/>
<ElButton
v-if="planForm.sections.length > 1"
link
type="danger"
size="small"
@click="removePlanSection(sIdx)"
>
删除类别
</ElButton>
</div>
<div v-for="(task, tIdx) in section.tasks" :key="tIdx" class="plan-task">
<div class="plan-task-head">
<span>{{ task.title }}</span>
<div class="plan-task-metas">
<em>{{ resolvePriorityLabel(task.priority) }}</em>
<em>进度 {{ task.progress }}%</em>
</div>
<ElButton link type="danger" size="small" @click="removePlanTask(sIdx, tIdx)">删除</ElButton>
</div>
</div>
<div class="plan-task-form">
<div class="inline-task-row">
<div class="field">
<label>任务名/事项名</label>
<ElInput
v-model="section.draft.title"
size="small"
placeholder="请输入任务名或事项名"
@focus="activePlanSectionIndex = sIdx"
/>
</div>
<div class="field">
<label>优先级</label>
<DictSelect
v-model="section.draft.priority"
:dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
:clearable="false"
style="width: 100%"
@focus="activePlanSectionIndex = sIdx"
/>
</div>
<div class="field">
<label>进度</label>
<ElInputNumber
v-model="section.draft.progress"
:min="0"
:max="100"
:step="5"
:precision="1"
controls-position="right"
size="small"
style="width: 100%"
@focus="activePlanSectionIndex = sIdx"
/>
</div>
</div>
<ElButton
class="dialog-inline-action"
type="primary"
plain
size="small"
:disabled="!section.draft.title.trim()"
@click="addPlanTask(sIdx)"
>
<ElIcon><Plus /></ElIcon>
<span>添加</span>
</ElButton>
</div>
</div>
<ElButton
v-if="!planForm.sections.length || planForm.sections.every(s => s.category.trim())"
class="dialog-inline-action dialog-inline-action--secondary"
type="primary"
plain
size="small"
@click="addPlanSection"
>
<ElIcon><Plus /></ElIcon>
<span>类别</span>
</ElButton>
</div>
</div>
<div class="field">
<label>对他人协助的需求</label>
<ElInput
v-model="planForm.supportNeed"
class="styled-textarea"
type="textarea"
:autosize="{ minRows: 3, maxRows: 5 }"
placeholder="请输入协助需求,没有可留空"
/>
</div>
</div>
</BusinessFormDialog>
</div>
</template>
<style scoped>
.card {
border: 1px solid rgba(216, 224, 232, 0.88);
border-radius: 18px;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.12);
padding-top: 20px;
}
.card-title {
margin: 0;
font-size: 18px;
font-weight: 900;
}
.card-subtitle {
margin-top: 5px;
color: #667085;
font-size: 12px;
}
.form-head {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 18px;
padding: 20px;
}
.form-head-actions {
display: flex;
align-items: flex-start;
justify-content: flex-end;
}
.tab-action-btn {
padding: 10px 20px;
border-radius: 13px;
background: #fff;
color: #164e63;
font-size: 14px;
font-weight: 800;
cursor: pointer;
border: 0;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
transition:
transform 0.16s ease,
box-shadow 0.16s ease,
background 0.16s ease;
}
.tab-action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
}
.report-title {
display: flex;
gap: 12px;
align-items: center;
}
.type-mark {
width: 48px;
height: 48px;
display: grid;
place-items: center;
flex-shrink: 0;
border-radius: 16px;
background: #ccfbf1;
color: #0f766e;
font-weight: 900;
}
.section {
margin: 0 20px 18px;
border: 1px solid #d8e0e8;
border-radius: 16px;
overflow: hidden;
background: #fff;
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 13px 16px;
background: #f8fbfc;
border-bottom: 1px solid #d8e0e8;
font-weight: 900;
}
.section-title-left {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.section-title-right {
display: flex;
align-items: center;
gap: 10px;
}
.source-chip {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 10px;
border-radius: 999px;
background: #f3f7f9;
color: #475467;
font-size: 12px;
font-weight: 800;
}
.compose-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 14px;
padding: 16px;
align-items: start;
}
@media (max-width: 1180px) {
.compose-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.compose-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.field {
display: grid;
gap: 6px;
}
.field label {
color: #667085;
font-size: 12px;
font-weight: 800;
}
.radio-group-full {
width: 100%;
display: flex;
gap: 12px;
}
.radio-group-full :deep(.el-radio) {
flex: 1;
min-width: 0;
justify-content: center;
margin-right: 0;
}
.radio-group-full :deep(.el-radio.is-checked) {
border-color: #0f766e;
background: #f0fdfa;
}
.radio-group-full :deep(.el-radio__input.is-checked + .el-radio__label) {
color: #0f766e;
}
.review-grid,
.plan-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
padding: 16px;
}
.review-grid.layout-row,
.plan-list.layout-row {
grid-template-columns: 1fr;
}
.review-card,
.plan-card {
display: grid;
gap: 12px;
padding: 14px;
border: 1px solid #e5edf1;
border-radius: 12px;
background: #fbfdfe;
}
.review-card {
grid-template-columns: 88px minmax(225px, 330px) minmax(0, 1.35fr) minmax(0, 1fr) 44px;
gap: 0;
padding: 0;
overflow: hidden;
}
.plan-card {
grid-template-columns: 88px minmax(225px, 330px) minmax(0, 1.35fr) minmax(0, 1fr) 44px;
gap: 0;
padding: 0;
overflow: hidden;
}
.review-index-cell,
.review-name-cell,
.review-editor-grid,
.review-action-cell,
.plan-index-cell,
.plan-name-cell,
.plan-editor-grid,
.plan-action-cell {
padding: 14px;
}
.review-index-cell,
.plan-index-cell {
display: grid;
place-items: center;
}
.review-name-cell,
.plan-name-cell {
display: grid;
align-items: center;
min-width: 0;
overflow: hidden;
border-left: 1px solid #e5edf1;
}
.review-name-cell .work-title-line,
.plan-name-cell {
justify-content: center;
text-align: center;
}
.review-name-cell .work-title-line {
min-width: 0;
flex-direction: column;
gap: 4px;
}
.review-name-cell strong,
.plan-name-cell strong {
display: block;
width: 100%;
min-width: 0;
flex: 0 1 auto;
color: #606266;
font-size: 14px;
font-weight: 400;
line-height: 1.3;
overflow: hidden;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.review-action-cell,
.plan-action-cell {
grid-column: 5;
grid-row: 1;
display: grid;
justify-items: center;
align-items: start;
border-left: 1px solid #e5edf1;
padding: 6px;
}
.review-card-head,
.plan-card-head {
display: grid;
grid-template-columns: 34px minmax(0, 1fr) 28px;
gap: 10px;
align-items: start;
}
.row-index {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 999px;
background: #0f766e;
color: #fff;
font-size: 13px;
font-weight: 900;
}
.review-title-fields,
.review-title-readonly,
.plan-title-readonly {
display: grid;
gap: 10px;
align-items: center;
}
.review-title-readonly {
grid-template-columns: 1fr;
}
.work-title-line {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.review-title-readonly strong,
.plan-title-readonly strong {
color: #606266;
font-size: 14px;
font-weight: 400;
line-height: 1.3;
}
.work-title-line span,
.inline-metrics {
display: flex;
align-items: center;
gap: 6px;
color: #667085;
font-size: 12px;
font-weight: 800;
}
.review-name-cell .work-title-line span {
justify-content: center;
width: 100%;
}
.inline-metrics :deep(.el-input-number) {
width: 92px;
}
.review-editor-grid,
.plan-editor-grid {
display: grid;
grid-template-columns: minmax(0, calc(57.45% - 6px + 50px)) minmax(0, calc(42.55% - 6px - 50px));
gap: 12px;
}
.review-editor-grid {
grid-column: 3 / span 2;
grid-row: 1;
border-left: 1px solid #e5edf1;
}
.plan-editor-grid {
grid-column: 3 / span 2;
grid-row: 1;
border-left: 1px solid #e5edf1;
}
.item-remove-btn {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 1px solid #fecaca;
border-radius: 999px;
background: #fff;
color: #dc2626;
font: inherit;
font-size: 18px;
line-height: 1;
cursor: pointer;
}
.item-remove-btn:hover {
background: #fef2f2;
}
.structured-input,
.fixed-textarea :deep(.el-textarea__inner),
.styled-textarea :deep(.el-textarea__inner) {
scrollbar-width: thin;
scrollbar-color: #94a3b8 transparent;
}
.structured-input::-webkit-scrollbar,
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar),
.styled-textarea :deep(.el-textarea__inner::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
.structured-input::-webkit-scrollbar-track,
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar-track),
.styled-textarea :deep(.el-textarea__inner::-webkit-scrollbar-track) {
background: transparent;
}
.structured-input::-webkit-scrollbar-thumb,
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar-thumb),
.styled-textarea :deep(.el-textarea__inner::-webkit-scrollbar-thumb) {
border-radius: 999px;
background: #94a3b8;
}
.structured-input::-webkit-scrollbar-button,
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar-button),
.styled-textarea :deep(.el-textarea__inner::-webkit-scrollbar-button) {
width: 0;
height: 0;
display: none;
}
.structured-input {
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
padding: 5px 11px;
cursor: pointer;
transition: border-color 0.2s;
height: 86px;
overflow: auto;
box-sizing: border-box;
}
.structured-input:hover {
border-color: #c0c4cc;
}
.structured-input.disabled {
background: #f5f7fa;
color: #a8abb2;
cursor: not-allowed;
}
.fixed-textarea :deep(.el-textarea__inner) {
height: 86px;
min-height: 86px;
max-height: 86px;
resize: none;
overflow: auto;
padding: 5px 11px;
line-height: 1.6;
white-space: pre-wrap;
}
.styled-textarea :deep(.el-textarea__inner) {
overflow: auto;
}
.auto-textarea,
.fixed-textarea {
width: 100%;
}
.auto-textarea :deep(.el-textarea__inner) {
resize: none;
overflow: hidden;
padding: 5px 11px;
line-height: 1.6;
white-space: pre-wrap;
}
.rich-editor {
width: 100%;
min-height: 86px;
padding: 5px 11px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
box-sizing: border-box;
color: #334155;
font-size: 13px;
line-height: 1.6;
outline: none;
overflow: auto;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.rich-editor:hover {
border-color: #c0c4cc;
}
.rich-editor:focus {
border-color: #0f766e;
box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.1);
}
.rich-editor:empty::before {
content: attr(data-placeholder);
color: #a8abb2;
}
.rich-editor :deep(.rich-section) {
display: grid;
gap: 4px;
min-width: 0;
}
.rich-editor :deep(.rich-section + .rich-section) {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #cbd5e1;
}
.rich-editor :deep(.rich-section-title) {
position: relative;
min-width: 0;
padding-left: 14px;
color: #0f766e;
font-size: 13px;
font-weight: 800;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-section-title::before) {
content: '';
position: absolute;
left: 0;
top: 4px;
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
}
.rich-editor :deep(.rich-section-tasks) {
display: grid;
gap: 6px;
padding-left: 14px;
color: #334155;
font-size: 13px;
line-height: 1.6;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-section-task) {
display: block;
white-space: normal;
}
.rich-editor :deep(.rich-section-task-title) {
color: #334155;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-inline-metrics) {
display: inline-flex;
align-items: center;
height: 18px;
margin-left: 3px;
padding: 0 6px;
border-radius: 999px;
background: #f3f7f9;
color: #475467;
font-size: 11px;
font-weight: 800;
vertical-align: 1px;
user-select: text;
cursor: text;
}
.rich-editor :deep(.rich-category-line) {
color: #0f766e;
font-size: 13px;
font-weight: 700;
line-height: 1.6;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-section-tasks) {
padding-left: 0;
}
.rich-editor :deep(.rich-task-line) {
color: #334155;
font-size: 13px;
line-height: 1.6;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.review-editor-grid .field,
.plan-editor-grid .field {
display: flex;
flex-direction: column;
min-height: 0;
}
.review-editor-grid .rich-editor,
.plan-editor-grid .rich-editor {
flex: 1;
height: 100%;
}
.review-editor-grid .fixed-textarea,
.plan-editor-grid .fixed-textarea {
display: flex;
flex: 1;
}
.review-editor-grid .fixed-textarea :deep(.el-textarea__inner),
.plan-editor-grid .fixed-textarea :deep(.el-textarea__inner) {
height: 100%;
max-height: none;
}
.structured-input-inner {
color: #334155;
line-height: 1.6;
font-size: 13px;
}
.structured-input-inner.plain {
white-space: pre-wrap;
}
.structured-section {
display: grid;
gap: 2px;
}
.structured-section + .structured-section {
margin-top: 0;
padding-top: 4px;
border-top: 1px dashed #cbd5e1;
}
.structured-section-title {
position: relative;
padding-left: 14px;
color: #0f766e;
font-size: 13px;
font-weight: 800;
}
.structured-section-title::before {
content: '';
position: absolute;
left: 0;
top: 4px;
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
}
.structured-task {
display: grid;
gap: 2px;
}
.structured-title + .structured-title {
padding-top: 10px;
border-top: 1px dashed #cbd5e1;
}
.structured-task-category {
position: relative;
padding-left: 14px;
color: #0f766e;
font-size: 13px;
font-weight: 800;
}
.structured-task-category::before {
content: '';
position: absolute;
left: 0;
top: 4px;
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
}
.structured-task-title {
color: #334155;
padding-left: 14px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
}
.plan-dialog-form {
display: grid;
gap: 16px;
}
.plan-dialog-form > .field {
gap: 8px;
}
.inline-task-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 132px 142px;
align-items: end;
gap: 12px;
}
.plan-sections {
display: grid;
gap: 10px;
}
.plan-section {
display: grid;
gap: 10px;
padding: 12px;
border: 1px solid #e5edf1;
border-radius: 12px;
background: #fafcfd;
box-shadow: none;
}
.plan-section.active {
border-color: #cfe3e0;
background: #f7fbfa;
box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.06);
}
.plan-section-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
}
.plan-section-head :deep(.el-select__wrapper) {
height: 36px;
min-height: 36px;
border-radius: 9px;
}
.plan-task {
display: grid;
gap: 3px;
padding: 8px 10px;
border: 1px solid #eef2f6;
border-radius: 9px;
background: #f8fbfc;
}
.plan-task + .plan-task {
margin-top: 0;
}
.plan-task-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.plan-task-head span {
flex: 1;
min-width: 0;
color: #14213d;
font-size: 13px;
font-weight: 700;
}
.plan-task-metas {
display: inline-flex;
gap: 5px;
flex: 0 0 auto;
}
.plan-task-metas em {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 6px;
border-radius: 999px;
background: #f3f7f9;
color: #475467;
font-size: 11px;
font-style: normal;
font-weight: 800;
white-space: nowrap;
}
.plan-task-metas em:first-child {
background: #fff7ed;
color: #c2410c;
}
.plan-task-form {
display: grid;
gap: 12px;
padding: 12px 0 0;
border: 0;
border-top: 1px dashed #d8e0e8;
border-radius: 0;
background: transparent;
}
.plan-dialog-form .inline-task-row .field {
gap: 6px;
}
.plan-dialog-form .inline-task-row :deep(.el-input__wrapper),
.plan-dialog-form .inline-task-row :deep(.el-select__wrapper),
.plan-dialog-form .inline-task-row :deep(.el-input-number),
.plan-dialog-form .inline-task-row :deep(.el-input-number .el-input__wrapper) {
height: 36px !important;
min-height: 36px !important;
overflow: hidden;
}
.plan-dialog-form .inline-task-row :deep(.el-input-number) {
width: 100%;
border-radius: 8px;
box-sizing: border-box;
background: #fff;
border: 1px solid var(--el-border-color);
overflow: hidden;
}
.plan-dialog-form .inline-task-row :deep(.el-input-number:focus-within) {
border-color: var(--el-color-primary);
}
.plan-dialog-form .inline-task-row :deep(.el-input-number .el-input__wrapper) {
box-shadow: none !important;
background: transparent;
border-radius: 0;
}
.plan-dialog-form .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__increase),
.plan-dialog-form .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__decrease) {
right: 0;
height: 18px;
}
.plan-dialog-form .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__increase) {
top: 0;
}
.plan-dialog-form .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__decrease) {
bottom: 0;
}
.inline-plan-card {
border-color: rgba(15, 118, 110, 0.42);
background: #f8fbfc;
}
.dialog-inline-action {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
padding-inline: 12px;
border-radius: 999px;
}
.dialog-inline-action--secondary {
justify-self: flex-end;
}
.dialog-inline-action :deep(.el-icon) {
font-size: 13px;
}
.plan-task-form > .dialog-inline-action {
justify-self: flex-end;
}
.inline-plan-card .plan-name-cell {
justify-content: stretch;
}
.inline-plan-name-input {
width: 100%;
min-width: 260px;
}
.inline-plan-actions {
grid-column: 3 / 4;
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 0 12px 12px;
}
.inline-plan-card :deep(.el-input__wrapper),
.inline-plan-card :deep(.el-select__wrapper) {
border-radius: 10px;
box-shadow: 0 0 0 1px #d8e0e8 inset;
}
.inline-plan-card :deep(.el-input__wrapper.is-focus),
.inline-plan-card :deep(.el-select__wrapper.is-focused) {
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1);
}
.inline-plan-card :deep(.el-textarea__inner:focus) {
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1);
}
.inline-plan-card .inline-task-row :deep(.el-select__selected-item),
.inline-plan-card .inline-task-row :deep(.el-select__placeholder) {
width: 100%;
justify-content: center;
text-align: center;
}
.inline-plan-card .inline-task-row :deep(.el-input__wrapper),
.inline-plan-card .inline-task-row :deep(.el-select__wrapper),
.inline-plan-card .inline-task-row :deep(.el-input-number),
.inline-plan-card .inline-task-row :deep(.el-input-number .el-input__wrapper) {
height: 36px !important;
min-height: 36px !important;
overflow: hidden;
}
.inline-plan-card .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__increase),
.inline-plan-card .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__decrease) {
right: 1px;
height: 18px;
}
.inline-plan-card .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__increase) {
top: 0;
border-top-right-radius: 9px;
}
.inline-plan-card .inline-task-row :deep(.el-input-number.is-controls-right .el-input-number__decrease) {
bottom: 0;
border-bottom-right-radius: 9px;
}
.inline-plan-card .inline-task-row :deep(.el-input-number:focus-within .el-input__wrapper) {
overflow: hidden;
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1) !important;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 20px;
border-top: 1px solid #d8e0e8;
background: #fff;
}
.btn-submit {
background: #0f766e !important;
border-color: #0f766e !important;
}
.btn-submit:hover {
background: #0d9488 !important;
border-color: #0d9488 !important;
}
@media (max-width: 1180px) {
.form-head,
.compose-grid,
.review-title-fields,
.review-title-readonly,
.plan-title-readonly,
.review-editor-grid,
.plan-editor-grid,
.inline-task-row {
grid-template-columns: 1fr;
}
.review-card,
.plan-card {
grid-template-columns: 1fr;
}
.review-index-cell,
.review-name-cell,
.review-editor-grid,
.review-action-cell,
.plan-index-cell,
.plan-name-cell,
.plan-editor-grid,
.plan-action-cell,
.inline-plan-actions {
grid-column: auto;
grid-row: auto;
border-left: 0;
}
.review-name-cell,
.review-editor-grid,
.review-action-cell,
.plan-name-cell,
.plan-editor-grid,
.plan-action-cell {
border-top: 1px solid #e5edf1;
}
.section-title {
align-items: flex-start;
}
}
</style>