Files
cn-rdms-web/src/views/personal-center/work-report/shared/components/create-dialog.vue
dk d53a8dfae5 fix(加班申请): 去掉撤销相关的状态和动作。
feat(工作报告): 开发工作报告功能
2026-06-11 10:56:24 +08:00

535 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>