2308 lines
64 KiB
Vue
2308 lines
64 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, onMounted, reactive, ref } from 'vue';
|
||
import { Plus } from '@element-plus/icons-vue';
|
||
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
|
||
import { fetchGetMyParticipatedProjectPage } from '@/service/api';
|
||
import { useDict } from '@/hooks/business/dict';
|
||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||
import DictSelect from '@/components/custom/dict-select.vue';
|
||
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;
|
||
detail?: string;
|
||
}
|
||
|
||
interface PlanTaskDraft {
|
||
title: string;
|
||
priority?: StructuredTask['priority'];
|
||
progress: number;
|
||
detail?: string;
|
||
}
|
||
|
||
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);
|
||
|
||
/** 工作事项下拉里的"我的事项"固定项,用于区分纯个人事务。 */
|
||
const MY_AFFAIRS_TITLE = '我的事项';
|
||
|
||
/** 「我参与的项目」项目名清单,下拉框数据源。 */
|
||
const participatedProjectNames = ref<string[]>([]);
|
||
|
||
async function loadParticipatedProjectNames() {
|
||
// pageSize=-1 一次拉全部(不分页),由后端按"进行中 + 创建时间升序"过滤排序。
|
||
const { data, error } = await fetchGetMyParticipatedProjectPage({ pageNo: 1, pageSize: -1 });
|
||
if (error || !data) {
|
||
participatedProjectNames.value = [];
|
||
return;
|
||
}
|
||
participatedProjectNames.value = data.list
|
||
.map(project => project.name?.trim())
|
||
.filter((name): name is string => Boolean(name));
|
||
}
|
||
|
||
onMounted(loadParticipatedProjectNames);
|
||
|
||
const reviewWorkItemOptions = computed(() => {
|
||
// 下拉数据来自「我参与的项目」API 拉取的项目名 + 「我的事项」固定项,
|
||
// 与周报共用同一份数据源(fetchGetMyParticipatedProjectPage, pageSize=-1)。
|
||
const names = new Set<string>(participatedProjectNames.value);
|
||
return [MY_AFFAIRS_TITLE, ...Array.from(names).filter(name => name !== MY_AFFAIRS_TITLE)];
|
||
});
|
||
|
||
/** 选中"我的事项"时,事项不参与项目优先级,需要隐藏优先级相关展示。 */
|
||
const isMyAffairsPlanItem = (workItem: string) => workItem.trim() === MY_AFFAIRS_TITLE;
|
||
const DEFAULT_SECTION_CATEGORY = '工作内容';
|
||
|
||
function createPlanTaskDraft(): PlanTaskDraft {
|
||
return { title: '', priority: '2', progress: 0 };
|
||
}
|
||
|
||
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,
|
||
detail: task.detail || ''
|
||
};
|
||
}
|
||
|
||
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 normalizeSectionCategory(value?: string | null, fallback = DEFAULT_SECTION_CATEGORY) {
|
||
const category = resolveTaskItemTypeLabel(value || '').trim();
|
||
return category || fallback;
|
||
}
|
||
|
||
function normalizeSection(section: WorkReportStructuredSection): StructuredSection {
|
||
return {
|
||
category: normalizeSectionCategory(section.category),
|
||
tasks: section.tasks.map(normalizeTask)
|
||
};
|
||
}
|
||
|
||
function mergeSectionsByCategory(sections: StructuredSection[]) {
|
||
const sectionMap = new Map<string, StructuredSection>();
|
||
|
||
sections.forEach(section => {
|
||
const category = normalizeSectionCategory(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)));
|
||
// 同时支持 "进度 XX%" 与裸 "XX%",避免失焦后丢失进度。
|
||
const progressText =
|
||
metricsText.match(/进度\s*(\d+(?:\.\d+)?)%/u)?.[1] ||
|
||
parts.find(item => /^\d+(?:\.\d+)?%$/u.test(item))?.replace(/%$/u, '');
|
||
const hoursText = metricsText.match(/(\d+(?:\.\d+)?)h/u)?.[1];
|
||
|
||
return {
|
||
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 = normalizeSectionCategory(section.category);
|
||
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 = normalizeSectionCategory(section.category);
|
||
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 = normalizeSectionCategory(categoryText, 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(/^(?!\d+[、..]\s*)(.+?)\s*[--]\s*(.+)$/u);
|
||
if (legacyMatch) {
|
||
const [, rawCategory, rawTaskText] = legacyMatch;
|
||
const category = rawCategory.trim();
|
||
const task = parseStructuredSectionTaskText(rawTaskText);
|
||
if (!category || !task) return;
|
||
|
||
ensureSection(category).tasks.push(task);
|
||
currentCategory = category;
|
||
return;
|
||
}
|
||
|
||
const task = parseStructuredSectionTaskText(trimmedLine);
|
||
if (!task) return;
|
||
|
||
const hasStructuredHint =
|
||
/^(\d+[..、]\s*)/u.test(trimmedLine) ||
|
||
trimmedLine.includes('(') ||
|
||
trimmedLine.includes('(') ||
|
||
trimmedLine.includes(':') ||
|
||
trimmedLine.includes(':');
|
||
|
||
if (!hasStructuredHint && !currentCategory) return;
|
||
|
||
ensureSection(currentCategory || defaultCategory).tasks.push(task);
|
||
});
|
||
|
||
return sections.filter(section => section.tasks.length);
|
||
}
|
||
|
||
function parseStructuredSectionTaskText(
|
||
text: string,
|
||
fallback?: Partial<StructuredTask>,
|
||
useFallback = false
|
||
): StructuredTask | null {
|
||
const normalizedText = stripStructuredTaskPrefixV2(text);
|
||
if (!normalizedText) return null;
|
||
|
||
const structuredMatch =
|
||
normalizedText.match(/^(.+?)(?:[((]([^()()]*)[))])?(?:\s*[::]\s*(.*))?$/u) ||
|
||
normalizedText.match(/^(.+?)(?:\(([^()]*)\))?(?::\s*(.*))?$/u);
|
||
|
||
if (!structuredMatch) return null;
|
||
|
||
const [, rawTitle, metricsText = '', detail = ''] = structuredMatch;
|
||
const title = stripStructuredTaskSuffixV2(rawTitle);
|
||
if (!title) return null;
|
||
|
||
return {
|
||
title,
|
||
detail: detail.trim(),
|
||
...resolveTaskMetrics(metricsText, fallback, useFallback)
|
||
};
|
||
}
|
||
|
||
function createFallbackTaskLookup(sections: StructuredSection[]) {
|
||
const lookup = new Map<string, StructuredTask[]>();
|
||
|
||
sections.forEach(section => {
|
||
const categoryKey = normalizeSectionCategory(section.category);
|
||
section.tasks.forEach(task => {
|
||
const titleKey = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(task.title));
|
||
const key = `${categoryKey}||${titleKey}`;
|
||
const list = lookup.get(key);
|
||
if (list) {
|
||
list.push(task);
|
||
} else {
|
||
lookup.set(key, [task]);
|
||
}
|
||
});
|
||
});
|
||
|
||
return lookup;
|
||
}
|
||
|
||
function parseStructuredSectionsFromEditorV2(
|
||
editor: HTMLElement,
|
||
fallbackSections: StructuredSection[] = [],
|
||
defaultCategory = DEFAULT_SECTION_CATEGORY
|
||
): StructuredSection[] {
|
||
const parsedSections = createStructuredSectionsFromTextV2(editor.innerText, defaultCategory);
|
||
if (!parsedSections.length) return [];
|
||
|
||
const fallbackLookup = createFallbackTaskLookup(fallbackSections);
|
||
|
||
return parsedSections.map((section, sectionIndex) => {
|
||
const fallbackSection = fallbackSections[sectionIndex];
|
||
const categoryKey = normalizeSectionCategory(section.category);
|
||
|
||
return {
|
||
category: section.category,
|
||
tasks: section.tasks.map((task, taskIndex) => {
|
||
const titleKey = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(task.title));
|
||
const matchedByKey = fallbackLookup.get(`${categoryKey}||${titleKey}`)?.shift();
|
||
const matchedByIndex = fallbackSection?.tasks[taskIndex];
|
||
const fallbackTask = matchedByKey || matchedByIndex;
|
||
|
||
return {
|
||
...task,
|
||
detail: fallbackTask?.detail || task.detail || '',
|
||
hours: task.hours ?? fallbackTask?.hours
|
||
};
|
||
})
|
||
};
|
||
});
|
||
}
|
||
function escapeHtml(value: string) {
|
||
return value
|
||
.replace(/&/g, '&')
|
||
.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] ||
|
||
metricParts.find(item => /^\d+(?:\.\d+)?%$/u.test(item))?.replace(/%$/u, '');
|
||
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 createSectionsJson(sections: StructuredSection[]) {
|
||
return sections.length ? JSON.stringify({ sections }) : null;
|
||
}
|
||
|
||
function getReviewItemSections(item: Api.WorkReport.Common.PersonalReportReviewItem) {
|
||
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.contentJson).map(normalizeSection));
|
||
if (structuredSections.length) return structuredSections;
|
||
|
||
return createStructuredSectionsFromTextV2(item.contentText || '', item.itemTitle || DEFAULT_SECTION_CATEGORY);
|
||
}
|
||
|
||
function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): ReviewItem {
|
||
const contentSections = getReviewItemSections(item);
|
||
|
||
return {
|
||
workItem: item.itemTitle || '未命名工作',
|
||
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: true,
|
||
source: item
|
||
};
|
||
}
|
||
|
||
function getPlanItemSections(item: Api.WorkReport.Common.PersonalReportPlanItem) {
|
||
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.targetJson).map(normalizeSection));
|
||
if (structuredSections.length) return structuredSections;
|
||
|
||
return createStructuredSectionsFromTextV2(item.targetText || '', item.itemTitle || DEFAULT_SECTION_CATEGORY);
|
||
}
|
||
|
||
function toPlanItem(item: Api.WorkReport.Common.PersonalReportPlanItem, index: number): PlanItem {
|
||
const targetSections = getPlanItemSections(item);
|
||
|
||
return {
|
||
workItem: item.itemTitle || '未命名计划',
|
||
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;
|
||
const task: StructuredTask = {
|
||
title: section.draft.title.trim(),
|
||
progress: section.draft.progress,
|
||
detail: section.draft.detail?.trim() || undefined
|
||
};
|
||
// 我的事项不参与项目优先级,存进任务前显式剔除,结构化展示括号里就不会出现优先级。
|
||
if (!isMyAffairsPlanItem(planForm.value.workItem)) {
|
||
task.priority = section.draft.priority;
|
||
}
|
||
section.tasks.push(task);
|
||
section.draft = createPlanTaskDraft();
|
||
}
|
||
|
||
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: normalizeSectionCategory(section.category),
|
||
tasks: section.tasks.map(task => ({ ...task }))
|
||
}));
|
||
if (!sections.length) return;
|
||
const target = createStructuredTextV2(sections);
|
||
const workItem = planForm.value.workItem.trim();
|
||
const sameWorkItem = props.model.planItems.find(item => item.itemTitle.trim() === workItem);
|
||
if (sameWorkItem) {
|
||
const mergedSections = mergeSectionsByCategory([...getPlanItemSections(sameWorkItem), ...sections]);
|
||
sameWorkItem.targetText = createStructuredTextV2(mergedSections);
|
||
sameWorkItem.targetJson = createSectionsJson(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: createSectionsJson(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 removeReviewItem(index: number) {
|
||
const item = reviewItems.value[index];
|
||
if (!item?.source) return;
|
||
const sourceIndex = props.model.reviewItems.indexOf(item.source);
|
||
if (sourceIndex >= 0) props.model.reviewItems.splice(sourceIndex, 1);
|
||
}
|
||
|
||
function focusEditField(key: string) {
|
||
activeEditField.value = key;
|
||
}
|
||
|
||
function blurEditField(key: string) {
|
||
if (activeEditField.value === key) activeEditField.value = '';
|
||
}
|
||
|
||
function isStructuredEditorUnchanged(text: string, sections: StructuredSection[] | undefined, showHours = false) {
|
||
if (!sections?.length) return false;
|
||
return normalizeEditorText(text) === createStructuredTextV2(sections, showHours);
|
||
}
|
||
|
||
function syncRichContent(item: ReviewItem, event: Event) {
|
||
const target = event.currentTarget as HTMLElement;
|
||
if (!item.source) return;
|
||
if (isStructuredEditorUnchanged(target.innerText, item.contentSections, true)) {
|
||
item.source.contentJson = createSectionsJson(item.contentSections || []);
|
||
item.source.contentText = createStructuredTextV2(item.contentSections || [], true);
|
||
return;
|
||
}
|
||
const sections = parseStructuredSectionsFromEditorV2(
|
||
target,
|
||
item.contentSections || [],
|
||
item.contentSections?.[0]?.category || DEFAULT_SECTION_CATEGORY
|
||
);
|
||
item.source.contentJson = createSectionsJson(sections);
|
||
item.source.contentText = createStructuredTextV2(sections, true);
|
||
}
|
||
|
||
function syncRichTarget(item: PlanItem, event: Event) {
|
||
const target = event.currentTarget as HTMLElement;
|
||
if (!item.source) return;
|
||
if (isStructuredEditorUnchanged(target.innerText, item.targetSections)) {
|
||
item.source.targetJson = createSectionsJson(item.targetSections || []);
|
||
item.source.targetText = createStructuredTextV2(item.targetSections || []);
|
||
return;
|
||
}
|
||
const sections = parseStructuredSectionsFromEditorV2(
|
||
target,
|
||
item.targetSections || [],
|
||
item.targetSections?.[0]?.category || DEFAULT_SECTION_CATEGORY
|
||
);
|
||
item.source.targetJson = createSectionsJson(sections);
|
||
item.source.targetText = createStructuredTextV2(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 v-if="mode === 'edit' && !isReadonly" class="section-title-right">
|
||
<ElButton size="small" plain type="primary" @click="emit('pullDefaultDraft')">
|
||
<template #icon>
|
||
<icon-mdi-refresh class="text-icon" />
|
||
</template>
|
||
刷新
|
||
</ElButton>
|
||
</div>
|
||
</div>
|
||
<div class="compose-grid">
|
||
<div class="field">
|
||
<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="removeReviewItem(index)"
|
||
>
|
||
<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"
|
||
: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" @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>
|
||
<ElSelect
|
||
v-model="planForm.workItem"
|
||
class="inline-plan-name-input"
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
clearable
|
||
placeholder="请选择项目名/我的事项"
|
||
>
|
||
<ElOption v-for="item in reviewWorkItemOptions" :key="item" :label="item" :value="item" />
|
||
</ElSelect>
|
||
</div>
|
||
<div class="field">
|
||
<label>具体目标</label>
|
||
<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">
|
||
<div class="field plan-section-head-field">
|
||
<label>类别</label>
|
||
<DictSelect
|
||
v-model="section.category"
|
||
:dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE"
|
||
size="small"
|
||
placeholder="选择类别"
|
||
style="width: 100%"
|
||
@focus="activePlanSectionIndex = sIdx"
|
||
@change="activePlanSectionIndex = sIdx"
|
||
/>
|
||
</div>
|
||
<ElButton
|
||
v-if="planForm.sections.length > 1"
|
||
link
|
||
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">
|
||
<template v-if="!isMyAffairsPlanItem(planForm.workItem)">
|
||
<em>{{ resolvePriorityLabel(task.priority) }}</em>
|
||
</template>
|
||
<em>进度 {{ task.progress }}%</em>
|
||
</div>
|
||
<ElButton link type="danger" size="small" @click="removePlanTask(sIdx, tIdx)">删除</ElButton>
|
||
</div>
|
||
<div v-if="task.detail" class="plan-task-detail">{{ task.detail }}</div>
|
||
</div>
|
||
<div class="plan-task-form">
|
||
<div
|
||
class="inline-task-row"
|
||
:class="{ 'inline-task-row--my-affairs': isMyAffairsPlanItem(planForm.workItem) }"
|
||
>
|
||
<div class="field">
|
||
<label>任务名/事项名</label>
|
||
<ElInput
|
||
v-model="section.draft.title"
|
||
size="small"
|
||
placeholder="请输入任务名或事项名"
|
||
@focus="activePlanSectionIndex = sIdx"
|
||
/>
|
||
</div>
|
||
<div v-if="!isMyAffairsPlanItem(planForm.workItem)" 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>
|
||
<div class="field">
|
||
<label>详细内容</label>
|
||
<ElInput
|
||
v-model="section.draft.detail"
|
||
size="small"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="详细内容(可选)"
|
||
@focus="activePlanSectionIndex = sIdx"
|
||
/>
|
||
</div>
|
||
<ElButton
|
||
class="dialog-inline-action"
|
||
type="primary"
|
||
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"
|
||
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: var(--el-color-primary-light-8);
|
||
color: var(--el-color-primary);
|
||
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: var(--el-color-primary);
|
||
background: var(--el-color-primary-light-9);
|
||
}
|
||
|
||
.radio-group-full :deep(.el-radio__input.is-checked + .el-radio__label) {
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.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: var(--el-color-primary);
|
||
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: var(--el-color-primary);
|
||
box-shadow: 0 0 0 2px var(--el-color-primary-light-8);
|
||
}
|
||
|
||
.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: var(--el-color-primary);
|
||
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: var(--el-color-primary);
|
||
}
|
||
|
||
.rich-editor :deep(.rich-section-task) {
|
||
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: var(--el-color-primary);
|
||
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: var(--el-color-primary);
|
||
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: var(--el-color-primary);
|
||
}
|
||
|
||
.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: var(--el-color-primary);
|
||
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: var(--el-color-primary);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.inline-task-row--my-affairs {
|
||
grid-template-columns: minmax(0, 1fr) 142px;
|
||
}
|
||
|
||
.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: var(--el-color-primary-light-7);
|
||
background: var(--el-color-primary-light-9);
|
||
box-shadow: inset 0 0 0 1px var(--el-color-primary-light-9);
|
||
}
|
||
|
||
.plan-section-head {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 8px;
|
||
align-items: end;
|
||
}
|
||
|
||
.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-detail {
|
||
color: #475569;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.plan-task-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
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: var(--el-color-primary-light-5);
|
||
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 :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 var(--el-color-primary) inset,
|
||
0 0 0 2px var(--el-color-primary-light-8);
|
||
}
|
||
|
||
.inline-plan-card :deep(.el-textarea__inner:focus) {
|
||
box-shadow:
|
||
0 0 0 1px var(--el-color-primary) inset,
|
||
0 0 0 2px var(--el-color-primary-light-8);
|
||
}
|
||
|
||
.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 var(--el-color-primary) inset,
|
||
0 0 0 2px var(--el-color-primary-light-8) !important;
|
||
}
|
||
|
||
.form-actions {
|
||
position: sticky;
|
||
z-index: 5;
|
||
bottom: 0;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
margin-top: auto;
|
||
margin-bottom: 0;
|
||
padding: 14px 20px;
|
||
border-top: 1px solid #d8e0e8;
|
||
border-bottom-left-radius: 18px;
|
||
border-bottom-right-radius: 18px;
|
||
background: #f5f7fa;
|
||
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
@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>
|