feat(performance): 新增绩效下签功能支持
- 在月报审批页面增加 sign-off 场景支持 - 添加绩效下签对话框组件实现批量下签功能 - 增加导入绩效压缩包和直接下签两种操作模式 - 实现电子签名预览和日期格式化显示功能 - 更新绩效权限配置增加下签相关权限 - 优化审批对话框标题和按钮文案显示 - 添加导入结果提示和文件验证功能 - 实现团队模式下的下签操作入口 - 完善绩效表格状态和操作权限判断逻辑
This commit is contained in:
@@ -44,6 +44,15 @@ type SheetPageResponse = {
|
||||
list: SheetResponse[];
|
||||
};
|
||||
|
||||
type ImportZipResultResponse = Omit<Api.Performance.Sheet.ImportZipResult, 'messages'> & {
|
||||
workbookCount?: number | string | null;
|
||||
sheetCount?: number | string | null;
|
||||
successCount?: number | string | null;
|
||||
skippedCount?: number | string | null;
|
||||
failedCount?: number | string | null;
|
||||
messages?: string[] | null;
|
||||
};
|
||||
|
||||
type StatusLogResponse = Omit<Api.Performance.Sheet.StatusLog, 'id' | 'sheetId' | 'operatorUserId'> & {
|
||||
id: StringIdResponse;
|
||||
sheetId: StringIdResponse;
|
||||
@@ -144,6 +153,8 @@ function normalizeSheet(response: SheetResponse): Api.Performance.Sheet.Sheet {
|
||||
fileId: normalizeNullableStringId(response.fileId),
|
||||
fileName: response.fileName ?? null,
|
||||
statusName: response.statusName || response.statusCode,
|
||||
signOffStatus: response.signOffStatus ?? null,
|
||||
signOffStatusName: response.signOffStatusName ?? null,
|
||||
actualScoreTotal: response.actualScoreTotal ?? null,
|
||||
baseScoreTotal: response.baseScoreTotal ?? null,
|
||||
extraScoreTotal: response.extraScoreTotal ?? null,
|
||||
@@ -282,6 +293,7 @@ function createSheetQuery(params: Api.Performance.Sheet.SearchParams = {}) {
|
||||
appendValue(query, 'employeeDeptId', params.employeeDeptId);
|
||||
appendValue(query, 'managerName', params.managerName);
|
||||
appendValue(query, 'statusCode', params.statusCode);
|
||||
appendValue(query, 'signOffStatus', params.signOffStatus);
|
||||
|
||||
return query.toString();
|
||||
}
|
||||
@@ -377,6 +389,46 @@ export async function createPerformanceSheet(data: Api.Performance.Sheet.CreateP
|
||||
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||
}
|
||||
|
||||
export async function importPerformanceSheetZip(data: Api.Performance.Sheet.ImportZipParams) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', data.file);
|
||||
formData.append('periodMonth', data.periodMonth);
|
||||
data.employeeIds?.forEach(id => {
|
||||
formData.append('employeeIds', id);
|
||||
});
|
||||
|
||||
const result = await request<ImportZipResultResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${SHEET_PREFIX}/import-zip`,
|
||||
method: 'post',
|
||||
data: formData
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<ImportZipResultResponse>, payload => ({
|
||||
workbookCount: normalizeTotal(payload.workbookCount),
|
||||
sheetCount: normalizeTotal(payload.sheetCount),
|
||||
successCount: normalizeTotal(payload.successCount),
|
||||
skippedCount: normalizeTotal(payload.skippedCount),
|
||||
failedCount: normalizeTotal(payload.failedCount),
|
||||
messages: payload.messages ?? []
|
||||
}));
|
||||
}
|
||||
|
||||
export async function signOffPerformanceSheets(data: Api.Performance.Sheet.SignOffParams) {
|
||||
const result = await request<StringIdResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${SHEET_PREFIX}/sign-off`,
|
||||
method: 'post',
|
||||
data: {
|
||||
...data,
|
||||
employeeIds: data.employeeIds && data.employeeIds.length ? data.employeeIds : undefined,
|
||||
remark: data.remark?.trim() || undefined
|
||||
}
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||
}
|
||||
|
||||
export function updatePerformanceSheetExcel(id: string, data: Api.Performance.Sheet.ExcelUpdateParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
|
||||
@@ -17,6 +17,7 @@ const DEPT_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dept`;
|
||||
const POST_PREFIX = `${SYSTEM_SERVICE_PREFIX}/post`;
|
||||
const ORG_LEADER_PREFIX = `${SYSTEM_SERVICE_PREFIX}/org-leader`;
|
||||
const USER_MANAGEMENT_RELATION_PREFIX = `${SYSTEM_SERVICE_PREFIX}/user-management-relation`;
|
||||
const USER_SIGNATURE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/user-signatures`;
|
||||
|
||||
function appendQueryValue(query: URLSearchParams, key: string, value: unknown) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
@@ -146,6 +147,12 @@ type MySubordinateTreeNodeResponse = Omit<Api.SystemManage.MySubordinateTreeNode
|
||||
children?: MySubordinateTreeNodeResponse[] | null;
|
||||
};
|
||||
|
||||
type UserSignatureResponse = Omit<Api.SystemManage.UserSignature, 'id' | 'userId' | 'fileId'> & {
|
||||
id: string | number;
|
||||
userId: string | number;
|
||||
fileId: string | number;
|
||||
};
|
||||
|
||||
function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple {
|
||||
return {
|
||||
...user,
|
||||
@@ -217,6 +224,20 @@ function normalizeMySubordinateTreeNode(node: MySubordinateTreeNodeResponse): Ap
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUserSignature(signature: UserSignatureResponse): Api.SystemManage.UserSignature {
|
||||
return {
|
||||
...signature,
|
||||
id: normalizeStringId(signature.id),
|
||||
userId: normalizeStringId(signature.userId),
|
||||
fileId: normalizeStringId(signature.fileId),
|
||||
fileName: signature.fileName ?? null,
|
||||
status: signature.status ?? null,
|
||||
remark: signature.remark ?? null,
|
||||
createTime: signature.createTime ?? null,
|
||||
updateTime: signature.updateTime ?? null
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取角色分页 */
|
||||
export async function fetchGetRolePage(params?: Api.SystemManage.RoleSearchParams) {
|
||||
const query = createRolePageQuery(params);
|
||||
@@ -742,6 +763,49 @@ export function fetchAssignUserRoles(data: Api.SystemManage.AssignUserRoleParams
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchGetUserSignature(userId: string) {
|
||||
const result = await request<UserSignatureResponse | null>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${USER_SIGNATURE_PREFIX}/get`,
|
||||
method: 'get',
|
||||
params: { userId }
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<UserSignatureResponse | null>, data =>
|
||||
data ? normalizeUserSignature(data) : null
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchSaveUserSignature(data: Api.SystemManage.SaveUserSignatureParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${USER_SIGNATURE_PREFIX}/save`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDeleteUserSignature(userId: string) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${USER_SIGNATURE_PREFIX}/delete`,
|
||||
method: 'delete',
|
||||
params: { userId }
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchGetMyUserSignature() {
|
||||
const result = await request<UserSignatureResponse | null>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${USER_SIGNATURE_PREFIX}/my`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<UserSignatureResponse | null>, data =>
|
||||
data ? normalizeUserSignature(data) : null
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 用户管理链路相关 API ====================
|
||||
/**
|
||||
* 获取用户管理链路树形结构
|
||||
|
||||
@@ -88,11 +88,18 @@ type ApprovalRecordResponse = Omit<
|
||||
|
||||
type MonthlyApprovalRecordResponse = Omit<
|
||||
Api.WorkReport.Monthly.MonthlyReportApprovalRecord,
|
||||
'id' | 'statusLogId' | 'auditorUserId'
|
||||
'id' | 'statusLogId' | 'auditorUserId' | 'employeeSignatureFileId' | 'supervisorSignatureFileId'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
statusLogId: StringIdResponse;
|
||||
auditorUserId: StringIdResponse;
|
||||
employeeSignatureFileId?: MaybeStringIdResponse;
|
||||
supervisorSignatureFileId?: MaybeStringIdResponse;
|
||||
};
|
||||
|
||||
type MonthlySignOffDetailResponse = {
|
||||
report: MonthlyReportResponse;
|
||||
latestApprovalRecord?: MonthlyApprovalRecordResponse | null;
|
||||
};
|
||||
|
||||
type ProjectOptionResponse = Omit<Api.WorkReport.Project.ProjectReportOwnerProjectOption, 'id'> & {
|
||||
@@ -229,6 +236,7 @@ function createWeeklyPageQuery(params: Api.WorkReport.Weekly.WeeklyReportSearchP
|
||||
function createMonthlyPageQuery(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||
const query = createBasePageQuery(params);
|
||||
appendNullableArrayFlag(query, 'reporterIds', params.reporterIds);
|
||||
appendValue(query, 'signOffStatus', params.signOffStatus);
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
@@ -298,6 +306,8 @@ function normalizeMonthlyReport(response: MonthlyReportResponse): Api.WorkReport
|
||||
reporterDeptName: response.reporterDeptName ?? null,
|
||||
reporterPostName: response.reporterPostName ?? null,
|
||||
statusName: response.statusName || response.statusCode,
|
||||
signOffStatus: response.signOffStatus ?? null,
|
||||
signOffStatusName: response.signOffStatusName ?? null,
|
||||
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||
terminal: normalizeBooleanFlag(response.terminal),
|
||||
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||
@@ -361,7 +371,12 @@ function normalizeMonthlyApprovalRecord(
|
||||
statusLogId: normalizeStringId(response.statusLogId),
|
||||
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||
conclusion: normalizeApprovalConclusion(response.conclusion),
|
||||
opinion: response.opinion ?? null
|
||||
opinion: response.opinion ?? null,
|
||||
meetingDate: normalizeDateText(response.meetingDate) ?? null,
|
||||
employeeSignatureFileId: normalizeNullableStringId(response.employeeSignatureFileId),
|
||||
employeeSignedDate: normalizeDateText(response.employeeSignedDate) ?? null,
|
||||
supervisorSignatureFileId: normalizeNullableStringId(response.supervisorSignatureFileId),
|
||||
supervisorSignedDate: normalizeDateText(response.supervisorSignedDate) ?? null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -697,6 +712,19 @@ export async function fetchGetMonthlyReportApprovalPage(params: Api.WorkReport.M
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetMonthlyReportSignOffPage(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||
const query = createMonthlyPageQuery(params);
|
||||
const result = await request<PageResponse<MonthlyReportResponse>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${MONTHLY_PREFIX}/sign-off-page?${query}` : `${MONTHLY_PREFIX}/sign-off-page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PageResponse<MonthlyReportResponse>>, data =>
|
||||
mapPage(data, normalizeMonthlyReport)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetMonthlyReportDetail(id: string) {
|
||||
const result = await request<MonthlyReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
@@ -799,6 +827,34 @@ export async function fetchGetMonthlyReportApprovalRecords(id: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetMonthlyReportSignOffDetail(id: string) {
|
||||
const result = await request<MonthlySignOffDetailResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/${id}/sign-off-detail`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<MonthlySignOffDetailResponse>, data => ({
|
||||
report: normalizeMonthlyReport(data.report),
|
||||
latestApprovalRecord: data.latestApprovalRecord ? normalizeMonthlyApprovalRecord(data.latestApprovalRecord) : null
|
||||
}));
|
||||
}
|
||||
|
||||
export function fetchConfirmMonthlyReportSignOff(
|
||||
id: string,
|
||||
data: Api.WorkReport.Monthly.MonthlyReportSignOffConfirmParams
|
||||
) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/${id}/confirm-sign-off`,
|
||||
method: 'post',
|
||||
data: {
|
||||
meetingDate: data.meetingDate,
|
||||
employeeSignedDate: data.employeeSignedDate || undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchExportMonthlyReports(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||
const query = createMonthlyPageQuery(params);
|
||||
return request<Blob, 'blob'>({
|
||||
|
||||
24
src/typings/api/performance.d.ts
vendored
24
src/typings/api/performance.d.ts
vendored
@@ -74,6 +74,8 @@ declare namespace Api {
|
||||
fileVersion: number;
|
||||
statusCode: Common.SheetStatusCode;
|
||||
statusName: string;
|
||||
signOffStatus?: string | null;
|
||||
signOffStatusName?: string | null;
|
||||
actualScoreTotal?: string | number | null;
|
||||
baseScoreTotal?: string | number | null;
|
||||
extraScoreTotal?: string | number | null;
|
||||
@@ -96,6 +98,7 @@ declare namespace Api {
|
||||
managerId: string;
|
||||
managerName: string;
|
||||
statusCode: Common.SheetStatusCode;
|
||||
signOffStatus: string;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -121,6 +124,27 @@ declare namespace Api {
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
interface ImportZipParams {
|
||||
file: File;
|
||||
periodMonth: string;
|
||||
employeeIds?: string[] | null;
|
||||
}
|
||||
|
||||
interface ImportZipResult {
|
||||
workbookCount: number;
|
||||
sheetCount: number;
|
||||
successCount: number;
|
||||
skippedCount: number;
|
||||
failedCount: number;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
interface SignOffParams {
|
||||
periodMonth: string;
|
||||
employeeIds?: string[] | null;
|
||||
remark?: string | null;
|
||||
}
|
||||
|
||||
interface StatusDict {
|
||||
statusCode: Common.SheetStatusCode;
|
||||
statusName: string;
|
||||
|
||||
19
src/typings/api/system-manage.d.ts
vendored
19
src/typings/api/system-manage.d.ts
vendored
@@ -239,6 +239,25 @@ declare namespace Api {
|
||||
roleIds: string[];
|
||||
}
|
||||
|
||||
interface UserSignature {
|
||||
id: string;
|
||||
userId: string;
|
||||
fileId: string;
|
||||
fileName?: string | null;
|
||||
status?: number | null;
|
||||
remark?: string | null;
|
||||
createTime?: string | null;
|
||||
updateTime?: string | null;
|
||||
}
|
||||
|
||||
interface SaveUserSignatureParams {
|
||||
userId: string;
|
||||
fileId: string;
|
||||
fileName?: string | null;
|
||||
status?: number | null;
|
||||
remark?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* menu type
|
||||
*
|
||||
|
||||
15
src/typings/api/work-report.d.ts
vendored
15
src/typings/api/work-report.d.ts
vendored
@@ -189,6 +189,8 @@ declare namespace Api {
|
||||
periodEndDate: string;
|
||||
statusCode: Common.WorkReportStatusCode | string;
|
||||
statusName: string;
|
||||
signOffStatus?: string | null;
|
||||
signOffStatusName?: string | null;
|
||||
allowEdit: boolean;
|
||||
terminal: boolean;
|
||||
totalWorkHours?: number | string | null;
|
||||
@@ -204,6 +206,7 @@ declare namespace Api {
|
||||
|
||||
type MonthlyReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||
reporterIds?: string[] | null;
|
||||
signOffStatus?: string | null;
|
||||
};
|
||||
|
||||
interface MonthlyReportSaveParams {
|
||||
@@ -245,10 +248,22 @@ declare namespace Api {
|
||||
improvementSuggestion?: string | null;
|
||||
performanceResult?: string | null;
|
||||
employeeSignName?: string | null;
|
||||
employeeSignatureFileId?: string | null;
|
||||
employeeSignedDate?: string | null;
|
||||
supervisorSignName?: string | null;
|
||||
supervisorSignatureFileId?: string | null;
|
||||
supervisorSignedDate?: string | null;
|
||||
}
|
||||
|
||||
interface MonthlyReportSignOffConfirmParams {
|
||||
meetingDate: string;
|
||||
employeeSignedDate?: string | null;
|
||||
}
|
||||
|
||||
interface MonthlyReportSignOffDetail {
|
||||
report: MonthlyReport;
|
||||
latestApprovalRecord: MonthlyReportApprovalRecord | null;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Project {
|
||||
|
||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@@ -130,11 +130,13 @@ declare module 'vue' {
|
||||
IconMdiFolderOpen: typeof import('~icons/mdi/folder-open')['default']
|
||||
IconMdiFolderOutline: typeof import('~icons/mdi/folder-outline')['default']
|
||||
IconMdiFolderPlusOutline: typeof import('~icons/mdi/folder-plus-outline')['default']
|
||||
IconMdiFolderZipOutline: typeof import('~icons/mdi/folder-zip-outline')['default']
|
||||
IconMdiInboxMultipleOutline: typeof import('~icons/mdi/inbox-multiple-outline')['default']
|
||||
IconMdiInformationOutline: typeof import('~icons/mdi/information-outline')['default']
|
||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
IconMdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
|
||||
IconMdiPenCheckOutline: typeof import('~icons/mdi/pen-check-outline')['default']
|
||||
IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline')['default']
|
||||
IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
|
||||
@@ -34,6 +34,7 @@ import PerformanceActionDialog from './modules/performance-action-dialog.vue';
|
||||
import PerformanceExcelEditorDrawer from './modules/performance-excel-editor-drawer.vue';
|
||||
import PerformanceRecordDialog from './modules/performance-record-dialog.vue';
|
||||
import PerformanceSearch from './modules/performance-search.vue';
|
||||
import PerformanceSignOffDialog from './modules/performance-sign-off-dialog.vue';
|
||||
import PerformanceSummary from './modules/performance-summary.vue';
|
||||
import PerformanceTemplateDialog from './modules/performance-template-dialog.vue';
|
||||
import {
|
||||
@@ -116,6 +117,7 @@ const excelMode = ref<'view' | 'edit' | 'create'>('view');
|
||||
const actionVisible = ref(false);
|
||||
const actionType = ref<'confirm' | 'reject'>('confirm');
|
||||
const recordVisible = ref(false);
|
||||
const signOffVisible = ref(false);
|
||||
|
||||
const exporting = ref(false);
|
||||
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||
@@ -138,6 +140,9 @@ const canUpdate = computed(() => hasAuth(PerformancePermission.SheetUpdate));
|
||||
const canDelete = computed(() => hasAuth(PerformancePermission.SheetDelete));
|
||||
const canConfirm = computed(() => hasAuth(PerformancePermission.SheetConfirm));
|
||||
const canReject = computed(() => hasAuth(PerformancePermission.SheetReject));
|
||||
const canSignOff = computed(
|
||||
() => hasAuth(PerformancePermission.SheetImport) && hasAuth(PerformancePermission.SheetSignOff)
|
||||
);
|
||||
const canExport = computed(() => hasAuth(PerformancePermission.SheetExport));
|
||||
const canManageTemplate = computed(
|
||||
() => hasAuth(PerformancePermission.TemplateQuery) || hasAuth(PerformancePermission.TemplateUpdate)
|
||||
@@ -303,6 +308,21 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination, r
|
||||
});
|
||||
|
||||
const totalCount = computed(() => mobilePagination.value.total || data.value.length);
|
||||
const currentActionEmployeeIds = computed(() => {
|
||||
if (!isTeamMode.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return currentEmployeeIds.value ?? undefined;
|
||||
});
|
||||
const signOffInitialPeriodMonth = computed(() => formattedPeriodMonthEnd.value || createDefaultPeriodMonth());
|
||||
|
||||
function canHandlePersonalSheet(row: Api.Performance.Sheet.Sheet) {
|
||||
return (
|
||||
row.statusCode === 'sent' &&
|
||||
['pending_employee_confirm', 'employee_rejected'].includes(String(row.signOffStatus || ''))
|
||||
);
|
||||
}
|
||||
|
||||
function getRowActions(row: Api.Performance.Sheet.Sheet): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
@@ -357,7 +377,7 @@ function getRowActions(row: Api.Performance.Sheet.Sheet): BusinessTableAction[]
|
||||
onClick: () => handleDelete(row)
|
||||
});
|
||||
}
|
||||
} else if (row.statusCode === 'sent') {
|
||||
} else if (canHandlePersonalSheet(row)) {
|
||||
if (canConfirm.value) {
|
||||
actions.push({
|
||||
key: 'confirm',
|
||||
@@ -419,6 +439,10 @@ function handleSelectionChange(rows: Api.Performance.Sheet.Sheet[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function openSignOffDialog() {
|
||||
signOffVisible.value = true;
|
||||
}
|
||||
|
||||
function openExcel(row: Api.Performance.Sheet.Sheet, mode: 'view' | 'edit') {
|
||||
currentRow.value = row;
|
||||
excelMode.value = mode;
|
||||
@@ -743,6 +767,12 @@ onMounted(async () => {
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
<ElButton v-if="isTeamMode && canSignOff" plain @click="openSignOffDialog">
|
||||
<template #icon>
|
||||
<icon-mdi-check-circle-outline class="text-icon" />
|
||||
</template>
|
||||
下签
|
||||
</ElButton>
|
||||
<ElButton v-if="isTeamMode && canManageTemplate" plain @click="templateVisible = true">
|
||||
<template #icon>
|
||||
<icon-mdi-file-cog-outline class="text-icon" />
|
||||
@@ -811,6 +841,15 @@ onMounted(async () => {
|
||||
/>
|
||||
|
||||
<PerformanceRecordDialog v-model:visible="recordVisible" :row-data="currentRow" />
|
||||
|
||||
<PerformanceSignOffDialog
|
||||
v-model:visible="signOffVisible"
|
||||
open-mode="sign-off"
|
||||
:employee-ids="currentActionEmployeeIds"
|
||||
:initial-period-month="signOffInitialPeriodMonth"
|
||||
:team-label="selectedTeamLabel"
|
||||
@submitted="reloadAfterMutation"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -41,15 +41,15 @@ const form = reactive<{
|
||||
|
||||
const isReject = computed(() => form.actionType === 'reject');
|
||||
const title = computed(() => {
|
||||
if (props.selectableActionType) return '绩效审批';
|
||||
if (props.selectableActionType) return '绩效确认';
|
||||
return isReject.value ? '退回绩效表' : '确认绩效表';
|
||||
});
|
||||
const confirmText = computed(() => {
|
||||
if (props.selectableActionType) return '确认提交';
|
||||
return isReject.value ? '确认退回' : '确认';
|
||||
});
|
||||
const opinionLabel = computed(() => (isReject.value ? '退回原因' : '审批意见'));
|
||||
const opinionPlaceholder = computed(() => (isReject.value ? `请输入${opinionLabel.value}` : '可填写审批意见'));
|
||||
const opinionLabel = computed(() => (isReject.value ? '退回原因' : '意见'));
|
||||
const opinionPlaceholder = computed(() => (isReject.value ? `请输入${opinionLabel.value}` : '可填写意见'));
|
||||
const rules = computed<FormRules>(() => ({
|
||||
reason: isReject.value ? [createRequiredRule('请输入退回原因')] : []
|
||||
}));
|
||||
@@ -104,7 +104,7 @@ watch(visible, isVisible => {
|
||||
|
||||
<div v-if="props.selectableActionType" class="performance-action-dialog__approval-form">
|
||||
<div class="audit-field">
|
||||
<label>审批结论</label>
|
||||
<label>结论</label>
|
||||
<div class="audit-conclusion">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -841,7 +841,7 @@ onMounted(() => {
|
||||
</template>
|
||||
<template v-else-if="showApprovalFooter">
|
||||
<ElButton @click="handleClose">退出</ElButton>
|
||||
<ElButton type="primary" @click="handleStartApproval">审批</ElButton>
|
||||
<ElButton type="primary" @click="handleStartApproval">确认</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,8 @@ export const PerformancePermission = {
|
||||
SheetDelete: 'project:performance-sheet:delete',
|
||||
SheetConfirm: 'project:performance-sheet:confirm',
|
||||
SheetReject: 'project:performance-sheet:reject',
|
||||
SheetImport: 'project:performance-sheet:import',
|
||||
SheetSignOff: 'project:performance-sheet:sign-off',
|
||||
SheetExport: 'project:performance-sheet:export',
|
||||
TeamDashboard: 'project:performance-sheet:team-dashboard'
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import type { UploadFile, UploadInstance } from 'element-plus';
|
||||
import { importPerformanceSheetZip, signOffPerformanceSheets } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'PerformanceSignOffDialog' });
|
||||
|
||||
type OpenMode = 'import' | 'sign-off';
|
||||
type ActionMode = 'direct' | 'import';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
openMode?: OpenMode;
|
||||
employeeIds?: string[] | null;
|
||||
initialPeriodMonth?: string;
|
||||
teamLabel?: string;
|
||||
}>(),
|
||||
{
|
||||
openMode: 'sign-off',
|
||||
employeeIds: undefined,
|
||||
initialPeriodMonth: '',
|
||||
teamLabel: ''
|
||||
}
|
||||
);
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
submitted: [];
|
||||
}>();
|
||||
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const submitting = ref(false);
|
||||
const importResult = ref<Api.Performance.Sheet.ImportZipResult | null>(null);
|
||||
|
||||
const form = reactive<{
|
||||
actionMode: ActionMode;
|
||||
periodMonth: string;
|
||||
remark: string;
|
||||
file: File | null;
|
||||
fileName: string;
|
||||
}>({
|
||||
actionMode: 'direct',
|
||||
periodMonth: '',
|
||||
remark: '',
|
||||
file: null,
|
||||
fileName: ''
|
||||
});
|
||||
|
||||
const isImportOnly = computed(() => props.openMode === 'import');
|
||||
const needsZipFile = computed(() => isImportOnly.value || form.actionMode === 'import');
|
||||
const title = computed(() => (isImportOnly.value ? '导入绩效压缩包' : '一键下签'));
|
||||
const confirmText = computed(() => (isImportOnly.value ? '开始导入' : '确认执行'));
|
||||
const teamLabelText = computed(() => {
|
||||
if (needsZipFile.value) {
|
||||
return '压缩包中含有的下属';
|
||||
}
|
||||
|
||||
return props.teamLabel || '当前范围';
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
form.actionMode = 'direct';
|
||||
form.periodMonth = props.initialPeriodMonth || '';
|
||||
form.remark = '';
|
||||
form.file = null;
|
||||
form.fileName = '';
|
||||
importResult.value = null;
|
||||
uploadRef.value?.clearFiles();
|
||||
}
|
||||
|
||||
function handleFileChange(file: UploadFile) {
|
||||
const rawFile = file.raw;
|
||||
uploadRef.value?.clearFiles();
|
||||
|
||||
if (!rawFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rawFile.name.toLowerCase().endsWith('.zip')) {
|
||||
window.$message?.warning('请选择 zip 压缩包');
|
||||
return;
|
||||
}
|
||||
|
||||
form.file = rawFile;
|
||||
form.fileName = rawFile.name;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.periodMonth) {
|
||||
window.$message?.warning('请选择绩效月份');
|
||||
return;
|
||||
}
|
||||
|
||||
if (needsZipFile.value && !form.file) {
|
||||
window.$message?.warning('请选择绩效压缩包');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
if (needsZipFile.value && form.file) {
|
||||
const importResponse = await importPerformanceSheetZip({
|
||||
file: form.file,
|
||||
periodMonth: form.periodMonth,
|
||||
employeeIds: props.employeeIds
|
||||
});
|
||||
|
||||
if (importResponse.error || !importResponse.data) {
|
||||
submitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
importResult.value = importResponse.data;
|
||||
|
||||
if (isImportOnly.value) {
|
||||
submitting.value = false;
|
||||
|
||||
if (importResponse.data.failedCount > 0) {
|
||||
window.$message?.warning('导入存在失败项,请处理后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('绩效压缩包导入成功');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
return;
|
||||
}
|
||||
|
||||
if (importResponse.data.failedCount > 0) {
|
||||
submitting.value = false;
|
||||
window.$message?.warning('导入存在失败项,已停止后续下签');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isImportOnly.value) {
|
||||
const signOffResponse = await signOffPerformanceSheets({
|
||||
periodMonth: form.periodMonth,
|
||||
employeeIds: props.employeeIds,
|
||||
remark: form.remark
|
||||
});
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (signOffResponse.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('绩效表下签成功');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (isVisible) {
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
preset="md"
|
||||
append-to-body
|
||||
:confirm-text="confirmText"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm label-position="top">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol v-if="!isImportOnly" :span="24">
|
||||
<ElFormItem label="执行方式">
|
||||
<ElRadioGroup v-model="form.actionMode">
|
||||
<ElRadio value="import">导入绩效压缩包后下签</ElRadio>
|
||||
<ElRadio value="direct">直接下签</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="绩效月份" required style="width: 100%">
|
||||
<ElDatePicker
|
||||
v-model="form.periodMonth"
|
||||
class="w-full"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
placeholder="请选择绩效月份"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="适用范围">
|
||||
<ElInput :model-value="teamLabelText" readonly />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
<ElCol v-if="needsZipFile" :span="24">
|
||||
<ElFormItem label="绩效压缩包" required>
|
||||
<div class="performance-sign-off-dialog__upload-row">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept=".zip"
|
||||
:limit="1"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<ElButton plain>选择 zip 文件</ElButton>
|
||||
</ElUpload>
|
||||
<div
|
||||
class="performance-sign-off-dialog__file-name"
|
||||
:class="{ 'performance-sign-off-dialog__file-name--placeholder': !form.fileName }"
|
||||
>
|
||||
{{ form.fileName || '未选择文件' }}
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
<ElCol v-if="!isImportOnly" :span="24">
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="可填写本次下签说明"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
|
||||
<ElAlert
|
||||
v-if="importResult"
|
||||
:type="importResult.failedCount > 0 ? 'warning' : 'success'"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-12px"
|
||||
>
|
||||
<template #title>
|
||||
导入结果:工作簿 {{ importResult.workbookCount }} 个,工作表 {{ importResult.sheetCount }} 个,成功
|
||||
{{ importResult.successCount }} 个,跳过 {{ importResult.skippedCount }} 个,失败
|
||||
{{ importResult.failedCount }} 个
|
||||
</template>
|
||||
<div v-if="importResult.messages.length" class="performance-sign-off-dialog__messages">
|
||||
<div v-for="(message, index) in importResult.messages" :key="`${index}-${message}`">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</ElAlert>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.performance-sign-off-dialog__upload-row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.performance-sign-off-dialog__file-name {
|
||||
min-width: 0;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
line-height: 34px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.performance-sign-off-dialog__file-name--placeholder {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.performance-sign-off-dialog__messages {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -20,16 +20,20 @@ const props = defineProps<{
|
||||
reportType: string;
|
||||
period: string;
|
||||
mode?: 'add' | 'edit' | 'detail';
|
||||
scene?: 'fill' | 'detail' | 'approval';
|
||||
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: [];
|
||||
}>();
|
||||
@@ -79,6 +83,9 @@ 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({
|
||||
@@ -95,7 +102,31 @@ const pageValidationModel = reactive({
|
||||
return props.approvalModel.performanceResult || '';
|
||||
}
|
||||
});
|
||||
const pageRules: FormRules = {
|
||||
|
||||
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('请输入绩效考核结果'),
|
||||
{
|
||||
@@ -110,7 +141,8 @@ const pageRules: FormRules = {
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
});
|
||||
const auditRules = computed<FormRules>(() => ({
|
||||
opinion: rejectOpinionRequired.value
|
||||
? [
|
||||
@@ -252,24 +284,12 @@ const mainForm = reactive({
|
||||
});
|
||||
|
||||
const signatureForm = reactive({
|
||||
get employeeSign() {
|
||||
return props.approvalModel.employeeSignName || '';
|
||||
},
|
||||
set employeeSign(value: string) {
|
||||
patchApproval('employeeSignName', value);
|
||||
},
|
||||
get employeeDate() {
|
||||
return props.approvalModel.employeeSignedDate || todayText.value;
|
||||
return props.approvalModel.employeeSignedDate || '';
|
||||
},
|
||||
set employeeDate(value: string) {
|
||||
patchApproval('employeeSignedDate', value);
|
||||
},
|
||||
get supervisorSign() {
|
||||
return props.approvalModel.supervisorSignName || '';
|
||||
},
|
||||
set supervisorSign(value: string) {
|
||||
patchApproval('supervisorSignName', value);
|
||||
},
|
||||
get supervisorDate() {
|
||||
return props.approvalModel.supervisorSignedDate || todayText.value;
|
||||
},
|
||||
@@ -560,7 +580,8 @@ async function submitAudit() {
|
||||
}
|
||||
}
|
||||
|
||||
patchApproval('employeeSignedDate', signatureForm.employeeDate);
|
||||
patchApproval('employeeSignName', undefined);
|
||||
patchApproval('employeeSignedDate', undefined);
|
||||
patchApproval('supervisorSignedDate', signatureForm.supervisorDate);
|
||||
patchApproval(
|
||||
'reason',
|
||||
@@ -617,6 +638,16 @@ 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;
|
||||
|
||||
@@ -670,7 +701,13 @@ watch(
|
||||
<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" style="width: 100%" />
|
||||
<ElDatePicker
|
||||
v-model="mainForm.meetingDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</div>
|
||||
@@ -731,10 +768,23 @@ watch(
|
||||
<span class="feedback-tag success">优势</span>
|
||||
</div>
|
||||
<div class="feedback-cell">
|
||||
<ElInput v-model="feedbackForm.strengthDesc" type="textarea" :rows="2" placeholder="请输入优势描述" />
|
||||
<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"
|
||||
@@ -747,10 +797,23 @@ watch(
|
||||
<span class="feedback-tag warning">劣势</span>
|
||||
</div>
|
||||
<div class="feedback-cell">
|
||||
<ElInput v-model="feedbackForm.weaknessDesc" type="textarea" :rows="2" placeholder="请输入劣势描述" />
|
||||
<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"
|
||||
@@ -760,7 +823,11 @@ watch(
|
||||
</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"
|
||||
@@ -778,7 +845,7 @@ watch(
|
||||
<div class="performance-result">
|
||||
<div class="performance-label">
|
||||
绩效考核结果
|
||||
<span class="field-required-mark">*</span>
|
||||
<span v-if="!isSignOffScene" class="field-required-mark">*</span>
|
||||
</div>
|
||||
<ElFormItem class="performance-inline-form-item" prop="performanceResult">
|
||||
<div class="performance-input-group">
|
||||
@@ -790,6 +857,7 @@ watch(
|
||||
@input="handlePerformanceScoreInput"
|
||||
/>
|
||||
<ElButton
|
||||
v-if="!isSignOffScene"
|
||||
plain
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -836,21 +904,68 @@ watch(
|
||||
</div>
|
||||
|
||||
<div class="signature-section">
|
||||
<template v-if="isSignOffScene">
|
||||
<div class="signature-row">
|
||||
<span>被考核人签名:</span>
|
||||
<ElInput v-model="signatureForm.employeeSign" class="signature-input" placeholder="请输入签名" />
|
||||
<span>日期:</span>
|
||||
<ElDatePicker
|
||||
v-model="signatureForm.employeeDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
class="signature-date"
|
||||
<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>
|
||||
<ElInput v-model="signatureForm.supervisorSign" class="signature-input" placeholder="请输入签名" />
|
||||
<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"
|
||||
@@ -860,15 +975,21 @@ watch(
|
||||
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 type="primary" @click="openAuditDialog">审批</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"
|
||||
@@ -937,6 +1058,7 @@ watch(
|
||||
</BusinessFormDialog>
|
||||
|
||||
<PerformanceExcelEditorDrawer
|
||||
v-if="!isSignOffScene"
|
||||
v-model:visible="performanceDrawerVisible"
|
||||
:row-data="performanceDrawerRow"
|
||||
:mode="performanceDrawerMode"
|
||||
@@ -1731,6 +1853,11 @@ watch(
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.feedback-readonly {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.feedback-cell {
|
||||
padding: 16px;
|
||||
border-right: 1px solid #f1f5f9;
|
||||
@@ -1800,27 +1927,37 @@ watch(
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.signature-input {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.signature-input :deep(.el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
background: transparent;
|
||||
.signature-readonly {
|
||||
min-width: 180px;
|
||||
padding: 0 4px 4px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
border-radius: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.signature-input :deep(.el-input__inner) {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #334155;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.signature-input :deep(.el-input__inner::placeholder) {
|
||||
color: #cbd5e1;
|
||||
font-weight: 400;
|
||||
.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 {
|
||||
@@ -1846,6 +1983,14 @@ watch(
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
downloadFile,
|
||||
fetchConfirmMonthlyReportSignOff,
|
||||
fetchGetMonthlyReportSignOffDetail,
|
||||
fetchGetMyUserSignature
|
||||
} from '@/service/api';
|
||||
import MonthlyReportApprovalPage from './approval-page.vue';
|
||||
|
||||
defineOptions({ name: 'MonthlySignOffPage' });
|
||||
|
||||
const props = defineProps<{
|
||||
reportId?: string;
|
||||
baseInfo?: Api.WorkReport.Monthly.MonthlyReport | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
back: [];
|
||||
submitted: [];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const detail = ref<Api.WorkReport.Monthly.MonthlyReportSignOffDetail | null>(null);
|
||||
const supervisorSignaturePreviewUrl = ref('');
|
||||
const supervisorSignatureObjectUrl = ref<string | null>(null);
|
||||
const employeeSignaturePreviewUrl = ref('');
|
||||
const employeeSignatureObjectUrl = ref<string | null>(null);
|
||||
|
||||
const report = computed(() => detail.value?.report || props.baseInfo || null);
|
||||
const monthlyModel = computed<Api.WorkReport.Monthly.MonthlyReportSaveParams>(() => ({
|
||||
periodKey: report.value?.periodKey || '',
|
||||
periodLabel: report.value?.periodLabel || '',
|
||||
periodStartDate: report.value?.periodStartDate || '',
|
||||
periodEndDate: report.value?.periodEndDate || '',
|
||||
reviewItems: report.value?.reviewItems || [],
|
||||
planItems: report.value?.planItems || []
|
||||
}));
|
||||
|
||||
const approvalDraft = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||
reason: '',
|
||||
meetingDate: dayjs().format('YYYY-MM-DD'),
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: undefined,
|
||||
employeeSignedDate: dayjs().format('YYYY-MM-DD'),
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
|
||||
function patchApprovalRecord(record: Api.WorkReport.Monthly.MonthlyReportApprovalRecord | null | undefined) {
|
||||
Object.assign(approvalDraft, {
|
||||
reason: '',
|
||||
meetingDate: dayjs().format('YYYY-MM-DD'),
|
||||
strengthDesc: record?.strengthDesc || '',
|
||||
strengthExample: record?.strengthExample || '',
|
||||
weaknessDesc: record?.weaknessDesc || '',
|
||||
weaknessExample: record?.weaknessExample || '',
|
||||
improvementSuggestion: record?.improvementSuggestion || '',
|
||||
performanceResult: record?.performanceResult || '',
|
||||
employeeSignName: record?.employeeSignName,
|
||||
employeeSignedDate: dayjs().format('YYYY-MM-DD'),
|
||||
supervisorSignName: record?.supervisorSignName || '',
|
||||
supervisorSignedDate: record?.supervisorSignedDate || ''
|
||||
});
|
||||
}
|
||||
|
||||
function revokePreviewUrl(target: 'supervisor' | 'employee') {
|
||||
const refValue = target === 'supervisor' ? supervisorSignatureObjectUrl : employeeSignatureObjectUrl;
|
||||
const previewRef = target === 'supervisor' ? supervisorSignaturePreviewUrl : employeeSignaturePreviewUrl;
|
||||
|
||||
if (refValue.value) {
|
||||
URL.revokeObjectURL(refValue.value);
|
||||
refValue.value = null;
|
||||
}
|
||||
|
||||
previewRef.value = '';
|
||||
}
|
||||
|
||||
async function loadSignaturePreview(fileId: string | null | undefined, target: 'supervisor' | 'employee') {
|
||||
revokePreviewUrl(target);
|
||||
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, data } = await downloadFile(fileId);
|
||||
if (error || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(data);
|
||||
if (target === 'supervisor') {
|
||||
supervisorSignatureObjectUrl.value = objectUrl;
|
||||
supervisorSignaturePreviewUrl.value = objectUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
employeeSignatureObjectUrl.value = objectUrl;
|
||||
employeeSignaturePreviewUrl.value = objectUrl;
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.reportId) {
|
||||
detail.value = null;
|
||||
patchApprovalRecord(null);
|
||||
revokePreviewUrl('supervisor');
|
||||
revokePreviewUrl('employee');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const [detailResult, mySignatureResult] = await Promise.all([
|
||||
fetchGetMonthlyReportSignOffDetail(props.reportId),
|
||||
fetchGetMyUserSignature()
|
||||
]);
|
||||
loading.value = false;
|
||||
|
||||
if (detailResult.error || !detailResult.data) {
|
||||
detail.value = null;
|
||||
patchApprovalRecord(null);
|
||||
revokePreviewUrl('supervisor');
|
||||
revokePreviewUrl('employee');
|
||||
return;
|
||||
}
|
||||
|
||||
detail.value = detailResult.data;
|
||||
patchApprovalRecord(detailResult.data.latestApprovalRecord);
|
||||
|
||||
await Promise.all([
|
||||
loadSignaturePreview(detailResult.data.latestApprovalRecord?.supervisorSignatureFileId, 'supervisor'),
|
||||
loadSignaturePreview(mySignatureResult.error ? null : mySignatureResult.data?.fileId, 'employee')
|
||||
]);
|
||||
}
|
||||
|
||||
function handleApprovalChange(payload: Api.WorkReport.Monthly.MonthlyReportApproveParams) {
|
||||
Object.assign(approvalDraft, payload);
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!props.reportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
const result = await fetchConfirmMonthlyReportSignOff(props.reportId, {
|
||||
meetingDate: approvalDraft.meetingDate || dayjs().format('YYYY-MM-DD'),
|
||||
employeeSignedDate: approvalDraft.employeeSignedDate || undefined
|
||||
});
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('月报下签确认成功');
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.reportId,
|
||||
() => {
|
||||
loadDetail();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokePreviewUrl('supervisor');
|
||||
revokePreviewUrl('employee');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<MonthlyReportApprovalPage
|
||||
report-type="个人月报"
|
||||
:period="report?.periodLabel || ''"
|
||||
mode="detail"
|
||||
scene="sign-off"
|
||||
:base-info="report"
|
||||
:model="monthlyModel"
|
||||
:approval-model="approvalDraft"
|
||||
:supervisor-signature-preview-url="supervisorSignaturePreviewUrl"
|
||||
:employee-signature-preview-url="employeeSignaturePreviewUrl"
|
||||
:supervisor-signature-loading="false"
|
||||
@back="emit('back')"
|
||||
@change-approval="handleApprovalChange"
|
||||
@request-confirm-sign-off="handleConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable complexity, no-nested-ternary, no-void, vue/no-deprecated-filter */
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
downloadFile,
|
||||
fetchApproveMonthlyReport,
|
||||
fetchApproveProjectReport,
|
||||
fetchApproveWeeklyReport,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
fetchCreateProjectReport,
|
||||
fetchCreateWeeklyReport,
|
||||
fetchGetMonthlyReportDetail,
|
||||
fetchGetMyUserSignature,
|
||||
fetchGetProjectReportDetail,
|
||||
fetchGetWeeklyReportDetail,
|
||||
fetchInitMonthlyReport,
|
||||
@@ -48,6 +50,7 @@ import WeeklyReportPage from '../../weekly/modules/fill-page.vue';
|
||||
import MonthlyReportPage from '../../monthly/modules/fill-page.vue';
|
||||
import ProjectReportPage from '../../project/modules/fill-page.vue';
|
||||
import MonthlyReportApprovalPage from '../../monthly/modules/approval-page.vue';
|
||||
import MonthlyReportSignOffPage from '../../monthly/modules/sign-off-page.vue';
|
||||
import WorkReportActionDialog from './action-dialog.vue';
|
||||
import WorkReportPageDialog from './page-dialog.vue';
|
||||
|
||||
@@ -63,7 +66,7 @@ interface PeriodPayload {
|
||||
interface Props {
|
||||
reportType: WorkReportType;
|
||||
mode?: 'add' | 'edit' | 'detail';
|
||||
scene?: 'fill' | 'detail' | 'approval';
|
||||
scene?: 'fill' | 'detail' | 'approval' | 'sign-off';
|
||||
rowData?: WorkReportRow | null;
|
||||
initialPeriod?: PeriodPayload | null;
|
||||
initialProjectId?: string;
|
||||
@@ -95,7 +98,7 @@ const loading = ref(false);
|
||||
const actionVisible = ref(false);
|
||||
const actionSubmitting = ref(false);
|
||||
const currentActionType = ref<'approve' | 'reject'>('approve');
|
||||
const currentStage = ref<'form' | 'approval'>('form');
|
||||
const currentStage = ref<'form' | 'approval' | 'sign-off'>('form');
|
||||
const currentReportId = ref('');
|
||||
const baseInfo = ref<WorkReportRow | null>(null);
|
||||
const monthlyApprovalDraft = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||
@@ -107,12 +110,15 @@ const monthlyApprovalDraft = reactive<Api.WorkReport.Monthly.MonthlyReportApprov
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
employeeSignName: undefined,
|
||||
employeeSignedDate: undefined,
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
const monthlyPerformanceAutoFilled = ref(false);
|
||||
const monthlySupervisorSignatureLoading = ref(false);
|
||||
const monthlySupervisorSignaturePreviewUrl = ref('');
|
||||
const monthlySupervisorSignatureObjectUrl = ref<string | null>(null);
|
||||
|
||||
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
|
||||
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
|
||||
@@ -129,14 +135,25 @@ const currentScene = computed(() => {
|
||||
return 'approval';
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly' && currentStage.value === 'sign-off') {
|
||||
return 'sign-off';
|
||||
}
|
||||
|
||||
return props.scene;
|
||||
});
|
||||
const currentPageScene = computed<'fill' | 'detail' | 'approval'>(() =>
|
||||
currentScene.value === 'sign-off' ? 'detail' : (currentScene.value ?? 'detail')
|
||||
);
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.reportType === 'monthly' && currentStage.value === 'approval') {
|
||||
return `${REPORT_TYPE_TEXT.monthly}审批页`;
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly' && currentStage.value === 'sign-off') {
|
||||
return `${REPORT_TYPE_TEXT.monthly}下签确认`;
|
||||
}
|
||||
|
||||
if (currentScene.value === 'approval') {
|
||||
return `${REPORT_TYPE_TEXT[props.reportType]}审批`;
|
||||
}
|
||||
@@ -164,20 +181,53 @@ function resetModels() {
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
employeeSignName: undefined,
|
||||
employeeSignedDate: undefined,
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
monthlyPerformanceAutoFilled.value = false;
|
||||
}
|
||||
|
||||
function revokeMonthlySupervisorSignaturePreview() {
|
||||
if (monthlySupervisorSignatureObjectUrl.value) {
|
||||
URL.revokeObjectURL(monthlySupervisorSignatureObjectUrl.value);
|
||||
monthlySupervisorSignatureObjectUrl.value = null;
|
||||
}
|
||||
|
||||
monthlySupervisorSignaturePreviewUrl.value = '';
|
||||
}
|
||||
|
||||
async function loadMonthlySupervisorSignature() {
|
||||
revokeMonthlySupervisorSignaturePreview();
|
||||
|
||||
if (props.reportType !== 'monthly' || currentStage.value !== 'approval') {
|
||||
return;
|
||||
}
|
||||
|
||||
monthlySupervisorSignatureLoading.value = true;
|
||||
const signatureResult = await fetchGetMyUserSignature();
|
||||
|
||||
if (signatureResult.error || !signatureResult.data?.fileId) {
|
||||
monthlySupervisorSignatureLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadResult = await downloadFile(signatureResult.data.fileId);
|
||||
monthlySupervisorSignatureLoading.value = false;
|
||||
|
||||
if (downloadResult.error || !downloadResult.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
monthlySupervisorSignatureObjectUrl.value = URL.createObjectURL(downloadResult.data);
|
||||
monthlySupervisorSignaturePreviewUrl.value = monthlySupervisorSignatureObjectUrl.value;
|
||||
}
|
||||
|
||||
function patchMonthlyApprovalDefaults(report?: Partial<Api.WorkReport.Monthly.MonthlyReport> | null) {
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
Object.assign(monthlyApprovalDraft, {
|
||||
employeeSignName: monthlyApprovalDraft.employeeSignName || report?.reporterName || '',
|
||||
employeeSignedDate: monthlyApprovalDraft.employeeSignedDate || today,
|
||||
supervisorSignName: monthlyApprovalDraft.supervisorSignName || report?.supervisorName || '',
|
||||
supervisorSignedDate: monthlyApprovalDraft.supervisorSignedDate || today
|
||||
});
|
||||
@@ -462,12 +512,22 @@ async function loadInitData() {
|
||||
}
|
||||
|
||||
watch(visible, async isVisible => {
|
||||
if (!isVisible) return;
|
||||
if (!isVisible) {
|
||||
revokeMonthlySupervisorSignaturePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
currentStage.value = props.reportType === 'monthly' && props.scene === 'approval' ? 'approval' : 'form';
|
||||
if (props.reportType === 'monthly' && props.scene === 'approval') {
|
||||
currentStage.value = 'approval';
|
||||
} else if (props.reportType === 'monthly' && props.scene === 'sign-off') {
|
||||
currentStage.value = 'sign-off';
|
||||
} else {
|
||||
currentStage.value = 'form';
|
||||
}
|
||||
currentReportId.value = props.rowData?.id || '';
|
||||
baseInfo.value = null;
|
||||
resetModels();
|
||||
await loadMonthlySupervisorSignature();
|
||||
|
||||
if (props.mode === 'add') {
|
||||
if (props.reportType === 'project') {
|
||||
@@ -754,6 +814,13 @@ function resolveMonthlyPeriodMonth() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveMonthlyPerformanceResultValue(data: Api.Performance.Sheet.MonthlyResult | null | undefined) {
|
||||
const actualScoreTotal = data?.actualScoreTotal;
|
||||
if (actualScoreTotal === null || actualScoreTotal === undefined) return '';
|
||||
|
||||
return String(actualScoreTotal).trim();
|
||||
}
|
||||
|
||||
async function fillMonthlyPerformanceResult() {
|
||||
if (props.reportType !== 'monthly' || monthlyPerformanceAutoFilled.value) return;
|
||||
if (monthlyApprovalDraft.performanceResult?.trim()) return;
|
||||
@@ -762,12 +829,16 @@ async function fillMonthlyPerformanceResult() {
|
||||
const periodMonth = resolveMonthlyPeriodMonth();
|
||||
if (!employeeId || !periodMonth) return;
|
||||
|
||||
monthlyPerformanceAutoFilled.value = true;
|
||||
const { error, data } = await fetchPerformanceMonthlyResult(employeeId, periodMonth);
|
||||
if (error || !data?.actualScoreTotal) return;
|
||||
if (error || !data) return;
|
||||
|
||||
const performanceResult = resolveMonthlyPerformanceResultValue(data);
|
||||
if (!performanceResult) return;
|
||||
|
||||
monthlyPerformanceAutoFilled.value = true;
|
||||
if (monthlyApprovalDraft.performanceResult?.trim()) return;
|
||||
|
||||
monthlyApprovalDraft.performanceResult = String(data.actualScoreTotal);
|
||||
monthlyApprovalDraft.performanceResult = performanceResult;
|
||||
}
|
||||
|
||||
function handleRequestApprove() {
|
||||
@@ -796,6 +867,15 @@ function handleMonthlyApprovalChange(payload: Api.WorkReport.Monthly.MonthlyRepo
|
||||
Object.assign(monthlyApprovalDraft, payload);
|
||||
}
|
||||
|
||||
function handleMonthlySignOffSubmitted() {
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokeMonthlySupervisorSignaturePreview();
|
||||
});
|
||||
|
||||
async function handleActionSubmit(
|
||||
payload: Api.WorkReport.Common.StatusActionParams | Api.WorkReport.Monthly.MonthlyReportApproveParams,
|
||||
actionTypeOverride?: 'approve' | 'reject'
|
||||
@@ -841,7 +921,7 @@ async function handleActionSubmit(
|
||||
v-model:visible="visible"
|
||||
:title="dialogTitle"
|
||||
:loading="loading"
|
||||
:approval-mode="currentScene === 'approval'"
|
||||
:approval-mode="currentScene === 'approval' || currentScene === 'sign-off'"
|
||||
@close="currentStage = 'form'"
|
||||
>
|
||||
<WeeklyReportPage
|
||||
@@ -849,7 +929,7 @@ async function handleActionSubmit(
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:scene="currentPageScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Weekly.WeeklyReport | null"
|
||||
:model="weeklyModel"
|
||||
@back="handleBack"
|
||||
@@ -869,18 +949,28 @@ async function handleActionSubmit(
|
||||
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||
:model="monthlyModel"
|
||||
:approval-model="monthlyApprovalDraft"
|
||||
:supervisor-signature-preview-url="monthlySupervisorSignaturePreviewUrl"
|
||||
:supervisor-signature-loading="monthlySupervisorSignatureLoading"
|
||||
@back="handleBack"
|
||||
@change-approval="handleMonthlyApprovalChange"
|
||||
@request-approve="handleRequestApprove"
|
||||
@request-reject="handleRequestReject"
|
||||
/>
|
||||
|
||||
<MonthlyReportSignOffPage
|
||||
v-else-if="reportType === 'monthly' && currentStage === 'sign-off'"
|
||||
:report-id="currentReportId"
|
||||
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||
@back="handleBack"
|
||||
@submitted="handleMonthlySignOffSubmitted"
|
||||
/>
|
||||
|
||||
<MonthlyReportPage
|
||||
v-else-if="reportType === 'monthly'"
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:scene="currentPageScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||
:model="monthlyModel"
|
||||
@back="handleBack"
|
||||
@@ -895,7 +985,7 @@ async function handleActionSubmit(
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:scene="currentPageScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Project.ProjectReport | null"
|
||||
:model="projectModel"
|
||||
@back="handleBack"
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus';
|
||||
import { userGenderOptions } from '@/constants/business';
|
||||
import {
|
||||
deleteFile,
|
||||
downloadFile,
|
||||
fetchAssignUserRoles,
|
||||
fetchCreateUser,
|
||||
fetchDeleteUserSignature,
|
||||
fetchGetUser,
|
||||
fetchGetUserRoleIds,
|
||||
fetchUpdateUser
|
||||
fetchGetUserSignature,
|
||||
fetchSaveUserSignature,
|
||||
fetchUpdateUser,
|
||||
uploadFile
|
||||
} from '@/service/api';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import { translateOptions } from '@/utils/common';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
@@ -40,9 +48,25 @@ const visible = defineModel<boolean>('visible', {
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule, patternRules } = useFormRules();
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
const SIGNATURE_QUERY_PERMISSION = 'system:user-signature:query';
|
||||
const SIGNATURE_UPDATE_PERMISSION = 'system:user-signature:update';
|
||||
const SIGNATURE_MAX_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const signatureLoading = ref(false);
|
||||
const signatureUploading = ref(false);
|
||||
const signatureUploadRef = ref<UploadInstance>();
|
||||
const originalSignature = ref<Api.SystemManage.UserSignature | null>(null);
|
||||
const currentSignatureFileId = ref<string | null>(null);
|
||||
const currentSignatureFileName = ref<string | null>(null);
|
||||
const currentSignatureSource = ref<'none' | 'original' | 'uploaded'>('none');
|
||||
const currentSignaturePreviewUrl = ref('');
|
||||
const currentSignatureObjectUrl = ref<string | null>(null);
|
||||
const signatureDirty = ref(false);
|
||||
const keepUploadedSignatureOnClose = ref(false);
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<UI.TableOperateType, string> = {
|
||||
@@ -54,6 +78,11 @@ const title = computed(() => {
|
||||
});
|
||||
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
const canQuerySignature = computed(() => hasAuth(SIGNATURE_QUERY_PERMISSION));
|
||||
const canUpdateSignature = computed(() => hasAuth(SIGNATURE_UPDATE_PERMISSION));
|
||||
const showSignatureSection = computed(() => canQuerySignature.value);
|
||||
const hasSignaturePreview = computed(() => Boolean(currentSignaturePreviewUrl.value));
|
||||
const signatureReadonly = computed(() => !canUpdateSignature.value);
|
||||
|
||||
type Model = Api.SystemManage.SaveUserParams & {
|
||||
roleIds: string[];
|
||||
@@ -96,6 +125,201 @@ function getNullableText(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function revokeSignaturePreviewUrl() {
|
||||
if (currentSignatureObjectUrl.value) {
|
||||
URL.revokeObjectURL(currentSignatureObjectUrl.value);
|
||||
currentSignatureObjectUrl.value = null;
|
||||
}
|
||||
|
||||
currentSignaturePreviewUrl.value = '';
|
||||
}
|
||||
|
||||
function setSignaturePreviewFromBlob(blob: Blob) {
|
||||
revokeSignaturePreviewUrl();
|
||||
currentSignatureObjectUrl.value = URL.createObjectURL(blob);
|
||||
currentSignaturePreviewUrl.value = currentSignatureObjectUrl.value;
|
||||
}
|
||||
|
||||
function syncSignatureDirtyState() {
|
||||
signatureDirty.value = (originalSignature.value?.fileId ?? null) !== currentSignatureFileId.value;
|
||||
}
|
||||
|
||||
async function cleanupUploadedSignatureFile(fileId?: string | null) {
|
||||
if (!fileId) return;
|
||||
|
||||
await deleteFile(fileId).catch(() => undefined);
|
||||
}
|
||||
|
||||
async function cleanupSignatureState(options?: { preserveUploaded?: boolean }) {
|
||||
const preserveUploaded = Boolean(options?.preserveUploaded);
|
||||
const uploadedFileId =
|
||||
currentSignatureSource.value === 'uploaded' &&
|
||||
currentSignatureFileId.value &&
|
||||
currentSignatureFileId.value !== originalSignature.value?.fileId
|
||||
? currentSignatureFileId.value
|
||||
: null;
|
||||
|
||||
if (!preserveUploaded && uploadedFileId) {
|
||||
await cleanupUploadedSignatureFile(uploadedFileId);
|
||||
}
|
||||
|
||||
signatureUploadRef.value?.clearFiles();
|
||||
revokeSignaturePreviewUrl();
|
||||
originalSignature.value = null;
|
||||
currentSignatureFileId.value = null;
|
||||
currentSignatureFileName.value = null;
|
||||
currentSignatureSource.value = 'none';
|
||||
signatureDirty.value = false;
|
||||
signatureLoading.value = false;
|
||||
signatureUploading.value = false;
|
||||
}
|
||||
|
||||
async function loadSignaturePreview(fileId: string) {
|
||||
const { error, data } = await downloadFile(fileId);
|
||||
|
||||
if (error || !data) {
|
||||
revokeSignaturePreviewUrl();
|
||||
return;
|
||||
}
|
||||
|
||||
setSignaturePreviewFromBlob(data);
|
||||
}
|
||||
|
||||
async function initSignatureState() {
|
||||
await cleanupSignatureState({ preserveUploaded: true });
|
||||
|
||||
if (!showSignatureSection.value || !isEdit.value || !props.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
signatureLoading.value = true;
|
||||
const { error, data } = await fetchGetUserSignature(String(props.userId));
|
||||
signatureLoading.value = false;
|
||||
|
||||
if (error || !data?.fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
originalSignature.value = data;
|
||||
currentSignatureFileId.value = data.fileId;
|
||||
currentSignatureFileName.value = data.fileName ?? null;
|
||||
currentSignatureSource.value = 'original';
|
||||
syncSignatureDirtyState();
|
||||
await loadSignaturePreview(data.fileId);
|
||||
}
|
||||
|
||||
function validateSignatureFile(file: UploadRawFile) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
window.$message?.warning('电子签名仅支持图片文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size > SIGNATURE_MAX_SIZE) {
|
||||
window.$message?.warning('电子签名图片不能超过 5MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSignatureFileChange(file: UploadFile) {
|
||||
const rawFile = file.raw;
|
||||
signatureUploadRef.value?.clearFiles();
|
||||
|
||||
if (!rawFile || !canUpdateSignature.value || !validateSignatureFile(rawFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
signatureUploading.value = true;
|
||||
const { error, data } = await uploadFile(rawFile, 'system/user-signatures');
|
||||
signatureUploading.value = false;
|
||||
|
||||
if (error || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousUploadedFileId =
|
||||
currentSignatureSource.value === 'uploaded' &&
|
||||
currentSignatureFileId.value &&
|
||||
currentSignatureFileId.value !== originalSignature.value?.fileId
|
||||
? currentSignatureFileId.value
|
||||
: null;
|
||||
|
||||
if (previousUploadedFileId && previousUploadedFileId !== data.id) {
|
||||
await cleanupUploadedSignatureFile(previousUploadedFileId);
|
||||
}
|
||||
|
||||
currentSignatureFileId.value = data.id;
|
||||
currentSignatureFileName.value = data.path.split('/').pop() || rawFile.name;
|
||||
currentSignatureSource.value = 'uploaded';
|
||||
setSignaturePreviewFromBlob(rawFile);
|
||||
syncSignatureDirtyState();
|
||||
window.$message?.success('电子签名上传成功');
|
||||
}
|
||||
|
||||
async function clearSignature() {
|
||||
if (!canUpdateSignature.value) return;
|
||||
|
||||
const uploadedFileId =
|
||||
currentSignatureSource.value === 'uploaded' &&
|
||||
currentSignatureFileId.value &&
|
||||
currentSignatureFileId.value !== originalSignature.value?.fileId
|
||||
? currentSignatureFileId.value
|
||||
: null;
|
||||
|
||||
if (uploadedFileId) {
|
||||
await cleanupUploadedSignatureFile(uploadedFileId);
|
||||
}
|
||||
|
||||
currentSignatureFileId.value = null;
|
||||
currentSignatureFileName.value = null;
|
||||
currentSignatureSource.value = 'none';
|
||||
signatureUploadRef.value?.clearFiles();
|
||||
revokeSignaturePreviewUrl();
|
||||
syncSignatureDirtyState();
|
||||
}
|
||||
|
||||
async function syncUserSignature(userId: number) {
|
||||
if (!showSignatureSection.value || !canUpdateSignature.value || !signatureDirty.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const originalFileId = originalSignature.value?.fileId ?? null;
|
||||
const currentFileId = currentSignatureFileId.value;
|
||||
|
||||
if (!currentFileId) {
|
||||
if (!originalFileId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = await fetchDeleteUserSignature(String(userId));
|
||||
if (result.error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await cleanupUploadedSignatureFile(originalFileId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = await fetchSaveUserSignature({
|
||||
userId: String(userId),
|
||||
fileId: currentFileId,
|
||||
fileName: currentSignatureFileName.value ?? null,
|
||||
status: 0,
|
||||
remark: null
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (originalFileId && originalFileId !== currentFileId) {
|
||||
await cleanupUploadedSignatureFile(originalFileId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const rules = computed(() => {
|
||||
const passwordRules = isEdit.value
|
||||
? []
|
||||
@@ -114,6 +338,7 @@ const rules = computed(() => {
|
||||
|
||||
async function handleInitModel() {
|
||||
model.value = createDefaultModel();
|
||||
await initSignatureState();
|
||||
|
||||
if (!isEdit.value || !props.userId) {
|
||||
return;
|
||||
@@ -205,9 +430,17 @@ async function handleSubmit() {
|
||||
submitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const signatureSynced = await syncUserSignature(userId);
|
||||
|
||||
if (!signatureSynced) {
|
||||
submitting.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
submitting.value = false;
|
||||
keepUploadedSignatureOnClose.value = true;
|
||||
|
||||
window.$message?.success(isEdit.value ? $t('common.updateSuccess') : $t('common.addSuccess'));
|
||||
closeDialog();
|
||||
@@ -216,6 +449,9 @@ async function handleSubmit() {
|
||||
|
||||
watch(visible, async value => {
|
||||
if (!value) {
|
||||
const preserveUploaded = keepUploadedSignatureOnClose.value;
|
||||
keepUploadedSignatureOnClose.value = false;
|
||||
await cleanupSignatureState({ preserveUploaded });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -223,6 +459,10 @@ watch(visible, async value => {
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokeSignaturePreviewUrl();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -362,6 +602,40 @@ watch(visible, async value => {
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection v-if="showSignatureSection" title="电子签名">
|
||||
<div v-loading="signatureLoading" class="signature-section">
|
||||
<div class="signature-preview-card">
|
||||
<div v-if="hasSignaturePreview" class="signature-preview-box">
|
||||
<img :src="currentSignaturePreviewUrl" alt="电子签名预览" class="signature-preview-image" />
|
||||
</div>
|
||||
<div v-else class="signature-preview-empty">
|
||||
{{ signatureReadonly ? '未维护电子签名' : '请上传电子签名图片' }}
|
||||
</div>
|
||||
<div class="signature-preview-meta">
|
||||
<div class="signature-preview-name">{{ currentSignatureFileName || '未选择文件' }}</div>
|
||||
<div class="signature-preview-tip">仅支持图片,大小不超过 5MB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canUpdateSignature" class="signature-actions">
|
||||
<ElUpload
|
||||
ref="signatureUploadRef"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
accept="image/*"
|
||||
:on-change="handleSignatureFileChange"
|
||||
>
|
||||
<ElButton plain type="primary" :loading="signatureUploading">
|
||||
{{ hasSignaturePreview ? '重新上传' : '上传签名' }}
|
||||
</ElButton>
|
||||
</ElUpload>
|
||||
<ElButton plain :disabled="!currentSignatureFileId || signatureUploading" @click="clearSignature">
|
||||
清空
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
@@ -380,4 +654,57 @@ watch(visible, async value => {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.signature-section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.signature-preview-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.signature-preview-box,
|
||||
.signature-preview-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 160px;
|
||||
padding: 12px;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.signature-preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 136px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.signature-preview-empty {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.signature-preview-meta {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.signature-preview-name {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.signature-preview-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.signature-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
fetchChangePersonalItemStatus,
|
||||
fetchChangeProjectTaskStatus,
|
||||
fetchGetMonthlyReportApprovalPage,
|
||||
fetchGetMonthlyReportSignOffPage,
|
||||
fetchGetMyTaskPage,
|
||||
fetchGetOvertimeApplicationApprovalPage,
|
||||
fetchGetPersonalItemDetail,
|
||||
@@ -70,6 +71,7 @@ type SortKey = 'created' | 'priority' | 'deadline';
|
||||
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||||
type PerformanceApprovalActionType = 'confirm' | 'reject';
|
||||
type ApprovalBizType = 'overtime_application' | 'performance' | WorkReportType;
|
||||
type WorkReportDetailScene = 'approval' | 'sign-off';
|
||||
|
||||
defineOptions({ name: 'WorkbenchTodoPanel' });
|
||||
|
||||
@@ -103,7 +105,8 @@ const { loading, refresh } = useWorkbenchRefresh(async () => {
|
||||
loadPersonalTodoItems(),
|
||||
loadOvertimeApprovalItems(),
|
||||
loadPerformanceApprovalItems(),
|
||||
loadWorkReportApprovalItems()
|
||||
loadWorkReportApprovalItems(),
|
||||
loadMonthlySignOffItems()
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -147,8 +150,9 @@ const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
||||
{ key: 'project', label: '项目半月报' },
|
||||
{ key: 'overtime_application', label: '业务学习' }
|
||||
];
|
||||
const confirmBizTabs: Array<{ key: Extract<ApprovalBizType, 'performance'>; label: string }> = [
|
||||
{ key: 'performance', label: '绩效表' }
|
||||
const confirmBizTabs: Array<{ key: Extract<ApprovalBizType, 'performance' | 'monthly'>; label: string }> = [
|
||||
{ key: 'performance', label: '绩效表' },
|
||||
{ key: 'monthly', label: '月报' }
|
||||
];
|
||||
|
||||
const hasWorkReportApprovePermission = computed(() => hasButtonPermission('project:work-report:approve'));
|
||||
@@ -157,10 +161,11 @@ const hasPerformanceApprovePermission = computed(
|
||||
() =>
|
||||
hasButtonPermission(PerformancePermission.SheetConfirm) && hasButtonPermission(PerformancePermission.SheetReject)
|
||||
);
|
||||
const hasMonthlySignOffQueryPermission = computed(() => hasButtonPermission('project:work-report:sign-off-query'));
|
||||
const visibleMainTabs = computed(() =>
|
||||
mainTabs.filter(tab => {
|
||||
if (tab.key === 'confirm') {
|
||||
return hasPerformanceApprovePermission.value;
|
||||
return hasPerformanceApprovePermission.value || hasMonthlySignOffQueryPermission.value;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
@@ -178,7 +183,15 @@ const visibleApprovalBizTabs = computed(() =>
|
||||
return hasWorkReportApprovePermission.value;
|
||||
})
|
||||
);
|
||||
const visibleConfirmBizTabs = computed(() => (hasPerformanceApprovePermission.value ? confirmBizTabs : []));
|
||||
const visibleConfirmBizTabs = computed(() =>
|
||||
confirmBizTabs.filter(tab => {
|
||||
if (tab.key === 'performance') {
|
||||
return hasPerformanceApprovePermission.value;
|
||||
}
|
||||
|
||||
return hasMonthlySignOffQueryPermission.value;
|
||||
})
|
||||
);
|
||||
|
||||
const myTaskItems = ref<WorkbenchTodoItem[]>([]);
|
||||
// 保留任务原始行,供操作图标按 availableActions 渲染并取 projectId / executionId 调状态变更接口
|
||||
@@ -190,6 +203,8 @@ const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
||||
const performanceApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const performanceApprovalRows = ref<Api.Performance.Sheet.Sheet[]>([]);
|
||||
const monthlySignOffItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const monthlySignOffRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||
@@ -200,6 +215,7 @@ const mergedItems = computed(() => [
|
||||
...personalTodoItems.value,
|
||||
...overtimeApprovalItems.value,
|
||||
...performanceApprovalItems.value,
|
||||
...monthlySignOffItems.value,
|
||||
...workReportApprovalItems.value
|
||||
]);
|
||||
|
||||
@@ -231,6 +247,7 @@ const batchSubmitting = ref(false);
|
||||
const workReportDetailVisible = ref(false);
|
||||
const currentWorkReport = ref<WorkReportRow | null>(null);
|
||||
const currentWorkReportType = ref<WorkReportType>('weekly');
|
||||
const currentWorkReportScene = ref<WorkReportDetailScene>('approval');
|
||||
const performanceDetailVisible = ref(false);
|
||||
const performanceActionVisible = ref(false);
|
||||
const currentPerformanceSheet = ref<Api.Performance.Sheet.Sheet | null>(null);
|
||||
@@ -595,7 +612,7 @@ const filteredItems = computed(() => {
|
||||
}
|
||||
|
||||
if (activeTab.value === 'confirm') {
|
||||
return itemsInTab.value.filter(item => item.approvalBizType === 'performance');
|
||||
return itemsInTab.value.filter(item => item.approvalBizType === activeApprovalBizType.value);
|
||||
}
|
||||
|
||||
return filterWorkbenchTodoItemsByDeadline(itemsInTab.value, activeDeadlineFilter.value);
|
||||
@@ -660,6 +677,19 @@ watch([activeTab, activeDeadlineFilter, activeSort], () => {
|
||||
watch(
|
||||
visibleApprovalBizTabs,
|
||||
tabs => {
|
||||
if (activeTab.value !== 'approval') return;
|
||||
if (!tabs.length) return;
|
||||
if (!tabs.some(tab => tab.key === activeApprovalBizType.value)) {
|
||||
activeApprovalBizType.value = tabs[0].key;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
visibleConfirmBizTabs,
|
||||
tabs => {
|
||||
if (activeTab.value !== 'confirm') return;
|
||||
if (!tabs.length) return;
|
||||
if (!tabs.some(tab => tab.key === activeApprovalBizType.value)) {
|
||||
activeApprovalBizType.value = tabs[0].key;
|
||||
@@ -679,12 +709,12 @@ watch(
|
||||
);
|
||||
|
||||
function handleSelectTab(key: WorkbenchTodoMainTab) {
|
||||
if (key === 'confirm' && !hasPerformanceApprovePermission.value) return;
|
||||
if (key === 'confirm' && !visibleConfirmBizTabs.value.length) return;
|
||||
if (activeTab.value === key) return;
|
||||
activeTab.value = key;
|
||||
activeDeadlineFilter.value = null;
|
||||
if (key === 'confirm') {
|
||||
activeApprovalBizType.value = 'performance';
|
||||
activeApprovalBizType.value = visibleConfirmBizTabs.value[0]?.key ?? 'performance';
|
||||
} else if (key === 'approval' && activeApprovalBizType.value === 'performance') {
|
||||
activeApprovalBizType.value = visibleApprovalBizTabs.value[0]?.key ?? 'weekly';
|
||||
}
|
||||
@@ -787,7 +817,8 @@ function findWorkReportApprovalRow(item: WorkbenchTodoItem) {
|
||||
}
|
||||
|
||||
if (item.approvalBizType === 'monthly') {
|
||||
return monthlyApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||
const sourceRows = item.category === 'confirm' ? monthlySignOffRows.value : monthlyApprovalRows.value;
|
||||
return sourceRows.find(row => row.id === item.approvalBizId) || null;
|
||||
}
|
||||
|
||||
return projectApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||
@@ -799,6 +830,7 @@ function openWorkReportDetail(item: WorkbenchTodoItem) {
|
||||
|
||||
currentWorkReport.value = row;
|
||||
currentWorkReportType.value = item.approvalBizType;
|
||||
currentWorkReportScene.value = item.category === 'confirm' ? 'sign-off' : 'approval';
|
||||
workReportDetailVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -825,7 +857,7 @@ async function handleOvertimeActionSubmit(payload: { actionType: OvertimeApprova
|
||||
|
||||
async function handleWorkReportSubmitted() {
|
||||
workReportDetailVisible.value = false;
|
||||
await loadWorkReportApprovalItems();
|
||||
await Promise.all([loadWorkReportApprovalItems(), loadMonthlySignOffItems()]);
|
||||
}
|
||||
|
||||
async function handlePerformanceActionSubmitted() {
|
||||
@@ -1035,7 +1067,8 @@ async function loadPerformanceApprovalItems() {
|
||||
const { error, data } = await fetchPerformanceSheetPage({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
statusCode: 'sent'
|
||||
statusCode: 'sent',
|
||||
signOffStatus: 'pending_employee_confirm'
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
@@ -1061,6 +1094,47 @@ async function loadPerformanceApprovalItems() {
|
||||
);
|
||||
}
|
||||
|
||||
async function loadMonthlySignOffItems() {
|
||||
if (!hasMonthlySignOffQueryPermission.value) {
|
||||
monthlySignOffRows.value = [];
|
||||
monthlySignOffItems.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, data } = await fetchGetMonthlyReportSignOffPage({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
statusCode: 'approved',
|
||||
signOffStatus: 'pending_employee_confirm',
|
||||
keyword: undefined,
|
||||
periodStartDate: undefined,
|
||||
submitTime: undefined,
|
||||
supervisorName: undefined
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
monthlySignOffRows.value = [];
|
||||
monthlySignOffItems.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
monthlySignOffRows.value = data.list;
|
||||
monthlySignOffItems.value = buildWorkbenchTodoItems(
|
||||
data.list.map(item => ({
|
||||
id: `monthly-sign-off-${item.id}`,
|
||||
category: 'confirm',
|
||||
categoryLabel: '月报',
|
||||
title: `${WORK_REPORT_TYPE_LABEL.monthly} · ${formatPeriod(item)} 待确认`,
|
||||
createdTime: item.approvalTime || item.submitTime || item.createTime || '',
|
||||
deadline: item.approvalTime || item.submitTime || item.createTime || null,
|
||||
source: `${WORK_REPORT_TYPE_LABEL.monthly} · ${item.reporterName}`,
|
||||
priority: 'mid',
|
||||
approvalBizType: 'monthly',
|
||||
approvalBizId: item.id
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function buildWorkReportApprovalItems<T extends WorkReportRow>(
|
||||
bizType: WorkReportType,
|
||||
rows: T[]
|
||||
@@ -1522,7 +1596,7 @@ onActivated(refresh);
|
||||
<WorkReportPrototypePageDialog
|
||||
v-model:visible="workReportDetailVisible"
|
||||
mode="detail"
|
||||
scene="approval"
|
||||
:scene="currentWorkReportScene"
|
||||
:report-type="currentWorkReportType"
|
||||
:row-data="currentWorkReport"
|
||||
@submitted="handleWorkReportSubmitted"
|
||||
|
||||
Reference in New Issue
Block a user