<template>
|
<div class="donation-process-detail">
|
<el-card class="process-card">
|
<div class="process-container">
|
<!-- 左侧时间线 -->
|
<div class="timeline-section">
|
<div class="section-header">
|
<h3>捐献进程时间线</h3>
|
</div>
|
|
<div class="timeline-scroll-container">
|
<div class="timeline-container">
|
<div
|
v-for="stage in processStages"
|
:key="stage.key"
|
class="timeline-item"
|
:class="{
|
active: stage.status == 'active',
|
completed: stage.status == 'completed',
|
progress: stage.status == 'progress',
|
pending: stage.status == 'pending'
|
}"
|
@click="handleStageClick(stage)"
|
>
|
<div class="timeline-marker">
|
<i
|
v-if="stage.status == 'completed'"
|
class="el-icon-check"
|
></i>
|
<i
|
v-else-if="stage.status == 'progress'"
|
class="el-icon-loading"
|
></i>
|
<i v-else class="el-icon-time"></i>
|
</div>
|
|
<div class="timeline-content">
|
<div class="stage-header">
|
<span class="stage-name">{{ stage.name }}</span>
|
<dict-tag
|
:options="dict.type[stage.dict]"
|
:value="stage.state"
|
/>
|
</div>
|
|
<div class="stage-info">
|
<div v-if="stage.createtime" class="time-info">
|
创建时间: {{ formatTime(stage.createtime) }}
|
</div>
|
<div v-if="stage.updateTime" class="time-info">
|
最近更新: {{ formatTime(stage.updateTime) }}
|
</div>
|
<div v-if="stage.operator" class="operator-info">
|
负责人: {{ stage.operator }}
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 右侧内容 -->
|
<div class="content-section">
|
<case-basic-info :case-id="caseId" :show-attachment="true" />
|
|
<div class="stage-detail-section">
|
<div class="section-header">
|
<h3>{{ activeStageName }} - 阶段详情</h3>
|
</div>
|
|
<div class="stage-content-wrapper">
|
<component
|
:is="getStageComponent()"
|
:stageData="activeStageData"
|
:caseInfo="caseInfo"
|
:infoid="caseId"
|
/>
|
</div>
|
</div>
|
</div>
|
</div>
|
</el-card>
|
</div>
|
</template>
|
|
<script>
|
// ===================== 字典 Label → UI 主题 =====================
|
function mapDictLabelToTheme(label) {
|
if (!label) return "pending";
|
|
const l = label.trim();
|
|
// 待 XX
|
if (/^待/.test(l)) {
|
return "progress";
|
}
|
|
// XX中
|
if (/中$/.test(l)) {
|
return "active";
|
}
|
|
// 完成 / 完成XX
|
if (/完成/.test(l)) {
|
return "completed";
|
}
|
|
// 弃用 / 放弃
|
if (/弃用|放弃/.test(l)) {
|
return "danger";
|
}
|
|
return "pending";
|
}
|
import { getDonatebaseinfoflow } from "@/api/project/donatebaseinfo";
|
import CaseBasicInfo from "@/components/CaseBasicInfo";
|
|
import DonorMaintenanceStage from "./components/DonorMaintenanceStage";
|
import DeathJudgmentStage from "./components/DeathJudgmentStage";
|
import MedicalAssessmentStage from "./components/MedicalAssessmentStage";
|
import DonationConfirmStage from "./components/DonationConfirmStage";
|
import EthicalReviewStage from "./components/EthicalReviewStage";
|
import OrganAllocationStage from "./components/OrganAllocationStage";
|
import OrganProcurementStage from "./components/OrganProcurementStage";
|
import OrganUtilizationStage from "./components/OrganUtilizationStage";
|
|
import dayjs from "dayjs";
|
|
// ============== 字典映射(你后面自己改) ==============
|
const STAGE_DICT_MAP = {
|
donatemaintenance: "maintain_type",
|
deathinfo: "decide_type",
|
medicalevaluation: "state_Evaluation",
|
relativesconfirmation: "affirm_type",
|
donateflowcharts: "sys_ethical",
|
donateorgansService: "allocation_Status",
|
donationwitness: "Obtain_status",
|
donatecompletioninfo: "utilize_statue"
|
};
|
|
// state -> 流程状态
|
const STATE_MAP = {
|
0: "pending",
|
1: "progress",
|
2: "completed",
|
3: "terminated"
|
};
|
|
// 阶段配置
|
const STAGE_CONFIG = [
|
{ key: "donor_maintenance", name: "供者维护", apiKey: "donatemaintenance" },
|
{ key: "death_judgment", name: "死亡判定", apiKey: "deathinfo" },
|
{ key: "medical_assessment", name: "医学评估", apiKey: "medicalevaluation" },
|
{
|
key: "donation_confirm",
|
name: "捐献确认",
|
apiKey: "relativesconfirmation"
|
},
|
{ key: "ethical_review", name: "伦理审查", apiKey: "donateflowcharts" },
|
{ key: "organ_allocation", name: "器官分配", apiKey: "donateorgansService" },
|
{ key: "organ_procurement", name: "器官获取", apiKey: "donationwitness" },
|
{ key: "organ_utilization", name: "器官利用", apiKey: "donatecompletioninfo" }
|
];
|
|
export default {
|
name: "DonationProcessDetail",
|
components: {
|
CaseBasicInfo,
|
DonorMaintenanceStage,
|
DeathJudgmentStage,
|
MedicalAssessmentStage,
|
DonationConfirmStage,
|
EthicalReviewStage,
|
OrganAllocationStage,
|
OrganProcurementStage,
|
OrganUtilizationStage
|
},
|
dicts: [
|
"decide_type",
|
"maintain_type",
|
"state_Evaluation",
|
"affirm_type",
|
"sys_ethical",
|
"allocation_Status",
|
"Obtain_status",
|
"utilize_statue"
|
], // 这里只声明一个即可,其余通过 dict.type[xxx] 使用
|
data() {
|
return {
|
caseId: null,
|
caseInfo: {},
|
processStages: [],
|
activeStage: "",
|
activeStageName: "",
|
activeStageData: {}
|
};
|
},
|
created() {
|
this.caseId = this.$route.query.id;
|
if (this.caseId) {
|
this.getDetail();
|
}
|
},
|
methods: {
|
async getDetail() {
|
try {
|
const res = await getDonatebaseinfoflow(this.caseId);
|
|
const data = res;
|
|
this.caseInfo = data.donatebaseinfo || {};
|
|
this.processStages = STAGE_CONFIG.map(stage => {
|
const obj = data[stage.apiKey] || {};
|
console.log(stage.apiKey, "stage.apiKey");
|
console.log(this.dict?.type?.[STAGE_DICT_MAP[stage.apiKey]]);
|
|
const dictLabel =
|
this.dict?.type?.[STAGE_DICT_MAP[stage.apiKey]]?.find(
|
d => d.value == obj.state
|
)?.label || "";
|
|
const theme = mapDictLabelToTheme(dictLabel);
|
|
return {
|
key: stage.key,
|
name: stage.name,
|
dict: STAGE_DICT_MAP[stage.apiKey],
|
state: obj.state,
|
dictLabel,
|
status: theme, // ✅ 核心:UI 主题由 dictLabel 决定
|
createtime: obj.createtime,
|
updateTime: obj.updatetime,
|
operator: obj.updateperson || obj.createperson
|
};
|
});
|
|
const active =
|
this.processStages.find(s => s.status == "progress") ||
|
[...this.processStages].reverse().find(s => s.status == "completed");
|
|
this.setActiveStage(active?.key || STAGE_CONFIG[0].key);
|
} catch (e) {
|
console.error(e);
|
this.$message.error("获取流程详情失败");
|
}
|
},
|
|
setActiveStage(key) {
|
const stage = this.processStages.find(s => s.key == key);
|
if (!stage) return;
|
this.activeStage = key;
|
this.activeStageName = stage.name;
|
this.activeStageData = stage;
|
},
|
|
getStageComponent() {
|
const map = {
|
donor_maintenance: "DonorMaintenanceStage",
|
death_judgment: "DeathJudgmentStage",
|
medical_assessment: "MedicalAssessmentStage",
|
donation_confirm: "DonationConfirmStage",
|
ethical_review: "EthicalReviewStage",
|
organ_allocation: "OrganAllocationStage",
|
organ_procurement: "OrganProcurementStage",
|
organ_utilization: "OrganUtilizationStage"
|
};
|
return map[this.activeStage];
|
},
|
|
handleStageClick(stage) {
|
if (stage.status == "pending") {
|
this.$message.warning("该阶段尚未开始");
|
return;
|
}
|
this.setActiveStage(stage.key);
|
},
|
|
formatTime(time) {
|
return time ? dayjs(time).format("YYYY-MM-DD HH:mm") : "-";
|
}
|
}
|
};
|
</script>
|
|
<style scoped>
|
.donation-process-detail {
|
padding: 20px;
|
background-color: #f5f7fa;
|
min-height: 100vh;
|
box-sizing: border-box;
|
}
|
|
.process-card {
|
border-radius: 8px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
margin-bottom: 20px;
|
}
|
|
.process-container {
|
display: flex;
|
min-height: 600px;
|
/* 设置一个最小高度 */
|
gap: 20px;
|
align-items: flex-start;
|
/* 顶部对齐 */
|
}
|
|
/* 左侧时间线样式 - 固定高度,内部滚动 */
|
.timeline-section {
|
flex: 0 0 320px;
|
/* 固定宽度 */
|
display: flex;
|
flex-direction: column;
|
background: white;
|
border-radius: 6px;
|
padding: 20px;
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
height: calc(120vh - 120px);
|
/* 根据视口高度自适应 */
|
max-height: 1200px;
|
/* 设置最大高度 */
|
position: sticky;
|
/* 使用 sticky 定位 */
|
top: 20px;
|
/* 距离顶部 20px */
|
}
|
|
.timeline-scroll-container {
|
flex: 1;
|
overflow-y: auto;
|
/* 内部可滚动 */
|
margin-top: 20px;
|
padding-right: 8px;
|
/* 为滚动条留出空间 */
|
}
|
|
.timeline-scroll-container::-webkit-scrollbar {
|
width: 6px;
|
}
|
|
.timeline-scroll-container::-webkit-scrollbar-track {
|
background: #f1f1f1;
|
border-radius: 3px;
|
}
|
|
.timeline-scroll-container::-webkit-scrollbar-thumb {
|
background: #c1c1c1;
|
border-radius: 3px;
|
}
|
|
.timeline-scroll-container::-webkit-scrollbar-thumb:hover {
|
background: #a8a8a8;
|
}
|
|
.timeline-container {
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
padding-bottom: 10px;
|
}
|
|
/* 右侧内容区域样式 - 自适应高度 */
|
.content-section {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 20px;
|
min-height: 0;
|
/* 重要:允许flex子项压缩 */
|
}
|
|
.basic-info-section {
|
background: white;
|
border-radius: 6px;
|
padding: 20px;
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
/* 重要 */
|
}
|
|
.basic-info-content {
|
flex: 1;
|
max-height: 300px;
|
/* 基本信息区域最大高度 */
|
overflow-y: auto;
|
/* 基本信息内部可滚动 */
|
margin-top: 20px;
|
padding-right: 8px;
|
}
|
|
.basic-info-content::-webkit-scrollbar {
|
width: 6px;
|
}
|
|
.basic-info-content::-webkit-scrollbar-track {
|
background: #f1f1f1;
|
border-radius: 3px;
|
}
|
|
.basic-info-content::-webkit-scrollbar-thumb {
|
background: #c1c1c1;
|
border-radius: 3px;
|
}
|
|
.basic-info-content::-webkit-scrollbar-thumb:hover {
|
background: #a8a8a8;
|
}
|
|
.basic-info-content .el-descriptions {
|
width: 100%;
|
}
|
|
.stage-detail-section {
|
flex: 1;
|
/* 占据剩余空间 */
|
background: white;
|
border-radius: 6px;
|
padding: 20px;
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
display: flex;
|
flex-direction: column;
|
min-height: 400px;
|
/* 最小高度 */
|
max-height: 800px;
|
/* 最大高度,可根据需要调整 */
|
overflow: hidden;
|
/* 隐藏外层溢出 */
|
}
|
|
.stage-content-wrapper {
|
flex: 1;
|
overflow-y: auto;
|
/* 阶段详情内部可滚动 */
|
margin-top: 20px;
|
padding-right: 8px;
|
min-height: 0;
|
/* 重要 */
|
}
|
|
.stage-content-wrapper::-webkit-scrollbar {
|
width: 6px;
|
}
|
|
.stage-content-wrapper::-webkit-scrollbar-track {
|
background: #f1f1f1;
|
border-radius: 3px;
|
}
|
|
.stage-content-wrapper::-webkit-scrollbar-thumb {
|
background: #c1c1c1;
|
border-radius: 3px;
|
}
|
|
.stage-content-wrapper::-webkit-scrollbar-thumb:hover {
|
background: #a8a8a8;
|
}
|
|
/* 原有样式保持不变 */
|
.section-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
flex-shrink: 0;
|
/* 防止被压缩 */
|
}
|
|
.section-header h3 {
|
margin: 0;
|
color: #303133;
|
font-size: 16px;
|
white-space: nowrap;
|
}
|
|
.timeline-item {
|
display: flex;
|
align-items: flex-start;
|
padding: 15px;
|
border-radius: 6px;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
border: 1px solid #e4e7ed;
|
flex-shrink: 0;
|
/* 防止被压缩 */
|
}
|
|
.timeline-item:hover {
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
transform: translateY(-1px);
|
}
|
.timeline-item.danger {
|
border-color: #f56c6c;
|
background-color: #fef0f0;
|
}
|
|
.timeline-item.danger .timeline-marker {
|
background-color: #f56c6c;
|
}
|
.timeline-item.active {
|
border-color: #409eff;
|
background-color: #f0f9ff;
|
}
|
|
.timeline-item.completed {
|
border-color: #67c23a;
|
background-color: #f0f9e8;
|
}
|
|
.timeline-item.progress {
|
border-color: #e6a23c;
|
background-color: #fdf6ec;
|
}
|
|
.timeline-item.pending {
|
border-color: #909399;
|
background-color: #f4f4f5;
|
}
|
|
.timeline-marker {
|
flex: 0 0 40px;
|
height: 40px;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin-right: 15px;
|
font-size: 18px;
|
color: white;
|
}
|
|
.timeline-item.completed .timeline-marker {
|
background-color: #67c23a;
|
}
|
.timeline-item.active .timeline-marker {
|
background-color: #409eff;
|
}
|
|
.timeline-item.progress .timeline-marker {
|
background-color: #e6a23c;
|
}
|
|
.timeline-item.pending .timeline-marker {
|
background-color: #909399;
|
}
|
|
.timeline-content {
|
flex: 1;
|
}
|
|
.stage-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 8px;
|
}
|
|
.stage-name {
|
font-weight: 600;
|
color: #303133;
|
font-size: 14px;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
max-width: 150px;
|
}
|
|
.stage-info {
|
font-size: 12px;
|
color: #606266;
|
}
|
|
.time-info,
|
.operator-info {
|
margin-bottom: 4px;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.stage-actions {
|
display: flex;
|
gap: 10px;
|
flex-wrap: wrap;
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 1200px) {
|
.process-container {
|
flex-direction: column;
|
}
|
|
.timeline-section {
|
flex: none;
|
width: 100%;
|
height: auto;
|
max-height: 300px;
|
position: static;
|
/* 小屏幕取消 sticky */
|
}
|
|
.timeline-scroll-container {
|
max-height: 250px;
|
}
|
|
.stage-detail-section {
|
max-height: 500px;
|
}
|
}
|
|
@media (max-width: 768px) {
|
.donation-process-detail {
|
padding: 10px;
|
}
|
|
.process-container {
|
gap: 15px;
|
}
|
|
.timeline-section,
|
.basic-info-section,
|
.stage-detail-section {
|
padding: 15px;
|
}
|
|
.section-header {
|
flex-direction: column;
|
align-items: flex-start;
|
gap: 10px;
|
}
|
|
.stage-name {
|
max-width: 120px;
|
}
|
|
.basic-info-content {
|
max-height: 250px;
|
}
|
|
.stage-detail-section {
|
min-height: 300px;
|
max-height: 400px;
|
}
|
}
|
</style>
|