2026-06-11 10:56:24 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
/* eslint-disable no-useless-escape */
|
2026-06-13 13:06:39 +08:00
|
|
|
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
|
|
|
|
|
import type { FormRules } from 'element-plus';
|
2026-06-11 10:56:24 +08:00
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
|
2026-07-07 16:51:34 +08:00
|
|
|
|
import { fetchPerformanceSheetPage } from '@/service/api';
|
2026-06-13 13:06:39 +08:00
|
|
|
|
import { useForm, useFormRules } from '@/hooks/common/form';
|
2026-06-11 10:56:24 +08:00
|
|
|
|
import { useDict } from '@/hooks/business/dict';
|
|
|
|
|
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
2026-07-07 16:51:34 +08:00
|
|
|
|
import PerformanceExcelEditorDrawer from '../../../my-performance/modules/performance-excel-editor-drawer.vue';
|
2026-06-11 10:56:24 +08:00
|
|
|
|
import {
|
|
|
|
|
|
type WorkReportStructuredSection,
|
|
|
|
|
|
type WorkReportStructuredTask,
|
|
|
|
|
|
getStructuredSections
|
|
|
|
|
|
} from '../../shared/types';
|
|
|
|
|
|
import { formatPeriodLabel } from '../../shared/types';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
reportType: string;
|
|
|
|
|
|
period: string;
|
|
|
|
|
|
mode?: 'add' | 'edit' | 'detail';
|
2026-07-19 18:21:14 +08:00
|
|
|
|
scene?: 'fill' | 'detail' | 'approval' | 'sign-off';
|
2026-06-11 10:56:24 +08:00
|
|
|
|
baseInfo?: Api.WorkReport.Monthly.MonthlyReport | null;
|
|
|
|
|
|
model: Api.WorkReport.Monthly.MonthlyReportSaveParams;
|
|
|
|
|
|
approvalModel: Api.WorkReport.Monthly.MonthlyReportApproveParams;
|
2026-07-19 18:21:14 +08:00
|
|
|
|
supervisorSignaturePreviewUrl?: string;
|
|
|
|
|
|
supervisorSignatureLoading?: boolean;
|
|
|
|
|
|
employeeSignaturePreviewUrl?: string;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
back: [];
|
|
|
|
|
|
requestApprove: [];
|
|
|
|
|
|
requestReject: [];
|
2026-07-19 18:21:14 +08:00
|
|
|
|
requestConfirmSignOff: [];
|
2026-06-11 10:56:24 +08:00
|
|
|
|
changeApproval: [payload: Api.WorkReport.Monthly.MonthlyReportApproveParams];
|
|
|
|
|
|
viewAudit: [];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const { formRef: pageFormRef, validate: validatePageForm } = useForm();
|
|
|
|
|
|
const { formRef: auditFormRef } = useForm();
|
|
|
|
|
|
const { createRequiredRule } = useFormRules();
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
interface ReviewItem {
|
|
|
|
|
|
workItem: string;
|
|
|
|
|
|
days: number;
|
|
|
|
|
|
hours?: number;
|
|
|
|
|
|
content: string;
|
|
|
|
|
|
contentHtml: string;
|
|
|
|
|
|
contentSections?: StructuredSection[];
|
|
|
|
|
|
reflection: string;
|
|
|
|
|
|
removable?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PlanItem {
|
|
|
|
|
|
workItem: string;
|
|
|
|
|
|
target: string;
|
|
|
|
|
|
targetHtml: string;
|
|
|
|
|
|
targetSections?: StructuredSection[];
|
|
|
|
|
|
supportNeed: string;
|
|
|
|
|
|
removable?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface StructuredTask {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
priority?: string;
|
|
|
|
|
|
progress?: number;
|
|
|
|
|
|
hours?: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface StructuredSection {
|
|
|
|
|
|
category: string;
|
|
|
|
|
|
tasks: StructuredTask[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formTitle = computed(() => `${props.reportType || '个人月报'}审批页`);
|
|
|
|
|
|
const EMPTY_TEXT = '';
|
|
|
|
|
|
const { getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE);
|
|
|
|
|
|
const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
|
|
|
|
|
const todayText = computed(() => dayjs().format('YYYY-MM-DD'));
|
2026-07-07 16:51:34 +08:00
|
|
|
|
const performanceDrawerVisible = ref(false);
|
|
|
|
|
|
const performanceDrawerLoading = ref(false);
|
|
|
|
|
|
const performanceDrawerMode = ref<'create' | 'edit'>('create');
|
|
|
|
|
|
const performanceDrawerRow = ref<Api.Performance.Sheet.Sheet | null>(null);
|
2026-07-19 18:21:14 +08:00
|
|
|
|
const isSignOffScene = computed(() => props.scene === 'sign-off');
|
|
|
|
|
|
const hasSupervisorSignaturePreview = computed(() => Boolean(props.supervisorSignaturePreviewUrl));
|
|
|
|
|
|
const hasEmployeeSignaturePreview = computed(() => Boolean(props.employeeSignaturePreviewUrl));
|
2026-06-11 10:56:24 +08:00
|
|
|
|
|
|
|
|
|
|
const auditDialogVisible = ref(false);
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const auditForm = reactive({
|
2026-06-11 10:56:24 +08:00
|
|
|
|
conclusion: '通过',
|
|
|
|
|
|
opinion: ''
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const rejectOpinionRequired = computed(() => auditForm.conclusion === '退回');
|
|
|
|
|
|
const pageValidationModel = reactive({
|
|
|
|
|
|
get meetingDate() {
|
|
|
|
|
|
return props.approvalModel.meetingDate || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
get performanceResult() {
|
|
|
|
|
|
return props.approvalModel.performanceResult || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
function formatDisplayDate(value: unknown) {
|
|
|
|
|
|
const text = String(value ?? '')
|
|
|
|
|
|
.replace(/^\[\s*/u, '')
|
|
|
|
|
|
.replace(/\s*\]$/u, '')
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
if (!text) return '--';
|
|
|
|
|
|
|
|
|
|
|
|
const commaDateMatch = text.match(/^(\d{4})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})$/u);
|
|
|
|
|
|
if (commaDateMatch) {
|
|
|
|
|
|
const [, year, month, day] = commaDateMatch;
|
|
|
|
|
|
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const pageRules = computed<FormRules>(() => {
|
|
|
|
|
|
if (isSignOffScene.value) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
meetingDate: [createRequiredRule('请选择面谈日期')]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
performanceResult: [
|
|
|
|
|
|
createRequiredRule('请输入绩效考核结果'),
|
|
|
|
|
|
{
|
|
|
|
|
|
validator: (_rule, value: string, callback) => {
|
|
|
|
|
|
if (!value?.trim()) {
|
|
|
|
|
|
callback(new Error('请输入绩效考核结果'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
|
},
|
|
|
|
|
|
trigger: 'blur'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const auditRules = computed<FormRules>(() => ({
|
|
|
|
|
|
opinion: rejectOpinionRequired.value
|
|
|
|
|
|
? [
|
|
|
|
|
|
createRequiredRule('请输入退回原因'),
|
|
|
|
|
|
{
|
|
|
|
|
|
validator: (_rule, value: string, callback) => {
|
|
|
|
|
|
if (!value?.trim()) {
|
|
|
|
|
|
callback(new Error('请输入退回原因'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
|
},
|
|
|
|
|
|
trigger: 'blur'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
: []
|
|
|
|
|
|
}));
|
2026-06-11 10:56:24 +08:00
|
|
|
|
|
|
|
|
|
|
function patchApproval<K extends keyof Api.WorkReport.Monthly.MonthlyReportApproveParams>(
|
|
|
|
|
|
key: K,
|
|
|
|
|
|
value: Api.WorkReport.Monthly.MonthlyReportApproveParams[K]
|
|
|
|
|
|
) {
|
|
|
|
|
|
emit('changeApproval', {
|
|
|
|
|
|
...props.approvalModel,
|
|
|
|
|
|
[key]: value
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const feedbackForm = reactive({
|
|
|
|
|
|
get strengthDesc() {
|
|
|
|
|
|
return props.approvalModel.strengthDesc || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set strengthDesc(value: string) {
|
|
|
|
|
|
patchApproval('strengthDesc', value);
|
|
|
|
|
|
},
|
|
|
|
|
|
get strengthExample() {
|
|
|
|
|
|
return props.approvalModel.strengthExample || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set strengthExample(value: string) {
|
|
|
|
|
|
patchApproval('strengthExample', value);
|
|
|
|
|
|
},
|
|
|
|
|
|
get weaknessDesc() {
|
|
|
|
|
|
return props.approvalModel.weaknessDesc || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set weaknessDesc(value: string) {
|
|
|
|
|
|
patchApproval('weaknessDesc', value);
|
|
|
|
|
|
},
|
|
|
|
|
|
get weaknessExample() {
|
|
|
|
|
|
return props.approvalModel.weaknessExample || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set weaknessExample(value: string) {
|
|
|
|
|
|
patchApproval('weaknessExample', value);
|
|
|
|
|
|
},
|
|
|
|
|
|
get improvementSuggestion() {
|
|
|
|
|
|
return props.approvalModel.improvementSuggestion || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set improvementSuggestion(value: string) {
|
|
|
|
|
|
patchApproval('improvementSuggestion', value);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const performanceForm = reactive({
|
|
|
|
|
|
get score() {
|
|
|
|
|
|
return props.approvalModel.performanceResult || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set score(value: string) {
|
|
|
|
|
|
patchApproval('performanceResult', value);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-07 16:51:34 +08:00
|
|
|
|
const performanceTargetOption = computed(() => {
|
|
|
|
|
|
const reporterId = props.baseInfo?.reporterId || '';
|
|
|
|
|
|
const reporterName = props.baseInfo?.reporterName || '';
|
|
|
|
|
|
|
|
|
|
|
|
if (!reporterId || !reporterName) return [];
|
|
|
|
|
|
|
|
|
|
|
|
return [{ label: reporterName, value: reporterId }];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const performanceInitialEmployeeId = computed(() => props.baseInfo?.reporterId || '');
|
|
|
|
|
|
const performanceInitialPeriodMonth = computed(() => {
|
|
|
|
|
|
const periodDate = props.baseInfo?.periodStartDate || props.baseInfo?.periodEndDate || '';
|
|
|
|
|
|
if (!periodDate) return '';
|
|
|
|
|
|
|
|
|
|
|
|
const targetMonth = dayjs(periodDate);
|
|
|
|
|
|
return targetMonth.isValid() ? targetMonth.format('YYYY-MM') : '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
/** 绩效分数输入限制:0-100,最多一位小数 */
|
|
|
|
|
|
function handlePerformanceScoreInput(value: string) {
|
|
|
|
|
|
// 只允许数字和一个小数点
|
|
|
|
|
|
let filtered = value.replace(/[^\d.]/g, '');
|
|
|
|
|
|
// 只保留第一个小数点
|
|
|
|
|
|
const dotIndex = filtered.indexOf('.');
|
|
|
|
|
|
if (dotIndex !== -1) {
|
|
|
|
|
|
filtered = filtered.slice(0, dotIndex + 1) + filtered.slice(dotIndex + 1).replace(/\./g, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
// 最多一位小数
|
|
|
|
|
|
const match = filtered.match(/^(\d+)(?:\.(\d{0,1}))?/);
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
filtered = match[1] + (match[2] !== undefined ? `.${match[2]}` : '');
|
|
|
|
|
|
}
|
|
|
|
|
|
// 限制 0-100
|
|
|
|
|
|
if (filtered !== '' && filtered !== '.') {
|
|
|
|
|
|
const num = Number(filtered);
|
|
|
|
|
|
if (num > 100) {
|
|
|
|
|
|
filtered = '100';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 防止前导零(如 007),但允许 0 和 0.x
|
|
|
|
|
|
if (/^0\d/.test(filtered) && !filtered.startsWith('0.')) {
|
|
|
|
|
|
filtered = filtered.replace(/^0+/, '') || '0';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (filtered !== value) {
|
|
|
|
|
|
performanceForm.score = filtered;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 || '--';
|
|
|
|
|
|
},
|
|
|
|
|
|
get meetingDate() {
|
|
|
|
|
|
return props.approvalModel.meetingDate || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set meetingDate(value: string) {
|
|
|
|
|
|
patchApproval('meetingDate', value);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const signatureForm = reactive({
|
|
|
|
|
|
get employeeDate() {
|
2026-07-19 18:21:14 +08:00
|
|
|
|
return props.approvalModel.employeeSignedDate || '';
|
2026-06-11 10:56:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
set employeeDate(value: string) {
|
|
|
|
|
|
patchApproval('employeeSignedDate', value);
|
|
|
|
|
|
},
|
|
|
|
|
|
get supervisorDate() {
|
|
|
|
|
|
return props.approvalModel.supervisorSignedDate || todayText.value;
|
|
|
|
|
|
},
|
|
|
|
|
|
set supervisorDate(value: string) {
|
|
|
|
|
|
patchApproval('supervisorSignedDate', value);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeTask(task: WorkReportStructuredTask): StructuredTask {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: task.title || '',
|
|
|
|
|
|
priority: normalizePriorityCode(task.priority),
|
|
|
|
|
|
progress: typeof task.progress === 'number' ? task.progress : undefined,
|
|
|
|
|
|
hours: typeof task.hours === 'number' ? task.hours : undefined
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizePriorityCode(value?: string | number | null) {
|
|
|
|
|
|
const text = String(value ?? '').trim();
|
|
|
|
|
|
if (!text) return undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const matchedByValue = priorityDictData.value.find(item => String(item.value) === text);
|
|
|
|
|
|
if (matchedByValue) return String(matchedByValue.value);
|
|
|
|
|
|
|
|
|
|
|
|
const matchedByLabel = priorityDictData.value.find(item => item.label === text);
|
|
|
|
|
|
if (matchedByLabel) return String(matchedByLabel.value);
|
|
|
|
|
|
|
|
|
|
|
|
const priorityLabelMatch = text.match(/^P(\d+)$/iu);
|
|
|
|
|
|
if (priorityLabelMatch) return priorityLabelMatch[1];
|
|
|
|
|
|
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolvePriorityLabel(value?: string | number | null) {
|
|
|
|
|
|
const priorityCode = normalizePriorityCode(value);
|
|
|
|
|
|
if (!priorityCode) return '';
|
|
|
|
|
|
return getPriorityLabel(priorityCode, {
|
|
|
|
|
|
fallback: /^P\d+$/iu.test(priorityCode) ? priorityCode : `P${priorityCode}`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTaskItemTypeLabel(value: string) {
|
|
|
|
|
|
return getTaskItemTypeLabel(value, { fallback: value || '工作内容' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeSection(section: WorkReportStructuredSection): StructuredSection {
|
|
|
|
|
|
return {
|
|
|
|
|
|
category: resolveTaskItemTypeLabel(section.category || '工作内容'),
|
|
|
|
|
|
tasks: section.tasks.map(normalizeTask)
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mergeSectionsByCategory(sections: StructuredSection[]) {
|
|
|
|
|
|
const sectionMap = new Map<string, StructuredSection>();
|
|
|
|
|
|
|
|
|
|
|
|
sections.forEach(section => {
|
|
|
|
|
|
const category = section.category || '未分类';
|
|
|
|
|
|
const existing = sectionMap.get(category);
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
|
existing.tasks.push(...section.tasks);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sectionMap.set(category, { category, tasks: [...section.tasks] });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return Array.from(sectionMap.values());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function escapeHtml(value: string) {
|
|
|
|
|
|
return value
|
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
function stripStructuredTaskPrefixV2(value: string) {
|
|
|
|
|
|
return value.trim().replace(/^\d+[..、]\s*/u, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stripStructuredTaskSuffixV2(value: string) {
|
|
|
|
|
|
return value.trim().replace(/[。.!!??]+$/u, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTaskMetricsV2(metricsText: string) {
|
|
|
|
|
|
const parts = metricsText
|
|
|
|
|
|
.split('/')
|
|
|
|
|
|
.map(item => item.trim())
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
const priority = normalizePriorityCode(parts.find(item => /^P?\d+$/iu.test(item)));
|
|
|
|
|
|
const progressText =
|
|
|
|
|
|
metricsText.match(/进度\s*(\d+(?:\.\d+)?)%/u)?.[1] ||
|
|
|
|
|
|
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));
|
2026-06-11 10:56:24 +08:00
|
|
|
|
const metrics = [
|
2026-06-13 13:06:39 +08:00
|
|
|
|
task.priority ? resolvePriorityLabel(task.priority) : '',
|
|
|
|
|
|
typeof task.progress === 'number' ? `${task.progress}%` : '',
|
2026-06-11 10:56:24 +08:00
|
|
|
|
showHours && typeof task.hours === 'number' ? `${task.hours}h` : ''
|
|
|
|
|
|
].filter(Boolean);
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
return `${title}${metrics.length ? `(${metrics.join(' / ')})` : ''}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createStructuredHtmlV2(sections: StructuredSection[], showHours = false) {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
return sections
|
|
|
|
|
|
.map(section => {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const categoryLabel = resolveTaskItemTypeLabel(section.category.trim());
|
2026-06-11 10:56:24 +08:00
|
|
|
|
const tasks = section.tasks
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.map(
|
|
|
|
|
|
(task, index) =>
|
|
|
|
|
|
`<div class="rich-task-line">${escapeHtml(`${index + 1}、${formatStructuredTaskLineV2(task, showHours)}`)}</div>`
|
|
|
|
|
|
)
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.join('');
|
2026-06-13 13:06:39 +08:00
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
return `
|
|
|
|
|
|
<div class="rich-section">
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="rich-category-line"># ${escapeHtml(categoryLabel)}</div>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
${tasks ? `<div class="rich-section-tasks">${tasks}</div>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
})
|
|
|
|
|
|
.join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
function createStructuredSectionsFromTextV2(text: string, defaultCategory: string): StructuredSection[] {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
const lines = normalizeEditorText(text).split('\n').filter(Boolean);
|
|
|
|
|
|
if (!lines.length || lines.join('\n') === EMPTY_TEXT) return [];
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const sections: StructuredSection[] = [];
|
|
|
|
|
|
const ensureSection = (categoryText: string) => {
|
|
|
|
|
|
const category = categoryText.trim() || defaultCategory;
|
|
|
|
|
|
let section = sections.find(item => item.category === category);
|
|
|
|
|
|
if (!section) {
|
|
|
|
|
|
section = { category, tasks: [] };
|
|
|
|
|
|
sections.push(section);
|
|
|
|
|
|
}
|
|
|
|
|
|
return section;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let currentCategory = '';
|
2026-06-11 10:56:24 +08:00
|
|
|
|
|
|
|
|
|
|
lines.forEach(line => {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const trimmedLine = line.trim();
|
|
|
|
|
|
if (!trimmedLine) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (trimmedLine.startsWith('#')) {
|
|
|
|
|
|
currentCategory = trimmedLine.replace(/^#\s*/u, '').trim();
|
|
|
|
|
|
if (currentCategory) ensureSection(currentCategory);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-11 10:56:24 +08:00
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const legacyMatch = trimmedLine.match(/^(.+?)\s*-\s*(.+?)(?:[((]([^()()]*)[))])?[。.!!??]*$/u);
|
|
|
|
|
|
if (legacyMatch) {
|
|
|
|
|
|
const [, rawCategory, rawTitle, metricsText = ''] = legacyMatch;
|
|
|
|
|
|
const category = rawCategory.trim();
|
|
|
|
|
|
const title = rawTitle.trim();
|
|
|
|
|
|
if (!category || !title) return;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
ensureSection(category).tasks.push({
|
|
|
|
|
|
title,
|
|
|
|
|
|
...resolveTaskMetricsV2(metricsText)
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
const normalizedLine = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(trimmedLine));
|
|
|
|
|
|
if (!normalizedLine) return;
|
|
|
|
|
|
const inlineMatch = normalizedLine.match(/^(.*?)(?:[((]([^()()]*)[))])?$/u);
|
|
|
|
|
|
const title = inlineMatch?.[1]?.trim() || '';
|
|
|
|
|
|
const metricsText = inlineMatch?.[2]?.trim() || '';
|
|
|
|
|
|
if (!title) return;
|
|
|
|
|
|
|
|
|
|
|
|
ensureSection(currentCategory || defaultCategory).tasks.push({
|
2026-06-11 10:56:24 +08:00
|
|
|
|
title,
|
2026-06-13 13:06:39 +08:00
|
|
|
|
...resolveTaskMetricsV2(metricsText)
|
2026-06-11 10:56:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
return sections.filter(section => section.tasks.length);
|
|
|
|
|
|
}
|
2026-06-11 10:56:24 +08:00
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
function normalizeEditorText(value: string) {
|
|
|
|
|
|
return value
|
|
|
|
|
|
.replace(/\u00A0/g, ' ')
|
|
|
|
|
|
.split('\n')
|
|
|
|
|
|
.map(line => line.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join('\n');
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): ReviewItem {
|
|
|
|
|
|
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.contentJson).map(normalizeSection));
|
|
|
|
|
|
const contentSections = structuredSections.length
|
|
|
|
|
|
? structuredSections
|
2026-06-13 13:06:39 +08:00
|
|
|
|
: createStructuredSectionsFromTextV2(item.contentText || '', item.itemTitle || '未分类');
|
2026-06-11 10:56:24 +08:00
|
|
|
|
const contentHtml = contentSections.length
|
2026-06-13 13:06:39 +08:00
|
|
|
|
? createStructuredHtmlV2(contentSections, true)
|
2026-06-11 10:56:24 +08:00
|
|
|
|
: escapeHtml(item.contentText || '').replace(/\n/g, '<br>') || EMPTY_TEXT;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
workItem: item.itemTitle || '未命名工作',
|
|
|
|
|
|
days: 0,
|
|
|
|
|
|
hours: item.workHours || 0,
|
|
|
|
|
|
content: item.contentText || EMPTY_TEXT,
|
|
|
|
|
|
contentHtml,
|
|
|
|
|
|
contentSections,
|
|
|
|
|
|
reflection: item.reflectionText || EMPTY_TEXT,
|
|
|
|
|
|
removable: false
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toPlanItem(item: Api.WorkReport.Common.PersonalReportPlanItem): PlanItem {
|
|
|
|
|
|
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.targetJson).map(normalizeSection));
|
|
|
|
|
|
const targetSections = structuredSections.length
|
|
|
|
|
|
? structuredSections
|
2026-06-13 13:06:39 +08:00
|
|
|
|
: createStructuredSectionsFromTextV2(item.targetText || '', item.itemTitle || '未分类');
|
2026-06-11 10:56:24 +08:00
|
|
|
|
const targetHtml = targetSections.length
|
2026-06-13 13:06:39 +08:00
|
|
|
|
? createStructuredHtmlV2(targetSections)
|
2026-06-11 10:56:24 +08:00
|
|
|
|
: escapeHtml(item.targetText || '').replace(/\n/g, '<br>') || EMPTY_TEXT;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
workItem: item.itemTitle || '未命名计划',
|
|
|
|
|
|
target: item.targetText || EMPTY_TEXT,
|
|
|
|
|
|
targetHtml,
|
|
|
|
|
|
targetSections,
|
|
|
|
|
|
supportNeed: item.supportNeed || EMPTY_TEXT,
|
|
|
|
|
|
removable: false
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 formatTaskTitle(title: string) {
|
|
|
|
|
|
const rawTitle = title.trim().replace(/^\d+[.、.]\s*/, '');
|
|
|
|
|
|
return rawTitle.replace(/[。.!!??]$/, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTaskPunctuation(title: string) {
|
|
|
|
|
|
const rawTitle = title.trim().replace(/^\d+[.、.]\s*/, '');
|
|
|
|
|
|
return rawTitle.match(/[。.!!??]$/)?.[0] || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
async function submitAudit() {
|
|
|
|
|
|
if (!auditForm.conclusion) {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
window.$message?.warning('请选择审批结论');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-13 13:06:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 仅"通过"时才校验面谈时间、绩效考核结果等必填项;"退回"时跳过这些校验。
|
|
|
|
|
|
if (auditForm.conclusion !== '退回') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await validatePageForm();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rejectOpinionRequired.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await auditFormRef.value?.validateField?.('opinion');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
patchApproval('employeeSignName', undefined);
|
|
|
|
|
|
patchApproval('employeeSignedDate', undefined);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
patchApproval('supervisorSignedDate', signatureForm.supervisorDate);
|
2026-06-13 13:06:39 +08:00
|
|
|
|
patchApproval(
|
|
|
|
|
|
'reason',
|
|
|
|
|
|
rejectOpinionRequired.value ? auditForm.opinion.trim() : auditForm.opinion.trim() || auditForm.conclusion
|
|
|
|
|
|
);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
patchApproval('meetingDate', mainForm.meetingDate);
|
|
|
|
|
|
auditDialogVisible.value = false;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
if (auditForm.conclusion === '退回') {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
emit('requestReject');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
emit('requestApprove');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openAuditDialog() {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
Object.assign(auditForm, {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
conclusion: '通过',
|
|
|
|
|
|
opinion: ''
|
|
|
|
|
|
});
|
|
|
|
|
|
auditDialogVisible.value = true;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
auditFormRef.value?.clearValidate();
|
|
|
|
|
|
});
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
2026-06-13 13:06:39 +08:00
|
|
|
|
|
2026-07-07 16:51:34 +08:00
|
|
|
|
async function openPerformanceDrawer() {
|
|
|
|
|
|
const reporterId = props.baseInfo?.reporterId || '';
|
|
|
|
|
|
const periodMonth = performanceInitialPeriodMonth.value;
|
|
|
|
|
|
|
|
|
|
|
|
performanceDrawerMode.value = 'create';
|
|
|
|
|
|
performanceDrawerRow.value = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (reporterId && periodMonth) {
|
|
|
|
|
|
performanceDrawerLoading.value = true;
|
|
|
|
|
|
const { error, data } = await fetchPerformanceSheetPage({
|
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
|
pageSize: 1,
|
|
|
|
|
|
employeeId: reporterId,
|
|
|
|
|
|
periodMonthRange: [periodMonth, periodMonth]
|
|
|
|
|
|
});
|
|
|
|
|
|
performanceDrawerLoading.value = false;
|
|
|
|
|
|
|
|
|
|
|
|
const existingSheet = !error && data?.list?.length ? data.list[0] : null;
|
|
|
|
|
|
if (existingSheet) {
|
|
|
|
|
|
performanceDrawerMode.value = 'edit';
|
|
|
|
|
|
performanceDrawerRow.value = existingSheet;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
performanceDrawerVisible.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePerformanceSaved(payload: { actualScoreTotal: string }) {
|
|
|
|
|
|
patchApproval('performanceResult', payload.actualScoreTotal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
async function handleConfirmSignOff() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await validatePageForm();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
emit('requestConfirmSignOff');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
watch(rejectOpinionRequired, async () => {
|
|
|
|
|
|
if (!auditDialogVisible.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
auditFormRef.value?.clearValidate('opinion');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => auditForm.opinion,
|
|
|
|
|
|
value => {
|
|
|
|
|
|
if (!rejectOpinionRequired.value || !value?.trim()) return;
|
|
|
|
|
|
auditFormRef.value?.clearValidate('opinion');
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="card form-page">
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<ElForm
|
|
|
|
|
|
ref="pageFormRef"
|
|
|
|
|
|
:model="pageValidationModel"
|
|
|
|
|
|
:rules="pageRules"
|
|
|
|
|
|
label-position="top"
|
|
|
|
|
|
:validate-on-rule-change="false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<span>基础信息</span>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<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 class="field field-form-item">
|
2026-07-07 16:51:34 +08:00
|
|
|
|
<label>面谈时间</label>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<ElFormItem class="field-inline-form-item" prop="meetingDate">
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<ElDatePicker
|
|
|
|
|
|
v-model="mainForm.meetingDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</ElFormItem>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<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>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</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>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="review-name-cell">
|
|
|
|
|
|
<div class="work-title-line">
|
|
|
|
|
|
<strong>{{ item.workItem }}</strong>
|
|
|
|
|
|
<span>共 {{ item.hours }}h</span>
|
|
|
|
|
|
</div>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="review-action-cell"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="review-editor-grid">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>具体工作内容及成果描述</label>
|
|
|
|
|
|
<div class="rich-editor readonly" v-html="item.contentHtml"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>工作感悟</label>
|
|
|
|
|
|
<div class="rich-editor readonly">{{ item.reflection || '' }}</div>
|
|
|
|
|
|
</div>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<span>当期工作反馈</span>
|
|
|
|
|
|
<ElTag type="info" size="small" round>直接上级/分管领导填写</ElTag>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="feedback-table">
|
|
|
|
|
|
<div class="feedback-header">
|
|
|
|
|
|
<div class="feedback-cell header-cell">优势/劣势项</div>
|
|
|
|
|
|
<div class="feedback-cell header-cell">优势/劣势描述</div>
|
|
|
|
|
|
<div class="feedback-cell header-cell">行为事例</div>
|
|
|
|
|
|
<div class="feedback-cell header-cell">改进提升建议</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="feedback-body">
|
|
|
|
|
|
<div class="feedback-left">
|
|
|
|
|
|
<div class="feedback-row-content">
|
|
|
|
|
|
<div class="feedback-cell label-cell">
|
|
|
|
|
|
<span class="feedback-tag success">优势</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="feedback-cell">
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
|
|
|
|
|
|
{{ feedbackForm.strengthDesc || '' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ElInput
|
|
|
|
|
|
v-else
|
|
|
|
|
|
v-model="feedbackForm.strengthDesc"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="2"
|
|
|
|
|
|
placeholder="请输入优势描述"
|
|
|
|
|
|
/>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="feedback-cell">
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
|
|
|
|
|
|
{{ feedbackForm.strengthExample || '' }}
|
|
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<ElInput
|
2026-07-19 18:21:14 +08:00
|
|
|
|
v-else
|
2026-06-13 13:06:39 +08:00
|
|
|
|
v-model="feedbackForm.strengthExample"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="2"
|
|
|
|
|
|
placeholder="请输入行为事例"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="feedback-row-content">
|
|
|
|
|
|
<div class="feedback-cell label-cell">
|
|
|
|
|
|
<span class="feedback-tag warning">劣势</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="feedback-cell">
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
|
|
|
|
|
|
{{ feedbackForm.weaknessDesc || '' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ElInput
|
|
|
|
|
|
v-else
|
|
|
|
|
|
v-model="feedbackForm.weaknessDesc"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="2"
|
|
|
|
|
|
placeholder="请输入劣势描述"
|
|
|
|
|
|
/>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="feedback-cell">
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
|
|
|
|
|
|
{{ feedbackForm.weaknessExample || '' }}
|
|
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<ElInput
|
2026-07-19 18:21:14 +08:00
|
|
|
|
v-else
|
2026-06-13 13:06:39 +08:00
|
|
|
|
v-model="feedbackForm.weaknessExample"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="2"
|
|
|
|
|
|
placeholder="请输入行为事例"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="feedback-right">
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
|
|
|
|
|
|
{{ feedbackForm.improvementSuggestion || '' }}
|
|
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<ElInput
|
2026-07-19 18:21:14 +08:00
|
|
|
|
v-else
|
2026-06-13 13:06:39 +08:00
|
|
|
|
v-model="feedbackForm.improvementSuggestion"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="5"
|
|
|
|
|
|
placeholder="请输入改进提升建议"
|
|
|
|
|
|
/>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<span>当期绩效考核结果</span>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="performance-result">
|
|
|
|
|
|
<div class="performance-label">
|
|
|
|
|
|
绩效考核结果
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<span v-if="!isSignOffScene" class="field-required-mark">*</span>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<ElFormItem class="performance-inline-form-item" prop="performanceResult">
|
2026-07-07 16:51:34 +08:00
|
|
|
|
<div class="performance-input-group">
|
|
|
|
|
|
<ElInput
|
|
|
|
|
|
v-model="performanceForm.score"
|
|
|
|
|
|
class="performance-input"
|
|
|
|
|
|
placeholder="请输入考核分数"
|
|
|
|
|
|
readonly
|
|
|
|
|
|
@input="handlePerformanceScoreInput"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ElButton
|
2026-07-19 18:21:14 +08:00
|
|
|
|
v-if="!isSignOffScene"
|
2026-07-07 16:51:34 +08:00
|
|
|
|
plain
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:loading="performanceDrawerLoading"
|
|
|
|
|
|
:disabled="!performanceTargetOption.length"
|
|
|
|
|
|
@click="openPerformanceDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
填写绩效
|
|
|
|
|
|
</ElButton>
|
|
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</ElFormItem>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<div class="section-title-left">
|
|
|
|
|
|
<span>下周期重点工作计划</span>
|
|
|
|
|
|
<span class="source-chip">{{ nextPlans.length }} 项计划</span>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</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>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="plan-name-cell">
|
|
|
|
|
|
<strong>{{ item.workItem }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="plan-action-cell"></div>
|
|
|
|
|
|
<div class="plan-editor-grid">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>具体目标</label>
|
|
|
|
|
|
<div class="rich-editor readonly" v-html="item.targetHtml"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>对他人协助的需求</label>
|
|
|
|
|
|
<div class="rich-editor readonly">{{ item.supportNeed || '' }}</div>
|
|
|
|
|
|
</div>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<div class="signature-section">
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<template v-if="isSignOffScene">
|
|
|
|
|
|
<div class="signature-row">
|
|
|
|
|
|
<span>被考核人签名:</span>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="signature-preview-panel"
|
|
|
|
|
|
:class="{ 'signature-preview-panel--empty': !hasEmployeeSignaturePreview }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="hasEmployeeSignaturePreview"
|
|
|
|
|
|
:src="props.employeeSignaturePreviewUrl"
|
|
|
|
|
|
alt="被考核人电子签名"
|
|
|
|
|
|
class="signature-preview-image"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span v-else>未维护电子签名</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span>日期:</span>
|
|
|
|
|
|
<div class="signature-readonly">{{ formatDisplayDate(signatureForm.employeeDate) }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="signature-row">
|
|
|
|
|
|
<span>上级签名:</span>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-loading="props.supervisorSignatureLoading"
|
|
|
|
|
|
class="signature-preview-panel"
|
|
|
|
|
|
:class="{ 'signature-preview-panel--empty': !hasSupervisorSignaturePreview }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="hasSupervisorSignaturePreview"
|
|
|
|
|
|
:src="props.supervisorSignaturePreviewUrl"
|
|
|
|
|
|
alt="上级电子签名"
|
|
|
|
|
|
class="signature-preview-image"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span v-else>未维护电子签名</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span>日期:</span>
|
|
|
|
|
|
<div class="signature-readonly">{{ formatDisplayDate(props.approvalModel.supervisorSignedDate) }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="!hasEmployeeSignaturePreview" class="signature-warning">
|
|
|
|
|
|
当前账号未维护电子签名,确认后后端将无法回填员工签名图片。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="signature-row">
|
|
|
|
|
|
<span>被考核人签名:</span>
|
|
|
|
|
|
<div class="signature-readonly">将在下签确认时自动补齐</div>
|
|
|
|
|
|
<span>日期:</span>
|
|
|
|
|
|
<div class="signature-readonly">将在下签确认时自动补齐</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="signature-row">
|
|
|
|
|
|
<span>上级签名:</span>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-loading="props.supervisorSignatureLoading"
|
|
|
|
|
|
class="signature-preview-panel"
|
|
|
|
|
|
:class="{ 'signature-preview-panel--empty': !hasSupervisorSignaturePreview }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="hasSupervisorSignaturePreview"
|
|
|
|
|
|
:src="props.supervisorSignaturePreviewUrl"
|
|
|
|
|
|
alt="上级电子签名"
|
|
|
|
|
|
class="signature-preview-image"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span v-else>未维护电子签名</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span>日期:</span>
|
|
|
|
|
|
<ElDatePicker
|
|
|
|
|
|
v-model="signatureForm.supervisorDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
|
placeholder="选择日期"
|
|
|
|
|
|
class="signature-date"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="!hasSupervisorSignaturePreview" class="signature-warning">
|
|
|
|
|
|
当前登录用户未维护电子签名,审批提交后将无法回填上级签名图片。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
</ElForm>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="form-actions approval-form-actions">
|
2026-07-08 20:08:04 +08:00
|
|
|
|
<ElButton @click="emit('back')">退出</ElButton>
|
2026-07-19 18:21:14 +08:00
|
|
|
|
<ElButton v-if="isSignOffScene" type="primary" @click="handleConfirmSignOff">确认</ElButton>
|
|
|
|
|
|
<ElButton v-else type="primary" @click="openAuditDialog">审批</ElButton>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<BusinessFormDialog
|
2026-07-19 18:21:14 +08:00
|
|
|
|
v-if="!isSignOffScene"
|
2026-06-11 10:56:24 +08:00
|
|
|
|
v-model="auditDialogVisible"
|
|
|
|
|
|
title="月报审批"
|
|
|
|
|
|
preset="sm"
|
|
|
|
|
|
confirm-text="确认提交"
|
|
|
|
|
|
@confirm="submitAudit"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="audit-form">
|
|
|
|
|
|
<div class="audit-field">
|
|
|
|
|
|
<label>审批结论</label>
|
|
|
|
|
|
<div class="audit-conclusion">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="conclusion-btn"
|
|
|
|
|
|
:class="{ active: auditForm.conclusion === '通过', pass: auditForm.conclusion === '通过' }"
|
|
|
|
|
|
@click="auditForm.conclusion = '通过'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
|
|
|
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M5 8.5L7 10.5L11 6"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
stroke-width="1.5"
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
通过
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="conclusion-btn"
|
2026-06-13 13:06:39 +08:00
|
|
|
|
:class="{ active: auditForm.conclusion === '退回', reject: auditForm.conclusion === '退回' }"
|
|
|
|
|
|
@click="auditForm.conclusion = '退回'"
|
2026-06-11 10:56:24 +08:00
|
|
|
|
>
|
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
|
|
|
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
|
|
|
|
|
<path d="M6 6L10 10M10 6L6 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
|
|
|
|
</svg>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
退回
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-06-13 13:06:39 +08:00
|
|
|
|
<ElForm
|
|
|
|
|
|
ref="auditFormRef"
|
|
|
|
|
|
:model="auditForm"
|
|
|
|
|
|
:rules="auditRules"
|
|
|
|
|
|
label-position="top"
|
|
|
|
|
|
:validate-on-rule-change="false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ElFormItem
|
|
|
|
|
|
:label="rejectOpinionRequired ? '退回原因' : '审批意见'"
|
|
|
|
|
|
prop="opinion"
|
|
|
|
|
|
:required="rejectOpinionRequired"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ElInput
|
|
|
|
|
|
v-model="auditForm.opinion"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="3"
|
|
|
|
|
|
maxlength="1000"
|
|
|
|
|
|
show-word-limit
|
|
|
|
|
|
:placeholder="rejectOpinionRequired ? '请输入退回原因' : '请输入审批意见'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</ElFormItem>
|
|
|
|
|
|
</ElForm>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</BusinessFormDialog>
|
2026-07-07 16:51:34 +08:00
|
|
|
|
|
|
|
|
|
|
<PerformanceExcelEditorDrawer
|
2026-07-19 18:21:14 +08:00
|
|
|
|
v-if="!isSignOffScene"
|
2026-07-07 16:51:34 +08:00
|
|
|
|
v-model:visible="performanceDrawerVisible"
|
|
|
|
|
|
:row-data="performanceDrawerRow"
|
|
|
|
|
|
:mode="performanceDrawerMode"
|
|
|
|
|
|
:subordinate-options="performanceTargetOption"
|
|
|
|
|
|
:initial-employee-id="performanceInitialEmployeeId"
|
|
|
|
|
|
:initial-period-month="performanceInitialPeriodMonth"
|
|
|
|
|
|
hide-draft-action
|
|
|
|
|
|
@saved="handlePerformanceSaved"
|
|
|
|
|
|
@saved-and-sent="handlePerformanceSaved"
|
|
|
|
|
|
/>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</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;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary-light-8);
|
|
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
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) {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.radio-group-full :deep(.el-radio__input.is-checked + .el-radio__label) {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
grid-template-columns: 88px minmax(225px, 330px) minmax(0, 1.35fr) minmax(0, 1fr) 44px;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
gap: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.plan-card {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
grid-template-columns: 88px minmax(225px, 330px) minmax(0, 1.35fr) minmax(0, 1fr) 44px;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
gap: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-index-cell,
|
|
|
|
|
|
.review-name-cell,
|
|
|
|
|
|
.review-editor-grid,
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.review-action-cell,
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.plan-index-cell,
|
|
|
|
|
|
.plan-name-cell,
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.plan-editor-grid,
|
|
|
|
|
|
.plan-action-cell {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
padding: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-index-cell,
|
|
|
|
|
|
.plan-index-cell {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-name-cell,
|
|
|
|
|
|
.plan-name-cell {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
align-items: center;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
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 {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
min-width: 0;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-name-cell strong,
|
|
|
|
|
|
.plan-name-cell strong {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
display: block;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 0 1 auto;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
overflow: hidden;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
text-overflow: clip;
|
|
|
|
|
|
white-space: normal;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
word-break: break-word;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
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;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
grid-template-columns: minmax(0, calc(57.45% - 6px + 50px)) minmax(0, calc(42.55% - 6px - 50px));
|
2026-06-11 10:56:24 +08:00
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.review-editor-grid {
|
|
|
|
|
|
grid-column: 3 / span 2;
|
|
|
|
|
|
grid-row: 1;
|
|
|
|
|
|
border-left: 1px solid #e5edf1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor,
|
|
|
|
|
|
.structured-input,
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner),
|
|
|
|
|
|
.styled-textarea :deep(.el-textarea__inner) {
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
scrollbar-color: #94a3b8 transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar,
|
|
|
|
|
|
.structured-input::-webkit-scrollbar,
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar),
|
|
|
|
|
|
.styled-textarea :deep(.el-textarea__inner::-webkit-scrollbar) {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar-track,
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar-thumb,
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar-button,
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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.readonly {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
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;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 18:22:44 +08:00
|
|
|
|
.rich-editor :deep(.rich-section-task) {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 6px;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
padding-left: 14px;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
white-space: normal;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.rich-editor :deep(.rich-category-line) {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
color: var(--el-color-primary);
|
2026-06-13 13:06:39 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.rich-editor :deep(.rich-section-task) {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
display: block;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
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;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
user-select: text;
|
|
|
|
|
|
cursor: text;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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: #f8fafc;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-editor-grid .field,
|
|
|
|
|
|
.plan-editor-grid .field {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-editor-grid .rich-editor,
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.plan-editor-grid .rich-editor {
|
|
|
|
|
|
flex: 1;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-input.auto-height {
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
min-height: 86px;
|
|
|
|
|
|
max-height: none;
|
|
|
|
|
|
overflow: visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-section + .structured-section {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
|
border-top: 1px dashed #cbd5e1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-section-title {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding-left: 14px;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-section-title::before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
top: 4px;
|
|
|
|
|
|
width: 4px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
border-radius: 999px;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-section-tasks {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
padding-left: 14px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-section-task {
|
|
|
|
|
|
display: inline;
|
|
|
|
|
|
margin-right: 5px;
|
|
|
|
|
|
white-space: normal;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-section-task-title {
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-inline-metrics {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
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: none;
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.structured-inline-metrics span + span::before {
|
|
|
|
|
|
content: '•';
|
|
|
|
|
|
margin-right: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.audit-form {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.audit-field {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.audit-field label {
|
|
|
|
|
|
color: #475467;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.audit-conclusion {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.conclusion-btn {
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
border: 1px solid #d8e0e8;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #475467;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.18s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.conclusion-btn:hover {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.conclusion-btn.active.pass {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.conclusion-btn.active.reject {
|
|
|
|
|
|
border-color: #dc2626;
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.field-form-item {
|
|
|
|
|
|
align-self: start;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.field-required-mark {
|
|
|
|
|
|
margin-left: 2px;
|
|
|
|
|
|
color: #f04438;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-inline-form-item,
|
|
|
|
|
|
.performance-inline-form-item {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-inline-form-item :deep(.el-form-item__content),
|
|
|
|
|
|
.performance-inline-form-item :deep(.el-form-item__content) {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
display: flex;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
line-height: 1;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.field-inline-form-item :deep(.el-form-item__error),
|
|
|
|
|
|
.performance-inline-form-item :deep(.el-form-item__error) {
|
|
|
|
|
|
padding-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-inline-form-item {
|
|
|
|
|
|
margin-top: -1px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-inline-form-item :deep(.el-date-editor) {
|
|
|
|
|
|
width: 100%;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-table {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
margin: 16px 16px 16px 16px;
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-header {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 120px 1fr 1fr 280px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-body {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 350px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-left {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-row-content {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 120px 1fr 1fr;
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
transition: background 0.15s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-row-content:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-row-content:hover {
|
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-right {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border-left: 1px solid #f1f5f9;
|
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-right :deep(.el-textarea__inner) {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
.feedback-readonly {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 60px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.feedback-cell {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border-right: 1px solid #f1f5f9;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-cell:last-child {
|
|
|
|
|
|
border-right: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-cell {
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
letter-spacing: 0.3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.label-cell {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-tag {
|
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-tag.success {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
border: 1px solid var(--el-color-primary-light-7);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feedback-tag.warning {
|
|
|
|
|
|
background: #fff7ed;
|
|
|
|
|
|
color: #c2410c;
|
|
|
|
|
|
border: 1px solid #fed7aa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.signature-section {
|
|
|
|
|
|
margin: 24px 20px;
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.signature-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.signature-row + .signature-row {
|
|
|
|
|
|
border-top: 1px solid #f1f5f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
.signature-readonly {
|
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
|
padding: 0 4px 4px;
|
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
text-align: center;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
.signature-preview-panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 116px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
padding: 3px;
|
|
|
|
|
|
border: 1px dashed #cbd5e1;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
box-sizing: border-box;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
.signature-preview-panel--empty {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 13px;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
.signature-preview-image {
|
|
|
|
|
|
max-width: 98px;
|
|
|
|
|
|
max-height: 28px;
|
|
|
|
|
|
object-fit: contain;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.signature-date {
|
|
|
|
|
|
width: 130px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.signature-date :deep(.el-input__wrapper) {
|
|
|
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.signature-date :deep(.el-input__inner) {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.signature-date :deep(.el-input__inner::placeholder) {
|
|
|
|
|
|
color: #cbd5e1;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 18:21:14 +08:00
|
|
|
|
.signature-warning {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
color: #c2410c;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.performance-result {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
margin: 16px 16px 16px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.performance-label {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
flex: 0 0 auto;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.performance-inline-form-item {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 16:51:34 +08:00
|
|
|
|
.performance-input-group {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.performance-input {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.performance-input :deep(.el-input__wrapper) {
|
|
|
|
|
|
box-shadow: none !important;
|
2026-07-07 16:51:34 +08:00
|
|
|
|
background: #f8fafc;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.performance-input :deep(.el-input__inner) {
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 16:51:34 +08:00
|
|
|
|
.performance-input :deep(.el-input__inner[readonly]) {
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.performance-input :deep(.el-input__inner::placeholder) {
|
|
|
|
|
|
color: #cbd5e1;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 14px 20px;
|
|
|
|
|
|
border-top: 1px solid #d8e0e8;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-card,
|
|
|
|
|
|
.plan-card {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-index-cell,
|
|
|
|
|
|
.review-name-cell,
|
|
|
|
|
|
.review-editor-grid,
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.review-action-cell,
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.plan-index-cell,
|
|
|
|
|
|
.plan-name-cell,
|
2026-06-13 13:06:39 +08:00
|
|
|
|
.plan-editor-grid,
|
|
|
|
|
|
.plan-action-cell {
|
2026-06-11 10:56:24 +08:00
|
|
|
|
grid-column: auto;
|
|
|
|
|
|
grid-row: auto;
|
|
|
|
|
|
border-left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-name-cell,
|
|
|
|
|
|
.review-editor-grid,
|
|
|
|
|
|
.plan-name-cell,
|
|
|
|
|
|
.plan-editor-grid {
|
|
|
|
|
|
border-top: 1px solid #e5edf1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|