feat(performance、notify、weekly): 添加绩效考核功能、通知跳转链接、优化工作日志分割逻辑。
This commit is contained in:
@@ -27,19 +27,25 @@ interface Props {
|
||||
mode: 'view' | 'edit' | 'create';
|
||||
subordinateOptions?: SubordinateOption[];
|
||||
showApprovalFooter?: boolean;
|
||||
initialEmployeeId?: string;
|
||||
initialPeriodMonth?: string;
|
||||
hideDraftAction?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rowData: null,
|
||||
subordinateOptions: () => [],
|
||||
showApprovalFooter: false
|
||||
showApprovalFooter: false,
|
||||
initialEmployeeId: '',
|
||||
initialPeriodMonth: '',
|
||||
hideDraftAction: false
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
saved: [];
|
||||
savedAndSent: [];
|
||||
saved: [payload: PerformanceSheetSavePayload];
|
||||
savedAndSent: [payload: PerformanceSheetSavePayload];
|
||||
startApproval: [];
|
||||
}>();
|
||||
|
||||
@@ -60,6 +66,13 @@ const createForm = reactive({
|
||||
employeeId: ''
|
||||
});
|
||||
|
||||
interface PerformanceSheetSavePayload {
|
||||
sheet: Api.Performance.Sheet.Sheet;
|
||||
actualScoreTotal: string;
|
||||
baseScoreTotal: string;
|
||||
extraScoreTotal: string;
|
||||
}
|
||||
|
||||
const createFormRules = computed<FormRules>(() => ({
|
||||
periodMonth: [createRequiredRule('请选择绩效月份')],
|
||||
employeeId: [createRequiredRule('请选择下属')]
|
||||
@@ -526,7 +539,16 @@ async function ensureCreatedSheet() {
|
||||
return sheetResult.data;
|
||||
}
|
||||
|
||||
async function executeSave(): Promise<Api.Performance.Sheet.Sheet | null> {
|
||||
function createInitialPeriodMonth() {
|
||||
return props.initialPeriodMonth || createDefaultPeriodMonth();
|
||||
}
|
||||
|
||||
function initializeCreateForm() {
|
||||
createForm.periodMonth = createInitialPeriodMonth();
|
||||
createForm.employeeId = props.initialEmployeeId || '';
|
||||
}
|
||||
|
||||
async function executeSave(): Promise<PerformanceSheetSavePayload | null> {
|
||||
const workbook = getActiveWorkbook();
|
||||
if (!workbook) return null;
|
||||
|
||||
@@ -573,18 +595,23 @@ async function executeSave(): Promise<Api.Performance.Sheet.Sheet | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sheet;
|
||||
return {
|
||||
sheet,
|
||||
actualScoreTotal: scores.actualScoreTotal,
|
||||
baseScoreTotal: scores.baseScoreTotal,
|
||||
extraScoreTotal: scores.extraScoreTotal
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSaveDraft() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const sheet = await executeSave();
|
||||
if (!sheet) return;
|
||||
const saveResult = await executeSave();
|
||||
if (!saveResult) return;
|
||||
|
||||
window.$message?.success(isCreateMode.value ? '绩效表已保存为草稿' : '绩效 Excel 已保存');
|
||||
visible.value = false;
|
||||
emit('saved');
|
||||
emit('saved', saveResult);
|
||||
} catch (error) {
|
||||
window.$message?.error(error instanceof Error ? error.message : '绩效 Excel 保存失败');
|
||||
} finally {
|
||||
@@ -595,15 +622,15 @@ async function handleSaveDraft() {
|
||||
async function handleSaveAndSend() {
|
||||
sending.value = true;
|
||||
try {
|
||||
const sheet = await executeSave();
|
||||
if (!sheet) return;
|
||||
const saveResult = await executeSave();
|
||||
if (!saveResult) return;
|
||||
|
||||
const sendResult = await sendPerformanceSheet(sheet.id);
|
||||
const sendResult = await sendPerformanceSheet(saveResult.sheet.id);
|
||||
if (sendResult.error) return;
|
||||
|
||||
window.$message?.success('绩效表已保存并发送');
|
||||
visible.value = false;
|
||||
emit('savedAndSent');
|
||||
emit('savedAndSent', saveResult);
|
||||
} catch (error) {
|
||||
window.$message?.error(error instanceof Error ? error.message : '绩效表发送失败');
|
||||
} finally {
|
||||
@@ -616,12 +643,14 @@ watch(visible, async isVisible => {
|
||||
disposeUniver();
|
||||
currentSheet.value = null;
|
||||
currentTemplate.value = null;
|
||||
// 重置创建表单
|
||||
createForm.periodMonth = createDefaultPeriodMonth();
|
||||
createForm.employeeId = '';
|
||||
initializeCreateForm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreateMode.value) {
|
||||
initializeCreateForm();
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
await loadWorkbook();
|
||||
});
|
||||
@@ -698,7 +727,9 @@ onMounted(() => {
|
||||
<template v-if="canSave || showApprovalFooter" #footer>
|
||||
<div class="performance-excel-editor__footer">
|
||||
<template v-if="canSave">
|
||||
<ElButton :loading="saving" :disabled="sending" @click="handleSaveDraft">保存草稿</ElButton>
|
||||
<ElButton v-if="!props.hideDraftAction" :loading="saving" :disabled="sending" @click="handleSaveDraft">
|
||||
保存草稿
|
||||
</ElButton>
|
||||
<ElButton type="primary" :loading="sending" :disabled="saving" @click="handleSaveAndSend">发送绩效</ElButton>
|
||||
</template>
|
||||
<template v-else-if="showApprovalFooter">
|
||||
|
||||
@@ -4,9 +4,11 @@ import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import type { FormRules } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchPerformanceSheetPage } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import PerformanceExcelEditorDrawer from '../../../my-performance/modules/performance-excel-editor-drawer.vue';
|
||||
import {
|
||||
type WorkReportStructuredSection,
|
||||
type WorkReportStructuredTask,
|
||||
@@ -73,6 +75,10 @@ const EMPTY_TEXT = '';
|
||||
const { getLabel: getTaskItemTypeLabel } = useDict(RDMS_TASK_ITEM_TYPE_DICT_CODE);
|
||||
const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
||||
const todayText = computed(() => dayjs().format('YYYY-MM-DD'));
|
||||
const performanceDrawerVisible = ref(false);
|
||||
const performanceDrawerLoading = ref(false);
|
||||
const performanceDrawerMode = ref<'create' | 'edit'>('create');
|
||||
const performanceDrawerRow = ref<Api.Performance.Sheet.Sheet | null>(null);
|
||||
|
||||
const auditDialogVisible = ref(false);
|
||||
const auditForm = reactive({
|
||||
@@ -90,7 +96,6 @@ const pageValidationModel = reactive({
|
||||
}
|
||||
});
|
||||
const pageRules: FormRules = {
|
||||
meetingDate: [createRequiredRule('请选择面谈时间')],
|
||||
performanceResult: [
|
||||
createRequiredRule('请输入绩效考核结果'),
|
||||
{
|
||||
@@ -177,6 +182,24 @@ const performanceForm = reactive({
|
||||
}
|
||||
});
|
||||
|
||||
const performanceTargetOption = computed(() => {
|
||||
const reporterId = props.baseInfo?.reporterId || '';
|
||||
const reporterName = props.baseInfo?.reporterName || '';
|
||||
|
||||
if (!reporterId || !reporterName) return [];
|
||||
|
||||
return [{ label: reporterName, value: reporterId }];
|
||||
});
|
||||
|
||||
const performanceInitialEmployeeId = computed(() => props.baseInfo?.reporterId || '');
|
||||
const performanceInitialPeriodMonth = computed(() => {
|
||||
const periodDate = props.baseInfo?.periodStartDate || props.baseInfo?.periodEndDate || '';
|
||||
if (!periodDate) return '';
|
||||
|
||||
const targetMonth = dayjs(periodDate);
|
||||
return targetMonth.isValid() ? targetMonth.format('YYYY-MM') : '';
|
||||
});
|
||||
|
||||
/** 绩效分数输入限制:0-100,最多一位小数 */
|
||||
function handlePerformanceScoreInput(value: string) {
|
||||
// 只允许数字和一个小数点
|
||||
@@ -563,6 +586,37 @@ function openAuditDialog() {
|
||||
});
|
||||
}
|
||||
|
||||
async function openPerformanceDrawer() {
|
||||
const reporterId = props.baseInfo?.reporterId || '';
|
||||
const periodMonth = performanceInitialPeriodMonth.value;
|
||||
|
||||
performanceDrawerMode.value = 'create';
|
||||
performanceDrawerRow.value = null;
|
||||
|
||||
if (reporterId && periodMonth) {
|
||||
performanceDrawerLoading.value = true;
|
||||
const { error, data } = await fetchPerformanceSheetPage({
|
||||
pageNo: 1,
|
||||
pageSize: 1,
|
||||
employeeId: reporterId,
|
||||
periodMonthRange: [periodMonth, periodMonth]
|
||||
});
|
||||
performanceDrawerLoading.value = false;
|
||||
|
||||
const existingSheet = !error && data?.list?.length ? data.list[0] : null;
|
||||
if (existingSheet) {
|
||||
performanceDrawerMode.value = 'edit';
|
||||
performanceDrawerRow.value = existingSheet;
|
||||
}
|
||||
}
|
||||
|
||||
performanceDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
function handlePerformanceSaved(payload: { actualScoreTotal: string }) {
|
||||
patchApproval('performanceResult', payload.actualScoreTotal);
|
||||
}
|
||||
|
||||
watch(rejectOpinionRequired, async () => {
|
||||
if (!auditDialogVisible.value) return;
|
||||
|
||||
@@ -614,10 +668,7 @@ watch(
|
||||
<ElInput v-model="mainForm.supervisor" disabled />
|
||||
</div>
|
||||
<div class="field field-form-item">
|
||||
<label>
|
||||
面谈时间
|
||||
<span class="field-required-mark">*</span>
|
||||
</label>
|
||||
<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%" />
|
||||
</ElFormItem>
|
||||
@@ -730,12 +781,25 @@ watch(
|
||||
<span class="field-required-mark">*</span>
|
||||
</div>
|
||||
<ElFormItem class="performance-inline-form-item" prop="performanceResult">
|
||||
<ElInput
|
||||
v-model="performanceForm.score"
|
||||
class="performance-input"
|
||||
placeholder="请输入考核分数"
|
||||
@input="handlePerformanceScoreInput"
|
||||
/>
|
||||
<div class="performance-input-group">
|
||||
<ElInput
|
||||
v-model="performanceForm.score"
|
||||
class="performance-input"
|
||||
placeholder="请输入考核分数"
|
||||
readonly
|
||||
@input="handlePerformanceScoreInput"
|
||||
/>
|
||||
<ElButton
|
||||
plain
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="performanceDrawerLoading"
|
||||
:disabled="!performanceTargetOption.length"
|
||||
@click="openPerformanceDrawer"
|
||||
>
|
||||
填写绩效
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</div>
|
||||
@@ -871,6 +935,18 @@ watch(
|
||||
</ElForm>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
|
||||
<PerformanceExcelEditorDrawer
|
||||
v-model:visible="performanceDrawerVisible"
|
||||
:row-data="performanceDrawerRow"
|
||||
:mode="performanceDrawerMode"
|
||||
:subordinate-options="performanceTargetOption"
|
||||
:initial-employee-id="performanceInitialEmployeeId"
|
||||
:initial-period-month="performanceInitialPeriodMonth"
|
||||
hide-draft-action
|
||||
@saved="handlePerformanceSaved"
|
||||
@saved-and-sent="handlePerformanceSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1794,13 +1870,19 @@ watch(
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.performance-input-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.performance-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.performance-input :deep(.el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
background: transparent;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
border-radius: 0;
|
||||
padding: 0 4px;
|
||||
@@ -1812,6 +1894,10 @@ watch(
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.performance-input :deep(.el-input__inner[readonly]) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.performance-input :deep(.el-input__inner::placeholder) {
|
||||
color: #cbd5e1;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -359,6 +359,8 @@ function resolveTaskItemTypeLabel(value?: string | null) {
|
||||
}
|
||||
|
||||
const STRUCTURED_TASK_PREFIX_RE = /^(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*/u;
|
||||
const WORKLOG_DAY_SEPARATOR = '[[WR_DAY_SPLIT]]';
|
||||
const LEGACY_WORKLOG_DAY_SEPARATOR = ';';
|
||||
|
||||
function stripStructuredTaskPrefixV2(value: string) {
|
||||
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
|
||||
@@ -385,9 +387,10 @@ function formatStructuredTaskDisplayLine(task: StructuredTask, index: number, sh
|
||||
|
||||
function getWorkLogEntries(detail: string): string[] {
|
||||
if (!detail) return [];
|
||||
// 仅按中文分号切分,避免误伤文本中的其他标点;每段作为单独一天的工作日志展示。
|
||||
// 新默认稿优先按显式分隔标记切分;历史数据继续兼容中文分号。
|
||||
const separator = detail.includes(WORKLOG_DAY_SEPARATOR) ? WORKLOG_DAY_SEPARATOR : LEGACY_WORKLOG_DAY_SEPARATOR;
|
||||
return detail
|
||||
.split(';')
|
||||
.split(separator)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -825,7 +825,8 @@ watch(
|
||||
() => props.execution?.id,
|
||||
async () => {
|
||||
await loadExecutionAssigneeOptions();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 范围维度变化(scopedExecutionIds / scopedExecutionInvolveUserId / scopedExecutionStatusCodes 引用变:
|
||||
|
||||
Reference in New Issue
Block a user