383 lines
8.7 KiB
Vue
383 lines
8.7 KiB
Vue
<template>
|
||
<view v-if="ticket_" class="ticket-status-wrap">
|
||
<!-- 审批状态摘要 -->
|
||
<view class="ticket-status-row">
|
||
<view class="ticket-status-left">
|
||
<text class="ticket-label">审批状态</text>
|
||
<uni-tag :text="actStateEnum[ticket_?.act_state]?.text" :circle="true" size="small"
|
||
:type="actStateEnum[ticket_?.act_state]?.type" :inverted="true" style="font-weight: 500;"></uni-tag>
|
||
<text class="ticket-state-name">{{ticket_?.state_.name}}</text>
|
||
</view>
|
||
<view class="ticket-detail-btn" @click="handleDetail">
|
||
<text class="ticket-detail-text">详情</text>
|
||
<text class="ticket-detail-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 撤回操作 -->
|
||
<view v-if="ticket_?.state_.enable_retreat && isOwn" class="ticket-action-row">
|
||
<button class="ticket-retreat-btn" @click="handleRetreat">撤回申请</button>
|
||
</view>
|
||
|
||
<!-- 可处理人 -->
|
||
<view v-if="ticket_.participant_" class="ticket-participant-row">
|
||
<text class="ticket-label">处理人</text>
|
||
<view class="ticket-participant-list" v-if="ticket_.participant_type == 2 || ticket_.participant_type == 1">
|
||
<text v-for="item in ticket_.participant_" :key="item.id" class="ticket-participant-tag">{{ item.name }}</text>
|
||
</view>
|
||
<text v-else class="ticket-participant-none">无</text>
|
||
</view>
|
||
|
||
<!-- 审批详情弹窗 -->
|
||
<uni-popup ref="popup" background-color="#fff">
|
||
<view class="ticket-popup">
|
||
<view class="ticket-popup-header">
|
||
<text class="ticket-popup-title">审批流程</text>
|
||
<view class="ticket-popup-status">
|
||
<uni-tag :text="actStateEnum[ticket_f?.act_state]?.text" :circle="true" size="small"
|
||
:type="actStateEnum[ticket_f?.act_state]?.type" :inverted="true" style="font-weight: 500;"></uni-tag>
|
||
<text class="ticket-state-name">{{ticket_f?.state_.name}}</text>
|
||
<button v-if="ticket_f?.state_.enable_retreat && isOwn" class="ticket-retreat-btn-sm" @click="handleRetreat">撤回</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 可处理人 -->
|
||
<view v-if="ticket_f.participant_" class="ticket-participant-row" style="margin-bottom: 24rpx;">
|
||
<text class="ticket-label">当前处理人</text>
|
||
<view class="ticket-participant-list" v-if="ticket_f.participant_type == 2 || ticket_f.participant_type == 1">
|
||
<text v-for="item in ticket_f.participant_" :key="item.id" class="ticket-participant-tag">{{ item.name }}</text>
|
||
</view>
|
||
<text v-else class="ticket-participant-none">无</text>
|
||
</view>
|
||
|
||
<!-- 时间线 -->
|
||
<scroll-view scroll-y="true" class="ticket-timeline-scroll">
|
||
<view class="ticket-timeline">
|
||
<view v-for="(item, idx) in flowsteps" :key="item.id" class="timeline-item">
|
||
<view class="timeline-dot-wrap">
|
||
<view class="timeline-dot" :class="{'timeline-dot-success': item.transition_?.attribute_type === 1, 'timeline-dot-reject': item.transition_?.attribute_type === 2}"></view>
|
||
<view v-if="idx < flowsteps.length - 1" class="timeline-line"></view>
|
||
</view>
|
||
<view class="timeline-content">
|
||
<view class="timeline-header">
|
||
<text class="timeline-state">{{ item.state_?.name || "未知状态" }}</text>
|
||
<text v-if="item.transition_" class="timeline-transition"
|
||
:class="{'text-success': item.transition_.attribute_type === 1, 'text-danger': item.transition_.attribute_type !== 1}">
|
||
{{ item.transition_.name }}
|
||
</text>
|
||
</view>
|
||
<view class="timeline-meta">
|
||
<text class="timeline-time">{{item.create_time}}</text>
|
||
<text v-if="item.participant_" class="timeline-person">{{ item.participant_.name }}</text>
|
||
<uni-tag v-if="item.intervene_type != 0"
|
||
:type="interveneTypeEnum[item.intervene_type]?.type" :inverted="true" :circle="true"
|
||
:text="interveneTypeEnum[item.intervene_type]?.text" size="small" style="font-weight: 500;">
|
||
</uni-tag>
|
||
</view>
|
||
<view v-if="item.suggestion" class="timeline-suggestion">
|
||
<text>{{ item.suggestion.length > 40 ? item.suggestion.slice(0, 40) + '...' : item.suggestion }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
ref,
|
||
reactive,
|
||
onMounted,
|
||
defineEmits,
|
||
watch
|
||
} from 'vue'
|
||
import API from '@/utils/api';
|
||
import {
|
||
actStateEnum, interveneTypeEnum
|
||
} from "@/utils/enum.js"
|
||
|
||
const props = defineProps({
|
||
ticket_: {
|
||
type: Object,
|
||
default: null,
|
||
required: false
|
||
}
|
||
})
|
||
|
||
const ticket_f = ref({});
|
||
const isOwn = ref(false);
|
||
onMounted(() => {
|
||
watch(
|
||
() => props.ticket_,
|
||
async (newVal) => {
|
||
if (newVal && Object.keys(newVal).length > 0) {
|
||
if(props.ticket_.create_by === uni.getStorageSync("userInfo").id){
|
||
isOwn.value = true;
|
||
}
|
||
}
|
||
},
|
||
{ immediate: false, deep: true }
|
||
)
|
||
})
|
||
|
||
const popup = ref(null);
|
||
const flowsteps = ref([]);
|
||
const handleDetail = async () => {
|
||
ticket_f.value = await API.getTicketItem(props.ticket_.id)
|
||
popup.value.open("top");
|
||
API.getTicketFlowLogs(props.ticket_.id).then(res=>{
|
||
flowsteps.value = res
|
||
})
|
||
}
|
||
const handleRetreat = () => {
|
||
uni.showModal({
|
||
title: "撤回原因",
|
||
editable: true,
|
||
success(res) {
|
||
if(res.confirm){
|
||
API.ticketRetreat(props.ticket_.id, {"suggestion": res.content}).then(res=>{
|
||
uni.navigateBack()
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.ticket-status-wrap {
|
||
padding: 4rpx 0;
|
||
}
|
||
|
||
.ticket-status-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.ticket-status-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.ticket-label {
|
||
font-size: 26rpx;
|
||
color: #9CA3AF;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.ticket-state-name {
|
||
font-size: 26rpx;
|
||
color: #4B5563;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
.ticket-detail-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8rpx 16rpx;
|
||
background: #E0F5EA;
|
||
border-radius: 24rpx;
|
||
}
|
||
|
||
.ticket-detail-text {
|
||
font-size: 24rpx;
|
||
color: #2BA471;
|
||
}
|
||
|
||
.ticket-detail-arrow {
|
||
font-size: 28rpx;
|
||
color: #2BA471;
|
||
margin-left: 4rpx;
|
||
}
|
||
|
||
.ticket-action-row {
|
||
padding: 8rpx 0;
|
||
}
|
||
|
||
.ticket-retreat-btn {
|
||
display: inline-block;
|
||
font-size: 24rpx;
|
||
color: #EF4444;
|
||
background: #FEF2F2;
|
||
border: 1rpx solid #FECACA;
|
||
border-radius: 12rpx;
|
||
padding: 8rpx 24rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.ticket-retreat-btn-sm {
|
||
display: inline-block;
|
||
font-size: 22rpx;
|
||
color: #EF4444;
|
||
background: #FEF2F2;
|
||
border: 1rpx solid #FECACA;
|
||
border-radius: 10rpx;
|
||
padding: 4rpx 16rpx;
|
||
margin-left: 12rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.ticket-participant-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12rpx 0;
|
||
flex-wrap: wrap;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.ticket-participant-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.ticket-participant-tag {
|
||
font-size: 24rpx;
|
||
color: #2BA471;
|
||
background: #C4EACF;
|
||
padding: 4rpx 16rpx;
|
||
border-radius: 16rpx;
|
||
border: 1rpx solid #6CC294;
|
||
}
|
||
|
||
.ticket-participant-none {
|
||
font-size: 24rpx;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
// ========== 弹窗样式 ==========
|
||
.ticket-popup {
|
||
padding: 32rpx 28rpx;
|
||
max-height: 70vh;
|
||
}
|
||
|
||
.ticket-popup-header {
|
||
margin-bottom: 24rpx;
|
||
padding-bottom: 20rpx;
|
||
border-bottom: 1rpx solid #F3F4F6;
|
||
}
|
||
|
||
.ticket-popup-title {
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #1F2937;
|
||
display: block;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.ticket-popup-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
// ========== 时间线 ==========
|
||
.ticket-timeline-scroll {
|
||
max-height: 480rpx;
|
||
}
|
||
|
||
.ticket-timeline {
|
||
padding-left: 8rpx;
|
||
}
|
||
|
||
.timeline-item {
|
||
display: flex;
|
||
min-height: 100rpx;
|
||
}
|
||
|
||
.timeline-dot-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 40rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.timeline-dot {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
border-radius: 50%;
|
||
background: #D1D5DB;
|
||
border: 3rpx solid #E5E7EB;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.timeline-dot-success {
|
||
background: #10B981;
|
||
border-color: #A7F3D0;
|
||
}
|
||
|
||
.timeline-dot-reject {
|
||
background: #EF4444;
|
||
border-color: #FECACA;
|
||
}
|
||
|
||
.timeline-line {
|
||
width: 2rpx;
|
||
flex: 1;
|
||
background: #E5E7EB;
|
||
margin: 4rpx 0;
|
||
}
|
||
|
||
.timeline-content {
|
||
flex: 1;
|
||
padding: 0 0 24rpx 16rpx;
|
||
}
|
||
|
||
.timeline-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.timeline-state {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #1F2937;
|
||
}
|
||
|
||
.timeline-transition {
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.text-success { color: #10B981; }
|
||
.text-danger { color: #EF4444; }
|
||
|
||
.timeline-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.timeline-time {
|
||
font-size: 22rpx;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
.timeline-person {
|
||
font-size: 22rpx;
|
||
color: #6B7280;
|
||
background: #F3F4F6;
|
||
padding: 2rpx 12rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.timeline-suggestion {
|
||
margin-top: 8rpx;
|
||
padding: 12rpx 16rpx;
|
||
background: #F9FAFB;
|
||
border-radius: 10rpx;
|
||
border-left: 4rpx solid #D1D5DB;
|
||
font-size: 24rpx;
|
||
color: #6B7280;
|
||
}
|
||
</style> |