Files
cn-rdms-web/src/views/personal-center/overtime-application/modules/overtime-application-batch-detail-dialog.vue
dk 7f31bed6f6 feat(personal-center): 将加班申请功能重构为业务学习功能。
fix(week-report): 修复周报中工作日志的显示问题。
feat(work-report): 点击编辑时,自动刷新出最新的内容。
2026-07-19 14:04:51 +08:00

395 lines
11 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, 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';
import IconMdiChevronLeft from '~icons/mdi/chevron-left';
import IconMdiChevronRight from '~icons/mdi/chevron-right';
defineOptions({ name: 'OvertimeApplicationBatchDetailDialog' });
type ActionType = 'approve' | 'reject';
interface Props {
/** 选中的业务学习 id 列表(原始 id */
selectedIds: string[];
/** 全部业务学习行数据,用于通过 id 查找 */
rows: Api.OvertimeApplication.OvertimeApplication[];
actionLoading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
actionLoading: false
});
const emit = defineEmits<{
submit: [payload: { actionType: ActionType; reason: string | null }];
}>();
const visible = defineModel<boolean>('visible', {
default: false
});
const currentIndex = ref(0);
const detailData = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
const detailLoading = ref(false);
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const approvalModel = reactive({
conclusion: 'approve' as ActionType,
opinion: ''
});
const currentId = computed(() => props.selectedIds[currentIndex.value] ?? null);
const total = computed(() => props.selectedIds.length);
const canGoPrev = computed(() => currentIndex.value > 0);
const canGoNext = computed(() => currentIndex.value < props.selectedIds.length - 1);
async function loadDetail() {
const id = currentId.value;
if (!id) {
detailData.value = null;
return;
}
const row = props.rows.find(r => r.id === id);
if (!row) {
detailData.value = null;
return;
}
detailLoading.value = true;
const { error, data } = await fetchGetOvertimeApplicationDetail(id);
detailLoading.value = false;
detailData.value = error || !data ? row : data;
}
function goPrev() {
if (!canGoPrev.value) return;
currentIndex.value -= 1;
loadDetail();
}
function goNext() {
if (!canGoNext.value) return;
currentIndex.value += 1;
loadDetail();
}
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'
}
]
: []
}));
function resetApprovalForm() {
approvalModel.conclusion = 'approve';
approvalModel.opinion = '';
nextTick(() => {
formRef.value?.clearValidate();
});
}
watch(opinionRequired, async () => {
if (!visible.value) 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) {
currentIndex.value = 0;
loadDetail();
resetApprovalForm();
} else {
detailData.value = null;
}
}
);
</script>
<template>
<BusinessFormDialog v-model="visible" title="批量审批" preset="md" :loading="detailLoading" :show-footer="true">
<!-- 左右导航 -->
<div class="batch-detail__nav">
<button type="button" class="batch-detail__nav-btn" :disabled="!canGoPrev" @click.stop="goPrev">
<IconMdiChevronLeft class="text-20px" />
</button>
<span class="batch-detail__nav-counter">{{ currentIndex + 1 }} / {{ total }}</span>
<button type="button" class="batch-detail__nav-btn" :disabled="!canGoNext" @click.stop="goNext">
<IconMdiChevronRight class="text-20px" />
</button>
</div>
<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 class="batch-detail__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="batch-detail__footer">
<span class="batch-detail__footer-hint">将对全部 {{ total }} 项统一执行操作</span>
<div class="batch-detail__footer-actions">
<ElButton @click="visible = false">取消</ElButton>
<ElButton
type="primary"
:loading="props.actionLoading"
:disabled="props.actionLoading || !detailData"
@click="handleSubmit"
>
确认提交
</ElButton>
</div>
</div>
</template>
</BusinessFormDialog>
</template>
<style scoped>
.batch-detail__nav {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 16px;
}
.batch-detail__nav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid rgb(226 232 240 / 90%);
border-radius: 8px;
background-color: rgb(255 255 255 / 98%);
color: rgb(71 85 105 / 94%);
cursor: pointer;
transition: all 160ms ease;
}
.batch-detail__nav-btn:hover:not(:disabled) {
border-color: rgb(14 116 144 / 60%);
color: rgb(14 116 144 / 96%);
}
.batch-detail__nav-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.batch-detail__nav-counter {
font-size: 14px;
font-weight: 600;
color: rgb(15 23 42 / 96%);
min-width: 60px;
text-align: center;
}
.batch-detail__footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.batch-detail__footer-hint {
font-size: 13px;
color: rgb(100 116 139 / 92%);
}
.batch-detail__footer-actions {
display: flex;
gap: 12px;
}
.batch-detail__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;
}
</style>