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

910 lines
26 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">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import type { FormRules } from 'element-plus';
import JSZip from 'jszip';
import '@univerjs/preset-sheets-core/lib/index.css';
import {
createPerformanceSheet,
downloadFile,
fetchPerformanceSheet,
fetchPerformanceSheetPrefill,
fetchPerformanceTemplateCurrent,
sendPerformanceSheet,
updatePerformanceSheetExcel,
uploadFile
} from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import { createDefaultPeriodMonth, normalizeScoreText } from './performance-shared';
defineOptions({ name: 'PerformanceExcelEditorDrawer' });
interface SubordinateOption {
label: string;
value: string;
}
interface Props {
rowData?: Api.Performance.Sheet.Sheet | null;
mode: 'view' | 'edit' | 'create';
subordinateOptions?: SubordinateOption[];
showApprovalFooter?: boolean;
initialEmployeeId?: string;
initialPeriodMonth?: string;
hideDraftAction?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
rowData: null,
subordinateOptions: () => [],
showApprovalFooter: false,
initialEmployeeId: '',
initialPeriodMonth: '',
hideDraftAction: false
});
const visible = defineModel<boolean>('visible', { default: false });
const emit = defineEmits<{
saved: [payload: PerformanceSheetSavePayload];
savedAndSent: [payload: PerformanceSheetSavePayload];
startApproval: [];
}>();
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const containerRef = ref<HTMLDivElement>();
const loading = ref(false);
const prefillLoading = ref(false);
const saving = ref(false);
const sending = ref(false);
const currentSheet = ref<Api.Performance.Sheet.Sheet | null>(null);
const currentTemplate = ref<Api.Performance.Template.Template | null>(null);
const errorMessage = ref('');
const prefillSourcePeriodMonth = ref('');
const viewportWidth = ref(typeof window === 'undefined' ? 1920 : window.innerWidth);
const loadedCreateContextKey = ref('');
const createContextInitializing = ref(false);
const createForm = reactive({
periodMonth: createDefaultPeriodMonth(),
employeeId: ''
});
interface PerformanceSheetSavePayload {
sheet: Api.Performance.Sheet.Sheet;
actualScoreTotal: string;
baseScoreTotal: string;
extraScoreTotal: string;
}
const createFormRules = computed<FormRules>(() => ({
periodMonth: [createRequiredRule('请选择绩效月份')],
employeeId: [createRequiredRule('请选择下属')]
}));
const ASSESSED_EMPLOYEE_TEXT_PATTERN = /(被考核人\s*[:]\s*)(.+)/u;
let univerInstance: any = null;
let univerAPI: any = null;
let LuckyExcelModule: any = null;
let createUniverFn: any = null;
let UniverSheetsCorePresetFn: any = null;
let univerLocales: Record<string, unknown> | null = null;
let excelRuntimeLoading: Promise<void> | null = null;
const DEFAULT_SHEET_ZOOM_RATIO = 0.4;
const isCreateMode = computed(() => props.mode === 'create');
const drawerTitle = computed(() => {
let action = '查看';
if (isCreateMode.value) action = '新增';
else if (props.mode === 'edit') action = '编辑';
const selectedEmployee = props.subordinateOptions.find(opt => opt.value === createForm.employeeId);
const name = currentSheet.value?.employeeName || props.rowData?.employeeName || selectedEmployee?.label || '';
return `${action}绩效 Excel${name ? ` - ${name}` : ''}`;
});
const canSave = computed(() => props.mode !== 'view');
const showApprovalFooter = computed(() => props.mode === 'view' && props.showApprovalFooter);
const drawerSize = computed(() => (viewportWidth.value >= 2560 ? '60%' : '88%'));
function syncViewportWidth() {
viewportWidth.value = window.innerWidth;
}
function handleClose() {
visible.value = false;
}
function handleStartApproval() {
emit('startApproval');
}
function disposeUniver() {
try {
univerInstance?.dispose?.();
} catch {
// ignore dispose errors so closing the drawer still works
}
univerInstance = null;
univerAPI = null;
if (containerRef.value) {
containerRef.value.innerHTML = '';
}
}
async function ensureExcelRuntime() {
if (LuckyExcelModule && createUniverFn && UniverSheetsCorePresetFn && univerLocales) {
return;
}
if (!excelRuntimeLoading) {
excelRuntimeLoading = Promise.all([
import('@zwight/luckyexcel'),
import('@univerjs/presets'),
import('@univerjs/preset-sheets-core'),
import('@univerjs/preset-sheets-core/locales/zh-CN'),
import('@univerjs/preset-sheets-core/locales/en-US')
]).then(([luckyexcelModule, presetsModule, sheetsCoreModule, zhCNLocaleModule, enUSLocaleModule]) => {
LuckyExcelModule = luckyexcelModule.default || luckyexcelModule;
createUniverFn = presetsModule.createUniver;
UniverSheetsCorePresetFn = sheetsCoreModule.UniverSheetsCorePreset;
univerLocales = {
'zh-CN': zhCNLocaleModule.default || zhCNLocaleModule,
'en-US': enUSLocaleModule.default || enUSLocaleModule
};
});
}
await excelRuntimeLoading;
}
function transformExcelToUniver(file: File) {
return new Promise<any>((resolve, reject) => {
LuckyExcelModule.transformExcelToUniver(
file,
(snapshot: any) => resolve(snapshot || {}),
(error: unknown) => reject(error)
);
});
}
function transformUniverToExcel(snapshot: any, fileName: string) {
return new Promise<BlobPart>((resolve, reject) => {
LuckyExcelModule.transformUniverToExcel({
snapshot,
fileName,
getBuffer: true,
success: (buffer?: unknown) => {
if (!buffer) {
reject(new Error('Excel 导出结果为空'));
return;
}
resolve(buffer as BlobPart);
},
error: (error: Error) => reject(error)
});
});
}
async function normalizeExcelBuffer(buffer: BlobPart): Promise<ArrayBuffer> {
if (buffer instanceof ArrayBuffer) {
return buffer;
}
if (ArrayBuffer.isView(buffer)) {
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength).slice().buffer;
}
if (buffer instanceof Blob) {
return buffer.arrayBuffer();
}
throw new Error('Excel 导出结果格式不受支持');
}
function applySheetZoomRatio(snapshot: any, zoomRatio = DEFAULT_SHEET_ZOOM_RATIO) {
const data = snapshot || {};
if (data.sheets && typeof data.sheets === 'object') {
Object.values(data.sheets).forEach((sheet: any) => {
if (sheet && typeof sheet === 'object') {
sheet.zoomRatio = zoomRatio;
}
});
}
return data;
}
function replaceAssessedEmployeeText(value: string, employeeName: string) {
if (!ASSESSED_EMPLOYEE_TEXT_PATTERN.test(value)) {
return value;
}
return value.replace(ASSESSED_EMPLOYEE_TEXT_PATTERN, `$1${employeeName}`);
}
function injectAssessedEmployeeName(snapshot: any, employeeName: string) {
if (!snapshot || !employeeName) return snapshot;
const visited = new WeakSet<object>();
const walk = (target: unknown) => {
if (typeof target === 'string') {
return replaceAssessedEmployeeText(target, employeeName);
}
if (!target || typeof target !== 'object') {
return target;
}
if (visited.has(target as object)) {
return target;
}
visited.add(target as object);
if (Array.isArray(target)) {
target.forEach((item, index) => {
target[index] = walk(item);
});
return target;
}
Object.entries(target).forEach(([key, value]) => {
(target as Record<string, unknown>)[key] = walk(value);
});
return target;
};
return walk(snapshot);
}
function findFirstElementByLocalName(parent: Element | Document, localName: string) {
return Array.from(parent.childNodes).find(
node => node.nodeType === Node.ELEMENT_NODE && (node as Element).localName === localName
) as Element | undefined;
}
function ensureChildElement(document: XMLDocument, parent: Element, localName: string) {
const existing = Array.from(parent.childNodes).find(
node => node.nodeType === Node.ELEMENT_NODE && (node as Element).localName === localName
) as Element | undefined;
if (existing) return existing;
const namespace = parent.namespaceURI || document.documentElement?.namespaceURI || null;
const element = namespace ? document.createElementNS(namespace, localName) : document.createElement(localName);
parent.appendChild(element);
return element;
}
function createXmlRoot(document: XMLDocument, localName: string, namespace?: string | null) {
const root = namespace ? document.createElementNS(namespace, localName) : document.createElement(localName);
document.appendChild(root);
return root;
}
function applyWorksheetZoomXml(xmlText: string, zoomScale: number) {
const document = new DOMParser().parseFromString(xmlText, 'application/xml');
const root =
document.documentElement && document.documentElement.localName !== 'parsererror'
? document.documentElement
: createXmlRoot(document, 'worksheet', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
const sheetViews = ensureChildElement(document, root, 'sheetViews');
const sheetView =
findFirstElementByLocalName(sheetViews, 'sheetView') || ensureChildElement(document, sheetViews, 'sheetView');
sheetView.setAttribute('workbookViewId', sheetView.getAttribute('workbookViewId') || '0');
sheetView.setAttribute('zoomScale', String(zoomScale));
sheetView.setAttribute('zoomScaleNormal', String(zoomScale));
sheetView.setAttribute('zoomScalePageLayoutView', String(zoomScale));
return new XMLSerializer().serializeToString(document);
}
function applyWorkbookZoomXml(xmlText: string, zoomScale: number) {
const document = new DOMParser().parseFromString(xmlText, 'application/xml');
const root =
document.documentElement && document.documentElement.localName !== 'parsererror'
? document.documentElement
: createXmlRoot(document, 'workbook', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
const bookViews = ensureChildElement(document, root, 'bookViews');
const workbookView =
findFirstElementByLocalName(bookViews, 'workbookView') || ensureChildElement(document, bookViews, 'workbookView');
workbookView.setAttribute('zoomScale', String(zoomScale));
workbookView.setAttribute('zoomScaleNormal', String(zoomScale));
return new XMLSerializer().serializeToString(document);
}
async function applyExcelZoomMetadata(buffer: BlobPart, zoomRatio = DEFAULT_SHEET_ZOOM_RATIO) {
const zoomScale = Math.max(10, Math.min(400, Math.round(zoomRatio * 100)));
const zip = await JSZip.loadAsync(await normalizeExcelBuffer(buffer));
const worksheetPaths = Object.keys(zip.files).filter(path => /^xl\/worksheets\/sheet\d+\.xml$/u.test(path));
await Promise.all(
worksheetPaths.map(async path => {
const entry = zip.file(path);
if (!entry) return;
const xmlText = await entry.async('string');
zip.file(path, applyWorksheetZoomXml(xmlText, zoomScale));
})
);
const workbookEntry = zip.file('xl/workbook.xml');
if (workbookEntry) {
const workbookXml = await workbookEntry.async('string');
zip.file('xl/workbook.xml', applyWorkbookZoomXml(workbookXml, zoomScale));
}
return zip.generateAsync({ type: 'arraybuffer' });
}
function createWorkbook(snapshot: any) {
if (!containerRef.value) return;
disposeUniver();
const { univer, univerAPI: api } = createUniverFn({
locale: 'zh-CN',
locales: univerLocales || undefined,
presets: [
UniverSheetsCorePresetFn({
container: containerRef.value,
header: true,
toolbar: props.mode !== 'view',
formulaBar: props.mode !== 'view'
})
]
});
univerInstance = univer;
univerAPI = api;
const unitType = api?.Enum?.UniverInstanceType?.UNIVER_SHEET;
if (!unitType) {
throw new Error('Univer 工作簿初始化失败');
}
// 在 snapshot 数据中预设缩放比例,保证在线查看和导出文件使用同一套缩放值
const data = applySheetZoomRatio(snapshot);
univer.createUnit(unitType, data);
}
function getActiveWorkbook() {
return univerAPI?.getActiveWorkbook?.();
}
function getCreateContextKey() {
return `${createForm.periodMonth || ''}::${createForm.employeeId || ''}`;
}
function parseCellAddress(address?: string | null) {
const text = String(address || '')
.trim()
.toUpperCase();
const matched = text.match(/^([A-Z]+)(\d+)$/u);
if (!matched) return null;
const [, letters, rowText] = matched;
let column = 0;
for (const letter of letters) {
column = column * 26 + letter.charCodeAt(0) - 64;
}
return {
row: Number(rowText) - 1,
column: column - 1
};
}
function readCell(address?: string | null) {
const position = parseCellAddress(address);
const workbook = getActiveWorkbook();
const sheet = workbook?.getActiveSheet?.();
if (!position || !sheet) return '';
try {
const range = sheet.getRange(position.row, position.column);
const value = range?.getValue?.();
return normalizeScoreText(value);
} catch {
return '';
}
}
function writeCell(address: string, value: string) {
const position = parseCellAddress(address);
const workbook = getActiveWorkbook();
const sheet = workbook?.getActiveSheet?.();
if (!position || !sheet) return false;
try {
const range = sheet.getRange(position.row, position.column);
range?.setValue?.(value);
return true;
} catch {
return false;
}
}
function applyPrefillValues(cellValues: Record<string, string>) {
Object.entries(cellValues).forEach(([address, value]) => {
if (!address) return;
writeCell(address, value);
});
}
function readScores() {
const mapping = currentTemplate.value?.scoreCellMapping;
return {
actualScoreTotal: readCell(mapping?.actualScoreTotalCell),
baseScoreTotal: readCell(mapping?.baseScoreTotalCell),
extraScoreTotal: readCell(mapping?.extraScoreTotalCell)
};
}
function getCreateEmployeeName() {
return props.subordinateOptions.find(opt => opt.value === createForm.employeeId)?.label || '';
}
function applyCreateEmployeeName(snapshot: any) {
if (!isCreateMode.value) return snapshot;
const employeeName = getCreateEmployeeName();
if (!employeeName) return snapshot;
return injectAssessedEmployeeName(snapshot, employeeName);
}
function createInitialFileName() {
const sheet = currentSheet.value;
if (sheet) {
return sheet.fileName || `${sheet.periodMonth}-绩效表_${sheet.employeeName}.xlsx`;
}
if (isCreateMode.value && createForm.periodMonth && createForm.employeeId) {
return `${createForm.periodMonth}-绩效表_${getCreateEmployeeName()}.xlsx`;
}
return '绩效表.xlsx';
}
async function applyCreatePrefill() {
prefillSourcePeriodMonth.value = '';
if (!isCreateMode.value || !createForm.employeeId || !createForm.periodMonth) {
return;
}
prefillLoading.value = true;
try {
const { error, data } = await fetchPerformanceSheetPrefill(createForm.employeeId, createForm.periodMonth);
if (error || !data) {
return;
}
prefillSourcePeriodMonth.value = data.sourcePeriodMonth ?? '';
if (Object.keys(data.cellValues).length) {
applyPrefillValues(data.cellValues);
}
} finally {
prefillLoading.value = false;
}
}
async function loadWorkbook() {
loading.value = true;
prefillLoading.value = false;
errorMessage.value = '';
prefillSourcePeriodMonth.value = '';
currentSheet.value = null;
currentTemplate.value = null;
try {
let sourceFileId = '';
if (isCreateMode.value) {
const templateResult = await fetchPerformanceTemplateCurrent();
if (templateResult.error || !templateResult.data) {
errorMessage.value = '当前没有可用的绩效模板';
return;
}
currentTemplate.value = templateResult.data;
sourceFileId = templateResult.data.fileId;
} else {
if (!props.rowData?.id) return;
const [sheetResult, templateResult] = await Promise.all([
fetchPerformanceSheet(props.rowData.id),
fetchPerformanceTemplateCurrent()
]);
if (sheetResult.error || !sheetResult.data) {
errorMessage.value = '绩效表详情加载失败';
return;
}
currentSheet.value = sheetResult.data;
currentTemplate.value = templateResult.error ? null : templateResult.data;
sourceFileId = currentSheet.value.fileId || currentTemplate.value?.fileId || '';
}
if (!sourceFileId) {
errorMessage.value = '当前绩效表没有可用的 Excel 文件';
return;
}
const fileResult = await downloadFile(sourceFileId);
if (fileResult.error || !fileResult.data) {
errorMessage.value = 'Excel 文件下载失败';
return;
}
await ensureExcelRuntime();
const file = new File([fileResult.data], createInitialFileName(), {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const snapshot = await transformExcelToUniver(file);
await nextTick();
createWorkbook(applyCreateEmployeeName(snapshot));
if (isCreateMode.value) {
await nextTick();
await applyCreatePrefill();
loadedCreateContextKey.value = getCreateContextKey();
} else {
loadedCreateContextKey.value = '';
}
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : 'Excel 解析失败';
} finally {
loading.value = false;
}
}
async function ensureCreatedSheet() {
if (currentSheet.value) {
return currentSheet.value;
}
if (!createForm.periodMonth || !createForm.employeeId) {
throw new Error('请先填写绩效月份和下属');
}
const createResult = await createPerformanceSheet({
periodMonth: createForm.periodMonth,
employeeId: createForm.employeeId
});
if (createResult.error || !createResult.data) {
throw new Error('创建绩效记录失败');
}
const sheetResult = await fetchPerformanceSheet(createResult.data);
if (sheetResult.error || !sheetResult.data) {
throw new Error('绩效记录已创建,但加载详情失败');
}
currentSheet.value = sheetResult.data;
return sheetResult.data;
}
function createInitialPeriodMonth() {
return props.initialPeriodMonth || createDefaultPeriodMonth();
}
function initializeCreateForm() {
createForm.periodMonth = createInitialPeriodMonth();
createForm.employeeId = props.initialEmployeeId || '';
prefillSourcePeriodMonth.value = '';
}
async function executeSave(): Promise<PerformanceSheetSavePayload | null> {
const workbook = getActiveWorkbook();
if (!workbook) return null;
const scores = readScores();
if (!scores.actualScoreTotal || !scores.baseScoreTotal || !scores.extraScoreTotal) {
window.$message?.warning('未能读取完整的三种得分总计,请检查模板单元格映射配置');
return null;
}
// create 模式先校验表单
if (isCreateMode.value) {
try {
await validate();
} catch {
return null;
}
}
await ensureExcelRuntime();
const sheet = await ensureCreatedSheet();
const snapshot = applySheetZoomRatio(workbook.save());
const fileName = createInitialFileName();
const buffer = await transformUniverToExcel(snapshot, fileName);
const excelBuffer = await applyExcelZoomMetadata(buffer);
const file = new File([excelBuffer], fileName, {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const uploadResult = await uploadFile(file, `performance/sheets/${sheet.periodMonth}`);
if (uploadResult.error || !uploadResult.data) {
return null;
}
const updateResult = await updatePerformanceSheetExcel(sheet.id, {
fileId: uploadResult.data.id,
fileName,
fileVersion: sheet.fileVersion,
actualScoreTotal: scores.actualScoreTotal,
baseScoreTotal: scores.baseScoreTotal,
extraScoreTotal: scores.extraScoreTotal
});
if (updateResult.error) {
return null;
}
return {
sheet,
actualScoreTotal: scores.actualScoreTotal,
baseScoreTotal: scores.baseScoreTotal,
extraScoreTotal: scores.extraScoreTotal
};
}
async function handleSaveDraft() {
saving.value = true;
try {
const saveResult = await executeSave();
if (!saveResult) return;
window.$message?.success(isCreateMode.value ? '绩效表已保存为草稿' : '绩效 Excel 已保存');
visible.value = false;
emit('saved', saveResult);
} catch (error) {
window.$message?.error(error instanceof Error ? error.message : '绩效 Excel 保存失败');
} finally {
saving.value = false;
}
}
async function handleSaveAndSend() {
sending.value = true;
try {
const saveResult = await executeSave();
if (!saveResult) return;
const sendResult = await sendPerformanceSheet(saveResult.sheet.id);
if (sendResult.error) return;
window.$message?.success('绩效表已保存并发送');
visible.value = false;
emit('savedAndSent', saveResult);
} catch (error) {
window.$message?.error(error instanceof Error ? error.message : '绩效表发送失败');
} finally {
sending.value = false;
}
}
watch(visible, async isVisible => {
if (!isVisible) {
disposeUniver();
currentSheet.value = null;
currentTemplate.value = null;
loadedCreateContextKey.value = '';
createContextInitializing.value = false;
prefillLoading.value = false;
prefillSourcePeriodMonth.value = '';
initializeCreateForm();
return;
}
createContextInitializing.value = isCreateMode.value;
try {
if (isCreateMode.value) {
initializeCreateForm();
}
await nextTick();
await loadWorkbook();
} finally {
createContextInitializing.value = false;
}
});
watch(
() => [createForm.employeeId, createForm.periodMonth] as const,
async ([employeeId, periodMonth], [previousEmployeeId, previousPeriodMonth]) => {
if (!visible.value || !isCreateMode.value || createContextInitializing.value) {
return;
}
const currentKey = `${periodMonth || ''}::${employeeId || ''}`;
const previousKey = `${previousPeriodMonth || ''}::${previousEmployeeId || ''}`;
if (
currentKey === previousKey ||
currentKey === loadedCreateContextKey.value ||
loading.value ||
prefillLoading.value
) {
return;
}
await nextTick();
await loadWorkbook();
}
);
onBeforeUnmount(() => {
window.removeEventListener('resize', syncViewportWidth);
disposeUniver();
});
onMounted(() => {
syncViewportWidth();
window.addEventListener('resize', syncViewportWidth);
});
</script>
<template>
<ElDrawer
v-model="visible"
class="performance-excel-editor-drawer"
:title="drawerTitle"
body-class="performance-excel-editor-drawer__body"
:size="drawerSize"
:close-on-click-modal="false"
append-to-body
>
<!-- 创建模式下的表单区域 -->
<div v-if="isCreateMode" class="performance-excel-editor__create-form">
<ElForm ref="formRef" :model="createForm" :rules="createFormRules" label-position="top" inline>
<ElFormItem label="绩效月份" prop="periodMonth" class="performance-excel-editor__form-item">
<ElDatePicker
v-model="createForm.periodMonth"
type="month"
value-format="YYYY-MM"
placeholder="选择绩效月份"
:disabled="loading || prefillLoading || saving || sending"
/>
</ElFormItem>
<ElFormItem label="下属" prop="employeeId" class="performance-excel-editor__form-item">
<ElSelect
v-model="createForm.employeeId"
filterable
placeholder="选择下属"
style="width: 200px"
:disabled="loading || prefillLoading || saving || sending"
>
<ElOption v-for="opt in props.subordinateOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</ElSelect>
</ElFormItem>
</ElForm>
<div v-if="prefillLoading || prefillSourcePeriodMonth" class="performance-excel-editor__prefill-tip">
{{ prefillLoading ? '正在自动带出上一份绩效数据...' : `已自动带出 ${prefillSourcePeriodMonth} 的绩效数据` }}
</div>
</div>
<div v-loading="loading" class="performance-excel-editor">
<ElAlert
v-if="errorMessage"
class="performance-excel-editor__alert"
type="error"
:title="errorMessage"
show-icon
:closable="false"
/>
<div ref="containerRef" class="performance-excel-editor__container" />
</div>
<template v-if="canSave || showApprovalFooter" #footer>
<div class="performance-excel-editor__footer">
<template v-if="canSave">
<ElButton
v-if="!props.hideDraftAction"
:loading="saving"
:disabled="sending || loading || prefillLoading"
@click="handleSaveDraft"
>
保存草稿
</ElButton>
<ElButton
type="primary"
:loading="sending"
:disabled="saving || loading || prefillLoading"
@click="handleSaveAndSend"
>
发送绩效
</ElButton>
</template>
<template v-else-if="showApprovalFooter">
<ElButton @click="handleClose">退出</ElButton>
<ElButton type="primary" @click="handleStartApproval">确认</ElButton>
</template>
</div>
</template>
</ElDrawer>
</template>
<style scoped lang="scss">
:global(.performance-excel-editor-drawer__body) {
display: flex;
flex-direction: column;
min-height: 0;
padding: 20px 24px 0;
}
.performance-excel-editor__create-form {
flex-shrink: 0;
margin-bottom: 16px;
padding: 16px 20px;
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
background: var(--el-fill-color-blank);
}
.performance-excel-editor__form-item {
margin-bottom: 0;
margin-right: 24px;
}
.performance-excel-editor__prefill-tip {
margin-top: 12px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.performance-excel-editor__footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 12px 24px 20px;
border-top: 1px solid var(--el-border-color-lighter);
background: var(--el-bg-color);
}
.performance-excel-editor {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
height: 100%;
}
.performance-excel-editor__alert {
margin-bottom: 12px;
flex-shrink: 0;
}
.performance-excel-editor__container {
flex: 1;
min-height: 0;
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
overflow: hidden;
background: var(--el-bg-color);
}
</style>