fix(加班申请): 去掉撤销相关的状态和动作。
feat(工作报告): 开发工作报告功能
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
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';
|
||||
|
||||
defineOptions({ name: 'WorkReportActionDialog' });
|
||||
|
||||
type ActionType = 'approve' | 'reject';
|
||||
type ApprovalConclusion = 'approve' | 'reject';
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
reportType: WorkReportType;
|
||||
actionType: ActionType;
|
||||
initialMonthlyApproveData?: Partial<Api.WorkReport.Monthly.MonthlyReportApproveParams> | null;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
initialMonthlyApproveData: null,
|
||||
loading: false
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'submit',
|
||||
payload: Api.WorkReport.Common.StatusActionParams | Api.WorkReport.Monthly.MonthlyReportApproveParams,
|
||||
actionType?: ActionType
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const reasonModel = reactive<Api.WorkReport.Common.StatusActionParams>({
|
||||
reason: ''
|
||||
});
|
||||
const commonApprovalModel = reactive<{
|
||||
conclusion: ApprovalConclusion | '';
|
||||
opinion: string;
|
||||
}>({
|
||||
conclusion: 'approve',
|
||||
opinion: ''
|
||||
});
|
||||
|
||||
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||
reason: '',
|
||||
meetingDate: '',
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
|
||||
const isMonthlyApprove = computed(() => props.reportType === 'monthly' && props.actionType === 'approve');
|
||||
const isCommonApprove = computed(() => props.reportType !== 'monthly' && props.actionType === 'approve');
|
||||
const title = computed(() => {
|
||||
if (isCommonApprove.value) {
|
||||
return `审批${WORK_REPORT_TYPE_LABEL[props.reportType]}`;
|
||||
}
|
||||
|
||||
const actionLabel = props.actionType === 'approve' ? '审批通过' : '退回';
|
||||
|
||||
return `${actionLabel}${WORK_REPORT_TYPE_LABEL[props.reportType]}`;
|
||||
});
|
||||
|
||||
const preset = computed(() => (isMonthlyApprove.value ? 'lg' : 'sm'));
|
||||
const confirmText = computed(() => {
|
||||
if (isCommonApprove.value) return '确认提交';
|
||||
if (props.actionType === 'approve') return '通过';
|
||||
return '退回';
|
||||
});
|
||||
const confirmDisabled = computed(() => isCommonApprove.value && !commonApprovalModel.conclusion);
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (!isVisible) return;
|
||||
reasonModel.reason = '';
|
||||
Object.assign(commonApprovalModel, {
|
||||
conclusion: 'approve',
|
||||
opinion: ''
|
||||
});
|
||||
Object.assign(monthlyModel, {
|
||||
reason: '',
|
||||
meetingDate: dayjs().format('YYYY-MM-DD'),
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: dayjs().format('YYYY-MM-DD'),
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: dayjs().format('YYYY-MM-DD')
|
||||
});
|
||||
|
||||
if (props.initialMonthlyApproveData) {
|
||||
Object.assign(monthlyModel, props.initialMonthlyApproveData);
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
if (isCommonApprove.value) {
|
||||
if (!commonApprovalModel.conclusion) {
|
||||
window.$message?.warning('请选择审批结论');
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
'submit',
|
||||
{
|
||||
reason: commonApprovalModel.opinion || (commonApprovalModel.conclusion === 'approve' ? '通过' : '不通过')
|
||||
},
|
||||
commonApprovalModel.conclusion
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit('submit', isMonthlyApprove.value ? { ...monthlyModel } : { ...reasonModel });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
:preset="preset"
|
||||
:confirm-loading="loading"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
:confirm-text="confirmText"
|
||||
max-body-height="76vh"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<template v-if="isCommonApprove">
|
||||
<div class="audit-form">
|
||||
<div class="audit-field">
|
||||
<label>审批结论</label>
|
||||
<div class="audit-conclusion">
|
||||
<button
|
||||
type="button"
|
||||
class="conclusion-btn"
|
||||
:class="{
|
||||
active: commonApprovalModel.conclusion === 'approve',
|
||||
pass: commonApprovalModel.conclusion === 'approve'
|
||||
}"
|
||||
@click="commonApprovalModel.conclusion = 'approve'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||
<path
|
||||
d="M5 8.5L7 10.5L11 6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
通过
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="conclusion-btn"
|
||||
:class="{
|
||||
active: commonApprovalModel.conclusion === 'reject',
|
||||
reject: commonApprovalModel.conclusion === 'reject'
|
||||
}"
|
||||
@click="commonApprovalModel.conclusion = 'reject'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isMonthlyApprove">
|
||||
<BusinessFormSection title="当期工作反馈">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="面谈时间">
|
||||
<ElDatePicker v-model="monthlyModel.meetingDate" class="w-full" type="date" value-format="YYYY-MM-DD" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="绩效考核结果">
|
||||
<ElInput v-model="monthlyModel.performanceResult" placeholder="请输入绩效结果" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="审批意见">
|
||||
<ElInput v-model="monthlyModel.reason" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="优势与不足">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="优势描述">
|
||||
<ElInput v-model="monthlyModel.strengthDesc" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="优势行为事例">
|
||||
<ElInput v-model="monthlyModel.strengthExample" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="劣势描述">
|
||||
<ElInput v-model="monthlyModel.weaknessDesc" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="劣势行为事例">
|
||||
<ElInput v-model="monthlyModel.weaknessExample" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="改进建议">
|
||||
<ElInput v-model="monthlyModel.improvementSuggestion" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="签字区">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="被考核人签名">
|
||||
<ElInput v-model="monthlyModel.employeeSignName" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="被考核人签字日期">
|
||||
<ElDatePicker
|
||||
v-model="monthlyModel.employeeSignedDate"
|
||||
class="w-full"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="上级签名">
|
||||
<ElInput v-model="monthlyModel.supervisorSignName" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="上级签字日期">
|
||||
<ElDatePicker
|
||||
v-model="monthlyModel.supervisorSignedDate"
|
||||
class="w-full"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
|
||||
<ElForm v-else label-position="top">
|
||||
<ElFormItem :label="actionType === 'approve' ? '审批意见' : '原因'">
|
||||
<ElInput v-model="reasonModel.reason" type="textarea" :rows="5" placeholder="请输入原因或意见" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.audit-form {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.audit-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.audit-field label {
|
||||
color: #475467;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.audit-conclusion {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.conclusion-btn {
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: 1px solid #d8e0e8;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #475467;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.conclusion-btn:hover {
|
||||
border-color: #0f766e;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.conclusion-btn.active.pass {
|
||||
border-color: #0f766e;
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.conclusion-btn.active.reject {
|
||||
border-color: #dc2626;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable no-void */
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
fetchGetMonthlyReportApprovalRecords,
|
||||
fetchGetProjectReportApprovalRecords,
|
||||
fetchGetWeeklyReportApprovalRecords
|
||||
} from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
getWorkReportStatusLabel
|
||||
} from '../types';
|
||||
|
||||
/** 格式化文本,空值显示 -- */
|
||||
function formatTextOrDash(value?: string | number | null) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '--';
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WorkReportApprovalRecordDialog' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
reportType: WorkReportType;
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const records = ref<
|
||||
Array<Api.WorkReport.Common.WorkReportApprovalRecord | Api.WorkReport.Monthly.MonthlyReportApprovalRecord>
|
||||
>([]);
|
||||
const title = computed(() => `${WORK_REPORT_TYPE_LABEL[props.reportType]}审批记录`);
|
||||
const monthlyRecords = computed(() => records.value as Api.WorkReport.Monthly.MonthlyReportApprovalRecord[]);
|
||||
|
||||
watch(
|
||||
[visible, () => props.rowData?.id, () => props.reportType],
|
||||
([isVisible, currentId]) => {
|
||||
if (!isVisible) return;
|
||||
// visible 为 true(首次打开、换行、换报告类型)时都重新加载记录
|
||||
if (currentId) {
|
||||
loadRecords();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function loadRecords() {
|
||||
if (!props.rowData?.id) return;
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportApprovalRecords(props.rowData.id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportApprovalRecords(props.rowData.id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportApprovalRecords(props.rowData.id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
records.value = !result.error && result.data ? result.data : [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
|
||||
<ElTable v-if="reportType !== 'monthly'" border :data="records">
|
||||
<ElTableColumn prop="approvalRound" label="轮次" width="80" />
|
||||
<ElTableColumn label="结论" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getWorkReportStatusLabel(row.conclusion) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="opinion" label="审批意见" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="auditorName" label="审批人" width="120" />
|
||||
<ElTableColumn label="审批时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createTime) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div v-else class="work-report-approval-records">
|
||||
<ElCard v-for="item in monthlyRecords" :key="item.id">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-12px">
|
||||
<span>第 {{ item.approvalRound }} 轮 · {{ getWorkReportStatusLabel(item.conclusion) }}</span>
|
||||
<span class="text-12px text-#64748b">{{ item.auditorName }} · {{ formatDateTime(item.createTime) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="审批意见" :span="2">{{ formatTextOrDash(item.opinion) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="面谈时间">{{ formatDate(item.meetingDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="绩效结果">{{ formatTextOrDash(item.performanceResult) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="优势描述">{{ formatTextOrDash(item.strengthDesc) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="优势事例">{{ formatTextOrDash(item.strengthExample) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="劣势描述">{{ formatTextOrDash(item.weaknessDesc) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="劣势事例">{{ formatTextOrDash(item.weaknessExample) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="改进建议" :span="2">
|
||||
{{ formatTextOrDash(item.improvementSuggestion) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="被考核人签字">
|
||||
{{ formatTextOrDash(item.employeeSignName) }} / {{ formatDate(item.employeeSignedDate) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="上级签字">
|
||||
{{ formatTextOrDash(item.supervisorSignName) }} / {{ formatDate(item.supervisorSignedDate) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-approval-records {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,534 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Calendar } from '@element-plus/icons-vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
type WorkReportPeriodOption,
|
||||
buildMonthlyPeriodFromMonth,
|
||||
buildProjectPeriodFromMonth,
|
||||
buildWeeklyPeriodFromDate,
|
||||
formatPeriodDisplayLabel,
|
||||
getReportTypePeriodOptions
|
||||
} from '../utils';
|
||||
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportCreateDialog' });
|
||||
|
||||
interface Props {
|
||||
defaultReportType?: WorkReportType;
|
||||
projectVisible?: boolean;
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
defaultReportType: 'weekly',
|
||||
projectVisible: false,
|
||||
projectOptions: () => []
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'confirm',
|
||||
payload:
|
||||
| { reportType: 'weekly' | 'monthly'; period: WorkReportPeriodOption['period'] }
|
||||
| {
|
||||
reportType: 'project';
|
||||
projectId: string;
|
||||
flag: number;
|
||||
period: WorkReportPeriodOption['period'];
|
||||
}
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const selectedPeriodKey = ref('');
|
||||
const selectedProjectId = ref('');
|
||||
const customWeekDate = ref('');
|
||||
const customMonth = ref('');
|
||||
const customProjectMonth = ref('');
|
||||
const customProjectFlag = ref(1);
|
||||
|
||||
const selectedReportType = computed<WorkReportType>(() => {
|
||||
if (props.defaultReportType === 'project' && !props.projectVisible) return 'weekly';
|
||||
return props.defaultReportType;
|
||||
});
|
||||
|
||||
const periodOptionMap = computed(() => getReportTypePeriodOptions());
|
||||
const activePeriodOptions = computed(() => periodOptionMap.value[selectedReportType.value]);
|
||||
const dialogTitle = computed(() => `新增${WORK_REPORT_TYPE_LABEL[selectedReportType.value]}`);
|
||||
const projectHalfOptions = [
|
||||
{ label: '上半月', value: 1 },
|
||||
{ label: '下半月', value: 2 }
|
||||
];
|
||||
|
||||
const defaultCustomMonth = computed(() => {
|
||||
const period = activePeriodOptions.value[0]?.period;
|
||||
return period?.periodStartDate.slice(0, 7) || '';
|
||||
});
|
||||
|
||||
const customPeriod = computed<WorkReportPeriodOption['period'] | null>(() => {
|
||||
if (selectedPeriodKey.value !== 'custom') return null;
|
||||
|
||||
if (selectedReportType.value === 'weekly') {
|
||||
if (!customWeekDate.value) return null;
|
||||
return buildWeeklyPeriodFromDate(customWeekDate.value);
|
||||
}
|
||||
|
||||
if (selectedReportType.value === 'monthly') {
|
||||
if (!customMonth.value) return null;
|
||||
return buildMonthlyPeriodFromMonth(customMonth.value);
|
||||
}
|
||||
|
||||
if (!customProjectMonth.value) return null;
|
||||
return buildProjectPeriodFromMonth(customProjectMonth.value, customProjectFlag.value);
|
||||
});
|
||||
|
||||
const selectedPeriod = computed(
|
||||
() => activePeriodOptions.value.find(item => item.key === selectedPeriodKey.value) ?? activePeriodOptions.value[0]
|
||||
);
|
||||
|
||||
const selectedPeriodValue = computed(() =>
|
||||
selectedPeriodKey.value === 'custom' ? customPeriod.value : selectedPeriod.value?.period
|
||||
);
|
||||
const customPeriodPreviewLabel = computed(() =>
|
||||
customPeriod.value ? formatPeriodDisplayLabel(customPeriod.value.periodLabel) : ''
|
||||
);
|
||||
|
||||
const confirmDisabled = computed(() => {
|
||||
if (!selectedPeriodValue.value) return true;
|
||||
if (selectedReportType.value === 'project' && !selectedProjectId.value) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
watch(
|
||||
selectedReportType,
|
||||
type => {
|
||||
selectedPeriodKey.value = periodOptionMap.value[type][0]?.key || '';
|
||||
|
||||
if (type === 'project' && !selectedProjectId.value) {
|
||||
selectedProjectId.value = props.projectOptions[0]?.id || '';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (!isVisible) return;
|
||||
|
||||
selectedProjectId.value = props.projectOptions[0]?.id || '';
|
||||
selectedPeriodKey.value = periodOptionMap.value[selectedReportType.value][0]?.key || '';
|
||||
customWeekDate.value = activePeriodOptions.value[0]?.period.periodStartDate || '';
|
||||
customMonth.value = defaultCustomMonth.value;
|
||||
customProjectMonth.value = defaultCustomMonth.value;
|
||||
customProjectFlag.value = activePeriodOptions.value[0]?.flag || 1;
|
||||
});
|
||||
|
||||
function handleConfirm() {
|
||||
const period = selectedPeriodValue.value;
|
||||
if (!period) return;
|
||||
|
||||
if (selectedReportType.value === 'project') {
|
||||
emit('confirm', {
|
||||
reportType: 'project',
|
||||
projectId: selectedProjectId.value,
|
||||
flag: selectedPeriodKey.value === 'custom' ? customProjectFlag.value : selectedPeriod.value.flag || 1,
|
||||
period
|
||||
});
|
||||
} else {
|
||||
emit('confirm', {
|
||||
reportType: selectedReportType.value,
|
||||
period
|
||||
});
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
class="work-report-create-dialog"
|
||||
preset="md"
|
||||
confirm-text="确认新增"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<div v-if="selectedReportType === 'project'" class="work-report-create-dialog__project-select">
|
||||
<label class="work-report-create-dialog__label">项目</label>
|
||||
<ElSelect v-model="selectedProjectId" class="w-full" placeholder="请选择项目" filterable>
|
||||
<ElOption
|
||||
v-for="item in props.projectOptions"
|
||||
:key="item.id"
|
||||
:label="item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<div class="work-report-create-dialog__section">
|
||||
<div class="work-report-create-dialog__grid is-period">
|
||||
<button
|
||||
v-for="item in activePeriodOptions"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="work-report-create-dialog__choice"
|
||||
:class="{ 'is-active': selectedPeriodKey === item.key }"
|
||||
@click="selectedPeriodKey = item.key"
|
||||
>
|
||||
<div class="work-report-create-dialog__choice-title">{{ item.label }}</div>
|
||||
<div class="work-report-create-dialog__choice-desc">{{ item.description }}</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="work-report-create-dialog__choice"
|
||||
:class="{ 'is-active': selectedPeriodKey === 'custom' }"
|
||||
@click="selectedPeriodKey = 'custom'"
|
||||
>
|
||||
<div class="work-report-create-dialog__choice-title">自定义周期</div>
|
||||
<div class="work-report-create-dialog__choice-desc">
|
||||
{{
|
||||
selectedReportType === 'weekly'
|
||||
? '选择某一周作为周报周期。'
|
||||
: selectedReportType === 'monthly'
|
||||
? '选择某一月作为月报周期。'
|
||||
: '选择某个月的上半月或下半月。'
|
||||
}}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedPeriodKey === 'custom'" class="work-report-create-dialog__custom-period">
|
||||
<div v-if="selectedReportType === 'weekly'" class="work-report-create-dialog__custom-row">
|
||||
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
|
||||
<label class="work-report-create-dialog__label">周报周期</label>
|
||||
<ElDatePicker
|
||||
v-model="customWeekDate"
|
||||
type="date"
|
||||
format="YYYY[年第]ww[周]"
|
||||
value-format="YYYY-MM-DD"
|
||||
popper-class="work-report-create-date-popper"
|
||||
placeholder="请选择周报周期"
|
||||
/>
|
||||
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||
{{ customPeriodPreviewLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedReportType === 'monthly'" class="work-report-create-dialog__custom-row">
|
||||
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
|
||||
<label class="work-report-create-dialog__label">月报周期</label>
|
||||
<ElDatePicker
|
||||
v-model="customMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
popper-class="work-report-create-date-popper"
|
||||
placeholder="请选择月份"
|
||||
/>
|
||||
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||
{{ customPeriodPreviewLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="work-report-create-dialog__custom-project">
|
||||
<div class="work-report-create-dialog__custom-project-grid">
|
||||
<div class="work-report-create-dialog__custom-project-item">
|
||||
<div class="work-report-create-dialog__custom-project-item-label">选择月份</div>
|
||||
<ElDatePicker
|
||||
v-model="customProjectMonth"
|
||||
class="w-full"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
popper-class="work-report-create-date-popper"
|
||||
placeholder="请选择月份"
|
||||
/>
|
||||
</div>
|
||||
<div class="work-report-create-dialog__custom-project-item">
|
||||
<div class="work-report-create-dialog__custom-project-item-label">选择半月</div>
|
||||
<ElSegmented
|
||||
v-model="customProjectFlag"
|
||||
:options="projectHalfOptions"
|
||||
class="work-report-create-dialog__half-segmented"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||
<ElIcon class="work-report-create-dialog__period-preview-icon"><Calendar /></ElIcon>
|
||||
<span class="work-report-create-dialog__period-preview-text">已选周期:</span>
|
||||
<span class="work-report-create-dialog__period-preview-value">{{ customPeriodPreviewLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer="{ close }">
|
||||
<div class="work-report-create-dialog__footer">
|
||||
<ElButton @click="close">取消</ElButton>
|
||||
<ElButton type="primary" :disabled="confirmDisabled" @click="handleConfirm">确认新增</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-create-dialog__header {
|
||||
padding: 0 0 14px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__subtitle {
|
||||
margin-top: 5px;
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__section + .work-report-create-dialog__section {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__grid.is-period {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice {
|
||||
padding: 16px;
|
||||
border: 2px solid #e5edf1;
|
||||
border-radius: 16px;
|
||||
background: #fbfdfe;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.16s ease,
|
||||
background 0.16s ease,
|
||||
box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice:hover {
|
||||
border-color: rgba(15, 118, 110, 0.28);
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice.is-active {
|
||||
border-color: #0f766e;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice-title {
|
||||
font-weight: 900;
|
||||
color: #14213d;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice-desc {
|
||||
margin-top: 7px;
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__project-select {
|
||||
margin: 4px 0 18px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/** 行内字段:label 和控件在同一行,绿色 label 紧贴日期选择器右边 */
|
||||
.work-report-create-dialog__field--inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline .work-report-create-dialog__label {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline :deep(.el-date-editor) {
|
||||
width: auto;
|
||||
min-width: 160px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__label {
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-period {
|
||||
margin-top: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #f8fffd 0%, #ffffff 100%);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-row > .work-report-create-dialog__field--inline {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e5edf1;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
transition: border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item:hover {
|
||||
border-color: rgba(15, 118, 110, 0.4);
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item-label {
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item :deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__half-segmented {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__half-segmented :deep(.el-segmented__group) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__half-segmented :deep(.el-segmented__item) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||
border-radius: 999px;
|
||||
background: #ecfdf5;
|
||||
color: #0f766e;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview-icon {
|
||||
font-size: 14px;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview-text {
|
||||
color: #475467;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview-value {
|
||||
color: #0f766e;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.work-report-create-dialog__grid,
|
||||
.work-report-create-dialog__grid.is-period {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-row,
|
||||
.work-report-create-dialog__custom-project-grid {
|
||||
flex-direction: column;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline :deep(.el-date-editor) {
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.work-report-create-date-popper) {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.work-report-create-date-popper .el-picker-panel__body-wrapper) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:global(.work-report-create-date-popper .el-date-table td.current:not(.disabled) .el-date-table-cell__text),
|
||||
:global(.work-report-create-date-popper .el-month-table td.current:not(.disabled) .cell) {
|
||||
background-color: #0f766e;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { fetchGetMonthlyReportDetail, fetchGetProjectReportDetail, fetchGetWeeklyReportDetail } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType,
|
||||
formatDate,
|
||||
formatEmptyText,
|
||||
formatPeriod,
|
||||
getProjectReportFlagLabel,
|
||||
getWorkReportStatusLabel
|
||||
} from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportDetailDialog' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
reportType: WorkReportType;
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const detail = ref<WorkReportRow | null>(null);
|
||||
const title = computed(() => `${WORK_REPORT_TYPE_LABEL[props.reportType]}详情`);
|
||||
const weeklyDetail = computed(() =>
|
||||
props.reportType === 'weekly' ? (detail.value as Api.WorkReport.Weekly.WeeklyReport | null) : null
|
||||
);
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (isVisible) loadDetail();
|
||||
});
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.rowData?.id) return;
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportDetail(props.rowData.id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportDetail(props.rowData.id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportDetail(props.rowData.id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (!result.error && result.data) {
|
||||
detail.value = result.data;
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectDetail() {
|
||||
return detail.value as Api.WorkReport.Project.ProjectReport | null;
|
||||
}
|
||||
|
||||
function getPersonalDetail() {
|
||||
return detail.value as Api.WorkReport.Weekly.WeeklyReport | Api.WorkReport.Monthly.MonthlyReport | null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
|
||||
<div v-if="detail" class="work-report-detail">
|
||||
<BusinessFormSection title="基础信息">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptionsItem label="报告周期">{{ formatPeriod(detail) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
{{ getWorkReportStatusLabel(detail.statusCode, detail.statusName) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="直属上级">{{ detail.supervisorName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始日期">{{ formatDate(detail.periodStartDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="结束日期">{{ formatDate(detail.periodEndDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总工时">{{ formatEmptyText(detail.totalWorkHours) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提交时间">{{ formatEmptyText(detail.submitTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审批时间">{{ formatEmptyText(detail.approvalTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审批意见">{{ formatEmptyText(detail.approvalComment) }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</BusinessFormSection>
|
||||
|
||||
<template v-if="reportType === 'project'">
|
||||
<BusinessFormSection title="项目信息">
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="项目名称">{{ getProjectDetail()?.projectName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="半月周期">
|
||||
{{ getProjectReportFlagLabel(getProjectDetail()?.flag) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="项目负责人">{{ getProjectDetail()?.projectOwnerName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="技术负责人">
|
||||
{{ formatEmptyText(getProjectDetail()?.technicalOwnerName) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="项目状态" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectStatusDesc) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="整体计划进度" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectProgressPlan) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="要点描述" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectKeyPoints) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="项目问题" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectProblems) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="本期工作内容">
|
||||
<ElTable border :data="getProjectDetail()?.currentItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="工作内容" min-width="180" />
|
||||
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||
<ElTableColumn prop="priorityCode" label="优先级" width="120" />
|
||||
<ElTableColumn prop="progressRate" label="进度" width="100" />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下期计划工作内容">
|
||||
<ElTable border :data="getProjectDetail()?.nextItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="工作内容" min-width="180" />
|
||||
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||
<ElTableColumn prop="priorityCode" label="优先级" width="120" />
|
||||
<ElTableColumn prop="progressRate" label="进度" width="100" />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<BusinessFormSection title="当期重点工作回顾">
|
||||
<ElTable border :data="getPersonalDetail()?.reviewItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="事项" min-width="160" />
|
||||
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||
<ElTableColumn prop="contentText" label="工作内容" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="reflectionText" label="复盘反思" min-width="220" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下周期重点工作计划">
|
||||
<ElTable border :data="getPersonalDetail()?.planItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="事项" min-width="160" />
|
||||
<ElTableColumn prop="targetText" label="目标" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="supportNeed" label="支持需求" min-width="220" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection v-if="reportType === 'weekly'" title="出差信息">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptionsItem label="是否出差">
|
||||
{{ weeklyDetail?.isBusinessTrip ? '是' : '否' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="出差天数">
|
||||
{{ formatEmptyText(weeklyDetail?.totalTravelDays) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<ElTable class="mt-12px" border :data="weeklyDetail?.travelSegments || []">
|
||||
<ElTableColumn prop="startDate" label="开始日期" width="120" />
|
||||
<ElTableColumn prop="endDate" label="结束日期" width="120" />
|
||||
<ElTableColumn prop="travelDays" label="天数" width="100" />
|
||||
<ElTableColumn prop="location" label="地点" min-width="160" />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-detail {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,564 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
fetchCreateMonthlyReport,
|
||||
fetchCreateProjectReport,
|
||||
fetchCreateWeeklyReport,
|
||||
fetchGetMonthlyReportDetail,
|
||||
fetchGetProjectReportDetail,
|
||||
fetchGetWeeklyReportDetail,
|
||||
fetchInitMonthlyReport,
|
||||
fetchInitProjectReport,
|
||||
fetchInitWeeklyReport,
|
||||
fetchPreviewMonthlyReportDefaultDraft,
|
||||
fetchPreviewProjectReportDefaultDraft,
|
||||
fetchPreviewWeeklyReportDefaultDraft,
|
||||
fetchUpdateMonthlyReport,
|
||||
fetchUpdateProjectReport,
|
||||
fetchUpdateWeeklyReport
|
||||
} from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType,
|
||||
createBlankPlanItem,
|
||||
createBlankProjectItem,
|
||||
createBlankReviewItem,
|
||||
createMonthlySaveParams,
|
||||
createProjectSaveParams,
|
||||
createWeeklySaveParams,
|
||||
normalizePlanItems,
|
||||
normalizeProjectItems,
|
||||
normalizeReviewItems
|
||||
} from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportOperateDialog' });
|
||||
|
||||
interface PeriodPayload {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
operateType: 'add' | 'edit';
|
||||
reportType: WorkReportType;
|
||||
rowData?: WorkReportRow | null;
|
||||
initialPeriod?: PeriodPayload | null;
|
||||
initialProjectId?: string;
|
||||
initialFlag?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rowData: null,
|
||||
initialPeriod: null,
|
||||
initialProjectId: '',
|
||||
initialFlag: 1
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const baseInfo = ref<WorkReportRow | null>(null);
|
||||
|
||||
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
|
||||
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
|
||||
const projectModel = reactive<Api.WorkReport.Project.ProjectReportSaveParams>(createProjectSaveParams());
|
||||
|
||||
const title = computed(
|
||||
() => `${props.operateType === 'add' ? '新增' : '编辑'}${WORK_REPORT_TYPE_LABEL[props.reportType]}`
|
||||
);
|
||||
const dialogPreset = computed(() => (props.reportType === 'weekly' ? 'md' : 'lg'));
|
||||
const activeModel = computed(() => {
|
||||
if (props.reportType === 'monthly') return monthlyModel;
|
||||
if (props.reportType === 'project') return projectModel;
|
||||
return weeklyModel;
|
||||
});
|
||||
const baseReporterName = computed(() => {
|
||||
if (!baseInfo.value) return '--';
|
||||
if ('projectOwnerName' in baseInfo.value) return baseInfo.value.projectOwnerName || '--';
|
||||
return baseInfo.value.reporterName || '--';
|
||||
});
|
||||
const baseDeptName = computed(() => {
|
||||
if (!baseInfo.value || 'projectOwnerName' in baseInfo.value) return '--';
|
||||
return baseInfo.value.reporterDeptName || '--';
|
||||
});
|
||||
const basePostName = computed(() => {
|
||||
if (!baseInfo.value || 'projectOwnerName' in baseInfo.value) return '--';
|
||||
return baseInfo.value.reporterPostName || '--';
|
||||
});
|
||||
|
||||
function patchPeriod(target: {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}) {
|
||||
if (!props.initialPeriod) return;
|
||||
Object.assign(target, props.initialPeriod);
|
||||
}
|
||||
|
||||
function patchWeekly(report?: Partial<Api.WorkReport.Weekly.WeeklyReport>) {
|
||||
Object.assign(
|
||||
weeklyModel,
|
||||
createWeeklySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems,
|
||||
travelSegments: report?.travelSegments
|
||||
})
|
||||
);
|
||||
patchPeriod(weeklyModel);
|
||||
}
|
||||
|
||||
function patchMonthly(report?: Partial<Api.WorkReport.Monthly.MonthlyReport>) {
|
||||
Object.assign(
|
||||
monthlyModel,
|
||||
createMonthlySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems
|
||||
})
|
||||
);
|
||||
patchPeriod(monthlyModel);
|
||||
}
|
||||
|
||||
function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
|
||||
Object.assign(
|
||||
projectModel,
|
||||
createProjectSaveParams({
|
||||
...report,
|
||||
projectId: report?.projectId || props.initialProjectId,
|
||||
flag: report?.flag ?? props.initialFlag,
|
||||
currentItems: report?.currentItems,
|
||||
nextItems: report?.nextItems
|
||||
})
|
||||
);
|
||||
patchPeriod(projectModel);
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.rowData?.id) return;
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportDetail(props.rowData.id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportDetail(props.rowData.id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportDetail(props.rowData.id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
baseInfo.value = result.data;
|
||||
|
||||
if (props.reportType === 'weekly') patchWeekly(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(result.data as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
async function loadInitAndDraft() {
|
||||
loading.value = true;
|
||||
let initResult;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
initResult = await fetchInitWeeklyReport();
|
||||
} else if (props.reportType === 'monthly') {
|
||||
initResult = await fetchInitMonthlyReport();
|
||||
} else {
|
||||
initResult = await fetchInitProjectReport(props.initialProjectId);
|
||||
}
|
||||
|
||||
if (!initResult.error && initResult.data) {
|
||||
baseInfo.value = initResult.data;
|
||||
if (props.reportType === 'weekly') patchWeekly(initResult.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(initResult.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(initResult.data as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
await pullDefaultDraft(false);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function pullDefaultDraft(confirmOverwrite = true) {
|
||||
if (confirmOverwrite) {
|
||||
try {
|
||||
await ElMessageBox.confirm('重新拉取默认稿会覆盖当前已编辑内容,是否继续?', '覆盖确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const period = {
|
||||
periodKey: activeModel.value.periodKey,
|
||||
periodLabel: activeModel.value.periodLabel,
|
||||
periodStartDate: activeModel.value.periodStartDate,
|
||||
periodEndDate: activeModel.value.periodEndDate
|
||||
};
|
||||
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchPreviewWeeklyReportDefaultDraft(period);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchPreviewMonthlyReportDefaultDraft(period);
|
||||
} else {
|
||||
result = await fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
|
||||
...period,
|
||||
flag: projectModel.flag
|
||||
});
|
||||
}
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
weeklyModel.reviewItems = normalizeReviewItems((result.data as Api.WorkReport.Weekly.WeeklyReport).reviewItems);
|
||||
weeklyModel.planItems = normalizePlanItems((result.data as Api.WorkReport.Weekly.WeeklyReport).planItems);
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly') {
|
||||
monthlyModel.reviewItems = normalizeReviewItems((result.data as Api.WorkReport.Monthly.MonthlyReport).reviewItems);
|
||||
monthlyModel.planItems = normalizePlanItems((result.data as Api.WorkReport.Monthly.MonthlyReport).planItems);
|
||||
}
|
||||
|
||||
if (props.reportType === 'project') {
|
||||
projectModel.currentItems = normalizeProjectItems(
|
||||
(result.data as Api.WorkReport.Project.ProjectReport).currentItems
|
||||
);
|
||||
projectModel.nextItems = normalizeProjectItems((result.data as Api.WorkReport.Project.ProjectReport).nextItems);
|
||||
}
|
||||
}
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (!isVisible) return;
|
||||
baseInfo.value = null;
|
||||
if (props.operateType === 'edit') {
|
||||
loadDetail();
|
||||
} else {
|
||||
loadInitAndDraft();
|
||||
}
|
||||
});
|
||||
|
||||
function addReviewItem(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
items.push(createBlankReviewItem(items.length));
|
||||
}
|
||||
|
||||
function addPlanItem(items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||
items.push(createBlankPlanItem(items.length));
|
||||
}
|
||||
|
||||
function addProjectItem(items: Api.WorkReport.Project.ProjectReportItem[]) {
|
||||
items.push(createBlankProjectItem());
|
||||
}
|
||||
|
||||
function removeItem<T>(items: T[], index: number) {
|
||||
if (items.length <= 1) return;
|
||||
items.splice(index, 1);
|
||||
}
|
||||
|
||||
function validateBase() {
|
||||
if (!activeModel.value.periodKey || !activeModel.value.periodStartDate || !activeModel.value.periodEndDate) {
|
||||
window.$message?.warning('请先选择报告周期');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (props.reportType === 'project' && !projectModel.projectId) {
|
||||
window.$message?.warning('请选择项目');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!validateBase()) return;
|
||||
|
||||
submitting.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result =
|
||||
props.operateType === 'add'
|
||||
? await fetchCreateWeeklyReport(weeklyModel)
|
||||
: await fetchUpdateWeeklyReport(props.rowData!.id, weeklyModel);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result =
|
||||
props.operateType === 'add'
|
||||
? await fetchCreateMonthlyReport(monthlyModel)
|
||||
: await fetchUpdateMonthlyReport(props.rowData!.id, monthlyModel);
|
||||
} else {
|
||||
result =
|
||||
props.operateType === 'add'
|
||||
? await fetchCreateProjectReport(projectModel)
|
||||
: await fetchUpdateProjectReport(props.rowData!.id, projectModel);
|
||||
}
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) return;
|
||||
|
||||
window.$message?.success(props.operateType === 'add' ? '工作报告已创建' : '工作报告已保存');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
:preset="dialogPreset"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
max-body-height="76vh"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<div class="work-report-operate">
|
||||
<BusinessFormSection title="基础信息">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptionsItem label="填报人">
|
||||
{{ baseReporterName }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="部门/方向">{{ baseDeptName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="岗位">{{ basePostName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="直属上级">{{ baseInfo?.supervisorName || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="周期" :span="2">{{ activeModel.periodLabel || '--' }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div class="mt-12px flex justify-end">
|
||||
<ElButton plain type="primary" @click="pullDefaultDraft(true)">
|
||||
<template #icon>
|
||||
<icon-mdi-refresh class="text-icon" />
|
||||
</template>
|
||||
重新拉取默认稿
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<template v-if="reportType === 'project'">
|
||||
<BusinessFormSection title="项目状况">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="项目状态">
|
||||
<ElInput v-model="projectModel.projectStatusDesc" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="整体计划进度">
|
||||
<ElInput v-model="projectModel.projectProgressPlan" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="要点描述">
|
||||
<ElInput v-model="projectModel.projectKeyPoints" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="项目问题">
|
||||
<ElInput v-model="projectModel.projectProblems" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="本期工作内容">
|
||||
<div class="work-report-operate__items">
|
||||
<div v-for="(item, index) in projectModel.currentItems" :key="index" class="work-report-operate__item">
|
||||
<ElInput v-model="item.itemTitle" placeholder="工作内容" />
|
||||
<ElInputNumber v-model="item.workHours" :min="0" :precision="1" placeholder="工时" />
|
||||
<ElInput v-model="item.priorityCode" placeholder="优先级" />
|
||||
<ElInputNumber v-model="item.progressRate" :min="0" :max="100" :precision="0" placeholder="进度" />
|
||||
<ElButton link type="danger" @click="removeItem(projectModel.currentItems, index)">删除</ElButton>
|
||||
</div>
|
||||
<ElButton plain @click="addProjectItem(projectModel.currentItems)">新增本期工作</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下期计划工作内容">
|
||||
<div class="work-report-operate__items">
|
||||
<div v-for="(item, index) in projectModel.nextItems" :key="index" class="work-report-operate__item">
|
||||
<ElInput v-model="item.itemTitle" placeholder="工作内容" />
|
||||
<ElInputNumber v-model="item.workHours" :min="0" :precision="1" placeholder="工时" />
|
||||
<ElInput v-model="item.priorityCode" placeholder="优先级" />
|
||||
<ElInputNumber v-model="item.progressRate" :min="0" :max="100" :precision="0" placeholder="进度" />
|
||||
<ElButton link type="danger" @click="removeItem(projectModel.nextItems, index)">删除</ElButton>
|
||||
</div>
|
||||
<ElButton plain @click="addProjectItem(projectModel.nextItems)">新增下期工作</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<BusinessFormSection title="当期重点工作回顾">
|
||||
<div class="work-report-operate__cards">
|
||||
<div
|
||||
v-for="(item, index) in reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems"
|
||||
:key="index"
|
||||
class="work-report-operate__card"
|
||||
>
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="14">
|
||||
<ElFormItem label="事项标题">
|
||||
<ElInput v-model="item.itemTitle" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElFormItem label="工时">
|
||||
<ElInputNumber v-model="item.workHours" class="w-full" :min="0" :precision="1" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4" class="flex items-end justify-end pb-16px">
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
@click="
|
||||
removeItem(reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems, index)
|
||||
"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="工作内容">
|
||||
<ElInput v-model="item.contentText" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="复盘反思">
|
||||
<ElInput v-model="item.reflectionText" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
<ElButton
|
||||
plain
|
||||
@click="addReviewItem(reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems)"
|
||||
>
|
||||
新增回顾项
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下周期重点工作计划">
|
||||
<div class="work-report-operate__cards">
|
||||
<div
|
||||
v-for="(item, index) in reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems"
|
||||
:key="index"
|
||||
class="work-report-operate__card"
|
||||
>
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="20">
|
||||
<ElFormItem label="计划标题">
|
||||
<ElInput v-model="item.itemTitle" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4" class="flex items-end justify-end pb-16px">
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
@click="removeItem(reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems, index)"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="目标">
|
||||
<ElInput v-model="item.targetText" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="支持需求">
|
||||
<ElInput v-model="item.supportNeed" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
<ElButton
|
||||
plain
|
||||
@click="addPlanItem(reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems)"
|
||||
>
|
||||
新增计划项
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection v-if="reportType === 'weekly'" title="出差信息">
|
||||
<ElFormItem label="是否出差">
|
||||
<ElSwitch v-model="weeklyModel.isBusinessTrip" />
|
||||
</ElFormItem>
|
||||
<div v-if="weeklyModel.isBusinessTrip" class="work-report-operate__items">
|
||||
<div v-for="(item, index) in weeklyModel.travelSegments" :key="index" class="work-report-operate__item">
|
||||
<ElDatePicker v-model="item.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
|
||||
<ElDatePicker v-model="item.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
|
||||
<ElInputNumber v-model="item.travelDays" :min="0" :precision="1" placeholder="天数" />
|
||||
<ElInput v-model="item.location" placeholder="地点" />
|
||||
<ElButton link type="danger" @click="removeItem(weeklyModel.travelSegments, index)">删除</ElButton>
|
||||
</div>
|
||||
<ElButton
|
||||
plain
|
||||
@click="
|
||||
weeklyModel.travelSegments.push({
|
||||
sort: weeklyModel.travelSegments.length + 1,
|
||||
travelDays: 0,
|
||||
location: ''
|
||||
})
|
||||
"
|
||||
>
|
||||
新增出差分段
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-operate {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.work-report-operate__cards,
|
||||
.work-report-operate__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-report-operate__card,
|
||||
.work-report-operate__item {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-extra-light);
|
||||
}
|
||||
|
||||
.work-report-operate__item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 120px 120px 120px auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.work-report-operate__item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkReportPageDialog',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
loading?: boolean;
|
||||
showFooter?: boolean;
|
||||
approvalMode?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
loading: false,
|
||||
showFooter: false,
|
||||
approvalMode: false
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
const route = useRoute();
|
||||
const viewportWidth = ref(typeof window === 'undefined' ? 1920 : window.innerWidth);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const drawerSize = computed(() => (viewportWidth.value >= 2560 ? '60%' : '75%'));
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function syncViewportWidth() {
|
||||
viewportWidth.value = window.innerWidth;
|
||||
}
|
||||
|
||||
/** 抽屉关闭动画结束后触发 close 事件 */
|
||||
function onDrawerClosed() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
const drawerBodyClass = props.approvalMode
|
||||
? 'work-report-page-drawer__body work-report-page-drawer__body--approval'
|
||||
: 'work-report-page-drawer__body';
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
visible.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
syncViewportWidth();
|
||||
window.addEventListener('resize', syncViewportWidth);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', syncViewportWidth);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDrawer
|
||||
v-model="visible"
|
||||
class="work-report-page-drawer"
|
||||
:class="{ 'work-report-page-drawer--approval': props.approvalMode }"
|
||||
:body-class="drawerBodyClass"
|
||||
:title="props.title"
|
||||
:size="drawerSize"
|
||||
:close-on-click-modal="false"
|
||||
@closed="onDrawerClosed"
|
||||
>
|
||||
<div v-loading="props.loading" class="work-report-page-drawer__content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="props.showFooter" class="work-report-page-drawer__footer">
|
||||
<slot name="footer" :close="handleClose" />
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.work-report-page-drawer__body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.work-report-page-drawer__body--approval) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.work-report-page-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.work-report-page-drawer__content :deep(.form-page) {
|
||||
flex: 1 0 auto;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.work-report-page-drawer__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,774 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable complexity, no-nested-ternary, no-void, vue/no-deprecated-filter */
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchApproveMonthlyReport,
|
||||
fetchApproveProjectReport,
|
||||
fetchApproveWeeklyReport,
|
||||
fetchCreateMonthlyReport,
|
||||
fetchCreateProjectReport,
|
||||
fetchCreateWeeklyReport,
|
||||
fetchGetMonthlyReportDetail,
|
||||
fetchGetProjectReportDetail,
|
||||
fetchGetWeeklyReportDetail,
|
||||
fetchInitMonthlyReport,
|
||||
fetchInitProjectReport,
|
||||
fetchInitWeeklyReport,
|
||||
fetchPreviewMonthlyReportDefaultDraft,
|
||||
fetchPreviewProjectReportDefaultDraft,
|
||||
fetchPreviewWeeklyReportDefaultDraft,
|
||||
fetchRejectMonthlyReport,
|
||||
fetchRejectProjectReport,
|
||||
fetchRejectWeeklyReport,
|
||||
fetchSubmitMonthlyReport,
|
||||
fetchSubmitProjectReport,
|
||||
fetchSubmitWeeklyReport,
|
||||
fetchUpdateMonthlyReport,
|
||||
fetchUpdateProjectReport,
|
||||
fetchUpdateWeeklyReport
|
||||
} from '@/service/api';
|
||||
import type { WorkReportRow, WorkReportType } from '../types';
|
||||
import {
|
||||
createMonthlySaveParams,
|
||||
createProjectSaveParams,
|
||||
createWeeklySaveParams,
|
||||
formatPeriodLabel,
|
||||
normalizePlanItems,
|
||||
normalizeProjectItems,
|
||||
normalizeReviewItems
|
||||
} from '../types';
|
||||
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 WorkReportActionDialog from './action-dialog.vue';
|
||||
import WorkReportPageDialog from './page-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'WorkReportPrototypePageDialog' });
|
||||
|
||||
interface PeriodPayload {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
reportType: WorkReportType;
|
||||
mode?: 'add' | 'edit' | 'detail';
|
||||
scene?: 'fill' | 'detail' | 'approval';
|
||||
rowData?: WorkReportRow | null;
|
||||
initialPeriod?: PeriodPayload | null;
|
||||
initialProjectId?: string;
|
||||
initialFlag?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'detail',
|
||||
scene: 'detail',
|
||||
rowData: null,
|
||||
initialPeriod: null,
|
||||
initialProjectId: '',
|
||||
initialFlag: 1
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const REPORT_TYPE_TEXT: Record<WorkReportType, string> = {
|
||||
weekly: '个人周报',
|
||||
monthly: '个人月报',
|
||||
project: '项目半月报'
|
||||
};
|
||||
|
||||
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 currentReportId = ref('');
|
||||
const baseInfo = ref<WorkReportRow | null>(null);
|
||||
const monthlyApprovalDraft = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||
reason: '',
|
||||
meetingDate: '',
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
|
||||
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
|
||||
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
|
||||
const projectModel = reactive<Api.WorkReport.Project.ProjectReportSaveParams>(createProjectSaveParams());
|
||||
|
||||
const currentModel = computed(() => {
|
||||
if (props.reportType === 'monthly') return monthlyModel;
|
||||
if (props.reportType === 'project') return projectModel;
|
||||
return weeklyModel;
|
||||
});
|
||||
|
||||
const currentScene = computed(() => {
|
||||
if (props.reportType === 'monthly' && currentStage.value === 'approval') {
|
||||
return 'approval';
|
||||
}
|
||||
|
||||
return props.scene;
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.reportType === 'monthly' && currentStage.value === 'approval') {
|
||||
return `${REPORT_TYPE_TEXT.monthly}审批页`;
|
||||
}
|
||||
|
||||
if (currentScene.value === 'approval') {
|
||||
return `${REPORT_TYPE_TEXT[props.reportType]}审批`;
|
||||
}
|
||||
|
||||
if (props.mode === 'add') return `${REPORT_TYPE_TEXT[props.reportType]}填报页`;
|
||||
if (props.mode === 'edit') return `${REPORT_TYPE_TEXT[props.reportType]}编辑页`;
|
||||
return `${REPORT_TYPE_TEXT[props.reportType]}查看页`;
|
||||
});
|
||||
|
||||
const periodText = computed(() => {
|
||||
const label = currentModel.value.periodLabel || props.rowData?.periodLabel || props.initialPeriod?.periodLabel;
|
||||
return formatPeriodLabel(label) || '当前周期';
|
||||
});
|
||||
|
||||
function resetModels() {
|
||||
Object.assign(weeklyModel, createWeeklySaveParams());
|
||||
Object.assign(monthlyModel, createMonthlySaveParams());
|
||||
Object.assign(projectModel, createProjectSaveParams());
|
||||
Object.assign(monthlyApprovalDraft, {
|
||||
reason: '',
|
||||
meetingDate: '',
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
function patchPeriod(target: {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}) {
|
||||
if (!props.initialPeriod) return;
|
||||
target.periodKey = props.initialPeriod.periodKey;
|
||||
target.periodLabel = props.initialPeriod.periodLabel;
|
||||
target.periodStartDate = props.initialPeriod.periodStartDate;
|
||||
target.periodEndDate = props.initialPeriod.periodEndDate;
|
||||
}
|
||||
|
||||
function patchWeekly(report?: Partial<Api.WorkReport.Weekly.WeeklyReport>) {
|
||||
Object.assign(
|
||||
weeklyModel,
|
||||
createWeeklySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems,
|
||||
travelSegments: report?.travelSegments
|
||||
})
|
||||
);
|
||||
|
||||
if (report && Array.isArray(report.reviewItems) && !report.reviewItems.length) weeklyModel.reviewItems = [];
|
||||
if (report && Array.isArray(report.planItems) && !report.planItems.length) weeklyModel.planItems = [];
|
||||
if (report && Array.isArray(report.travelSegments) && !report.travelSegments.length) weeklyModel.travelSegments = [];
|
||||
|
||||
if (props.mode === 'add') patchPeriod(weeklyModel);
|
||||
}
|
||||
|
||||
function patchMonthly(report?: Partial<Api.WorkReport.Monthly.MonthlyReport>) {
|
||||
Object.assign(
|
||||
monthlyModel,
|
||||
createMonthlySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems
|
||||
})
|
||||
);
|
||||
|
||||
if (report && Array.isArray(report.reviewItems) && !report.reviewItems.length) monthlyModel.reviewItems = [];
|
||||
if (report && Array.isArray(report.planItems) && !report.planItems.length) monthlyModel.planItems = [];
|
||||
|
||||
if (props.mode === 'add') patchPeriod(monthlyModel);
|
||||
patchMonthlyApprovalDefaults(report);
|
||||
}
|
||||
|
||||
function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
|
||||
Object.assign(
|
||||
projectModel,
|
||||
createProjectSaveParams({
|
||||
...report,
|
||||
projectId: report?.projectId || props.initialProjectId,
|
||||
flag: report?.flag ?? props.initialFlag,
|
||||
currentItems: report?.currentItems,
|
||||
nextItems: report?.nextItems
|
||||
})
|
||||
);
|
||||
|
||||
if (report && Array.isArray(report.currentItems) && !report.currentItems.length) projectModel.currentItems = [];
|
||||
if (report && Array.isArray(report.nextItems) && !report.nextItems.length) projectModel.nextItems = [];
|
||||
|
||||
if (props.mode === 'add') patchPeriod(projectModel);
|
||||
}
|
||||
|
||||
function firstMeaningfulValue<T>(...values: Array<T | null | undefined | ''>) {
|
||||
return values.find(value => value !== null && value !== undefined && value !== '') as T | undefined;
|
||||
}
|
||||
|
||||
function firstPositiveWorkHours(...values: Array<string | number | null | undefined>) {
|
||||
const matchedValue = values.find(value => {
|
||||
const numberValue = Number(value ?? 0);
|
||||
return Number.isFinite(numberValue) && numberValue > 0;
|
||||
});
|
||||
|
||||
return matchedValue ?? firstMeaningfulValue(...values);
|
||||
}
|
||||
|
||||
function mergePersonalDetailBaseInfo<
|
||||
T extends Api.WorkReport.Weekly.WeeklyReport | Api.WorkReport.Monthly.MonthlyReport
|
||||
>(detail: T) {
|
||||
const rowData = props.rowData as Partial<T> | null;
|
||||
if (!rowData) return detail;
|
||||
|
||||
return {
|
||||
...rowData,
|
||||
...detail,
|
||||
reporterDeptName: firstMeaningfulValue(detail.reporterDeptName, rowData.reporterDeptName) ?? null,
|
||||
reporterPostName: firstMeaningfulValue(detail.reporterPostName, rowData.reporterPostName) ?? null,
|
||||
submitTime: firstMeaningfulValue(detail.submitTime, rowData.submitTime) ?? null,
|
||||
totalWorkHours: firstPositiveWorkHours(detail.totalWorkHours, rowData.totalWorkHours)
|
||||
} as T;
|
||||
}
|
||||
|
||||
function mergeProjectDetailBaseInfo(detail: Api.WorkReport.Project.ProjectReport) {
|
||||
const rowData = props.rowData as Partial<Api.WorkReport.Project.ProjectReport> | null;
|
||||
if (!rowData) return detail;
|
||||
|
||||
return {
|
||||
...rowData,
|
||||
...detail,
|
||||
submitTime: firstMeaningfulValue(detail.submitTime, rowData.submitTime) ?? null,
|
||||
totalWorkHours: firstPositiveWorkHours(detail.totalWorkHours, rowData.totalWorkHours)
|
||||
};
|
||||
}
|
||||
|
||||
async function loadDetail(id: string) {
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportDetail(id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportDetail(id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportDetail(id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
const detail =
|
||||
props.reportType === 'weekly'
|
||||
? mergePersonalDetailBaseInfo(result.data as Api.WorkReport.Weekly.WeeklyReport)
|
||||
: props.reportType === 'monthly'
|
||||
? mergePersonalDetailBaseInfo(result.data as Api.WorkReport.Monthly.MonthlyReport)
|
||||
: mergeProjectDetailBaseInfo(result.data as Api.WorkReport.Project.ProjectReport);
|
||||
|
||||
currentReportId.value = detail.id;
|
||||
baseInfo.value = detail;
|
||||
|
||||
if (props.reportType === 'weekly') patchWeekly(detail as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(detail as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(detail as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
async function pullDefaultDraft(confirmOverwrite = false) {
|
||||
const period = {
|
||||
periodKey: currentModel.value.periodKey,
|
||||
periodLabel: currentModel.value.periodLabel,
|
||||
periodStartDate: currentModel.value.periodStartDate,
|
||||
periodEndDate: currentModel.value.periodEndDate
|
||||
};
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchPreviewWeeklyReportDefaultDraft(period);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchPreviewMonthlyReportDefaultDraft(period);
|
||||
} else {
|
||||
result = await fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
|
||||
...period,
|
||||
flag: projectModel.flag
|
||||
});
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
baseInfo.value = {
|
||||
...(baseInfo.value || {}),
|
||||
...result.data
|
||||
} as WorkReportRow;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
const data = result.data as Api.WorkReport.Weekly.WeeklyReport;
|
||||
weeklyModel.reviewItems = data.reviewItems?.length ? normalizeReviewItems(data.reviewItems) : [];
|
||||
weeklyModel.planItems = data.planItems?.length ? normalizePlanItems(data.planItems) : [];
|
||||
if (confirmOverwrite) {
|
||||
weeklyModel.travelSegments = data.travelSegments || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly') {
|
||||
const data = result.data as Api.WorkReport.Monthly.MonthlyReport;
|
||||
monthlyModel.reviewItems = data.reviewItems?.length ? normalizeReviewItems(data.reviewItems) : [];
|
||||
monthlyModel.planItems = data.planItems?.length ? normalizePlanItems(data.planItems) : [];
|
||||
}
|
||||
|
||||
if (props.reportType === 'project') {
|
||||
const data = result.data as Api.WorkReport.Project.ProjectReport;
|
||||
projectModel.currentItems = data.currentItems?.length ? normalizeProjectItems(data.currentItems) : [];
|
||||
projectModel.nextItems = data.nextItems?.length ? normalizeProjectItems(data.nextItems) : [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInitData() {
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchInitWeeklyReport();
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchInitMonthlyReport();
|
||||
} else {
|
||||
result = await fetchInitProjectReport(props.initialProjectId);
|
||||
}
|
||||
|
||||
if (!result.error && result.data) {
|
||||
baseInfo.value = result.data;
|
||||
if (props.reportType === 'weekly') patchWeekly(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(result.data as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
await pullDefaultDraft();
|
||||
}
|
||||
|
||||
watch(visible, async isVisible => {
|
||||
if (!isVisible) return;
|
||||
|
||||
currentStage.value = props.reportType === 'monthly' && props.scene === 'approval' ? 'approval' : 'form';
|
||||
currentReportId.value = props.rowData?.id || '';
|
||||
baseInfo.value = null;
|
||||
resetModels();
|
||||
|
||||
if (props.mode === 'add') {
|
||||
if (props.reportType === 'project') {
|
||||
projectModel.projectId = props.initialProjectId;
|
||||
projectModel.flag = props.initialFlag;
|
||||
}
|
||||
patchPeriod(currentModel.value);
|
||||
await loadInitData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.rowData?.id) {
|
||||
await loadDetail(props.rowData.id);
|
||||
}
|
||||
});
|
||||
|
||||
function hasTextValue(value: unknown) {
|
||||
const text = String(value ?? '')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.trim();
|
||||
|
||||
return text.length > 0;
|
||||
}
|
||||
|
||||
function hasMeaningfulStructuredValue(value: unknown): boolean {
|
||||
if (value === null || value === undefined) return false;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim();
|
||||
if (!text || text === '{}' || text === '[]') return false;
|
||||
|
||||
try {
|
||||
return hasMeaningfulStructuredValue(JSON.parse(text));
|
||||
} catch {
|
||||
return hasTextValue(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(item => hasMeaningfulStructuredValue(item));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return Object.values(value as Record<string, unknown>).some(item => hasMeaningfulStructuredValue(item));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasReviewContent(item: Api.WorkReport.Common.PersonalReportReviewItem) {
|
||||
return hasTextValue(item.contentText) || hasMeaningfulStructuredValue(item.contentJson);
|
||||
}
|
||||
|
||||
function hasPlanTarget(item: Api.WorkReport.Common.PersonalReportPlanItem) {
|
||||
return hasTextValue(item.targetText) || hasMeaningfulStructuredValue(item.targetJson);
|
||||
}
|
||||
|
||||
function isCompleteWeeklyTravelSegment(segment: Api.WorkReport.Weekly.WeeklyReportTravelSegment) {
|
||||
const travelDays = Number(segment.travelDays);
|
||||
|
||||
return Boolean(
|
||||
segment.startDate &&
|
||||
segment.endDate &&
|
||||
segment.location?.trim() &&
|
||||
Number.isFinite(travelDays) &&
|
||||
travelDays >= 0.5 &&
|
||||
Number.isInteger(travelDays * 2)
|
||||
);
|
||||
}
|
||||
|
||||
function hasCompleteWeeklyTravelSegment(items: Api.WorkReport.Weekly.WeeklyReportTravelSegment[]) {
|
||||
return items.some(isCompleteWeeklyTravelSegment);
|
||||
}
|
||||
|
||||
function hasCompletePersonalReviewItem(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
return items.some(item => hasTextValue(item.itemTitle) && hasReviewContent(item));
|
||||
}
|
||||
|
||||
function hasCompletePersonalPlanItem(items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||
return items.some(item => hasTextValue(item.itemTitle) && hasPlanTarget(item));
|
||||
}
|
||||
|
||||
function getPersonalReviewValidationMessage(label: string, items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
if (!items.length) return `至少要有一项${label}`;
|
||||
if (!hasCompletePersonalReviewItem(items)) {
|
||||
return `请完善${label},项目名或我的事项、具体工作内容及成果描述不能为空`;
|
||||
}
|
||||
|
||||
const hasIncompleteItem = items.some(item => !hasTextValue(item.itemTitle) || !hasReviewContent(item));
|
||||
return hasIncompleteItem ? `请完善${label},项目名或我的事项、具体工作内容及成果描述不能为空` : '';
|
||||
}
|
||||
|
||||
function getPersonalPlanValidationMessage(label: string, items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||
if (!items.length) return `至少要有一项${label}`;
|
||||
if (!hasCompletePersonalPlanItem(items)) {
|
||||
return `请完善${label},项目名或我的事项、具体目标不能为空`;
|
||||
}
|
||||
|
||||
const hasIncompleteItem = items.some(item => !hasTextValue(item.itemTitle) || !hasPlanTarget(item));
|
||||
return hasIncompleteItem ? `请完善${label},项目名或我的事项、具体目标不能为空` : '';
|
||||
}
|
||||
|
||||
function hasProjectItem(items: Api.WorkReport.Project.ProjectReportItem[]) {
|
||||
return items.some(item => hasTextValue(item.itemTitle));
|
||||
}
|
||||
|
||||
function validateRequiredReportItems() {
|
||||
const messages: string[] = [];
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
const hasTravelReview = weeklyModel.isBusinessTrip && hasCompleteWeeklyTravelSegment(weeklyModel.travelSegments);
|
||||
const reviewMessage = hasTravelReview
|
||||
? weeklyModel.reviewItems.length
|
||||
? getPersonalReviewValidationMessage('当期重点工作回顾', weeklyModel.reviewItems)
|
||||
: ''
|
||||
: getPersonalReviewValidationMessage('当期重点工作回顾', weeklyModel.reviewItems);
|
||||
const planMessage = getPersonalPlanValidationMessage('下周期重点工作计划', weeklyModel.planItems);
|
||||
if (weeklyModel.isBusinessTrip && !hasTravelReview) messages.push('请至少新增一条完整的出差分段');
|
||||
if (reviewMessage) messages.push(reviewMessage);
|
||||
if (planMessage) messages.push(planMessage);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
const reviewMessage = getPersonalReviewValidationMessage('当期重点工作回顾', monthlyModel.reviewItems);
|
||||
const planMessage = getPersonalPlanValidationMessage('下周期重点工作计划', monthlyModel.planItems);
|
||||
if (reviewMessage) messages.push(reviewMessage);
|
||||
if (planMessage) messages.push(planMessage);
|
||||
} else {
|
||||
const missingLabels: string[] = [];
|
||||
|
||||
if (!hasProjectItem(projectModel.currentItems)) {
|
||||
missingLabels.push('本期工作内容');
|
||||
}
|
||||
if (!hasProjectItem(projectModel.nextItems)) {
|
||||
missingLabels.push('下期计划工作内容');
|
||||
}
|
||||
|
||||
if (missingLabels.length) messages.push(`至少要有一项${missingLabels.join('、')}`);
|
||||
}
|
||||
|
||||
if (!messages.length) return true;
|
||||
|
||||
window.$message?.warning(messages.join(';'));
|
||||
return false;
|
||||
}
|
||||
|
||||
async function persistReport(submitAfterSave: boolean) {
|
||||
if (!validateRequiredReportItems()) return false;
|
||||
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
if (currentReportId.value) {
|
||||
result = await fetchUpdateWeeklyReport(currentReportId.value, weeklyModel);
|
||||
} else {
|
||||
result = await fetchCreateWeeklyReport(weeklyModel);
|
||||
if (!result.error && result.data) currentReportId.value = result.data;
|
||||
}
|
||||
} else if (props.reportType === 'monthly') {
|
||||
if (currentReportId.value) {
|
||||
result = await fetchUpdateMonthlyReport(currentReportId.value, monthlyModel);
|
||||
} else {
|
||||
result = await fetchCreateMonthlyReport(monthlyModel);
|
||||
if (!result.error && result.data) currentReportId.value = result.data;
|
||||
}
|
||||
} else if (currentReportId.value) {
|
||||
result = await fetchUpdateProjectReport(currentReportId.value, projectModel);
|
||||
} else {
|
||||
result = await fetchCreateProjectReport(projectModel);
|
||||
if (!result.error && result.data) currentReportId.value = result.data;
|
||||
}
|
||||
|
||||
if (result.error) return false;
|
||||
|
||||
if (submitAfterSave) {
|
||||
if (props.reportType === 'weekly') {
|
||||
const submitResult = await fetchSubmitWeeklyReport(currentReportId.value);
|
||||
if (submitResult.error) return false;
|
||||
} else if (props.reportType === 'monthly') {
|
||||
const submitResult = await fetchSubmitMonthlyReport(currentReportId.value);
|
||||
if (submitResult.error) return false;
|
||||
} else {
|
||||
const submitResult = await fetchSubmitProjectReport(currentReportId.value);
|
||||
if (submitResult.error) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSaveDraft() {
|
||||
loading.value = true;
|
||||
const success = await persistReport(false);
|
||||
loading.value = false;
|
||||
|
||||
if (!success) return;
|
||||
|
||||
window.$message?.success('工作报告已保存');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
async function handleSubmitReport() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交当前工作报告吗?', '提交确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认提交',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const success = await persistReport(true);
|
||||
loading.value = false;
|
||||
|
||||
if (!success) return;
|
||||
|
||||
window.$message?.success('工作报告已提交');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleViewApproval() {
|
||||
currentStage.value = 'approval';
|
||||
}
|
||||
|
||||
function handleRequestApprove() {
|
||||
if (props.reportType === 'monthly') {
|
||||
handleActionSubmit({ ...monthlyApprovalDraft }, 'approve');
|
||||
return;
|
||||
}
|
||||
currentActionType.value = 'approve';
|
||||
actionVisible.value = true;
|
||||
}
|
||||
|
||||
function handleRequestReject() {
|
||||
if (props.reportType === 'monthly') {
|
||||
handleActionSubmit({ reason: monthlyApprovalDraft.reason }, 'reject');
|
||||
return;
|
||||
}
|
||||
currentActionType.value = 'reject';
|
||||
actionVisible.value = true;
|
||||
}
|
||||
|
||||
function handlePullDefaultDraft() {
|
||||
pullDefaultDraft(true);
|
||||
}
|
||||
|
||||
function handleMonthlyApprovalChange(payload: Api.WorkReport.Monthly.MonthlyReportApproveParams) {
|
||||
Object.assign(monthlyApprovalDraft, payload);
|
||||
}
|
||||
|
||||
async function handleActionSubmit(
|
||||
payload: Api.WorkReport.Common.StatusActionParams | Api.WorkReport.Monthly.MonthlyReportApproveParams,
|
||||
actionTypeOverride?: 'approve' | 'reject'
|
||||
) {
|
||||
if (!currentReportId.value) return;
|
||||
|
||||
const actionType = actionTypeOverride || currentActionType.value;
|
||||
actionSubmitting.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result =
|
||||
actionType === 'approve'
|
||||
? await fetchApproveWeeklyReport(currentReportId.value, payload)
|
||||
: await fetchRejectWeeklyReport(currentReportId.value, payload);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result =
|
||||
actionType === 'approve'
|
||||
? await fetchApproveMonthlyReport(
|
||||
currentReportId.value,
|
||||
payload as Api.WorkReport.Monthly.MonthlyReportApproveParams
|
||||
)
|
||||
: await fetchRejectMonthlyReport(currentReportId.value, payload);
|
||||
} else {
|
||||
result =
|
||||
actionType === 'approve'
|
||||
? await fetchApproveProjectReport(currentReportId.value, payload)
|
||||
: await fetchRejectProjectReport(currentReportId.value, payload);
|
||||
}
|
||||
actionSubmitting.value = false;
|
||||
|
||||
if (result.error) return;
|
||||
|
||||
actionVisible.value = false;
|
||||
window.$message?.success(actionType === 'approve' ? '审批已通过' : '工作报告已退回');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkReportPageDialog
|
||||
v-model:visible="visible"
|
||||
:title="dialogTitle"
|
||||
:loading="loading"
|
||||
:approval-mode="currentScene === 'approval'"
|
||||
@close="currentStage = 'form'"
|
||||
>
|
||||
<WeeklyReportPage
|
||||
v-if="reportType === 'weekly'"
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Weekly.WeeklyReport | null"
|
||||
:model="weeklyModel"
|
||||
@back="handleBack"
|
||||
@save="handleSaveDraft"
|
||||
@submit="handleSubmitReport"
|
||||
@request-approve="handleRequestApprove"
|
||||
@request-reject="handleRequestReject"
|
||||
@pull-default-draft="handlePullDefaultDraft"
|
||||
/>
|
||||
|
||||
<MonthlyReportApprovalPage
|
||||
v-else-if="reportType === 'monthly' && currentStage === 'approval'"
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
scene="approval"
|
||||
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||
:model="monthlyModel"
|
||||
:approval-model="monthlyApprovalDraft"
|
||||
@back="handleBack"
|
||||
@change-approval="handleMonthlyApprovalChange"
|
||||
@request-approve="handleRequestApprove"
|
||||
@request-reject="handleRequestReject"
|
||||
/>
|
||||
|
||||
<MonthlyReportPage
|
||||
v-else-if="reportType === 'monthly'"
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||
:model="monthlyModel"
|
||||
@back="handleBack"
|
||||
@save="handleSaveDraft"
|
||||
@submit="handleSubmitReport"
|
||||
@view-approval="handleViewApproval"
|
||||
@pull-default-draft="handlePullDefaultDraft"
|
||||
/>
|
||||
|
||||
<ProjectReportPage
|
||||
v-else
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Project.ProjectReport | null"
|
||||
:model="projectModel"
|
||||
@back="handleBack"
|
||||
@save="handleSaveDraft"
|
||||
@submit="handleSubmitReport"
|
||||
@request-approve="handleRequestApprove"
|
||||
@request-reject="handleRequestReject"
|
||||
@pull-default-draft="handlePullDefaultDraft"
|
||||
/>
|
||||
</WorkReportPageDialog>
|
||||
|
||||
<WorkReportActionDialog
|
||||
v-model:visible="actionVisible"
|
||||
:report-type="reportType"
|
||||
:action-type="currentActionType"
|
||||
:initial-monthly-approve-data="reportType === 'monthly' ? monthlyApprovalDraft : null"
|
||||
:loading="actionSubmitting"
|
||||
@submit="handleActionSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable no-void */
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { fetchGetWorkReportStatusDict } from '@/service/api';
|
||||
import type { SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||
import { BOOLEAN_TRUE_FALSE_OPTIONS, type WorkReportSearchParams, type WorkReportType } from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportSearch' });
|
||||
|
||||
interface Props {
|
||||
reportType: WorkReportType;
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
projectOptions: () => []
|
||||
});
|
||||
|
||||
const model = defineModel<WorkReportSearchParams>('model', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
|
||||
const statusDict = ref<Api.WorkReport.Common.WorkReportStatusDict[]>([]);
|
||||
|
||||
const statusOptions = computed(() =>
|
||||
[...statusDict.value]
|
||||
.sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
|
||||
.map(item => ({
|
||||
label: item.statusName || item.statusCode,
|
||||
value: item.statusCode
|
||||
}))
|
||||
);
|
||||
|
||||
const fields = computed<SearchField[]>(() => {
|
||||
const baseFields: SearchField[] = [
|
||||
{ key: 'statusCode', label: '状态', type: 'select', options: statusOptions.value, placeholder: '请选择状态' },
|
||||
{ key: 'periodStartDate', label: '周期', type: 'dateRange', placeholder: '请选择周期' }
|
||||
];
|
||||
|
||||
const monthPeriodField: SearchField = {
|
||||
key: 'periodStartDate',
|
||||
label: props.reportType === 'project' ? '月份' : '月份',
|
||||
type: 'dateRange',
|
||||
dateRangeType: 'monthrange',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
placeholder: '请选择月份'
|
||||
};
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
return [
|
||||
...baseFields,
|
||||
{
|
||||
key: 'isBusinessTrip',
|
||||
label: '是否出差',
|
||||
type: 'select',
|
||||
options: BOOLEAN_TRUE_FALSE_OPTIONS,
|
||||
placeholder: '请选择'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (props.reportType === 'project') {
|
||||
return [
|
||||
baseFields[0],
|
||||
monthPeriodField,
|
||||
{
|
||||
key: 'projectId',
|
||||
label: '项目',
|
||||
type: 'select',
|
||||
options: props.projectOptions.map(item => ({
|
||||
label: item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName,
|
||||
value: item.id
|
||||
})),
|
||||
placeholder: '请选择项目'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly') {
|
||||
return [baseFields[0], monthPeriodField];
|
||||
}
|
||||
|
||||
return baseFields;
|
||||
});
|
||||
|
||||
async function loadStatusDict() {
|
||||
const { error, data } = await fetchGetWorkReportStatusDict();
|
||||
statusDict.value = error || !data ? [] : data;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStatusDict();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchFields v-model="model" :columns="4" :fields="fields" @reset="emit('reset')" @search="emit('search')" />
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportTabs' });
|
||||
|
||||
interface TabOption {
|
||||
label: string;
|
||||
name: WorkReportType;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
tabs: TabOption[];
|
||||
}>();
|
||||
|
||||
const activeTab = defineModel<WorkReportType>('activeTab', { required: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard class="work-report-sidebar" body-class="work-report-sidebar__body">
|
||||
<div class="work-report-sidebar__header">报告类型</div>
|
||||
<div class="work-report-sidebar__list">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.name"
|
||||
class="work-report-sidebar__item"
|
||||
:class="{ 'work-report-sidebar__item--active': activeTab === tab.name }"
|
||||
@click="activeTab = tab.name"
|
||||
>
|
||||
<span class="work-report-sidebar__label">{{ tab.label || WORK_REPORT_TYPE_LABEL[tab.name] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-sidebar {
|
||||
height: 100%;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.work-report-sidebar :deep(.work-report-sidebar__body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.work-report-sidebar__header {
|
||||
padding: 20px 20px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.work-report-sidebar__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.work-report-sidebar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.work-report-sidebar__item:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.work-report-sidebar__item--active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.work-report-sidebar__item--active .work-report-sidebar__label {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.work-report-sidebar__label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user