feat(performance、workReport、workbench): 添加绩效表预填功能并优化工作台组件

This commit is contained in:
dk
2026-07-08 20:08:04 +08:00
parent 62b97a2fd7
commit e2094831e4
7 changed files with 187 additions and 26 deletions

View File

@@ -65,6 +65,11 @@ type MonthlyResultResponse = Omit<Api.Performance.Sheet.MonthlyResult, 'sheetId'
employeeId: StringIdResponse; employeeId: StringIdResponse;
}; };
type PrefillResponse = Omit<Api.Performance.Sheet.PrefillData, 'sourceSheetId' | 'cellValues'> & {
sourceSheetId?: StringIdResponse | null;
cellValues?: Record<string, string | number | null | undefined> | null;
};
type TeamSummaryResponse = Omit< type TeamSummaryResponse = Omit<
Api.Performance.Team.Summary, Api.Performance.Team.Summary,
| 'expectedSheetCount' | 'expectedSheetCount'
@@ -185,6 +190,28 @@ function normalizeMonthlyResult(response: MonthlyResultResponse): Api.Performanc
}; };
} }
function normalizePrefillData(response: PrefillResponse): Api.Performance.Sheet.PrefillData {
const cellValues = Object.fromEntries(
Object.entries(response.cellValues || {}).flatMap(([address, value]) => {
const normalizedAddress = String(address || '')
.trim()
.toUpperCase();
if (!normalizedAddress || value === null || value === undefined) {
return [];
}
return [[normalizedAddress, String(value)]];
})
);
return {
sourceSheetId: normalizeNullableStringId(response.sourceSheetId),
sourcePeriodMonth: response.sourcePeriodMonth ?? null,
cellValues
};
}
function normalizeTeamSummary(response: TeamSummaryResponse): Api.Performance.Team.Summary { function normalizeTeamSummary(response: TeamSummaryResponse): Api.Performance.Team.Summary {
return { return {
...response, ...response,
@@ -464,6 +491,17 @@ export async function fetchPerformanceMonthlyResult(employeeId: string, periodMo
); );
} }
export async function fetchPerformanceSheetPrefill(employeeId: string, periodMonth: string) {
const result = await request<PrefillResponse>({
...safeJsonRequestConfig,
url: `${SHEET_PREFIX}/prefill`,
method: 'get',
params: { employeeId, periodMonth }
});
return mapServiceResult(result as ServiceRequestResult<PrefillResponse>, normalizePrefillData);
}
export function fetchPerformanceSheetStatusDict() { export function fetchPerformanceSheetStatusDict() {
return request<Api.Performance.Sheet.StatusDict[]>({ return request<Api.Performance.Sheet.StatusDict[]>({
...safeJsonRequestConfig, ...safeJsonRequestConfig,

View File

@@ -21,6 +21,10 @@ declare namespace Api {
actualScoreTotalCell?: string | null; actualScoreTotalCell?: string | null;
baseScoreTotalCell?: string | null; baseScoreTotalCell?: string | null;
extraScoreTotalCell?: string | null; extraScoreTotalCell?: string | null;
resultDescriptionCells?: string[] | null;
actualScoreCells?: string[] | null;
baseScoreCells?: string[] | null;
extraScoreCells?: string[] | null;
} }
interface Template { interface Template {
@@ -172,6 +176,12 @@ declare namespace Api {
extraScoreTotal?: string | number | null; extraScoreTotal?: string | number | null;
statusCode?: Common.SheetStatusCode | null; statusCode?: Common.SheetStatusCode | null;
} }
interface PrefillData {
sourceSheetId?: string | null;
sourcePeriodMonth?: string | null;
cellValues: Record<string, string>;
}
} }
namespace Team { namespace Team {

View File

@@ -7,6 +7,7 @@ import {
createPerformanceSheet, createPerformanceSheet,
downloadFile, downloadFile,
fetchPerformanceSheet, fetchPerformanceSheet,
fetchPerformanceSheetPrefill,
fetchPerformanceTemplateCurrent, fetchPerformanceTemplateCurrent,
sendPerformanceSheet, sendPerformanceSheet,
updatePerformanceSheetExcel, updatePerformanceSheetExcel,
@@ -54,12 +55,16 @@ const { createRequiredRule } = useFormRules();
const containerRef = ref<HTMLDivElement>(); const containerRef = ref<HTMLDivElement>();
const loading = ref(false); const loading = ref(false);
const prefillLoading = ref(false);
const saving = ref(false); const saving = ref(false);
const sending = ref(false); const sending = ref(false);
const currentSheet = ref<Api.Performance.Sheet.Sheet | null>(null); const currentSheet = ref<Api.Performance.Sheet.Sheet | null>(null);
const currentTemplate = ref<Api.Performance.Template.Template | null>(null); const currentTemplate = ref<Api.Performance.Template.Template | null>(null);
const errorMessage = ref(''); const errorMessage = ref('');
const prefillSourcePeriodMonth = ref('');
const viewportWidth = ref(typeof window === 'undefined' ? 1920 : window.innerWidth); const viewportWidth = ref(typeof window === 'undefined' ? 1920 : window.innerWidth);
const loadedCreateContextKey = ref('');
const createContextInitializing = ref(false);
const createForm = reactive({ const createForm = reactive({
periodMonth: createDefaultPeriodMonth(), periodMonth: createDefaultPeriodMonth(),
@@ -380,6 +385,10 @@ function getActiveWorkbook() {
return univerAPI?.getActiveWorkbook?.(); return univerAPI?.getActiveWorkbook?.();
} }
function getCreateContextKey() {
return `${createForm.periodMonth || ''}::${createForm.employeeId || ''}`;
}
function parseCellAddress(address?: string | null) { function parseCellAddress(address?: string | null) {
const text = String(address || '') const text = String(address || '')
.trim() .trim()
@@ -415,6 +424,29 @@ function readCell(address?: string | null) {
} }
} }
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() { function readScores() {
const mapping = currentTemplate.value?.scoreCellMapping; const mapping = currentTemplate.value?.scoreCellMapping;
@@ -451,9 +483,36 @@ function createInitialFileName() {
return '绩效表.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() { async function loadWorkbook() {
loading.value = true; loading.value = true;
prefillLoading.value = false;
errorMessage.value = ''; errorMessage.value = '';
prefillSourcePeriodMonth.value = '';
currentSheet.value = null; currentSheet.value = null;
currentTemplate.value = null; currentTemplate.value = null;
@@ -505,6 +564,14 @@ async function loadWorkbook() {
const snapshot = await transformExcelToUniver(file); const snapshot = await transformExcelToUniver(file);
await nextTick(); await nextTick();
createWorkbook(applyCreateEmployeeName(snapshot)); createWorkbook(applyCreateEmployeeName(snapshot));
if (isCreateMode.value) {
await nextTick();
await applyCreatePrefill();
loadedCreateContextKey.value = getCreateContextKey();
} else {
loadedCreateContextKey.value = '';
}
} catch (error) { } catch (error) {
errorMessage.value = error instanceof Error ? error.message : 'Excel 解析失败'; errorMessage.value = error instanceof Error ? error.message : 'Excel 解析失败';
} finally { } finally {
@@ -546,6 +613,7 @@ function createInitialPeriodMonth() {
function initializeCreateForm() { function initializeCreateForm() {
createForm.periodMonth = createInitialPeriodMonth(); createForm.periodMonth = createInitialPeriodMonth();
createForm.employeeId = props.initialEmployeeId || ''; createForm.employeeId = props.initialEmployeeId || '';
prefillSourcePeriodMonth.value = '';
} }
async function executeSave(): Promise<PerformanceSheetSavePayload | null> { async function executeSave(): Promise<PerformanceSheetSavePayload | null> {
@@ -643,31 +711,48 @@ watch(visible, async isVisible => {
disposeUniver(); disposeUniver();
currentSheet.value = null; currentSheet.value = null;
currentTemplate.value = null; currentTemplate.value = null;
loadedCreateContextKey.value = '';
createContextInitializing.value = false;
prefillLoading.value = false;
prefillSourcePeriodMonth.value = '';
initializeCreateForm(); initializeCreateForm();
return; return;
} }
if (isCreateMode.value) { createContextInitializing.value = isCreateMode.value;
initializeCreateForm();
}
await nextTick(); try {
await loadWorkbook(); if (isCreateMode.value) {
initializeCreateForm();
}
await nextTick();
await loadWorkbook();
} finally {
createContextInitializing.value = false;
}
}); });
watch( watch(
() => createForm.employeeId, () => [createForm.employeeId, createForm.periodMonth] as const,
async (employeeId, previousEmployeeId) => { async ([employeeId, periodMonth], [previousEmployeeId, previousPeriodMonth]) => {
if (!visible.value || !isCreateMode.value || !employeeId || employeeId === previousEmployeeId) { if (!visible.value || !isCreateMode.value || createContextInitializing.value) {
return; return;
} }
const workbook = getActiveWorkbook(); const currentKey = `${periodMonth || ''}::${employeeId || ''}`;
if (!workbook) return; const previousKey = `${previousPeriodMonth || ''}::${previousEmployeeId || ''}`;
if (
currentKey === previousKey ||
currentKey === loadedCreateContextKey.value ||
loading.value ||
prefillLoading.value
) {
return;
}
const snapshot = workbook.save();
await nextTick(); await nextTick();
createWorkbook(applyCreateEmployeeName(snapshot)); await loadWorkbook();
} }
); );
@@ -701,14 +786,24 @@ onMounted(() => {
type="month" type="month"
value-format="YYYY-MM" value-format="YYYY-MM"
placeholder="选择绩效月份" placeholder="选择绩效月份"
:disabled="loading || prefillLoading || saving || sending"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="下属" prop="employeeId" class="performance-excel-editor__form-item"> <ElFormItem label="下属" prop="employeeId" class="performance-excel-editor__form-item">
<ElSelect v-model="createForm.employeeId" filterable placeholder="选择下属" style="width: 200px"> <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" /> <ElOption v-for="opt in props.subordinateOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
<div v-if="prefillLoading || prefillSourcePeriodMonth" class="performance-excel-editor__prefill-tip">
{{ prefillLoading ? '正在自动带出上一份绩效数据...' : `已自动带出 ${prefillSourcePeriodMonth} 的绩效数据` }}
</div>
</div> </div>
<div v-loading="loading" class="performance-excel-editor"> <div v-loading="loading" class="performance-excel-editor">
@@ -727,14 +822,26 @@ onMounted(() => {
<template v-if="canSave || showApprovalFooter" #footer> <template v-if="canSave || showApprovalFooter" #footer>
<div class="performance-excel-editor__footer"> <div class="performance-excel-editor__footer">
<template v-if="canSave"> <template v-if="canSave">
<ElButton v-if="!props.hideDraftAction" :loading="saving" :disabled="sending" @click="handleSaveDraft"> <ElButton
v-if="!props.hideDraftAction"
:loading="saving"
:disabled="sending || loading || prefillLoading"
@click="handleSaveDraft"
>
保存草稿 保存草稿
</ElButton> </ElButton>
<ElButton type="primary" :loading="sending" :disabled="saving" @click="handleSaveAndSend">发送绩效</ElButton> <ElButton
type="primary"
:loading="sending"
:disabled="saving || loading || prefillLoading"
@click="handleSaveAndSend"
>
发送绩效
</ElButton>
</template> </template>
<template v-else-if="showApprovalFooter"> <template v-else-if="showApprovalFooter">
<ElButton @click="handleClose">退出审批</ElButton> <ElButton @click="handleClose">退出</ElButton>
<ElButton type="primary" @click="handleStartApproval">开始审批</ElButton> <ElButton type="primary" @click="handleStartApproval">审批</ElButton>
</template> </template>
</div> </div>
</template> </template>
@@ -763,6 +870,12 @@ onMounted(() => {
margin-right: 24px; margin-right: 24px;
} }
.performance-excel-editor__prefill-tip {
margin-top: 12px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.performance-excel-editor__footer { .performance-excel-editor__footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@@ -864,8 +864,8 @@ watch(
</ElForm> </ElForm>
<div class="form-actions approval-form-actions"> <div class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出审批</ElButton> <ElButton @click="emit('back')">退出</ElButton>
<ElButton type="primary" @click="openAuditDialog">开始审批</ElButton> <ElButton type="primary" @click="openAuditDialog">审批</ElButton>
</div> </div>
<BusinessFormDialog <BusinessFormDialog

View File

@@ -539,8 +539,8 @@ function notifyTitleSaved(item: WorkItem) {
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton> <ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
</div> </div>
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions"> <div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出审批</ElButton> <ElButton @click="emit('back')">退出</ElButton>
<ElButton type="primary" @click="emit('requestApprove')">开始审批</ElButton> <ElButton type="primary" @click="emit('requestApprove')">审批</ElButton>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1500,8 +1500,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton> <ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
</div> </div>
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions"> <div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出审批</ElButton> <ElButton @click="emit('back')">退出</ElButton>
<ElButton type="primary" @click="emit('requestApprove')">开始审批</ElButton> <ElButton type="primary" @click="emit('requestApprove')">审批</ElButton>
</div> </div>
<BusinessFormDialog <BusinessFormDialog

View File

@@ -252,7 +252,7 @@ const TASK_ACTION_ICONS: Partial<Record<Api.Project.ProjectTaskActionCode, Compo
cancel: markRaw(IconMdiCloseCircleOutline) cancel: markRaw(IconMdiCloseCircleOutline)
}; };
function normalizeTaskTodoDeadline(deadline: string | null) { function normalizeTodoDeadline(deadline: string | null) {
if (!deadline) return null; if (!deadline) return null;
if (/^\d{4}-\d{2}-\d{2}$/.test(deadline)) { if (/^\d{4}-\d{2}-\d{2}$/.test(deadline)) {
@@ -831,7 +831,7 @@ async function loadMyTaskItems() {
category: 'task' as const, category: 'task' as const,
title: task.taskTitle, title: task.taskTitle,
createdTime: task.createTime, createdTime: task.createTime,
deadline: normalizeTaskTodoDeadline(task.plannedEndDate), deadline: normalizeTodoDeadline(task.plannedEndDate),
source: task.projectName, source: task.projectName,
progressRate: task.progressRate, progressRate: task.progressRate,
priority: mapTaskTodoPriority(task.priority), priority: mapTaskTodoPriority(task.priority),
@@ -861,7 +861,7 @@ async function loadPersonalTodoItems() {
category: 'personal', category: 'personal',
title: item.taskTitle, title: item.taskTitle,
createdTime: item.createTime, createdTime: item.createTime,
deadline: item.plannedEndDate, deadline: normalizeTodoDeadline(item.plannedEndDate),
source: '我的事项', source: '我的事项',
progressRate: item.progressRate, progressRate: item.progressRate,
routeKey: 'personal-center_my-item' routeKey: 'personal-center_my-item'