Files
fad_oa/ruoyi-ui/src/views/hrm/requests/travelDetail.vue

467 lines
9.3 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.

<template>
<BizDetailContainer :bizId="currentBizId" bizType="travel" :preview="preview">
<template slot-scope="{ detail }">
<div>
<!-- ===== 新增提前结束按钮区域 ===== -->
<div class="action-buttons" v-if="showEarlyEndButton(detail)">
<el-button
type="warning"
size="small"
icon="el-icon-finished"
:loading="earlyEndLoading"
@click="handleEarlyEnd"
>
提前结束
</el-button>
<span class="hint-text">提前结束将把当前时间记录为实际结束时间</span>
</div>
<div v-if="detail.actualEndTime" class="early-end-info">
<el-alert
type="info"
:closable="false"
show-icon>
<template slot="default">
该出差已于 {{ formatDate(detail.actualEndTime) }} 提前结束
</template>
</el-alert>
</div>
<div v-else-if="isTravelCompleted(detail)" class="early-end-info">
<el-alert
type="success"
:closable="false"
show-icon>
<template slot="default">
该出差已完成
</template>
</el-alert>
</div>
<!-- 出差时间与行程 -->
<div class="block-title">出差时间与行程</div>
<el-card class="inner-card" shadow="never">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="开始时间">
<span class="date-time">{{ formatDate(detail.startTime) }}</span>
</el-descriptions-item>
<el-descriptions-item label="结束时间">
<span class="date-time">{{ formatDate(detail.endTime) }}</span>
</el-descriptions-item>
<el-descriptions-item label="出差类型">{{ detail.travelType || '-' }}</el-descriptions-item>
<el-descriptions-item label="目的地">
<span class="destination-text">{{ detail.destination || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="出差事由" :span="2">
<div class="reason-content">{{ detail.reason || '未填写' }}</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
<div class="block-title">交通/住宿/行程附件</div>
<el-card class="inner-card" shadow="never">
<file-preview v-model="detail.accessoryApplyIds" />
</el-card>
<!-- 流程状态 -->
<div class="block-title">回执附件</div>
<el-card class="inner-card" shadow="never">
<file-preview v-model="detail.accessoryReceiptIds" />
</el-card>
</div>
</template>
</BizDetailContainer>
</template>
<script>
import FilePreview from "@/components/FilePreview/index.vue";
import BizDetailContainer from '@/views/hrm/components/BizDetailContainer/index.vue';
import { earlyEndTravel } from '@/api/hrm/travel'
export default {
name: 'TravelDetail',
props: {
bizId: { type: [String, Number], default: '' },
embedded: { type: Boolean, default: false },
preview: { type: Boolean, default: false }
},
components: {
BizDetailContainer,
FilePreview
},
data() {
return {
earlyEndLoading: false
}
},
computed: {
currentBizId () {
return this.bizId || this.$route?.params?.bizId || this.$route?.query?.bizId || this.$route?.params?.id
},
},
methods: {
formatDate (val) {
if (!val) return '-'
const d = new Date(val)
const p = n => (n < 10 ? `0${n}` : n)
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
},
isTravelCompleted (detail) {
if (!detail) return false
const endTime = detail.endTime ? new Date(detail.endTime).getTime() : 0
const now = Date.now()
return Boolean(detail.actualEndTime) || (endTime && endTime <= now)
},
showEarlyEndButton(detail) {
if (!detail) return false
if (detail.actualEndTime) return false
const status = detail.status
const endTime = detail.endTime ? new Date(detail.endTime).getTime() : 0
const now = Date.now()
return status === 'approved' && endTime > now
},
handleEarlyEnd() {
this.$confirm('确认提前结束本次出差吗?结束后的实际时间将记录为当前时间。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
this.earlyEndLoading = true
try {
const bizId = this.currentBizId
await earlyEndTravel(bizId)
this.$message.success('提前结束成功')
this.$emit('refresh')
this.$forceUpdate()
} catch (error) {
this.$message.error(error.message || '提前结束失败')
} finally {
this.earlyEndLoading = false
}
}).catch(() => {})
},
}
}
</script>
<style lang="scss" scoped>
.request-page {
padding: 16px 20px 32px;
background: #f8f9fb;
}
.form-card {
max-width: 980px;
margin: 0 auto;
border: 1px solid #d7d9df;
border-radius: 12px;
background: #ffffff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 700;
color: #2b2f36;
}
.actions {
display: flex;
gap: 8px;
}
.block-title {
margin: 12px 0 8px;
padding-left: 10px;
font-weight: 700;
color: #2f3440;
border-left: 3px solid #9aa3b2;
}
.hint-text {
margin: 6px 0 10px;
font-size: 12px;
color: #8a8f99;
}
.form-summary {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 12px;
margin-bottom: 12px;
border: 1px solid #e6e8ed;
border-radius: 10px;
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
}
.summary-title {
font-size: 16px;
font-weight: 800;
color: #2b2f36;
}
.summary-sub {
margin-top: 4px;
font-size: 12px;
color: #8a8f99;
}
.summary-right {
display: flex;
gap: 16px;
}
.summary-item .k {
font-size: 12px;
color: #8a8f99;
}
.summary-item .v {
margin-top: 2px;
font-weight: 700;
color: #2b2f36;
}
.inner-card {
border: 1px solid #e6e8ed;
}
.approver-info {
display: flex;
gap: 24px;
padding: 12px 0;
}
.approver-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.approver-label {
font-size: 12px;
color: #8a8f99;
}
.approver-value {
font-weight: 600;
color: #2b2f36;
}
.empty {
color: #a0a3ad;
font-size: 13px;
padding: 10px 4px;
}
.timeline-row .t-main {
font-weight: 600;
color: #2b2f36;
}
.timeline-row .t-remark {
margin-top: 4px;
color: #606266;
font-size: 13px;
}
.btn-row {
display: flex;
gap: 10px;
}
.attachment-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.attachment-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border: 1px solid #e6e8ed;
border-radius: 8px;
background: #fafbfc;
}
.file-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.file-icon {
font-size: 24px;
color: #9aa3b2;
}
.file-details {
flex: 1;
}
.file-name {
font-weight: 600;
color: #2b2f36;
margin-bottom: 4px;
}
.file-meta {
font-size: 12px;
color: #8a8f99;
display: flex;
gap: 12px;
}
.date-time {
font-weight: 600;
color: #2b2f36;
}
.destination-text {
font-weight: 600;
color: #2b2f36;
font-size: 14px;
}
.cost-text {
font-weight: 700;
color: #e6a23c;
font-size: 16px;
}
.reason-content {
padding: 8px 12px;
background: #f8f9fa;
border-radius: 6px;
color: #2b2f36;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.info-section {
padding: 12px;
border: 1px solid #e6e8ed;
border-radius: 8px;
background: #fafbfc;
}
.info-label {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.info-label i {
color: #9aa3b2;
}
.info-content {
min-height: 40px;
}
.info-text {
color: #2b2f36;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.info-placeholder {
color: #c0c4cc;
font-size: 12px;
font-style: italic;
}
.remark-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e6e8ed;
}
.remark-label {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
}
.remark-content {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
color: #2b2f36;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.file-time {
margin-left: 8px;
}
.file-actions {
display: flex;
gap: 8px;
}
.flow-status {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border: 1px solid #e6e8ed;
border-radius: 6px;
background: #fafbfc;
}
.status-label {
font-size: 13px;
font-weight: 600;
color: #606266;
min-width: 80px;
}
.status-value {
flex: 1;
color: #2b2f36;
font-weight: 500;
}
@media (max-width: 1200px) {
.summary-right {
display: none;
}
}
.action-buttons {
margin-bottom: 16px;
padding: 12px;
background: #fdf6ec;
border-radius: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.early-end-info {
margin-bottom: 16px;
}
</style>