Files
cn-rdms-web/src/views/personal-center/work-report/monthly/modules/approval-page.vue
dk 08fff8bbc5 feat(performance): 新增绩效下签功能支持
- 在月报审批页面增加 sign-off 场景支持
- 添加绩效下签对话框组件实现批量下签功能
- 增加导入绩效压缩包和直接下签两种操作模式
- 实现电子签名预览和日期格式化显示功能
- 更新绩效权限配置增加下签相关权限
- 优化审批对话框标题和按钮文案显示
- 添加导入结果提示和文件验证功能
- 实现团队模式下的下签操作入口
- 完善绩效表格状态和操作权限判断逻辑
2026-07-19 18:21:14 +08:00

2113 lines
53 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
/* eslint-disable no-useless-escape */
import { computed, nextTick, reactive, ref, watch } from 'vue';
import type { FormRules } from 'element-plus';
import dayjs from 'dayjs';
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchPerformanceSheetPage } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import PerformanceExcelEditorDrawer from '../../../my-performance/modules/performance-excel-editor-drawer.vue';
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';
scene?: 'fill' | 'detail' | 'approval' | 'sign-off';
baseInfo?: Api.WorkReport.Monthly.MonthlyReport | null;
model: Api.WorkReport.Monthly.MonthlyReportSaveParams;
approvalModel: Api.WorkReport.Monthly.MonthlyReportApproveParams;
supervisorSignaturePreviewUrl?: string;
supervisorSignatureLoading?: boolean;
employeeSignaturePreviewUrl?: string;
}>();
const emit = defineEmits<{
back: [];
requestApprove: [];
requestReject: [];
requestConfirmSignOff: [];
changeApproval: [payload: Api.WorkReport.Monthly.MonthlyReportApproveParams];
viewAudit: [];
}>();
const { formRef: pageFormRef, validate: validatePageForm } = useForm();
const { formRef: auditFormRef } = useForm();
const { createRequiredRule } = useFormRules();
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'));
const performanceDrawerVisible = ref(false);
const performanceDrawerLoading = ref(false);
const performanceDrawerMode = ref<'create' | 'edit'>('create');
const performanceDrawerRow = ref<Api.Performance.Sheet.Sheet | null>(null);
const isSignOffScene = computed(() => props.scene === 'sign-off');
const hasSupervisorSignaturePreview = computed(() => Boolean(props.supervisorSignaturePreviewUrl));
const hasEmployeeSignaturePreview = computed(() => Boolean(props.employeeSignaturePreviewUrl));
const auditDialogVisible = ref(false);
const auditForm = reactive({
conclusion: '通过',
opinion: ''
});
const rejectOpinionRequired = computed(() => auditForm.conclusion === '退回');
const pageValidationModel = reactive({
get meetingDate() {
return props.approvalModel.meetingDate || '';
},
get performanceResult() {
return props.approvalModel.performanceResult || '';
}
});
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'
}
]
};
});
const auditRules = computed<FormRules>(() => ({
opinion: rejectOpinionRequired.value
? [
createRequiredRule('请输入退回原因'),
{
validator: (_rule, value: string, callback) => {
if (!value?.trim()) {
callback(new Error('请输入退回原因'));
return;
}
callback();
},
trigger: 'blur'
}
]
: []
}));
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);
}
});
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') : '';
});
/** 绩效分数输入限制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() {
return props.approvalModel.employeeSignedDate || '';
},
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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));
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 createStructuredHtmlV2(sections: StructuredSection[], showHours = false) {
return sections
.map(section => {
const categoryLabel = resolveTaskItemTypeLabel(section.category.trim());
const tasks = section.tasks
.map(
(task, index) =>
`<div class="rich-task-line">${escapeHtml(`${index + 1}${formatStructuredTaskLineV2(task, showHours)}`)}</div>`
)
.join('');
return `
<div class="rich-section">
<div class="rich-category-line"># ${escapeHtml(categoryLabel)}</div>
${tasks ? `<div class="rich-section-tasks">${tasks}</div>` : ''}
</div>
`;
})
.join('');
}
function createStructuredSectionsFromTextV2(text: string, defaultCategory: string): StructuredSection[] {
const lines = normalizeEditorText(text).split('\n').filter(Boolean);
if (!lines.length || lines.join('\n') === EMPTY_TEXT) return [];
const sections: StructuredSection[] = [];
const ensureSection = (categoryText: string) => {
const category = categoryText.trim() || defaultCategory;
let section = sections.find(item => item.category === category);
if (!section) {
section = { category, tasks: [] };
sections.push(section);
}
return section;
};
let currentCategory = '';
lines.forEach(line => {
const trimmedLine = line.trim();
if (!trimmedLine) return;
if (trimmedLine.startsWith('#')) {
currentCategory = trimmedLine.replace(/^#\s*/u, '').trim();
if (currentCategory) ensureSection(currentCategory);
return;
}
const legacyMatch = trimmedLine.match(/^(.+?)\s*-\s*(.+?)(?:[(]([^()]*)[)])?[。.!?]*$/u);
if (legacyMatch) {
const [, rawCategory, rawTitle, metricsText = ''] = legacyMatch;
const category = rawCategory.trim();
const title = rawTitle.trim();
if (!category || !title) return;
ensureSection(category).tasks.push({
title,
...resolveTaskMetricsV2(metricsText)
});
return;
}
const normalizedLine = stripStructuredTaskSuffixV2(stripStructuredTaskPrefixV2(trimmedLine));
if (!normalizedLine) return;
const inlineMatch = normalizedLine.match(/^(.*?)(?:[(]([^()]*)[)])?$/u);
const title = inlineMatch?.[1]?.trim() || '';
const metricsText = inlineMatch?.[2]?.trim() || '';
if (!title) return;
ensureSection(currentCategory || defaultCategory).tasks.push({
title,
...resolveTaskMetricsV2(metricsText)
});
});
return sections.filter(section => section.tasks.length);
}
function normalizeEditorText(value: string) {
return value
.replace(/\u00A0/g, ' ')
.split('\n')
.map(line => line.trim())
.filter(Boolean)
.join('\n');
}
function toReviewItem(item: Api.WorkReport.Common.PersonalReportReviewItem): ReviewItem {
const structuredSections = mergeSectionsByCategory(getStructuredSections(item.contentJson).map(normalizeSection));
const contentSections = structuredSections.length
? structuredSections
: createStructuredSectionsFromTextV2(item.contentText || '', item.itemTitle || '未分类');
const contentHtml = contentSections.length
? createStructuredHtmlV2(contentSections, true)
: 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
: createStructuredSectionsFromTextV2(item.targetText || '', item.itemTitle || '未分类');
const targetHtml = targetSections.length
? createStructuredHtmlV2(targetSections)
: 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] || '';
}
async function submitAudit() {
if (!auditForm.conclusion) {
window.$message?.warning('请选择审批结论');
return;
}
// 仅"通过"时才校验面谈时间、绩效考核结果等必填项;"退回"时跳过这些校验。
if (auditForm.conclusion !== '退回') {
try {
await validatePageForm();
} catch {
return;
}
}
if (rejectOpinionRequired.value) {
try {
await auditFormRef.value?.validateField?.('opinion');
} catch {
return;
}
}
patchApproval('employeeSignName', undefined);
patchApproval('employeeSignedDate', undefined);
patchApproval('supervisorSignedDate', signatureForm.supervisorDate);
patchApproval(
'reason',
rejectOpinionRequired.value ? auditForm.opinion.trim() : auditForm.opinion.trim() || auditForm.conclusion
);
patchApproval('meetingDate', mainForm.meetingDate);
auditDialogVisible.value = false;
if (auditForm.conclusion === '退回') {
emit('requestReject');
} else {
emit('requestApprove');
}
}
function openAuditDialog() {
Object.assign(auditForm, {
conclusion: '通过',
opinion: ''
});
auditDialogVisible.value = true;
nextTick(() => {
auditFormRef.value?.clearValidate();
});
}
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);
}
async function handleConfirmSignOff() {
try {
await validatePageForm();
} catch {
return;
}
emit('requestConfirmSignOff');
}
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');
}
);
</script>
<template>
<div class="card form-page">
<ElForm
ref="pageFormRef"
:model="pageValidationModel"
:rules="pageRules"
label-position="top"
:validate-on-rule-change="false"
>
<div class="section">
<div class="section-title">
<span>基础信息</span>
</div>
<div class="compose-grid">
<div class="field">
<label>姓名</label>
<ElInput v-model="mainForm.reporter" disabled />
</div>
<div class="field">
<label>部门/方向</label>
<ElInput v-model="mainForm.deptName" disabled />
</div>
<div class="field">
<label>岗位</label>
<ElInput v-model="mainForm.postName" disabled />
</div>
<div class="field">
<label>周期</label>
<ElInput :model-value="periodText" disabled />
</div>
<div class="field">
<label>直接上级</label>
<ElInput v-model="mainForm.supervisor" disabled />
</div>
<div class="field field-form-item">
<label>面谈时间</label>
<ElFormItem class="field-inline-form-item" prop="meetingDate">
<ElDatePicker
v-model="mainForm.meetingDate"
type="date"
value-format="YYYY-MM-DD"
disabled
style="width: 100%"
/>
</ElFormItem>
</div>
</div>
</div>
<div class="section">
<div class="section-title">
<div class="section-title-left">
<span>当期重点工作回顾</span>
<span class="source-chip">{{ reviewItems.length }} 项工作</span>
<span class="source-chip"> {{ totalHours }}h</span>
</div>
</div>
<div class="review-grid layout-row">
<div v-if="!reviewItems.length">{{ EMPTY_TEXT }}</div>
<div v-for="(item, index) in reviewItems" :key="index" class="review-card">
<div class="review-index-cell">
<span class="row-index">{{ index + 1 }}</span>
</div>
<div class="review-name-cell">
<div class="work-title-line">
<strong>{{ item.workItem }}</strong>
<span> {{ item.hours }}h</span>
</div>
</div>
<div class="review-action-cell"></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>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">
<span>当期工作反馈</span>
<ElTag type="info" size="small" round>直接上级/分管领导填写</ElTag>
</div>
<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">
<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="请输入优势描述"
/>
</div>
<div class="feedback-cell">
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
{{ feedbackForm.strengthExample || '' }}
</div>
<ElInput
v-else
v-model="feedbackForm.strengthExample"
type="textarea"
:rows="2"
placeholder="请输入行为事例"
/>
</div>
</div>
<div class="feedback-row-content">
<div class="feedback-cell label-cell">
<span class="feedback-tag warning">劣势</span>
</div>
<div class="feedback-cell">
<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="请输入劣势描述"
/>
</div>
<div class="feedback-cell">
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
{{ feedbackForm.weaknessExample || '' }}
</div>
<ElInput
v-else
v-model="feedbackForm.weaknessExample"
type="textarea"
:rows="2"
placeholder="请输入行为事例"
/>
</div>
</div>
</div>
<div class="feedback-right">
<div v-if="isSignOffScene" class="rich-editor readonly feedback-readonly">
{{ feedbackForm.improvementSuggestion || '' }}
</div>
<ElInput
v-else
v-model="feedbackForm.improvementSuggestion"
type="textarea"
:rows="5"
placeholder="请输入改进提升建议"
/>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">
<span>当期绩效考核结果</span>
</div>
<div class="performance-result">
<div class="performance-label">
绩效考核结果
<span v-if="!isSignOffScene" class="field-required-mark">*</span>
</div>
<ElFormItem class="performance-inline-form-item" prop="performanceResult">
<div class="performance-input-group">
<ElInput
v-model="performanceForm.score"
class="performance-input"
placeholder="请输入考核分数"
readonly
@input="handlePerformanceScoreInput"
/>
<ElButton
v-if="!isSignOffScene"
plain
size="small"
type="primary"
:loading="performanceDrawerLoading"
:disabled="!performanceTargetOption.length"
@click="openPerformanceDrawer"
>
填写绩效
</ElButton>
</div>
</ElFormItem>
</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>
<div class="plan-list layout-row">
<div v-if="!nextPlans.length">{{ EMPTY_TEXT }}</div>
<div v-for="(item, index) in nextPlans" :key="index" class="plan-card">
<div class="plan-index-cell">
<span class="row-index">{{ index + 1 }}</span>
</div>
<div class="plan-name-cell">
<strong>{{ item.workItem }}</strong>
</div>
<div class="plan-action-cell"></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>
</div>
</div>
</div>
</div>
<div class="signature-section">
<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>
</div>
</ElForm>
<div class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出</ElButton>
<ElButton v-if="isSignOffScene" type="primary" @click="handleConfirmSignOff">确认</ElButton>
<ElButton v-else type="primary" @click="openAuditDialog">审批</ElButton>
</div>
<BusinessFormDialog
v-if="!isSignOffScene"
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"
:class="{ active: auditForm.conclusion === '退回', reject: 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="M6 6L10 10M10 6L6 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
退回
</button>
</div>
</div>
<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>
</div>
</BusinessFormDialog>
<PerformanceExcelEditorDrawer
v-if="!isSignOffScene"
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"
/>
</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-card-head,
.plan-card-head {
display: grid;
grid-template-columns: 34px minmax(0, 1fr) 28px;
gap: 10px;
align-items: start;
}
.row-index {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 999px;
background: var(--el-color-primary);
color: #fff;
font-size: 13px;
font-weight: 900;
}
.review-title-fields,
.review-title-readonly,
.plan-title-readonly {
display: grid;
gap: 10px;
align-items: center;
}
.review-title-readonly {
grid-template-columns: 1fr;
}
.work-title-line {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.review-title-readonly strong,
.plan-title-readonly strong {
color: #606266;
font-size: 14px;
font-weight: 400;
line-height: 1.3;
}
.work-title-line span,
.inline-metrics {
display: flex;
align-items: center;
gap: 6px;
color: #667085;
font-size: 12px;
font-weight: 800;
}
.review-name-cell .work-title-line span {
justify-content: center;
width: 100%;
}
.inline-metrics :deep(.el-input-number) {
width: 92px;
}
.review-editor-grid,
.plan-editor-grid {
display: grid;
grid-template-columns: minmax(0, calc(57.45% - 6px + 50px)) minmax(0, calc(42.55% - 6px - 50px));
gap: 12px;
}
.review-editor-grid {
grid-column: 3 / span 2;
grid-row: 1;
border-left: 1px solid #e5edf1;
}
.plan-editor-grid {
grid-column: 3 / span 2;
grid-row: 1;
border-left: 1px solid #e5edf1;
}
.item-remove-btn {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 1px solid #fecaca;
border-radius: 999px;
background: #fff;
color: #dc2626;
font: inherit;
font-size: 18px;
line-height: 1;
cursor: pointer;
}
.item-remove-btn:hover {
background: #fef2f2;
}
.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;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-section-title::before) {
content: '';
position: absolute;
left: 0;
top: 4px;
width: 4px;
height: 16px;
border-radius: 999px;
background: var(--el-color-primary);
}
.rich-editor :deep(.rich-section-task) {
display: grid;
gap: 6px;
padding-left: 14px;
color: #334155;
font-size: 13px;
line-height: 1.6;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-category-line) {
color: var(--el-color-primary);
font-size: 13px;
font-weight: 700;
line-height: 1.6;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-section-tasks) {
padding-left: 0;
}
.rich-editor :deep(.rich-task-line) {
color: #334155;
font-size: 13px;
line-height: 1.6;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-section-task) {
display: block;
white-space: normal;
}
.rich-editor :deep(.rich-section-task-title) {
color: #334155;
overflow-wrap: anywhere;
word-break: break-word;
}
.rich-editor :deep(.rich-inline-metrics) {
display: inline-flex;
align-items: center;
height: 18px;
margin-left: 3px;
padding: 0 6px;
border-radius: 999px;
background: #f3f7f9;
color: #475467;
font-size: 11px;
font-weight: 800;
vertical-align: 1px;
user-select: text;
cursor: text;
}
.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,
.plan-editor-grid .rich-editor {
flex: 1;
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;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
}
.structured-section-title::before {
content: '';
position: absolute;
left: 0;
top: 4px;
width: 4px;
height: 16px;
border-radius: 999px;
background: var(--el-color-primary);
}
.structured-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 {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.conclusion-btn.active.pass {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
.conclusion-btn.active.reject {
border-color: #dc2626;
background: #fef2f2;
color: #dc2626;
}
.field-form-item {
align-self: start;
}
.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) {
display: flex;
align-items: center;
line-height: 1;
}
.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%;
}
.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;
}
.feedback-readonly {
width: 100%;
min-height: 60px;
}
.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 {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border: 1px solid var(--el-color-primary-light-7);
}
.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;
}
.signature-readonly {
min-width: 180px;
padding: 0 4px 4px;
border-bottom: 1px solid #e2e8f0;
color: #64748b;
font-size: 14px;
text-align: center;
}
.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;
}
.signature-preview-panel--empty {
color: #94a3b8;
font-size: 13px;
}
.signature-preview-image {
max-width: 98px;
max-height: 28px;
object-fit: contain;
}
.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;
}
.signature-warning {
margin-top: 8px;
color: #c2410c;
font-size: 13px;
font-weight: 600;
text-align: right;
}
.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 {
flex: 0 0 auto;
color: #475569;
font-size: 14px;
font-weight: 700;
white-space: nowrap;
}
.performance-inline-form-item {
margin-bottom: 0;
flex: 0 0 auto;
}
.performance-input-group {
display: inline-flex;
align-items: center;
gap: 12px;
}
.performance-input {
width: 200px;
}
.performance-input :deep(.el-input__wrapper) {
box-shadow: none !important;
background: #f8fafc;
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;
}
.performance-input :deep(.el-input__inner[readonly]) {
cursor: default;
}
.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,
.review-action-cell,
.plan-index-cell,
.plan-name-cell,
.plan-editor-grid,
.plan-action-cell {
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>