feat(performance、workReport、workbench): 添加绩效表预填功能并优化工作台组件
This commit is contained in:
@@ -65,6 +65,11 @@ type MonthlyResultResponse = Omit<Api.Performance.Sheet.MonthlyResult, 'sheetId'
|
||||
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<
|
||||
Api.Performance.Team.Summary,
|
||||
| '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 {
|
||||
return {
|
||||
...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() {
|
||||
return request<Api.Performance.Sheet.StatusDict[]>({
|
||||
...safeJsonRequestConfig,
|
||||
|
||||
10
src/typings/api/performance.d.ts
vendored
10
src/typings/api/performance.d.ts
vendored
@@ -21,6 +21,10 @@ declare namespace Api {
|
||||
actualScoreTotalCell?: string | null;
|
||||
baseScoreTotalCell?: string | null;
|
||||
extraScoreTotalCell?: string | null;
|
||||
resultDescriptionCells?: string[] | null;
|
||||
actualScoreCells?: string[] | null;
|
||||
baseScoreCells?: string[] | null;
|
||||
extraScoreCells?: string[] | null;
|
||||
}
|
||||
|
||||
interface Template {
|
||||
@@ -172,6 +176,12 @@ declare namespace Api {
|
||||
extraScoreTotal?: string | number | null;
|
||||
statusCode?: Common.SheetStatusCode | null;
|
||||
}
|
||||
|
||||
interface PrefillData {
|
||||
sourceSheetId?: string | null;
|
||||
sourcePeriodMonth?: string | null;
|
||||
cellValues: Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Team {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createPerformanceSheet,
|
||||
downloadFile,
|
||||
fetchPerformanceSheet,
|
||||
fetchPerformanceSheetPrefill,
|
||||
fetchPerformanceTemplateCurrent,
|
||||
sendPerformanceSheet,
|
||||
updatePerformanceSheetExcel,
|
||||
@@ -54,12 +55,16 @@ 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(),
|
||||
@@ -380,6 +385,10 @@ function getActiveWorkbook() {
|
||||
return univerAPI?.getActiveWorkbook?.();
|
||||
}
|
||||
|
||||
function getCreateContextKey() {
|
||||
return `${createForm.periodMonth || ''}::${createForm.employeeId || ''}`;
|
||||
}
|
||||
|
||||
function parseCellAddress(address?: string | null) {
|
||||
const text = String(address || '')
|
||||
.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() {
|
||||
const mapping = currentTemplate.value?.scoreCellMapping;
|
||||
|
||||
@@ -451,9 +483,36 @@ function createInitialFileName() {
|
||||
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;
|
||||
|
||||
@@ -505,6 +564,14 @@ async function loadWorkbook() {
|
||||
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 {
|
||||
@@ -546,6 +613,7 @@ function createInitialPeriodMonth() {
|
||||
function initializeCreateForm() {
|
||||
createForm.periodMonth = createInitialPeriodMonth();
|
||||
createForm.employeeId = props.initialEmployeeId || '';
|
||||
prefillSourcePeriodMonth.value = '';
|
||||
}
|
||||
|
||||
async function executeSave(): Promise<PerformanceSheetSavePayload | null> {
|
||||
@@ -643,31 +711,48 @@ watch(visible, async isVisible => {
|
||||
disposeUniver();
|
||||
currentSheet.value = null;
|
||||
currentTemplate.value = null;
|
||||
loadedCreateContextKey.value = '';
|
||||
createContextInitializing.value = false;
|
||||
prefillLoading.value = false;
|
||||
prefillSourcePeriodMonth.value = '';
|
||||
initializeCreateForm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreateMode.value) {
|
||||
initializeCreateForm();
|
||||
}
|
||||
createContextInitializing.value = isCreateMode.value;
|
||||
|
||||
await nextTick();
|
||||
await loadWorkbook();
|
||||
try {
|
||||
if (isCreateMode.value) {
|
||||
initializeCreateForm();
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
await loadWorkbook();
|
||||
} finally {
|
||||
createContextInitializing.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => createForm.employeeId,
|
||||
async (employeeId, previousEmployeeId) => {
|
||||
if (!visible.value || !isCreateMode.value || !employeeId || employeeId === previousEmployeeId) {
|
||||
() => [createForm.employeeId, createForm.periodMonth] as const,
|
||||
async ([employeeId, periodMonth], [previousEmployeeId, previousPeriodMonth]) => {
|
||||
if (!visible.value || !isCreateMode.value || createContextInitializing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = getActiveWorkbook();
|
||||
if (!workbook) return;
|
||||
const currentKey = `${periodMonth || ''}::${employeeId || ''}`;
|
||||
const previousKey = `${previousPeriodMonth || ''}::${previousEmployeeId || ''}`;
|
||||
if (
|
||||
currentKey === previousKey ||
|
||||
currentKey === loadedCreateContextKey.value ||
|
||||
loading.value ||
|
||||
prefillLoading.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = workbook.save();
|
||||
await nextTick();
|
||||
createWorkbook(applyCreateEmployeeName(snapshot));
|
||||
await loadWorkbook();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -701,14 +786,24 @@ onMounted(() => {
|
||||
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">
|
||||
<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">
|
||||
@@ -727,14 +822,26 @@ onMounted(() => {
|
||||
<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" @click="handleSaveDraft">
|
||||
<ElButton
|
||||
v-if="!props.hideDraftAction"
|
||||
:loading="saving"
|
||||
:disabled="sending || loading || prefillLoading"
|
||||
@click="handleSaveDraft"
|
||||
>
|
||||
保存草稿
|
||||
</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 v-else-if="showApprovalFooter">
|
||||
<ElButton @click="handleClose">退出审批</ElButton>
|
||||
<ElButton type="primary" @click="handleStartApproval">开始审批</ElButton>
|
||||
<ElButton @click="handleClose">退出</ElButton>
|
||||
<ElButton type="primary" @click="handleStartApproval">审批</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -763,6 +870,12 @@ onMounted(() => {
|
||||
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;
|
||||
|
||||
@@ -864,8 +864,8 @@ watch(
|
||||
</ElForm>
|
||||
|
||||
<div class="form-actions approval-form-actions">
|
||||
<ElButton @click="emit('back')">退出审批</ElButton>
|
||||
<ElButton type="primary" @click="openAuditDialog">开始审批</ElButton>
|
||||
<ElButton @click="emit('back')">退出</ElButton>
|
||||
<ElButton type="primary" @click="openAuditDialog">审批</ElButton>
|
||||
</div>
|
||||
|
||||
<BusinessFormDialog
|
||||
|
||||
@@ -539,8 +539,8 @@ function notifyTitleSaved(item: WorkItem) {
|
||||
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
|
||||
</div>
|
||||
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
|
||||
<ElButton @click="emit('back')">退出审批</ElButton>
|
||||
<ElButton type="primary" @click="emit('requestApprove')">开始审批</ElButton>
|
||||
<ElButton @click="emit('back')">退出</ElButton>
|
||||
<ElButton type="primary" @click="emit('requestApprove')">审批</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1500,8 +1500,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
|
||||
</div>
|
||||
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
|
||||
<ElButton @click="emit('back')">退出审批</ElButton>
|
||||
<ElButton type="primary" @click="emit('requestApprove')">开始审批</ElButton>
|
||||
<ElButton @click="emit('back')">退出</ElButton>
|
||||
<ElButton type="primary" @click="emit('requestApprove')">审批</ElButton>
|
||||
</div>
|
||||
|
||||
<BusinessFormDialog
|
||||
|
||||
@@ -252,7 +252,7 @@ const TASK_ACTION_ICONS: Partial<Record<Api.Project.ProjectTaskActionCode, Compo
|
||||
cancel: markRaw(IconMdiCloseCircleOutline)
|
||||
};
|
||||
|
||||
function normalizeTaskTodoDeadline(deadline: string | null) {
|
||||
function normalizeTodoDeadline(deadline: string | null) {
|
||||
if (!deadline) return null;
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(deadline)) {
|
||||
@@ -831,7 +831,7 @@ async function loadMyTaskItems() {
|
||||
category: 'task' as const,
|
||||
title: task.taskTitle,
|
||||
createdTime: task.createTime,
|
||||
deadline: normalizeTaskTodoDeadline(task.plannedEndDate),
|
||||
deadline: normalizeTodoDeadline(task.plannedEndDate),
|
||||
source: task.projectName,
|
||||
progressRate: task.progressRate,
|
||||
priority: mapTaskTodoPriority(task.priority),
|
||||
@@ -861,7 +861,7 @@ async function loadPersonalTodoItems() {
|
||||
category: 'personal',
|
||||
title: item.taskTitle,
|
||||
createdTime: item.createTime,
|
||||
deadline: item.plannedEndDate,
|
||||
deadline: normalizeTodoDeadline(item.plannedEndDate),
|
||||
source: '我的事项',
|
||||
progressRate: item.progressRate,
|
||||
routeKey: 'personal-center_my-item'
|
||||
|
||||
Reference in New Issue
Block a user