395 lines
11 KiB
Vue
395 lines
11 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';
|
||
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>
|