2803 lines
81 KiB
Vue
2803 lines
81 KiB
Vue
<script setup lang="ts">
|
||
/* eslint-disable vue/no-mutating-props, unicorn/prefer-dom-node-text-content, no-useless-escape, no-nested-ternary */
|
||
import { computed, nextTick, onMounted, reactive, ref, watch } 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,
|
||
formatPeriodDateRange,
|
||
formatPeriodLabel,
|
||
getStructuredSections,
|
||
getStructuredTasks
|
||
} from '../../shared/types';
|
||
|
||
const props = defineProps<{
|
||
reportType: string;
|
||
period: string;
|
||
mode?: 'add' | 'edit' | 'detail';
|
||
scene?: 'fill' | 'detail' | 'approval';
|
||
baseInfo?: Api.WorkReport.Weekly.WeeklyReport | null;
|
||
model: Api.WorkReport.Weekly.WeeklyReportSaveParams;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
back: [];
|
||
save: [];
|
||
submit: [];
|
||
viewAudit: [];
|
||
requestApprove: [];
|
||
requestReject: [];
|
||
pullDefaultDraft: [];
|
||
}>();
|
||
|
||
interface ReviewItem {
|
||
workItem: string;
|
||
days: number;
|
||
hours?: number;
|
||
content: string;
|
||
contentHtml: string;
|
||
contentSections?: StructuredSection[];
|
||
contentTasks?: StructuredTask[];
|
||
travelTasks?: StructuredTask[];
|
||
reflection: string;
|
||
removable?: boolean;
|
||
source?: Api.WorkReport.Common.PersonalReportReviewItem;
|
||
}
|
||
|
||
interface PlanItem {
|
||
workItem: string;
|
||
target: string;
|
||
targetHtml: string;
|
||
targetSections?: StructuredSection[];
|
||
targetTasks?: StructuredTask[];
|
||
supportNeed: string;
|
||
removable?: boolean;
|
||
source?: Api.WorkReport.Common.PersonalReportPlanItem;
|
||
sourceIndex?: number;
|
||
}
|
||
|
||
interface StructuredTask {
|
||
title: string;
|
||
detail: string;
|
||
priority?: string;
|
||
progress?: number;
|
||
hours?: number;
|
||
kind?: string;
|
||
}
|
||
|
||
interface PlanTaskDraft {
|
||
title: string;
|
||
detail: string;
|
||
priority: StructuredTask['priority'];
|
||
progress: number;
|
||
}
|
||
|
||
interface PlanSectionDraft {
|
||
category: string;
|
||
tasks: StructuredTask[];
|
||
draft: PlanTaskDraft;
|
||
}
|
||
|
||
interface StructuredSection {
|
||
category: string;
|
||
tasks: StructuredTask[];
|
||
}
|
||
|
||
interface TravelSegment {
|
||
dateRange: [string, string] | null;
|
||
days: number | null;
|
||
workItem: string;
|
||
/** 是否为用户新增的分段,新增的才显示删除按钮 */
|
||
isNew?: boolean;
|
||
}
|
||
|
||
const activeEditField = ref('');
|
||
const travelDialogVisible = ref(false);
|
||
const isReadonly = computed(() => props.mode === 'detail' || props.scene === 'approval');
|
||
const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
||
const { getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE);
|
||
const DEFAULT_SECTION_CATEGORY = '工作内容';
|
||
const TRAVEL_SECTION_CATEGORY = '差旅';
|
||
|
||
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 || '--';
|
||
}
|
||
});
|
||
|
||
/** 是否出差:由父级 model 作为唯一提交源,本地编辑态只承载未完成的出差分段。 */
|
||
const businessTripValue = computed({
|
||
get: () => props.model.isBusinessTrip,
|
||
set: value => updateBusinessTrip(value)
|
||
});
|
||
|
||
const EMPTY_HTML = '本周期内暂无数据';
|
||
const TRAVEL_REVIEW_ITEM_TITLE = '本周差旅';
|
||
/** 工作事项下拉里的"我的事项"固定项,用于区分纯个人事务。 */
|
||
const MY_AFFAIRS_TITLE = '我的事项';
|
||
|
||
const planForm = ref({
|
||
workItem: '',
|
||
supportNeed: '',
|
||
sections: [] as PlanSectionDraft[]
|
||
});
|
||
const activePlanSectionIndex = ref(-1);
|
||
|
||
function createPlanTaskDraft(): PlanTaskDraft {
|
||
return { title: '', detail: '', priority: '2', progress: 0 };
|
||
}
|
||
|
||
const travelSegments = ref<TravelSegment[]>([]);
|
||
const planDialogVisible = ref(false);
|
||
|
||
/** 「我参与的项目」项目名清单,供“新增计划”工作事项下拉使用。 */
|
||
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 拉取的项目名 + 「我的事项」固定项,
|
||
// 供“新增计划-工作事项”下拉使用。
|
||
const names = new Set<string>(participatedProjectNames.value);
|
||
return [MY_AFFAIRS_TITLE, ...Array.from(names).filter(name => name !== MY_AFFAIRS_TITLE)];
|
||
});
|
||
|
||
const reviewItems = computed<ReviewItem[]>(() => {
|
||
return (props.model.reviewItems || []).map(toReviewItem);
|
||
});
|
||
|
||
const travelWorkItemOptions = computed(() => {
|
||
const names = reviewItems.value
|
||
.map(item => item.workItem.trim())
|
||
.filter(name => Boolean(name) && name !== TRAVEL_REVIEW_ITEM_TITLE && name !== MY_AFFAIRS_TITLE);
|
||
|
||
return [...new Set(names), MY_AFFAIRS_TITLE];
|
||
});
|
||
|
||
/** 选中"我的事项"时,事项不参与项目优先级,需要隐藏优先级相关展示。 */
|
||
const isMyAffairsPlanItem = (workItem: string) => workItem.trim() === MY_AFFAIRS_TITLE;
|
||
|
||
function normalizeTravelDays(value: unknown) {
|
||
const numberValue = Number(value);
|
||
if (!Number.isFinite(numberValue) || numberValue <= 0) return null;
|
||
return Math.round(numberValue * 2) / 2;
|
||
}
|
||
|
||
function isValidTravelDays(value: unknown) {
|
||
const normalizedValue = normalizeTravelDays(value);
|
||
return normalizedValue !== null && normalizedValue >= 0.5 && Number.isInteger(normalizedValue * 2);
|
||
}
|
||
|
||
function normalizeTravelDateText(value?: string | null) {
|
||
if (!value) return '';
|
||
const commaDateMatch = value.match(/^(\d{4}),(\d{1,2}),(\d{1,2})$/);
|
||
|
||
if (commaDateMatch) {
|
||
const [, year, month, day] = commaDateMatch;
|
||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
function toTravelSegments(segments: Api.WorkReport.Weekly.WeeklyReportTravelSegment[] | undefined): TravelSegment[] {
|
||
return (segments || []).map(item => ({
|
||
dateRange:
|
||
item.startDate && item.endDate
|
||
? [normalizeTravelDateText(item.startDate), normalizeTravelDateText(item.endDate)]
|
||
: null,
|
||
days: normalizeTravelDays(item.travelDays),
|
||
workItem: ''
|
||
/* 默认带入的分段不带 isNew,不可删除 */
|
||
}));
|
||
}
|
||
|
||
function isCompleteTravelSegment(segment: TravelSegment) {
|
||
return Boolean(
|
||
segment.dateRange?.[0] && segment.dateRange?.[1] && isValidTravelDays(segment.days) && segment.workItem.trim()
|
||
);
|
||
}
|
||
|
||
function toModelTravelSegments(segments: TravelSegment[]): Api.WorkReport.Weekly.WeeklyReportTravelSegment[] {
|
||
return segments.filter(isCompleteTravelSegment).map((item, index) => ({
|
||
sort: index + 1,
|
||
startDate: normalizeTravelDateText(item.dateRange?.[0]) || '',
|
||
endDate: normalizeTravelDateText(item.dateRange?.[1]) || '',
|
||
travelDays: normalizeTravelDays(item.days) || 0
|
||
}));
|
||
}
|
||
|
||
function isSameTravelSegments(
|
||
source: TravelSegment[],
|
||
target: Api.WorkReport.Weekly.WeeklyReportTravelSegment[] | undefined
|
||
) {
|
||
const sourceSegments = toModelTravelSegments(source);
|
||
const targetSegments = target || [];
|
||
if (sourceSegments.length !== targetSegments.length) return false;
|
||
|
||
return sourceSegments.every((item, index) => {
|
||
const targetItem = targetSegments[index];
|
||
return (
|
||
item.startDate === normalizeTravelDateText(targetItem.startDate) &&
|
||
item.endDate === normalizeTravelDateText(targetItem.endDate) &&
|
||
Number(item.travelDays || 0) === Number(targetItem.travelDays || 0)
|
||
);
|
||
});
|
||
}
|
||
|
||
function syncTravelSegmentsToModel() {
|
||
const nextSegments = toModelTravelSegments(travelSegments.value);
|
||
if (isSameTravelSegments(travelSegments.value, props.model.travelSegments)) return;
|
||
props.model.travelSegments = nextSegments;
|
||
}
|
||
|
||
watch(
|
||
() => props.model.travelSegments,
|
||
segments => {
|
||
if (isSameTravelSegments(travelSegments.value, segments)) return;
|
||
travelSegments.value = toTravelSegments(segments);
|
||
},
|
||
{ immediate: true, deep: true }
|
||
);
|
||
|
||
watch(travelSegments, () => syncTravelSegmentsToModel(), { deep: true });
|
||
|
||
function createStructuredText(tasks: StructuredTask[]) {
|
||
return tasks
|
||
.map(task => {
|
||
const title = task.title.trim();
|
||
const detail = task.detail.trim();
|
||
if (!detail || detail === title) return title;
|
||
return `${title}\n ${detail}`;
|
||
})
|
||
.join('\n');
|
||
}
|
||
|
||
function escapeHtml(value: string) {
|
||
return value
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function normalizeTask(task: WorkReportStructuredTask): StructuredTask {
|
||
return {
|
||
title: task.title || '',
|
||
detail: task.detail || '',
|
||
priority: normalizePriorityCode(task.priority),
|
||
progress: typeof task.progress === 'number' ? task.progress : undefined,
|
||
hours: typeof task.hours === 'number' ? task.hours : undefined,
|
||
kind: task.kind ? String(task.kind) : undefined
|
||
};
|
||
}
|
||
|
||
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()).filter(section => section.tasks.length);
|
||
}
|
||
|
||
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 | null) {
|
||
return getTaskItemTypeLabel(value, { fallback: value || '工作内容' });
|
||
}
|
||
|
||
const STRUCTURED_TASK_PREFIX_RE = /^(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*/u;
|
||
|
||
function stripStructuredTaskPrefixV2(value: string) {
|
||
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
|
||
}
|
||
|
||
function stripStructuredTaskSuffixV2(value: string) {
|
||
return value.trim().replace(/[。.!!??]+$/u, '');
|
||
}
|
||
|
||
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 formatStructuredTaskDisplayLine(task: StructuredTask, index: number, showHours = false) {
|
||
return `${index + 1}、${formatStructuredTaskLineV2(task, showHours)}`;
|
||
}
|
||
|
||
function getWorkLogEntries(detail: string): string[] {
|
||
if (!detail) return [];
|
||
// 仅按中文分号切分,避免误伤文本中的其他标点;每段作为单独一天的工作日志展示。
|
||
return detail
|
||
.split(';')
|
||
.map(item => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
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(formatStructuredTaskDisplayLine(task, index, 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 flattenSectionTasks(sections: StructuredSection[]) {
|
||
return sections.flatMap(section => section.tasks.map(task => ({ ...task })));
|
||
}
|
||
|
||
function createSectionsJson(sections: StructuredSection[]) {
|
||
return sections.length ? JSON.stringify({ sections }) : null;
|
||
}
|
||
|
||
function createTaskMetrics(task: StructuredTask, showHours: boolean) {
|
||
const metrics = [
|
||
task.priority
|
||
? `<span class="rich-task-meta priority">${escapeHtml(resolvePriorityLabel(task.priority))}</span>`
|
||
: '',
|
||
typeof task.progress === 'number' ? `<span class="rich-task-meta">进度 ${task.progress}%</span>` : '',
|
||
showHours && typeof task.hours === 'number' ? `<span class="rich-task-meta">${task.hours}h</span>` : ''
|
||
].filter(Boolean);
|
||
return metrics.length ? `<div class="rich-task-metas" contenteditable="false">${metrics.join('')}</div>` : '';
|
||
}
|
||
|
||
function isTravelTask(task: StructuredTask) {
|
||
return task.kind === 'travel';
|
||
}
|
||
|
||
function normalizeEditorText(value: string) {
|
||
return value
|
||
.replace(/\u00A0/g, ' ')
|
||
.split('\n')
|
||
.map(line => line.trim())
|
||
.filter(Boolean)
|
||
.join('\n');
|
||
}
|
||
|
||
function createStructuredTasksFromText(text: string, defaultTask?: Partial<StructuredTask>) {
|
||
const lines = normalizeEditorText(text).split('\n').filter(Boolean);
|
||
if (!lines.length || lines.join('\n') === EMPTY_HTML) return [];
|
||
const defaultTitle = String(defaultTask?.title || '').trim();
|
||
const tasks = lines
|
||
.map(line => {
|
||
const structuredMatch = line.match(/^(.+?)[((]([^))]*)[))](?:\s*[::]\s*(.*))?$/u);
|
||
if (!structuredMatch) return null;
|
||
const [, title, metricsText, detail = ''] = structuredMatch;
|
||
|
||
return {
|
||
...defaultTask,
|
||
title: title.trim(),
|
||
detail: detail.trim(),
|
||
...resolveTaskMetrics(metricsText, defaultTask)
|
||
};
|
||
})
|
||
.filter(Boolean) as StructuredTask[];
|
||
|
||
if (tasks.length) return tasks;
|
||
|
||
const detailLines = defaultTitle && lines[0] === defaultTitle ? lines.slice(1) : lines;
|
||
|
||
return [
|
||
{
|
||
...defaultTask,
|
||
title: defaultTitle || lines[0],
|
||
detail: defaultTitle ? detailLines.join('\n') : lines.slice(1).join('\n')
|
||
}
|
||
];
|
||
}
|
||
|
||
function stripStructuredTaskPrefix(value: string) {
|
||
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
|
||
}
|
||
|
||
function stripStructuredTaskSuffix(value: string) {
|
||
return value.trim().replace(/[。.!!??]+$/u, '');
|
||
}
|
||
|
||
function getElementText(element: Element | null) {
|
||
return normalizeEditorText((element as HTMLElement | null)?.innerText || '');
|
||
}
|
||
|
||
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));
|
||
// 同时支持 "进度 XX%" 与裸 "XX%",避免失焦后丢失进度。
|
||
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 isStructuredMetricsText(text: string) {
|
||
return /(?:\bP\d+\b|进度\s*\d+(?:\.\d+)?%|\d+(?:\.\d+)?%|\d+(?:\.\d+)?h)/iu.test(text.trim());
|
||
}
|
||
|
||
function extractStructuredTaskParts(normalizedText: string) {
|
||
const colonIndex = normalizedText.search(/[::]/u);
|
||
const mainText = colonIndex >= 0 ? normalizedText.slice(0, colonIndex).trim() : normalizedText.trim();
|
||
const detailText = colonIndex >= 0 ? normalizedText.slice(colonIndex + 1).trim() : '';
|
||
|
||
const bracketMatches = Array.from(mainText.matchAll(/[((]([^()()]*)[))]/gu));
|
||
const completeMetricsMatch = [...bracketMatches].reverse().find(match => isStructuredMetricsText(match[1] || ''));
|
||
|
||
if (completeMetricsMatch && completeMetricsMatch.index !== undefined) {
|
||
const fullMatch = completeMetricsMatch[0] || '';
|
||
const metricsText = completeMetricsMatch[1] || '';
|
||
const startIndex = completeMetricsMatch.index;
|
||
const endIndex = startIndex + fullMatch.length;
|
||
|
||
return {
|
||
rawTitle: `${mainText.slice(0, startIndex)}${mainText.slice(endIndex)}`.trim(),
|
||
metricsText,
|
||
detail: detailText
|
||
};
|
||
}
|
||
|
||
const lastOpenIndex = Math.max(mainText.lastIndexOf('('), mainText.lastIndexOf('('));
|
||
const lastCloseIndex = Math.max(mainText.lastIndexOf(')'), mainText.lastIndexOf(')'));
|
||
|
||
if (lastOpenIndex > lastCloseIndex) {
|
||
const metricsText = mainText.slice(lastOpenIndex + 1).trim();
|
||
if (isStructuredMetricsText(metricsText)) {
|
||
return {
|
||
rawTitle: mainText.slice(0, lastOpenIndex).trim(),
|
||
metricsText,
|
||
detail: detailText
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
rawTitle: mainText,
|
||
metricsText: '',
|
||
detail: detailText
|
||
};
|
||
}
|
||
|
||
function parseStructuredSectionTaskText(
|
||
text: string,
|
||
fallback?: Partial<StructuredTask>,
|
||
useFallback = false
|
||
): StructuredTask | null {
|
||
const normalizedText = stripStructuredTaskPrefix(text);
|
||
if (!normalizedText) return null;
|
||
|
||
const { rawTitle, metricsText, detail } = extractStructuredTaskParts(normalizedText);
|
||
const title = stripStructuredTaskSuffix(rawTitle);
|
||
if (!title) return null;
|
||
|
||
return {
|
||
title,
|
||
detail: detail.trim(),
|
||
...resolveTaskMetrics(metricsText, fallback, useFallback)
|
||
};
|
||
}
|
||
|
||
function createStructuredSectionsFromTextV2(
|
||
text: string,
|
||
defaultCategory: string,
|
||
fallbackTask?: Partial<StructuredTask>
|
||
): StructuredSection[] {
|
||
const lines = normalizeEditorText(text).split('\n').filter(Boolean);
|
||
if (!lines.length || lines.join('\n') === EMPTY_HTML) 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 = '';
|
||
let previousTask: StructuredTask | null = null;
|
||
const legacyTaskLineRe =
|
||
/^(?!(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*)(.+?)\s*[--]\s*(.+)$/u;
|
||
|
||
const isExplicitStructuredTaskLine = (line: string) => {
|
||
if (!STRUCTURED_TASK_PREFIX_RE.test(line)) return false;
|
||
const normalizedLine = stripStructuredTaskPrefix(line);
|
||
const { rawTitle, metricsText } = extractStructuredTaskParts(normalizedLine);
|
||
return Boolean(stripStructuredTaskSuffix(rawTitle) && isStructuredMetricsText(metricsText));
|
||
};
|
||
|
||
const shouldAppendToPreviousTaskDetail = (line: string) => {
|
||
if (!previousTask?.detail) return false;
|
||
if (line.startsWith('#')) return false;
|
||
if (legacyTaskLineRe.test(line)) return false;
|
||
if (isExplicitStructuredTaskLine(line)) return false;
|
||
return true;
|
||
};
|
||
|
||
lines.forEach(line => {
|
||
const trimmedLine = line.trim();
|
||
if (!trimmedLine) return;
|
||
|
||
if (trimmedLine.startsWith('#')) {
|
||
currentCategory = trimmedLine.replace(/^#\s*/u, '').trim();
|
||
if (currentCategory) ensureSection(currentCategory);
|
||
previousTask = null;
|
||
return;
|
||
}
|
||
|
||
// 仅当行首不是结构化任务前缀(如 "3、")时,才按旧式 "<分类> - <事项>" 解析;
|
||
// 否则会把 "2026-06-12 - 2026-06-19" 这种含 " - " 的出差行误判为分类。
|
||
const legacyMatch = trimmedLine.match(legacyTaskLineRe);
|
||
if (legacyMatch) {
|
||
const [, rawCategory, rawTaskText] = legacyMatch;
|
||
const category = rawCategory.trim();
|
||
const task = parseStructuredSectionTaskText(rawTaskText, fallbackTask);
|
||
if (!category || !task) return;
|
||
ensureSection(category).tasks.push(task);
|
||
currentCategory = category;
|
||
previousTask = task;
|
||
return;
|
||
}
|
||
|
||
if (shouldAppendToPreviousTaskDetail(trimmedLine)) {
|
||
const lastTask = previousTask;
|
||
if (!lastTask) return;
|
||
lastTask.detail = lastTask.detail ? `${lastTask.detail}\n${trimmedLine}` : trimmedLine;
|
||
return;
|
||
}
|
||
|
||
const task = parseStructuredSectionTaskText(trimmedLine, fallbackTask);
|
||
if (!task) return;
|
||
|
||
const hasStructuredHint =
|
||
STRUCTURED_TASK_PREFIX_RE.test(trimmedLine) ||
|
||
trimmedLine.includes('(') ||
|
||
trimmedLine.includes('(') ||
|
||
trimmedLine.includes(':') ||
|
||
trimmedLine.includes(':');
|
||
|
||
if (!hasStructuredHint && !currentCategory) return;
|
||
|
||
ensureSection(currentCategory || defaultCategory).tasks.push(task);
|
||
previousTask = task;
|
||
});
|
||
|
||
return mergeSectionsByCategory(sections);
|
||
}
|
||
|
||
function createFallbackTaskLookup(sections: StructuredSection[]) {
|
||
const lookup = new Map<string, StructuredTask[]>();
|
||
|
||
sections.forEach(section => {
|
||
const categoryKey = resolveTaskItemTypeLabel(section.category).trim();
|
||
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
|
||
) {
|
||
const parsedSections = createStructuredSectionsFromTextV2(editor.innerText, defaultCategory);
|
||
if (!parsedSections.length) return [];
|
||
|
||
const fallbackLookup = createFallbackTaskLookup(fallbackSections);
|
||
|
||
const merged: StructuredSection[] = parsedSections.map((section, sectionIndex) => {
|
||
const fallbackSection = fallbackSections[sectionIndex];
|
||
const categoryKey = resolveTaskItemTypeLabel(section.category).trim();
|
||
|
||
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 || '',
|
||
kind: fallbackTask?.kind,
|
||
hours: task.hours
|
||
};
|
||
})
|
||
};
|
||
});
|
||
|
||
// 将出差任务从其他分类中抽离,统一归到独立的"差旅"段落,
|
||
// 避免失焦后被错误归类到上一个分类下。
|
||
return groupTravelTasksIntoSection(merged);
|
||
}
|
||
|
||
function groupTravelTasksIntoSection(sections: StructuredSection[]) {
|
||
const travelTasks: StructuredTask[] = [];
|
||
const remainingSections = sections.map(section => {
|
||
const kept = section.tasks.filter(task => {
|
||
if (isTravelTask(task)) {
|
||
travelTasks.push(task);
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
return { category: section.category, tasks: kept };
|
||
});
|
||
|
||
if (!travelTasks.length) return remainingSections;
|
||
|
||
const existingTravelIndex = remainingSections.findIndex(section => section.category === TRAVEL_SECTION_CATEGORY);
|
||
if (existingTravelIndex >= 0) {
|
||
remainingSections[existingTravelIndex].tasks = [...remainingSections[existingTravelIndex].tasks, ...travelTasks];
|
||
} else {
|
||
remainingSections.push({ category: TRAVEL_SECTION_CATEGORY, tasks: travelTasks });
|
||
}
|
||
|
||
return remainingSections.filter(section => section.tasks.length);
|
||
}
|
||
|
||
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 || '未分类', {
|
||
hours: typeof item.workHours === 'number' ? item.workHours : Number(item.workHours || 0) || undefined
|
||
});
|
||
}
|
||
|
||
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 toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): ReviewItem {
|
||
const contentSections = getReviewItemSections(item);
|
||
const contentTasks = flattenSectionTasks(contentSections);
|
||
const contentHtml = contentSections.length
|
||
? createStructuredHtmlV2(contentSections, true)
|
||
: escapeHtml(item.contentText || '').replace(/\n/g, '<br>');
|
||
|
||
return {
|
||
workItem: item.itemTitle || '未命名工作',
|
||
days: 0,
|
||
hours: item.workHours || 0,
|
||
content: item.contentText || '',
|
||
contentHtml: contentHtml || EMPTY_HTML,
|
||
contentSections,
|
||
contentTasks,
|
||
reflection: item.reflectionText || '',
|
||
removable: true,
|
||
source: item
|
||
};
|
||
}
|
||
|
||
function toPlanItem(item: Api.WorkReport.Common.PersonalReportPlanItem, index: number): PlanItem {
|
||
const targetSections = getPlanItemSections(item);
|
||
const targetTasks = flattenSectionTasks(targetSections);
|
||
const targetHtml = targetSections.length
|
||
? createStructuredHtmlV2(targetSections)
|
||
: escapeHtml(item.targetText || '').replace(/\n/g, '<br>');
|
||
|
||
return {
|
||
workItem: item.itemTitle || '未命名计划',
|
||
target: item.targetText || '',
|
||
targetHtml: targetHtml || EMPTY_HTML,
|
||
targetSections,
|
||
targetTasks,
|
||
supportNeed: item.supportNeed || '',
|
||
/* 只有用户新增的计划才可删除,默认带入的不可删除 */
|
||
removable: true,
|
||
source: item,
|
||
sourceIndex: index
|
||
};
|
||
}
|
||
|
||
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(() => {
|
||
const rangeText = formatPeriodDateRange(
|
||
props.model.periodStartDate || undefined,
|
||
props.model.periodEndDate || undefined
|
||
);
|
||
if (rangeText !== '--') return rangeText;
|
||
|
||
return formatPeriodLabel(props.model.periodLabel || props.period) || '--';
|
||
});
|
||
const totalTravelDays = computed(() => {
|
||
const total = travelSegments.value.reduce((sum, segment) => sum + Number(segment.days || 0), 0);
|
||
return Math.round(total * 10) / 10;
|
||
});
|
||
const hasCompleteTravelSegment = computed(() => travelSegments.value.some(isCompleteTravelSegment));
|
||
|
||
function calcSegmentDays(segment: TravelSegment) {
|
||
if (!segment.dateRange || !segment.dateRange[0] || !segment.dateRange[1]) return segment.days || 0;
|
||
const start = new Date(segment.dateRange[0]);
|
||
const end = new Date(segment.dateRange[1]);
|
||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end < start) return segment.days || 0;
|
||
const diffDays = (end.getTime() - start.getTime()) / 86400000 + 1;
|
||
return Math.round(diffDays * 10) / 10;
|
||
}
|
||
|
||
function syncSegmentDays(index: number) {
|
||
const segment = travelSegments.value[index];
|
||
segment.days = normalizeTravelDays(calcSegmentDays(segment));
|
||
}
|
||
|
||
function buildGroupedTravelTasks() {
|
||
const groupedMap = new Map<string, StructuredTask[]>();
|
||
|
||
travelSegments.value.filter(isCompleteTravelSegment).forEach(segment => {
|
||
const key = segment.workItem.trim();
|
||
const task: StructuredTask = {
|
||
title: `${normalizeTravelDateText(segment.dateRange![0])} - ${normalizeTravelDateText(segment.dateRange![1])} · 共出差${segment.days}天`,
|
||
detail: '',
|
||
kind: 'travel'
|
||
};
|
||
const existing = groupedMap.get(key);
|
||
if (existing) {
|
||
existing.push(task);
|
||
} else {
|
||
groupedMap.set(key, [task]);
|
||
}
|
||
});
|
||
|
||
return Array.from(groupedMap.entries()).map(([workItem, tasks]) => ({ workItem, tasks }));
|
||
}
|
||
|
||
function handleTravelDaysChange(index: number, value: number | undefined) {
|
||
const segment = travelSegments.value[index];
|
||
if (!segment) return;
|
||
segment.days = normalizeTravelDays(value);
|
||
}
|
||
|
||
function syncTravelReviewItem() {
|
||
props.model.isBusinessTrip = true;
|
||
syncTravelSegmentsToModel();
|
||
}
|
||
|
||
function removeTravelTasksFromSections(sections: StructuredSection[]) {
|
||
return sections
|
||
.map(section => ({
|
||
category: section.category,
|
||
tasks: section.tasks.filter(task => !isTravelTask(task))
|
||
}))
|
||
.filter(section => section.tasks.length);
|
||
}
|
||
|
||
function appendTasksToSections(
|
||
sections: StructuredSection[],
|
||
tasks: StructuredTask[],
|
||
fallbackCategory = DEFAULT_SECTION_CATEGORY
|
||
) {
|
||
const nextSections = sections.map(section => ({
|
||
category: section.category,
|
||
tasks: [...section.tasks]
|
||
}));
|
||
|
||
if (!tasks.length) return nextSections;
|
||
|
||
// 出差任务始终归到独立的"差旅"段落,避免被误放到上一个普通分类下。
|
||
const travelTasks = tasks.filter(isTravelTask);
|
||
const otherTasks = tasks.filter(task => !isTravelTask(task));
|
||
|
||
if (otherTasks.length) {
|
||
const targetSection =
|
||
nextSections[nextSections.length - 1] ||
|
||
(() => {
|
||
const section = { category: fallbackCategory, tasks: [] as StructuredTask[] };
|
||
nextSections.push(section);
|
||
return section;
|
||
})();
|
||
targetSection.tasks.push(...otherTasks);
|
||
}
|
||
|
||
if (travelTasks.length) {
|
||
const travelSection =
|
||
nextSections.find(section => section.category === TRAVEL_SECTION_CATEGORY) ||
|
||
(() => {
|
||
const section = { category: TRAVEL_SECTION_CATEGORY, tasks: [] as StructuredTask[] };
|
||
nextSections.push(section);
|
||
return section;
|
||
})();
|
||
travelSection.tasks.push(...travelTasks);
|
||
}
|
||
|
||
return nextSections;
|
||
}
|
||
|
||
function removeTravelTasksFromReviewItems() {
|
||
(props.model.reviewItems || []).forEach(item => {
|
||
const currentSections = getReviewItemSections(item);
|
||
const nextSections = removeTravelTasksFromSections(currentSections);
|
||
if (
|
||
nextSections.length === currentSections.length &&
|
||
flattenSectionTasks(nextSections).length === flattenSectionTasks(currentSections).length
|
||
)
|
||
return;
|
||
item.contentJson = createSectionsJson(nextSections);
|
||
item.contentText = createStructuredTextV2(nextSections, true);
|
||
});
|
||
}
|
||
|
||
function updateBusinessTrip(value: boolean) {
|
||
if (!value) {
|
||
removeTravelReviewItem();
|
||
return;
|
||
}
|
||
|
||
props.model.isBusinessTrip = true;
|
||
travelDialogVisible.value = true;
|
||
if (!travelSegments.value.length) addTravelSegment();
|
||
syncTravelSegmentsToModel();
|
||
}
|
||
|
||
function removeTravelReviewItem() {
|
||
props.model.isBusinessTrip = false;
|
||
props.model.travelSegments = [];
|
||
travelSegments.value = [];
|
||
travelDialogVisible.value = false;
|
||
removeTravelTasksFromReviewItems();
|
||
}
|
||
|
||
function addTravelSegment() {
|
||
travelSegments.value.push({
|
||
dateRange: null,
|
||
days: null,
|
||
workItem: travelWorkItemOptions.value[0] || MY_AFFAIRS_TITLE,
|
||
isNew: true
|
||
});
|
||
}
|
||
|
||
function removeTravelSegment(index: number) {
|
||
travelSegments.value.splice(index, 1);
|
||
}
|
||
|
||
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 -= 1;
|
||
}
|
||
}
|
||
|
||
function addPlanTask(sectionIndex: number) {
|
||
const section = planForm.value.sections[sectionIndex];
|
||
if (!section?.draft.title.trim()) return;
|
||
const task: StructuredTask = {
|
||
title: section.draft.title.trim(),
|
||
detail: section.draft.detail.trim(),
|
||
progress: section.draft.progress
|
||
};
|
||
// 我的事项不参与项目优先级,存进任务前显式剔除,结构化展示括号里就不会出现优先级。
|
||
if (!isMyAffairsPlanItem(planForm.value.workItem)) {
|
||
task.priority = normalizePriorityCode(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: StructuredSection[] = 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 existingSections = getPlanItemSections(sameWorkItem);
|
||
const mergedSections = mergeSectionsByCategory([...existingSections, ...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.findIndex(source => source === item.source);
|
||
if (sourceIndex >= 0) props.model.reviewItems.splice(sourceIndex, 1);
|
||
}
|
||
|
||
function confirmTravelReviewItem() {
|
||
if (!hasCompleteTravelSegment.value) return;
|
||
const groupedTravelTasks = buildGroupedTravelTasks();
|
||
if (!groupedTravelTasks.length) return;
|
||
|
||
removeTravelTasksFromReviewItems();
|
||
|
||
groupedTravelTasks.forEach(({ workItem, tasks }) => {
|
||
const targetItem = props.model.reviewItems.find(item => item.itemTitle.trim() === workItem);
|
||
|
||
if (targetItem) {
|
||
const existingSections = getReviewItemSections(targetItem);
|
||
const mergedSections = appendTasksToSections(
|
||
existingSections,
|
||
tasks,
|
||
existingSections.at(-1)?.category || TRAVEL_SECTION_CATEGORY
|
||
);
|
||
targetItem.contentJson = createSectionsJson(mergedSections);
|
||
targetItem.contentText = createStructuredTextV2(mergedSections, true);
|
||
return;
|
||
}
|
||
|
||
const travelSections: StructuredSection[] = [{ category: TRAVEL_SECTION_CATEGORY, tasks }];
|
||
props.model.reviewItems.push({
|
||
itemNumber: props.model.reviewItems.length + 1,
|
||
itemTitle: workItem,
|
||
workHours: 0,
|
||
contentText: createStructuredTextV2(travelSections, true),
|
||
contentJson: createSectionsJson(travelSections),
|
||
reflectionText: ''
|
||
});
|
||
});
|
||
|
||
syncTravelReviewItem();
|
||
travelDialogVisible.value = false;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/** 编辑态下是否显示"具体工作内容"的结构化预览(含 ElPopover 工作日志) */
|
||
function showContentStructuredView(index: number) {
|
||
const item = reviewItems.value[index];
|
||
if (!item?.contentSections?.length) return false;
|
||
if (isReadonly.value) return true;
|
||
// 编辑/新增模式下,仅在该字段未聚焦时显示结构化预览
|
||
return activeEditField.value !== `content-${index}`;
|
||
}
|
||
|
||
/** 编辑态下是否显示"具体目标"的结构化预览(含 ElPopover 工作日志) */
|
||
function showTargetStructuredView(index: number) {
|
||
const item = nextPlans.value[index];
|
||
if (!item?.targetSections?.length) return false;
|
||
if (isReadonly.value) return true;
|
||
// 编辑/新增模式下,仅在该字段未聚焦时显示结构化预览
|
||
return activeEditField.value !== `target-${index}`;
|
||
}
|
||
|
||
/** 点击结构化预览区域时切换到编辑态并聚焦 */
|
||
function handleStructuredViewClick(fieldKey: string) {
|
||
if (isReadonly.value) return;
|
||
activeEditField.value = fieldKey;
|
||
nextTick(() => {
|
||
const editor = document.querySelector(`[data-field-key="${fieldKey}"]`) as HTMLElement;
|
||
editor?.focus();
|
||
});
|
||
}
|
||
|
||
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 v-model="mainForm.supervisor" disabled />
|
||
</div>
|
||
<div class="field">
|
||
<label>周期</label>
|
||
<ElInput :model-value="periodText" disabled />
|
||
</div>
|
||
<div class="field">
|
||
<label>是否出差</label>
|
||
<ElRadioGroup v-model="businessTripValue" class="radio-group-full" :disabled="isReadonly">
|
||
<ElRadio :value="false" border>否</ElRadio>
|
||
<ElRadio :value="true" border>是</ElRadio>
|
||
</ElRadioGroup>
|
||
</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_HTML }}</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
|
||
v-if="showContentStructuredView(index)"
|
||
class="rich-editor"
|
||
:class="{ 'rich-editor--preview': !isReadonly }"
|
||
@click="handleStructuredViewClick(`content-${index}`)"
|
||
>
|
||
<div
|
||
v-for="(section, sectionIndex) in item.contentSections"
|
||
:key="`${index}-${sectionIndex}`"
|
||
class="rich-section"
|
||
>
|
||
<div class="rich-category-line"># {{ resolveTaskItemTypeLabel(section.category) }}</div>
|
||
<div class="rich-section-tasks">
|
||
<template v-for="(task, taskIndex) in section.tasks" :key="`${index}-${sectionIndex}-${taskIndex}`">
|
||
<ElPopover v-if="task.detail" placement="top-start" trigger="hover" :width="420">
|
||
<template #reference>
|
||
<div class="rich-task-line rich-task-line--interactive">
|
||
{{ formatStructuredTaskDisplayLine(task, taskIndex, true) }}
|
||
</div>
|
||
</template>
|
||
<div class="structured-preview__popover">
|
||
<template v-if="getWorkLogEntries(task.detail).length">
|
||
<div
|
||
v-for="(entry, entryIndex) in getWorkLogEntries(task.detail)"
|
||
:key="`${index}-${sectionIndex}-${taskIndex}-${entryIndex}`"
|
||
class="structured-preview__log-entry"
|
||
>
|
||
<div class="structured-preview__log-text">{{ entry }}</div>
|
||
<div
|
||
v-if="entryIndex < getWorkLogEntries(task.detail).length - 1"
|
||
class="structured-preview__log-divider"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else>暂无内容</template>
|
||
</div>
|
||
</ElPopover>
|
||
<div v-else class="rich-task-line">
|
||
{{ formatStructuredTaskDisplayLine(task, taskIndex, true) }}
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else
|
||
class="rich-editor"
|
||
:contenteditable="!isReadonly"
|
||
spellcheck="false"
|
||
:data-field-key="`content-${index}`"
|
||
: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_HTML }}</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
|
||
v-if="showTargetStructuredView(index)"
|
||
class="rich-editor"
|
||
:class="{ 'rich-editor--preview': !isReadonly }"
|
||
@click="handleStructuredViewClick(`target-${index}`)"
|
||
>
|
||
<div
|
||
v-for="(section, sectionIndex) in item.targetSections"
|
||
:key="`${index}-${sectionIndex}`"
|
||
class="rich-section"
|
||
>
|
||
<div class="rich-category-line"># {{ resolveTaskItemTypeLabel(section.category) }}</div>
|
||
<div class="rich-section-tasks">
|
||
<template v-for="(task, taskIndex) in section.tasks" :key="`${index}-${sectionIndex}-${taskIndex}`">
|
||
<ElPopover v-if="task.detail" placement="top-start" trigger="hover" :width="420">
|
||
<template #reference>
|
||
<div class="rich-task-line rich-task-line--interactive">
|
||
{{ formatStructuredTaskDisplayLine(task, taskIndex) }}
|
||
</div>
|
||
</template>
|
||
<div class="structured-preview__popover">
|
||
<template v-if="getWorkLogEntries(task.detail).length">
|
||
<div
|
||
v-for="(entry, entryIndex) in getWorkLogEntries(task.detail)"
|
||
:key="`${index}-${sectionIndex}-${taskIndex}-${entryIndex}`"
|
||
class="structured-preview__log-entry"
|
||
>
|
||
<div class="structured-preview__log-text">{{ entry }}</div>
|
||
<div
|
||
v-if="entryIndex < getWorkLogEntries(task.detail).length - 1"
|
||
class="structured-preview__log-divider"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else>暂无内容</template>
|
||
</div>
|
||
</ElPopover>
|
||
<div v-else class="rich-task-line">
|
||
{{ formatStructuredTaskDisplayLine(task, taskIndex) }}
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else
|
||
class="rich-editor"
|
||
:contenteditable="!isReadonly"
|
||
spellcheck="false"
|
||
:data-field-key="`target-${index}`"
|
||
: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>
|
||
</div>
|
||
|
||
<div v-if="!isReadonly" class="form-actions">
|
||
<!-- <ElButton>重置表单</ElButton>-->
|
||
<ElButton @click="emit('save')">保存草稿</ElButton>
|
||
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
|
||
</div>
|
||
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
|
||
<ElButton @click="emit('back')">退出审批</ElButton>
|
||
<ElButton type="primary" @click="emit('requestApprove')">开始审批</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"
|
||
size="small"
|
||
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 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>
|
||
|
||
<BusinessFormDialog
|
||
v-model="travelDialogVisible"
|
||
title="新增本周差旅"
|
||
preset="md"
|
||
confirm-text="确认新增"
|
||
:confirm-disabled="!hasCompleteTravelSegment"
|
||
@confirm="confirmTravelReviewItem"
|
||
@cancel="businessTripValue = false"
|
||
>
|
||
<div class="travel-dialog-form">
|
||
<div class="travel-summary">
|
||
<span>出差总天数</span>
|
||
<strong>{{ totalTravelDays }} 天</strong>
|
||
</div>
|
||
<div v-for="(segment, segmentIndex) in travelSegments" :key="segmentIndex" class="travel-segment">
|
||
<div class="travel-segment-head">
|
||
<span class="row-index">{{ segmentIndex + 1 }}</span>
|
||
<strong>分段出差</strong>
|
||
<ElButton v-if="segment.isNew" link type="danger" @click="removeTravelSegment(segmentIndex)">删除</ElButton>
|
||
</div>
|
||
<div class="travel-grid">
|
||
<div class="field">
|
||
<label>工作事项</label>
|
||
<ElSelect
|
||
v-model="segment.workItem"
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
clearable
|
||
placeholder="请选择或输入工作事项"
|
||
>
|
||
<ElOption v-for="item in travelWorkItemOptions" :key="item" :label="item" :value="item" />
|
||
</ElSelect>
|
||
</div>
|
||
<div class="field travel-cycle-field">
|
||
<label>出差周期</label>
|
||
<ElDatePicker
|
||
v-model="segment.dateRange"
|
||
type="daterange"
|
||
value-format="YYYY-MM-DD"
|
||
range-separator="-"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
style="width: 100%"
|
||
@change="syncSegmentDays(segmentIndex)"
|
||
/>
|
||
</div>
|
||
<div class="field">
|
||
<label>天数</label>
|
||
<ElInputNumber
|
||
v-model="segment.days"
|
||
:min="0.5"
|
||
:step="0.5"
|
||
step-strictly
|
||
:precision="1"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
@change="value => handleTravelDaysChange(segmentIndex, value)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<ElButton
|
||
class="travel-add-btn dialog-inline-action"
|
||
type="primary"
|
||
plain
|
||
size="small"
|
||
@click="addTravelSegment"
|
||
>
|
||
<ElIcon><Plus /></ElIcon>
|
||
<span>分段</span>
|
||
</ElButton>
|
||
</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;
|
||
}
|
||
|
||
.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(6, 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 {
|
||
display: grid;
|
||
grid-column: 3 / span 2;
|
||
grid-row: 1;
|
||
grid-template-columns: minmax(0, calc(57.45% - 6px + 50px)) minmax(0, calc(42.55% - 6px - 50px));
|
||
gap: 12px;
|
||
border-left: 1px solid #e5edf1;
|
||
}
|
||
|
||
.plan-editor-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, calc(57.45% - 6px + 50px)) minmax(0, calc(42.55% - 6px - 50px));
|
||
gap: 12px;
|
||
}
|
||
|
||
.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-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) {
|
||
display: grid;
|
||
gap: 6px;
|
||
padding-left: 0;
|
||
color: #334155;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task-line--interactive) {
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 编辑态下结构化预览区域:点击可切换到编辑模式 */
|
||
.rich-editor--preview {
|
||
cursor: text;
|
||
}
|
||
|
||
.rich-editor--preview:hover {
|
||
border-color: var(--el-color-primary);
|
||
}
|
||
|
||
.structured-preview__popover {
|
||
max-width: 100%;
|
||
color: #334155;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
display: grid;
|
||
gap: 0;
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.structured-preview__log-entry {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.structured-preview__log-text {
|
||
white-space: pre-wrap;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.structured-preview__log-divider {
|
||
width: 100%;
|
||
height: 1px;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task) {
|
||
display: grid;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task-head) {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task + .rich-task) {
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed #cbd5e1;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task-title),
|
||
.rich-editor :deep(.rich-section-title) {
|
||
position: relative;
|
||
min-width: 0;
|
||
flex: 1;
|
||
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-task-metas) {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 5px;
|
||
flex-wrap: wrap;
|
||
flex: 0 0 auto;
|
||
user-select: none;
|
||
cursor: default;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task-meta) {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 20px;
|
||
padding: 0 6px;
|
||
border-radius: 999px;
|
||
background: #f3f7f9;
|
||
color: #475467;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task-meta.priority) {
|
||
background: #fff7ed;
|
||
color: #c2410c;
|
||
}
|
||
|
||
.rich-editor :deep(.rich-task-title::before),
|
||
.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-task-detail) {
|
||
padding-left: 14px;
|
||
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-task {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.structured-task + .structured-task {
|
||
padding-top: 10px;
|
||
border-top: 1px dashed #cbd5e1;
|
||
}
|
||
|
||
.structured-task-title {
|
||
position: relative;
|
||
padding-left: 14px;
|
||
color: var(--el-color-primary);
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.structured-task-title::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 4px;
|
||
width: 4px;
|
||
height: 16px;
|
||
border-radius: 999px;
|
||
background: var(--el-color-primary);
|
||
}
|
||
|
||
.structured-task-detail {
|
||
color: #334155;
|
||
padding-left: 14px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.travel-summary {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 14px;
|
||
border-radius: 10px;
|
||
background: var(--el-color-primary-light-9);
|
||
color: var(--el-color-primary);
|
||
font-size: 13px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.plan-dialog-form,
|
||
.travel-dialog-form {
|
||
display: grid;
|
||
gap: 16px;
|
||
}
|
||
|
||
.plan-dialog-form > .field,
|
||
.travel-dialog-form > .field {
|
||
gap: 8px;
|
||
}
|
||
|
||
.travel-segment {
|
||
display: grid;
|
||
gap: 14px;
|
||
padding: 14px;
|
||
border: 1px solid #e5edf1;
|
||
border-radius: 12px;
|
||
background: #fbfdfe;
|
||
}
|
||
|
||
.travel-segment-head {
|
||
display: grid;
|
||
grid-template-columns: 34px minmax(0, 1fr) auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.travel-segment-head strong {
|
||
color: #14213d;
|
||
}
|
||
|
||
.travel-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(200px, 1fr) minmax(280px, 1.2fr) 120px;
|
||
gap: 16px;
|
||
align-items: start;
|
||
}
|
||
|
||
.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: 4px;
|
||
padding: 9px 12px;
|
||
border: 1px solid #eef2f6;
|
||
border-radius: 9px;
|
||
background: #f8fbfc;
|
||
}
|
||
|
||
.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-detail {
|
||
color: #334155;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.plan-task-form {
|
||
display: grid;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border: 1px dashed #cbd5e1;
|
||
border-radius: 10px;
|
||
background: #f8fbfc;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.travel-dialog-form :deep(.el-input__wrapper),
|
||
.travel-dialog-form :deep(.el-select__wrapper),
|
||
.travel-dialog-form :deep(.el-input-number),
|
||
.travel-dialog-form :deep(.el-input-number .el-input__wrapper),
|
||
.travel-dialog-form :deep(.el-date-editor.el-input__wrapper) {
|
||
min-height: 36px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.travel-dialog-form :deep(.el-input-number) {
|
||
width: 100%;
|
||
border-radius: 8px;
|
||
box-sizing: border-box;
|
||
background: #fff;
|
||
border: 1px solid var(--el-border-color);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.travel-dialog-form :deep(.el-input-number:focus-within) {
|
||
border-color: var(--el-color-primary);
|
||
}
|
||
|
||
.travel-dialog-form :deep(.el-input-number .el-input__wrapper) {
|
||
box-shadow: none !important;
|
||
background: transparent;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.travel-dialog-form :deep(.el-input-number.is-controls-right .el-input-number__increase),
|
||
.travel-dialog-form :deep(.el-input-number.is-controls-right .el-input-number__decrease) {
|
||
right: 0;
|
||
height: 18px;
|
||
}
|
||
|
||
.travel-dialog-form :deep(.el-input-number.is-controls-right .el-input-number__increase) {
|
||
top: 0;
|
||
}
|
||
|
||
.travel-dialog-form :deep(.el-input-number.is-controls-right .el-input-number__decrease) {
|
||
bottom: 0;
|
||
}
|
||
|
||
.inline-plan-name-input {
|
||
width: 100%;
|
||
min-width: 260px;
|
||
}
|
||
|
||
.travel-add-btn {
|
||
justify-self: flex-end;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.approval-form-actions {
|
||
position: sticky;
|
||
z-index: 5;
|
||
bottom: 0;
|
||
margin-top: auto;
|
||
margin-bottom: 0;
|
||
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,
|
||
.travel-grid {
|
||
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>
|