Files
cn-rdms-web/src/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue
dk 9a5845708d feat(我的绩效): 开发我的绩效功能。
fix(加班申请、工作报告): 重构加班申请在审批时的样式,工作报告在新增时的对话框、报告详情页的样式。
2026-06-21 18:22:44 +08:00

315 lines
9.0 KiB
Vue

<script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { fetchGetOvertimeApplicationDetail } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { formatOvertimeDate, formatOvertimeDateTime } from './overtime-application-shared';
defineOptions({ name: 'OvertimeApplicationDetailDialog' });
type ActionType = 'approve' | 'reject';
interface Props {
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
showApprovalActions?: boolean;
actionLoading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showApprovalActions: false,
actionLoading: false
});
const emit = defineEmits<{
submit: [payload: { actionType: ActionType; reason: string | null }];
}>();
const visible = defineModel<boolean>('visible', {
default: false
});
const loading = ref(false);
const detailData = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const approvalModel = reactive({
conclusion: 'approve' as ActionType,
opinion: ''
});
const opinionLabel = computed(() => (approvalModel.conclusion === 'reject' ? '退回原因' : '审批意见'));
const opinionRequired = computed(() => approvalModel.conclusion === 'reject');
const opinionPlaceholder = computed(() => (opinionRequired.value ? `请输入${opinionLabel.value}` : '可填写审批意见'));
const rules = computed(() => ({
opinion: opinionRequired.value
? [
createRequiredRule(`请输入${opinionLabel.value}`),
{
validator: (_rule, value: string, callback) => {
if (!value?.trim()) {
callback(new Error(`请输入${opinionLabel.value}`));
return;
}
callback();
},
trigger: 'blur'
}
]
: []
}));
async function loadDetail() {
if (!props.rowData?.id) {
detailData.value = null;
return;
}
loading.value = true;
const { error, data } = await fetchGetOvertimeApplicationDetail(props.rowData.id);
loading.value = false;
detailData.value = error || !data ? props.rowData : data;
}
function resetApprovalForm() {
approvalModel.conclusion = 'approve';
approvalModel.opinion = '';
nextTick(() => {
formRef.value?.clearValidate();
});
}
watch(opinionRequired, async () => {
if (!visible.value || !props.showApprovalActions) return;
await nextTick();
formRef.value?.clearValidate('opinion');
});
async function handleSubmit() {
try {
await validate();
} catch {
return;
}
emit('submit', {
actionType: approvalModel.conclusion,
reason: approvalModel.opinion.trim() || null
});
}
watch(
() => visible.value,
value => {
if (value) {
loadDetail();
resetApprovalForm();
}
}
);
</script>
<template>
<BusinessFormDialog v-model="visible" title="加班申请详情" preset="md" :loading="loading">
<ElDescriptions v-if="detailData" class="overtime-application-detail-dialog__descriptions" :column="2" border>
<ElDescriptionsItem label="申请人" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.applicantName }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班日期" label-class-name="overtime-application-detail-dialog__label--compact">
{{ formatOvertimeDate(detailData.overtimeDate) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班时长" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeDuration }}
</ElDescriptionsItem>
<ElDescriptionsItem label="提交时间" label-class-name="overtime-application-detail-dialog__label--compact">
{{ formatOvertimeDateTime(detailData.submitTime) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeReason }}
</ElDescriptionsItem>
<ElDescriptionsItem label="加班内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
{{ detailData.overtimeContent }}
</ElDescriptionsItem>
</ElDescriptions>
<ElEmpty v-else description="未获取到加班申请详情" />
<div v-if="props.showApprovalActions" class="overtime-application-detail-dialog__approval-form">
<div class="audit-field">
<label>审批结论</label>
<div class="audit-conclusion">
<button
type="button"
class="conclusion-btn"
:class="{
active: approvalModel.conclusion === 'approve',
pass: approvalModel.conclusion === 'approve'
}"
@click="approvalModel.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: approvalModel.conclusion === 'reject',
reject: approvalModel.conclusion === 'reject'
}"
@click="approvalModel.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>
<ElForm ref="formRef" :model="approvalModel" :rules="rules" label-position="top" :validate-on-rule-change="false">
<ElFormItem :label="opinionLabel" prop="opinion">
<ElInput
v-model="approvalModel.opinion"
type="textarea"
:rows="5"
maxlength="1000"
show-word-limit
:placeholder="opinionPlaceholder"
/>
</ElFormItem>
</ElForm>
</div>
<template #footer>
<div class="overtime-application-detail-dialog__footer">
<ElButton @click="visible = false">取消</ElButton>
<ElButton
v-if="props.showApprovalActions"
type="primary"
:loading="props.actionLoading"
:disabled="props.actionLoading || !detailData"
@click="handleSubmit"
>
确认提交
</ElButton>
</div>
</template>
</BusinessFormDialog>
</template>
<style scoped>
.overtime-application-detail-dialog__footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.overtime-application-detail-dialog__approval-form {
display: grid;
gap: 18px;
margin-top: 16px;
}
.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: var(--el-color-primary);
color: var(--el-color-primary);
}
.conclusion-btn.active.pass {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
.conclusion-btn.active.reject {
border-color: #dc2626;
background: #fef2f2;
color: #dc2626;
}
:deep(.overtime-application-detail-dialog__descriptions .el-descriptions__cell) {
line-height: 1.7;
}
:deep(.overtime-application-detail-dialog__label),
:deep(.overtime-application-detail-dialog__label--compact) {
white-space: nowrap;
vertical-align: middle;
}
:deep(.overtime-application-detail-dialog__label) {
width: 96px;
min-width: 96px;
}
:deep(.overtime-application-detail-dialog__label--compact) {
width: 86px;
min-width: 86px;
}
:deep(.overtime-application-detail-dialog__readonly-input .el-input__wrapper) {
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
cursor: default;
}
:deep(.overtime-application-detail-dialog__readonly-input .el-input__wrapper:hover),
:deep(.overtime-application-detail-dialog__readonly-input.is-focus .el-input__wrapper) {
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
}
:deep(.overtime-application-detail-dialog__readonly-input .el-input__inner) {
color: rgb(51 65 85 / 96%);
cursor: default;
-webkit-text-fill-color: rgb(51 65 85 / 96%);
}
</style>