feat(performance): 新增绩效下签功能支持
- 在月报审批页面增加 sign-off 场景支持 - 添加绩效下签对话框组件实现批量下签功能 - 增加导入绩效压缩包和直接下签两种操作模式 - 实现电子签名预览和日期格式化显示功能 - 更新绩效权限配置增加下签相关权限 - 优化审批对话框标题和按钮文案显示 - 添加导入结果提示和文件验证功能 - 实现团队模式下的下签操作入口 - 完善绩效表格状态和操作权限判断逻辑
This commit is contained in:
@@ -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,22 +102,47 @@ const pageValidationModel = reactive({
|
||||
return props.approvalModel.performanceResult || '';
|
||||
}
|
||||
});
|
||||
const pageRules: FormRules = {
|
||||
performanceResult: [
|
||||
createRequiredRule('请输入绩效考核结果'),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error('请输入绩效考核结果'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
};
|
||||
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('请输入绩效考核结果'),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error('请输入绩效考核结果'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
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,39 +904,92 @@ watch(
|
||||
</div>
|
||||
|
||||
<div class="signature-section">
|
||||
<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>
|
||||
<div class="signature-row">
|
||||
<span>上级签名:</span>
|
||||
<ElInput v-model="signatureForm.supervisorSign" class="signature-input" placeholder="请输入签名" />
|
||||
<span>日期:</span>
|
||||
<ElDatePicker
|
||||
v-model="signatureForm.supervisorDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
class="signature-date"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="isSignOffScene">
|
||||
<div class="signature-row">
|
||||
<span>被考核人签名:</span>
|
||||
<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>
|
||||
<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"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user