fix(加班申请、工作报告、我的绩效): 重构页面样式、修复一系列bug、对不合理的地方进行调整。

This commit is contained in:
dk
2026-06-22 23:07:21 +08:00
parent b1d52b852f
commit 632c123112
30 changed files with 1574 additions and 451 deletions

View File

@@ -1,6 +1,7 @@
<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,
@@ -58,8 +59,9 @@ const createForm = reactive({
const createFormRules = computed<FormRules>(() => ({
periodMonth: [createRequiredRule('请选择绩效月份')],
employeeId: [createRequiredRule('请选择员工')]
employeeId: [createRequiredRule('请选择下属')]
}));
const ASSESSED_EMPLOYEE_TEXT_PATTERN = /(被考核人\s*[:]\s*)(.+)/u;
let univerInstance: any = null;
let univerAPI: any = null;
@@ -68,6 +70,7 @@ 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(() => {
@@ -160,6 +163,167 @@ function transformUniverToExcel(snapshot: any, fileName: string) {
});
}
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;
@@ -185,15 +349,8 @@ function createWorkbook(snapshot: any) {
throw new Error('Univer 工作簿初始化失败');
}
// 在 snapshot 数据中预设缩放比例 40%,避免调用不可用的 zoom API
const data = snapshot || {};
if (data.sheets) {
Object.values(data.sheets).forEach((sheet: any) => {
if (sheet && typeof sheet === 'object') {
sheet.zoomRatio = 0.4;
}
});
}
// 在 snapshot 数据中预设缩放比例,保证在线查看和导出文件使用同一套缩放值
const data = applySheetZoomRatio(snapshot);
univer.createUnit(unitType, data);
}
@@ -251,6 +408,15 @@ 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) {
@@ -317,7 +483,7 @@ async function loadWorkbook() {
});
const snapshot = await transformExcelToUniver(file);
await nextTick();
createWorkbook(snapshot);
createWorkbook(applyCreateEmployeeName(snapshot));
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : 'Excel 解析失败';
} finally {
@@ -331,7 +497,7 @@ async function ensureCreatedSheet() {
}
if (!createForm.periodMonth || !createForm.employeeId) {
throw new Error('请先填写绩效月份和员工');
throw new Error('请先填写绩效月份和下属');
}
const createResult = await createPerformanceSheet({
@@ -373,10 +539,11 @@ async function executeSave(): Promise<Api.Performance.Sheet.Sheet | null> {
await ensureExcelRuntime();
const sheet = await ensureCreatedSheet();
const snapshot = workbook.save();
const snapshot = applySheetZoomRatio(workbook.save());
const fileName = createInitialFileName();
const buffer = await transformUniverToExcel(snapshot, fileName);
const file = new File([buffer], 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}`);
@@ -451,6 +618,22 @@ watch(visible, async isVisible => {
await loadWorkbook();
});
watch(
() => createForm.employeeId,
async (employeeId, previousEmployeeId) => {
if (!visible.value || !isCreateMode.value || !employeeId || employeeId === previousEmployeeId) {
return;
}
const workbook = getActiveWorkbook();
if (!workbook) return;
const snapshot = workbook.save();
await nextTick();
createWorkbook(applyCreateEmployeeName(snapshot));
}
);
onBeforeUnmount(() => {
window.removeEventListener('resize', syncViewportWidth);
disposeUniver();
@@ -483,8 +666,8 @@ onMounted(() => {
placeholder="选择绩效月份"
/>
</ElFormItem>
<ElFormItem label="员工" prop="employeeId" class="performance-excel-editor__form-item">
<ElSelect v-model="createForm.employeeId" filterable placeholder="选择员工" style="width: 200px">
<ElFormItem label="下属" prop="employeeId" class="performance-excel-editor__form-item">
<ElSelect v-model="createForm.employeeId" filterable placeholder="选择下属" style="width: 200px">
<ElOption v-for="opt in props.subordinateOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</ElSelect>
</ElFormItem>