fix(工作报告): 修复工作报告存在的若干问题。
feat(加班申请): 支持批量审批。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { computed, nextTick, reactive, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||
@@ -33,6 +34,9 @@ const emit = defineEmits<{
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const reasonModel = reactive<Api.WorkReport.Common.StatusActionParams>({
|
||||
reason: ''
|
||||
});
|
||||
@@ -72,12 +76,35 @@ const title = computed(() => {
|
||||
});
|
||||
|
||||
const preset = computed(() => (isMonthlyApprove.value ? 'lg' : 'sm'));
|
||||
const rejectOpinionRequired = computed(() => isCommonApprove.value && commonApprovalModel.conclusion === 'reject');
|
||||
const opinionLabel = computed(() => (rejectOpinionRequired.value ? '退回原因' : '审批意见'));
|
||||
const opinionPlaceholder = computed(() =>
|
||||
rejectOpinionRequired.value ? `请输入${opinionLabel.value}` : '可填写审批意见'
|
||||
);
|
||||
const confirmText = computed(() => {
|
||||
if (isCommonApprove.value) return '确认提交';
|
||||
if (props.actionType === 'approve') return '通过';
|
||||
return '退回';
|
||||
});
|
||||
const confirmDisabled = computed(() => isCommonApprove.value && !commonApprovalModel.conclusion);
|
||||
const commonRules = computed(() => ({
|
||||
opinion: rejectOpinionRequired.value
|
||||
? [
|
||||
createRequiredRule(`请输入${opinionLabel.value}`),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error(`请输入${opinionLabel.value}`));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
: []
|
||||
}));
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (!isVisible) return;
|
||||
@@ -106,16 +133,36 @@ watch(visible, isVisible => {
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
watch(
|
||||
() => visible.value,
|
||||
async isVisible => {
|
||||
if (!isVisible || !isCommonApprove.value) return;
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
|
||||
watch(rejectOpinionRequired, async () => {
|
||||
if (!visible.value || !isCommonApprove.value) return;
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate('opinion');
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (isCommonApprove.value) {
|
||||
if (!commonApprovalModel.conclusion) {
|
||||
window.$message?.warning('请选择审批结论');
|
||||
return;
|
||||
}
|
||||
|
||||
await validate();
|
||||
|
||||
emit(
|
||||
'submit',
|
||||
{
|
||||
reason: commonApprovalModel.opinion || (commonApprovalModel.conclusion === 'approve' ? '通过' : '不通过')
|
||||
reason: commonApprovalModel.opinion.trim() || (commonApprovalModel.conclusion === 'approve' ? '通过' : '退回')
|
||||
},
|
||||
commonApprovalModel.conclusion
|
||||
);
|
||||
@@ -176,14 +223,29 @@ function handleSubmit() {
|
||||
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M6 6L10 10M10 6L6 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
不通过
|
||||
退回
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audit-field">
|
||||
<label>审批意见</label>
|
||||
<ElInput v-model="commonApprovalModel.opinion" type="textarea" :rows="3" placeholder="请输入审批意见" />
|
||||
</div>
|
||||
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="commonApprovalModel"
|
||||
:rules="commonRules"
|
||||
label-position="top"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<ElFormItem :label="opinionLabel" prop="opinion">
|
||||
<ElInput
|
||||
v-model="commonApprovalModel.opinion"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
:placeholder="opinionPlaceholder"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
formatDate,
|
||||
formatEmptyText,
|
||||
formatPeriod,
|
||||
formatPeriodDateRange,
|
||||
formatWeeklyPeriodLabel,
|
||||
getProjectReportFlagLabel,
|
||||
getWorkReportStatusLabel
|
||||
} from '../types';
|
||||
@@ -29,6 +31,14 @@ const title = computed(() => `${WORK_REPORT_TYPE_LABEL[props.reportType]}详情`
|
||||
const weeklyDetail = computed(() =>
|
||||
props.reportType === 'weekly' ? (detail.value as Api.WorkReport.Weekly.WeeklyReport | null) : null
|
||||
);
|
||||
const periodText = computed(() => {
|
||||
if (!detail.value) return '--';
|
||||
return props.reportType === 'weekly' ? formatWeeklyPeriodLabel(detail.value) : formatPeriod(detail.value);
|
||||
});
|
||||
const periodTooltip = computed(() => {
|
||||
if (!detail.value || props.reportType !== 'weekly') return '';
|
||||
return formatPeriodDateRange(detail.value);
|
||||
});
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (isVisible) loadDetail();
|
||||
@@ -68,7 +78,11 @@ function getPersonalDetail() {
|
||||
<div v-if="detail" class="work-report-detail">
|
||||
<BusinessFormSection title="基础信息">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptionsItem label="报告周期">{{ formatPeriod(detail) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="报告周期">
|
||||
<ElTooltip :disabled="!periodTooltip || periodTooltip === '--'" :content="periodTooltip" placement="top">
|
||||
<span>{{ periodText }}</span>
|
||||
</ElTooltip>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
{{ getWorkReportStatusLabel(detail.statusCode, detail.statusName) }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
@@ -335,7 +335,7 @@ async function handleSubmit() {
|
||||
<ElDescriptionsItem label="填报人">
|
||||
{{ baseReporterName }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="部门/方向">{{ baseDeptName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="部门">{{ baseDeptName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="岗位">{{ basePostName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="直属上级">{{ baseInfo?.supervisorName || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="周期" :span="2">{{ activeModel.periodLabel || '--' }}</ElDescriptionsItem>
|
||||
|
||||
@@ -76,6 +76,7 @@ onBeforeUnmount(() => {
|
||||
:title="props.title"
|
||||
:size="drawerSize"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
@closed="onDrawerClosed"
|
||||
>
|
||||
<div v-loading="props.loading" class="work-report-page-drawer__content">
|
||||
@@ -93,6 +94,7 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
:global(.work-report-page-drawer__body--approval) {
|
||||
@@ -107,6 +109,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.work-report-page-drawer__content :deep(.form-page) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
createProjectSaveParams,
|
||||
createWeeklySaveParams,
|
||||
formatPeriodLabel,
|
||||
getStructuredSections,
|
||||
normalizePlanItems,
|
||||
normalizeProjectItems,
|
||||
normalizeReviewItems
|
||||
@@ -459,7 +460,6 @@ function isCompleteWeeklyTravelSegment(segment: Api.WorkReport.Weekly.WeeklyRepo
|
||||
return Boolean(
|
||||
segment.startDate &&
|
||||
segment.endDate &&
|
||||
segment.location?.trim() &&
|
||||
Number.isFinite(travelDays) &&
|
||||
travelDays >= 0.5 &&
|
||||
Number.isInteger(travelDays * 2)
|
||||
@@ -470,6 +470,21 @@ function hasCompleteWeeklyTravelSegment(items: Api.WorkReport.Weekly.WeeklyRepor
|
||||
return items.some(isCompleteWeeklyTravelSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 周报"具体工作内容及成果描述"中已记录出差信息("本周差旅"分类、或含"差旅"分类、
|
||||
* 或结构化任务里带 kind=travel)时,视为已经有完整出差分段,不再强制弹出
|
||||
* "请至少新增一条完整的出差分段"。避免出差点位等历史录入原因导致保存时反复提示。
|
||||
*/
|
||||
function hasTravelInfoInWeeklyReview(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
return items.some(item => {
|
||||
if (item.itemTitle?.trim() === '本周差旅') return true;
|
||||
const sections = getStructuredSections(item.contentJson);
|
||||
if (sections.some(section => section.category?.trim() === '差旅')) return true;
|
||||
if (sections.some(section => section.tasks.some(task => task.kind === 'travel'))) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function hasCompletePersonalReviewItem(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
return items.some(item => hasTextValue(item.itemTitle) && hasReviewContent(item));
|
||||
}
|
||||
@@ -506,7 +521,12 @@ function validateRequiredReportItems() {
|
||||
const messages: string[] = [];
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
const hasTravelReview = weeklyModel.isBusinessTrip && hasCompleteWeeklyTravelSegment(weeklyModel.travelSegments);
|
||||
// 出差信息既可能来自 travelSegments,也可能来自具体工作内容及成果描述里的"差旅"段,
|
||||
// 任何一方已有完整出差信息就不再提示"请至少新增一条完整的出差分段"。
|
||||
const hasTravelReview =
|
||||
weeklyModel.isBusinessTrip &&
|
||||
(hasCompleteWeeklyTravelSegment(weeklyModel.travelSegments) ||
|
||||
hasTravelInfoInWeeklyReview(weeklyModel.reviewItems));
|
||||
const reviewMessage = hasTravelReview
|
||||
? weeklyModel.reviewItems.length
|
||||
? getPersonalReviewValidationMessage('当期重点工作回顾', weeklyModel.reviewItems)
|
||||
@@ -769,6 +789,7 @@ async function handleActionSubmit(
|
||||
:action-type="currentActionType"
|
||||
:initial-monthly-approve-data="reportType === 'monthly' ? monthlyApprovalDraft : null"
|
||||
:loading="actionSubmitting"
|
||||
append-to-body
|
||||
@submit="handleActionSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user