2196 lines
59 KiB
Vue
2196 lines
59 KiB
Vue
|
|
<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, '&')
|
|||
|
|
.replace(/</g, '<')
|
|||
|
|
.replace(/>/g, '>')
|
|||
|
|
.replace(/"/g, '"')
|
|||
|
|
.replace(/'/g, ''');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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>
|