WXL
2025-12-28 40bd04c1299a0edf63771b90b5f9e78bfb943474
办公及业务页面推送
已修改5个文件
已添加45个文件
21992 ■■■■■ 文件已修改
src/api/case/deathJudgment.js 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AttachmentPreview/ImagePreview/index.vue 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AttachmentPreview/PdfPreview/index.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AttachmentPreview/index.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UploadAttachment/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/checkingInInfo.vue 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue 588 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/AttendanceStatistics.vue 277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/AttendanceTable.vue 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/BusinessTripTable.vue 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/MileageCalculation.vue 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/PersonBusiness.vue 500 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue 682 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/components/PersonalAttendanceTable.vue 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/index.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/OfficeRelated/checkingIn/mockData.js 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/GetWitness/GetWitnessInfo.vue 1577 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/GetWitness/index.vue 372 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/GetWitness/organProcurement.js 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/OrganUtilization/OrganUtilizationInfo.vue 1656 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/OrganUtilization/index.vue 377 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/OrganUtilization/organUtilization.js 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/affirm/mockConfirmationApi.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/allocation/allocationInfo.vue 1014 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/allocation/index.vue 372 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/allocation/organAllocation.js 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/appear/caseDetail.vue 115 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/BaseStage.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/DeathJudgmentStage.vue 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/DonationConfirmStage.vue 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/DonorMaintenanceStage.vue 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/EthicalReviewStage.vue 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/MedicalAssessmentStage.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/OrganAllocationStage.vue 447 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/OrganProcurementStage.vue 499 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/components/OrganUtilizationStage.vue 812 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/donationProcess.js 453 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/course/index.vue 677 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/decide/DecideInfo.vue 653 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/decide/index.vue 437 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/decide/mockDeathJudgmentApi.js 391 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/ethicalReview/ethicalReviewInfo.vue 1526 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/ethicalReview/ethicsReview.js 392 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/ethicalReview/index.vue 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/maintain/components/BloodRoutinePanel.vue 693 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/maintain/components/LiverKidneyPanel.vue 492 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/maintain/components/UrineRoutinePanel.vue 751 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/maintain/maintainInfo.vue 1135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/donatebaseinfo/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/case/deathJudgment.js
src/components/AttachmentPreview/ImagePreview/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
<template>
  <div class="image-preview-container" v-loading="loading">
    <!-- æŽ§åˆ¶å·¥å…·æ  -->
    <div class="image-controls">
      <el-button-group>
        <el-button size="mini" @click="zoomOut" :disabled="scale <= 0.2">
          <i class="el-icon-zoom-out"></i> ç¼©å°
        </el-button>
        <el-button size="mini" @click="resetZoom">
          <i class="el-icon-refresh-left"></i> é‡ç½® ({{ Math.round(scale * 100) }}%)
        </el-button>
        <el-button size="mini" @click="zoomIn" :disabled="scale >= 3">
          <i class="el-icon-zoom-in"></i> æ”¾å¤§
        </el-button>
      </el-button-group>
      <el-button-group>
        <el-button size="mini" @click="rotate(-90)">
          <i class="el-icon-refresh-left"></i> å·¦æ—‹
        </el-button>
        <el-button size="mini" @click="rotate(90)">
          <i class="el-icon-refresh-right"></i> å³æ—‹
        </el-button>
      </el-button-group>
      <el-button size="mini" type="success" @click="handleDownload" icon="el-icon-download">
        ä¸‹è½½
      </el-button>
    </div>
    <!-- å›¾ç‰‡æ¸²æŸ“区域 -->
    <div class="image-render-area" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
      <div class="image-wrapper" :style="imageWrapperStyle">
        <img
          ref="imageEl"
          :src="imageUrl"
          :alt="fileName"
          :style="imageStyle"
          @load="onImageLoad"
          @error="onImageError"
        />
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'ImagePreview',
  props: {
    imageUrl: {
      type: String,
      required: true
    },
    fileName: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      loading: false,
      scale: 1.0,
      rotation: 0,
      naturalWidth: 0,
      naturalHeight: 0
    }
  },
  computed: {
    imageStyle() {
      return {
        transform: `scale(${this.scale}) rotate(${this.rotation}deg)`,
        cursor: 'default',
        maxWidth: '100%',
        maxHeight: '100%',
        transition: 'transform 0.3s ease'
      }
    },
    imageWrapperStyle() {
      return {
        width: '100%',
        height: '100%',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        overflow: 'auto'
      }
    }
  },
  methods: {
    onImageLoad() {
      this.loading = false;
    },
    onImageError() {
      this.loading = false;
      this.$message.error('图片加载失败');
    },
    zoomIn() {
      this.scale = Math.min(this.scale + 0.1, 3.0);
    },
    zoomOut() {
      this.scale = Math.max(this.scale - 0.1, 0.2);
    },
    resetZoom() {
      this.scale = 1.0;
      this.rotation = 0;
    },
    rotate(degrees) {
      this.rotation = (this.rotation + degrees) % 360;
    },
    onMouseMove() {
      // å¯æ·»åŠ é¼ æ ‡äº¤äº’æ•ˆæžœ
    },
    onMouseLeave() {
      // å¯æ·»åŠ é¼ æ ‡ç¦»å¼€æ•ˆæžœ
    },
    handleDownload() {
      const link = document.createElement('a');
      link.href = this.imageUrl;
      link.download = this.fileName;
      link.style.display = 'none';
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      this.$message.success('开始下载文件');
    }
  },
  mounted() {
    this.loading = true;
  }
}
</script>
<style scoped>
.image-preview-container {
  height: 100%;
  display: flex;
  flex-direction: column;
  border: 1px solid #ebeef5;
  border-radius: 4px;
}
.image-controls {
  padding: 15px 20px;
  background: #f5f7fa;
  border-bottom: 1px solid #ebeef5;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}
.image-render-area {
  flex: 1;
  position: relative;
  overflow: hidden;
  background: #f8f9fa;
}
.image-wrapper {
  padding: 20px;
}
.image-wrapper img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
  .image-controls {
    flex-direction: column;
    gap: 10px;
  }
}
</style>
src/components/AttachmentPreview/PdfPreview/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,200 @@
<template>
  <div class="pdf-preview-container" v-loading="loading">
    <!-- æŽ§åˆ¶å·¥å…·æ  -->
    <div class="pdf-controls">
      <el-button-group>
        <el-button
          size="mini"
          @click="prevPage"
          :disabled="currentPage <= 1"
          icon="el-icon-arrow-left"
        >
          ä¸Šä¸€é¡µ
        </el-button>
        <el-button size="mini" disabled>
          ç¬¬ {{ currentPage }} é¡µ / å…± {{ totalPages }} é¡µ
        </el-button>
        <el-button
          size="mini"
          @click="nextPage"
          :disabled="currentPage >= totalPages"
          icon="el-icon-arrow-right"
        >
          ä¸‹ä¸€é¡µ
        </el-button>
      </el-button-group>
      <el-button-group class="zoom-controls">
        <el-button size="mini" @click="zoomOut" :disabled="scale <= 50">
          <i class="el-icon-zoom-out"></i> ç¼©å°
        </el-button>
        <el-button size="mini" disabled>{{ scale }}%</el-button>
        <el-button size="mini" @click="zoomIn" :disabled="scale >= 200">
          <i class="el-icon-zoom-in"></i> æ”¾å¤§
        </el-button>
        <el-button size="mini" @click="resetZoom">
          <i class="el-icon-refresh-left"></i> é‡ç½®
        </el-button>
      </el-button-group>
      <el-button size="mini" type="success" @click="handleDownload" icon="el-icon-download">
        ä¸‹è½½
      </el-button>
    </div>
    <!-- PDF渲染区域 -->
    <div class="pdf-render-area">
      <pdf
        ref="pdf"
        :src="pdfUrl"
        :page="currentPage"
        @num-pages="totalPages = $event"
        @page-loaded="currentPage = $event"
        @loaded="loadPdfHandler"
        @error="pdfErrorHandler"
        :style="{
          width: scale + '%',
          transform: 'scale(' + scale / 100 + ')',
          transformOrigin: '0 0'
        }"
      ></pdf>
    </div>
  </div>
</template>
<script>
import pdf from 'vue-pdf'
export default {
  name: 'PdfPreview',
  components: {
    pdf
  },
  props: {
    pdfUrl: {
      type: String,
      required: true
    },
    fileName: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      loading: false,
      currentPage: 1,
      totalPages: 0,
      scale: 100
    }
  },
  methods: {
    loadPdfHandler() {
      this.loading = false;
    },
    pdfErrorHandler(error) {
      console.error('PDF加载失败:', error);
      this.loading = false;
      this.$message.error('PDF文件加载失败,请尝试下载后查看');
    },
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--;
      }
    },
    nextPage() {
      if (this.currentPage < this.totalPages) {
        this.currentPage++;
      }
    },
    zoomIn() {
      if (this.scale >= 200) {
        this.$message.info("已放大到最大比例");
        return;
      }
      this.scale += 10;
    },
    zoomOut() {
      if (this.scale <= 50) {
        this.$message.info("已缩小到最小比例");
        return;
      }
      this.scale -= 10;
    },
    resetZoom() {
      this.scale = 100;
    },
    handleDownload() {
      const link = document.createElement('a');
      link.href = this.pdfUrl;
      link.download = this.fileName;
      link.style.display = 'none';
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      this.$message.success('开始下载文件');
    }
  },
  mounted() {
    this.loading = true;
  },
  watch: {
    pdfUrl() {
      this.loading = true;
      this.currentPage = 1;
      this.scale = 100;
    }
  }
}
</script>
<style scoped>
.pdf-preview-container {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.pdf-controls {
  padding: 15px 20px;
  background: #f5f7fa;
  border-bottom: 1px solid #ebeef5;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}
.pdf-render-area {
  flex: 1;
  overflow: auto;
  padding: 20px;
  background: #f8f9fa;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}
.zoom-controls {
  margin: 0 15px;
}
@media (max-width: 768px) {
  .pdf-controls {
    flex-direction: column;
    gap: 10px;
  }
  .zoom-controls {
    margin: 10px 0;
  }
}
</style>
src/components/AttachmentPreview/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,154 @@
<template>
  <el-dialog
    :title="title"
    :visible.sync="visible"
    width="90%"
    top="5vh"
    :close-on-click-modal="true"
    class="attachment-preview-dialog"
    @close="handleClose"
  >
    <div class="attachment-preview">
      <el-tabs v-model="activeTab" type="card">
        <el-tab-pane
          v-for="(file, index) in fileList"
          :key="index"
          :label="getTabLabel(file)"
          :name="index.toString()"
        >
          <div class="preview-content">
            <!-- PDF预览 -->
            <PdfPreview
              v-if="getFileType(file.fileName) === 'pdf'"
              :pdf-url="file.fileUrl"
              :file-name="file.fileName"
            />
            <!-- å›¾ç‰‡é¢„览 -->
            <ImagePreview
              v-else-if="getFileType(file.fileName) === 'image'"
              :image-url="file.fileUrl"
              :file-name="file.fileName"
            />
            <!-- ä¸æ”¯æŒé¢„览的文件类型 -->
            <div v-else class="unsupported-preview">
              <el-alert
                title="该文件格式不支持在线预览,请下载后查看"
                type="warning"
                show-icon
                :closable="false"
              />
              <div class="download-action">
                <el-button type="primary" @click="handleDownload(file)">
                  <i class="el-icon-download"></i> ä¸‹è½½æ–‡ä»¶
                </el-button>
              </div>
            </div>
          </div>
        </el-tab-pane>
      </el-tabs>
    </div>
    <span slot="footer" class="dialog-footer">
      <el-button @click="$emit('close')">关闭</el-button>
    </span>
  </el-dialog>
</template>
<script>
import PdfPreview from "./PdfPreview";
import ImagePreview from "./ImagePreview";
export default {
  name: "AttachmentPreview",
  components: { PdfPreview, ImagePreview },
  props: {
    visible: Boolean,
    fileList: {
      type: Array,
      default: () => []
    },
    title: String
  },
  data() {
    return {
      activeTab: "0"
    };
  },
  methods: {
    getFileType(fileName) {
      const extension = fileName.split('.').pop().toLowerCase();
      const imageTypes = ["jpg", "jpeg", "png", "gif", "bmp", "webp"];
      const pdfTypes = ["pdf"];
      if (imageTypes.includes(extension)) return "image";
      if (pdfTypes.includes(extension)) return "pdf";
      return "other";
    },
    getTabLabel(file) {
      const type = this.getFileType(file.fileName);
      const icon = type === 'pdf' ? 'el-icon-document' :
                  type === 'image' ? 'el-icon-picture' : 'el-icon-files';
      return `${file.fileName}`;
    },
    handleDownload(file) {
      const link = document.createElement("a");
      link.href = file.fileUrl;
      link.download = file.fileName;
      link.style.display = "none";
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      this.$message.success("开始下载文件");
    },
    handleClose() {
      this.activeTab = "0";
      this.$emit('close');
    }
  }
};
</script>
<style scoped>
.attachment-preview-dialog >>> .el-dialog {
  min-height: 80vh;
  display: flex;
  flex-direction: column;
}
.attachment-preview-dialog >>> .el-dialog__body {
  flex: 1;
  padding: 0;
  display: flex;
  flex-direction: column;
}
.attachment-preview {
  flex: 1;
  display: flex;
  flex-direction: column;
}
.preview-content {
  flex: 1;
  height: 800px;
  padding: 0;
}
.unsupported-preview {
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 40px;
}
.download-action {
  margin-top: 20px;
}
</style>
src/components/UploadAttachment/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
<template>
  <div class="upload-attachment">
    <el-upload
      action="#"
      :file-list="fileList"
      :auto-upload="false"
      :on-change="handleFileChange"
      :on-remove="handleFileRemove"
      multiple
    >
      <el-button size="small" type="primary">点击上传</el-button>
      <div slot="tip" class="el-upload__tip">支持jpg、png、pdf、doc、docx等格式,单个文件不超过10MB</div>
    </el-upload>
  </div>
</template>
<script>
export default {
  name: "UploadAttachment",
  props: {
    fileList: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    handleFileChange(file, fileList) {
      this.$emit('change', fileList);
    },
    handleFileRemove(file, fileList) {
      this.$emit('change', fileList);
    }
  }
};
</script>
src/main.js
@@ -26,6 +26,7 @@
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config";
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";
import { formatTime } from "@/utils/index";
import dataV from '@jiaminghi/data-view';//dataV
// åˆ†é¡µç»„ä»¶
import Pagination from "@/components/Pagination";
@@ -65,6 +66,7 @@
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.formatTime = formatTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
src/views/OfficeRelated/checkingIn/checkingInInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,307 @@
<template>
  <div class="attendance-detail">
    <!-- å‘˜å·¥åŸºæœ¬ä¿¡æ¯ -->
    <el-card class="employee-info-card">
      <div class="employee-header">
        <div class="employee-basic">
          <el-avatar :size="60" :src="employeeInfo.avatar" class="employee-avatar">
            {{ employeeInfo.name.charAt(0) }}
          </el-avatar>
          <div class="employee-details">
            <h3>{{ employeeInfo.name }}</h3>
            <p class="employee-department">{{ employeeInfo.department }} Â· {{ employeeInfo.position }}</p>
            <p class="employee-contact">
              <span>工号: {{ employeeInfo.employeeId }}</span>
              <span>电话: {{ employeeInfo.phone }}</span>
            </p>
          </div>
        </div>
        <div class="employee-stats">
          <div class="stat-item">
            <div class="stat-value">{{ employeeStats.attendanceRate }}%</div>
            <div class="stat-label">本月出勤率</div>
          </div>
          <div class="stat-item">
            <div class="stat-value">{{ employeeStats.workHours }}h</div>
            <div class="stat-label">总工作时长</div>
          </div>
          <div class="stat-item">
            <div class="stat-value">{{ employeeStats.businessTripDays }}</div>
            <div class="stat-label">出差天数</div>
          </div>
        </div>
      </div>
    </el-card>
    <!-- é€‰é¡¹å¡ -->
    <el-card>
      <el-tabs v-model="activeTab">
        <el-tab-pane label="日历视图" name="calendar">
          <attendance-calendar
            :attendance-data="attendanceData"
            :business-trip-data="businessTripData"
          />
        </el-tab-pane>
        <el-tab-pane label="出勤记录" name="attendanceList">
          <personal-attendance-table
            :data="attendanceData"
            :loading="loading"
          />
        </el-tab-pane>
        <el-tab-pane label="出差记录" name="businessTripList">
          <personal-business-trip-table
            :data="businessTripData"
            :loading="loading"
          />
        </el-tab-pane>
        <el-tab-pane label="统计报表" name="report">
          <personal-attendance-report
            :stats="employeeStats"
            :attendance-data="attendanceData"
          />
        </el-tab-pane>
      </el-tabs>
    </el-card>
  </div>
</template>
<script>
  import { generateMockData } from './mockData'
import AttendanceCalendar from './components/AttendanceCalendar.vue'
import PersonalAttendanceTable from './components/PersonalAttendanceTable.vue'
import PersonBusiness from './components/PersonBusiness.vue'
import PersonalAttendanceReport from './components/PersonalAttendanceReport.vue'
export default {
  name: 'AttendanceDetail',
  components: {
    AttendanceCalendar,
    PersonalAttendanceTable,
    PersonBusiness,
    PersonalAttendanceReport
  },
  data() {
    return {
       activeTab: 'calendar',
      loading: false,
      employeeInfo: {
        name: '张三',
        department: 'OPO项目部',
        position: '项目经理',
        employeeId: 'OPO001',
        phone: '138-1234-5678',
        avatar: ''
      },
      employeeStats: {
        attendanceRate: 0,
        workHours: 0,
        businessTripDays: 0,
        lateTimes: 0,
        leaveEarlyTimes: 0
      },
      attendanceData: [],
      businessTripData: []
    }
  },
  created() {
        this.loadMockData()
    this.getEmployeeInfo()
    this.loadAttendanceData()
  },
  methods: {
    getEmployeeInfo() {
      const { employeeId, employeeName } = this.$route.query
      // æ¨¡æ‹Ÿå‘˜å·¥ä¿¡æ¯
      this.employeeInfo = {
        name: employeeName || '张三',
        department: 'OPO项目部',
        position: '项目经理',
        employeeId: employeeId || 'OPO001',
        phone: '138****1234',
        avatar: ''
      }
    },
 loadMockData() {
      this.loading = true
      // æ¨¡æ‹Ÿå¼‚步加载
      setTimeout(() => {
        const mockData = generateMockData()
        this.attendanceData = mockData.attendanceData
        this.businessTripData = mockData.businessTripData
        this.calculateStats()
        this.loading = false
      }, 500)
    },
    calculateStats() {
      const totalDays = 31 // 12月有31天
      const attendanceDays = this.attendanceData.filter(item =>
        item.status === 'present' || item.status === 'late'
      ).length
      const lateTimes = this.attendanceData.filter(item =>
        item.status === 'late'
      ).length
      // è®¡ç®—出差总天数
      const businessTripDays = this.businessTripData.reduce((total, trip) => {
        const start = new Date(trip.startDate)
        const end = new Date(trip.endDate)
        const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
        return total + days
      }, 0)
      // è®¡ç®—总工作时长
      const totalWorkHours = this.attendanceData.reduce((total, item) => {
        return total + (item.workHours || 0)
      }, 0)
      this.employeeStats = {
        attendanceRate: Math.round((attendanceDays / totalDays) * 100),
        workHours: totalWorkHours.toFixed(1),
        businessTripDays: businessTripDays,
        lateTimes: lateTimes,
        leaveEarlyTimes: this.attendanceData.filter(item =>
          item.status === 'leaveEarly'
        ).length
      }
    },
    async loadAttendanceData() {
      this.loading = true
      try {
        await new Promise(resolve => setTimeout(resolve, 500))
        // ç”Ÿæˆä¸ªäººè€ƒå‹¤æ¨¡æ‹Ÿæ•°æ®
        this.attendanceData = this.generatePersonalAttendanceData()
        this.businessTripData = this.generatePersonalBusinessTripData()
        this.calculateStats()
      } catch (error) {
        console.error('加载数据失败:', error)
      } finally {
        this.loading = false
      }
    },
    generatePersonalAttendanceData() {
      const data = []
      const currentMonth = 12 // 12月
      for (let day = 1; day <= 31; day++) {
        if (Math.random() > 0.2) { // 80%的出勤率
          data.push({
            id: day,
            date: `2024-${currentMonth.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`,
            checkIn: `08:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
            checkOut: `18:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
            status: Math.random() > 0.1 ? '正常' : '迟到',
            workHours: (8 + Math.random() * 2).toFixed(1)
          })
        }
      }
      return data
    },
    generatePersonalBusinessTripData() {
      return [
        {
          id: 1,
          tripNumber: 'BT202412001',
          startCity: '北京',
          endCity: '上海',
          startDate: '2024-12-05',
          endDate: '2024-12-08',
          distance: 1200,
          purpose: '客户会议',
          status: '已完成'
        },
        {
          id: 2,
          tripNumber: 'BT202412002',
          startCity: '北京',
          endCity: '广州',
          startDate: '2024-12-15',
          endDate: '2024-12-18',
          distance: 1900,
          purpose: '项目调研',
          status: '已完成'
        }
      ]
    },
  }
}
</script>
<style scoped>
.attendance-detail {
  padding: 20px;
}
.employee-info-card {
  margin-bottom: 20px;
}
.employee-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.employee-basic {
  display: flex;
  align-items: center;
}
.employee-avatar {
  margin-right: 16px;
  background-color: #409eff;
}
.employee-details h3 {
  margin: 0 0 8px 0;
  font-size: 24px;
  color: #303133;
}
.employee-department {
  margin: 0 0 8px 0;
  color: #606266;
}
.employee-contact {
  margin: 0;
  color: #909399;
  font-size: 14px;
}
.employee-contact span {
  margin-right: 16px;
}
.employee-stats {
  display: flex;
  gap: 30px;
}
.stat-item {
  text-align: center;
}
.stat-value {
  font-size: 28px;
  font-weight: bold;
  color: #409eff;
  margin-bottom: 4px;
}
.stat-label {
  color: #909399;
  font-size: 14px;
}
</style>
src/views/OfficeRelated/checkingIn/components/AttendanceCalendar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,588 @@
<template>
  <div class="attendance-calendar">
    <!-- æ—¥åŽ†å¤´éƒ¨ç»Ÿè®¡ä¿¡æ¯ -->
    <div class="calendar-stats">
      <div class="stat-item">
        <span class="stat-dot present"></span>
        <span>正常出勤: {{ stats.presentDays }}天</span>
      </div>
      <div class="stat-item">
        <span class="stat-dot absent"></span>
        <span>缺勤/异常: {{ stats.absentDays }}天</span>
      </div>
      <div class="stat-item">
        <span class="stat-dot trip"></span>
        <span>出差: {{ stats.tripDays }}天</span>
      </div>
      <div class="stat-item">
        <span class="stat-dot late"></span>
        <span>迟到/早退: {{ stats.lateDays }}天</span>
      </div>
    </div>
    <!-- Element UI æ—¥åŽ†ç»„ä»¶ -->
    <el-calendar v-model="calendarValue" :first-day-of-week="1">
      <template #date-cell="{ data }">
        <div
          class="calendar-date"
          :class="getDateStatusClass(data.day)"
          @click="handleDateClick(data)"
        >
          <div class="date-number">{{ data.day.split('-')[2] }}</div>
          <!-- çŠ¶æ€èƒŒæ™¯è‰²å— -->
          <div class="status-background" :class="getBackgroundStatus(data.day)"></div>
          <div class="date-events">
            <!-- å‡ºå‹¤çŠ¶æ€æ ‡è®° -->
            <div v-if="getAttendanceStatus(data.day) !== 'absent'" class="status-mark">
              <el-tooltip
                :content="getAttendanceTooltip(data.day)"
                placement="top"
              >
                <span class="status-dot" :class="getAttendanceStatus(data.day)"></span>
              </el-tooltip>
            </div>
            <!-- å‡ºå·®æ ‡è®° -->
            <div v-if="hasBusinessTrip(data.day)" class="trip-mark">
              <el-tooltip content="出差" placement="top">
                <i class="el-icon-location-outline"></i>
              </el-tooltip>
            </div>
            <!-- ç¼ºå‹¤æ ‡è®° -->
            <div v-if="getAttendanceStatus(data.day) === 'absent'" class="absent-mark">
              <el-tooltip content="缺勤" placement="top">
                <i class="el-icon-close"></i>
              </el-tooltip>
            </div>
          </div>
          <!-- ç®€ç•¥ä¿¡æ¯æ˜¾ç¤º -->
          <div class="brief-info">
            <div v-if="getAttendanceStatus(data.day) === 'present'" class="info-item present-info">
              âˆš
            </div>
            <div v-else-if="getAttendanceStatus(data.day) === 'late'" class="info-item late-info">
              !
            </div>
            <div v-else-if="getAttendanceStatus(data.day) === 'absent'" class="info-item absent-info">
              Ã—
            </div>
            <div v-if="hasBusinessTrip(data.day)" class="info-item trip-info">
              âœˆ
            </div>
          </div>
          <!-- æ—¥æœŸè¯¦ç»†ä¿¡æ¯ï¼ˆæ‚¬æµ®æ˜¾ç¤ºï¼‰ -->
          <div class="date-details">
            <div
              v-for="event in getDateEvents(data.day)"
              :key="event.id"
              class="detail-item"
            >
              <span class="detail-type">{{ event.type === 'attendance' ? '出勤' : '出差' }}</span>
              <span class="detail-info">{{ event.text }}</span>
            </div>
            <div v-if="getDateEvents(data.day).length === 0" class="detail-item">
              <span class="detail-type">无记录</span>
            </div>
          </div>
        </div>
      </template>
    </el-calendar>
    <!-- æ—¥æœŸè¯¦æƒ…对话框 -->
    <el-dialog
      :title="`${selectedDate} è€ƒå‹¤è¯¦æƒ…`"
      v-model="detailDialogVisible"
      width="500px"
    >
      <div v-if="selectedDateInfo">
        <div class="detail-section">
          <h4>出勤信息</h4>
          <div v-if="selectedDateInfo.attendance">
            <p>上班时间: {{ selectedDateInfo.attendance.checkIn || '未打卡' }}</p>
            <p>下班时间: {{ selectedDateInfo.attendance.checkOut || '未打卡' }}</p>
            <p>状态:
              <el-tag :type="getStatusTagType(selectedDateInfo.attendance.status)">
                {{ getStatusText(selectedDateInfo.attendance.status) }}
              </el-tag>
            </p>
          </div>
          <div v-else>
            <p class="no-data">无出勤记录</p>
          </div>
        </div>
        <div class="detail-section" v-if="selectedDateInfo.businessTrip">
          <h4>出差信息</h4>
          <p>目的地: {{ selectedDateInfo.businessTrip.destination }}</p>
          <p>事由: {{ selectedDateInfo.businessTrip.reason }}</p>
          <p>里程: {{ selectedDateInfo.businessTrip.distance }}公里</p>
        </div>
      </div>
      <template #footer>
        <el-button @click="detailDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script>
export default {
  name: 'AttendanceCalendar',
  props: {
    attendanceData: {
      type: Array,
      default: () => []
    },
    businessTripData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      calendarValue: new Date(),
      detailDialogVisible: false,
      selectedDate: '',
      selectedDateInfo: null,
      stats: {
        presentDays: 0,
        absentDays: 0,
        tripDays: 0,
        lateDays: 0
      }
    }
  },
  mounted() {
    this.calculateStats()
  },
  watch: {
    attendanceData: {
      handler() {
        this.calculateStats()
      },
      deep: true
    },
    businessTripData: {
      handler() {
        this.calculateStats()
      },
      deep: true
    }
  },
  methods: {
    // èŽ·å–æ—¥æœŸçŠ¶æ€ç±»å
    getDateStatusClass(date) {
      const classes = []
      if (this.isToday(date)) {
        classes.push('today')
      }
      if (this.isSelected(date)) {
        classes.push('selected')
      }
      // æ·»åŠ çŠ¶æ€ç±»å
      const status = this.getBackgroundStatus(date)
      if (status) {
        classes.push(status)
      }
      return classes
    },
    // èŽ·å–èƒŒæ™¯çŠ¶æ€ï¼ˆç”¨äºŽèƒŒæ™¯è‰²ï¼‰
    getBackgroundStatus(date) {
      const attendance = this.attendanceData.find(item => item.date === date)
      const hasTrip = this.hasBusinessTrip(date)
      if (hasTrip) {
        return 'has-trip'
      }
      if (attendance) {
        switch (attendance.status) {
          case 'present': return 'has-attendance'
          case 'late': return 'has-late'
          case 'absent': return 'has-absent'
          default: return ''
        }
      }
      return ''
    },
    // åˆ¤æ–­æ˜¯å¦ä¸ºä»Šå¤©
    isToday(date) {
      const today = new Date()
      const compareDate = new Date(date)
      return today.toDateString() === compareDate.toDateString()
    },
    // åˆ¤æ–­æ˜¯å¦è¢«é€‰ä¸­
    isSelected(date) {
      return this.selectedDate === date
    },
    // èŽ·å–è€ƒå‹¤çŠ¶æ€
    getAttendanceStatus(date) {
      const attendance = this.attendanceData.find(item => item.date === date)
      if (!attendance) return 'absent'
      switch (attendance.status) {
        case 'present': return 'present'
        case 'late': return 'late'
        case 'absent': return 'absent'
        default: return 'absent'
      }
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      const statusMap = {
        present: '正常出勤',
        late: '迟到/早退',
        absent: '缺勤/异常'
      }
      return statusMap[status] || '未知状态'
    },
    // èŽ·å–è€ƒå‹¤çŠ¶æ€æç¤º
    getAttendanceTooltip(date) {
      const statusMap = {
        present: '正常出勤',
        late: '迟到/早退',
        absent: '缺勤/异常'
      }
      return statusMap[this.getAttendanceStatus(date)] || '无记录'
    },
    // åˆ¤æ–­æ˜¯å¦æœ‰å‡ºå·®
    hasBusinessTrip(date) {
      return this.businessTripData.some(item =>
        date >= item.startDate && date <= item.endDate
      )
    },
    // èŽ·å–æ—¥æœŸäº‹ä»¶
    getDateEvents(date) {
      const events = []
      const attendance = this.attendanceData.find(item => item.date === date)
      if (attendance) {
        events.push({
          id: `attendance-${date}`,
          type: 'attendance',
          text: `${attendance.checkIn || '未打卡'} - ${attendance.checkOut || '未打卡'}`
        })
      }
      const businessTrip = this.businessTripData.find(item =>
        date >= item.startDate && date <= item.endDate
      )
      if (businessTrip) {
        events.push({
          id: `business-trip-${date}`,
          type: 'businessTrip',
          text: `前往${businessTrip.destination}`
        })
      }
      return events
    },
    // å¤„理日期点击事件
    handleDateClick(data) {
      this.selectedDate = data.day
      this.selectedDateInfo = {
        attendance: this.attendanceData.find(item => item.date === data.day),
        businessTrip: this.businessTripData.find(item =>
          data.day >= item.startDate && data.day <= item.endDate
        )
      }
      this.detailDialogVisible = true
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTagType(status) {
      const typeMap = {
        present: 'success',
        late: 'warning',
        absent: 'danger'
      }
      return typeMap[status] || 'info'
    },
    // è®¡ç®—统计信息
    calculateStats() {
      // é‡ç½®ç»Ÿè®¡
      this.stats = { presentDays: 0, absentDays: 0, tripDays: 0, lateDays: 0 }
      this.attendanceData.forEach(item => {
        switch (item.status) {
          case 'present': this.stats.presentDays++; break
          case 'late': this.stats.lateDays++; break
          case 'absent': this.stats.absentDays++; break
        }
      })
      // è®¡ç®—出差天数
      const tripDays = new Set()
      this.businessTripData.forEach(item => {
        const start = new Date(item.startDate)
        const end = new Date(item.endDate)
        for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
          tripDays.add(d.toISOString().split('T')[0])
        }
      })
      this.stats.tripDays = tripDays.size
    }
  }
}
</script>
<style scoped>
.attendance-calendar {
  padding: 20px;
  max-width: 100%;
}
.calendar-stats {
  display: flex;
  justify-content: space-around;
  margin-bottom: 20px;
  padding: 15px;
  background: #f5f7fa;
  border-radius: 8px;
}
.stat-item {
  display: flex;
  align-items: center;
  gap: 8px;
}
.stat-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  display: inline-block;
}
.stat-dot.present { background-color: #67c23a; }
.stat-dot.absent { background-color: #f56c6c; }
.stat-dot.trip { background-color: #409eff; }
.stat-dot.late { background-color: #e6a23c; }
.calendar-date {
  height: 80px;
  padding: 4px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
  position: relative;
  overflow: hidden;
}
.calendar-date:hover {
  background-color: #f0f9ff;
  border-color: #409eff;
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.calendar-date.today {
  border-color: #409eff;
  background-color: #f0f9ff;
}
.calendar-date.selected {
  background-color: #ecf5ff;
  border-color: #409eff;
}
/* çŠ¶æ€èƒŒæ™¯è‰² */
.status-background {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0.1;
  z-index: 0;
}
.calendar-date.has-attendance .status-background {
  background-color: #67c23a;
}
.calendar-date.has-late .status-background {
  background-color: #e6a23c;
}
.calendar-date.has-absent .status-background {
  background-color: #f56c6c;
}
.calendar-date.has-trip .status-background {
  background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
}
.date-number {
  font-weight: bold;
  font-size: 14px;
  margin-bottom: 2px;
  position: relative;
  z-index: 1;
}
.date-events {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: relative;
  z-index: 1;
}
.status-mark, .trip-mark, .absent-mark {
  display: flex;
  align-items: center;
  gap: 4px;
}
.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}
.status-dot.present { background-color: #67c23a; }
.status-dot.late { background-color: #e6a23c; }
.status-dot.absent { background-color: #f56c6c; }
.trip-mark i {
  color: #409eff;
  font-size: 12px;
}
.absent-mark i {
  color: #f56c6c;
  font-size: 12px;
}
/* ç®€ç•¥ä¿¡æ¯æ˜¾ç¤º */
.brief-info {
  position: absolute;
  bottom: 4px;
  right: 4px;
  display: flex;
  gap: 2px;
  z-index: 1;
}
.info-item {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: bold;
}
.present-info {
  background-color: #67c23a;
  color: white;
}
.late-info {
  background-color: #e6a23c;
  color: white;
}
.absent-info {
  background-color: #f56c6c;
  color: white;
}
.trip-info {
  background-color: #409eff;
  color: white;
}
.date-details {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 8px;
  z-index: 1000;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  display: none;
}
.calendar-date:hover .date-details {
  display: block;
}
.detail-item {
  font-size: 12px;
  margin-bottom: 4px;
  display: flex;
  align-items: center;
}
.detail-type {
  font-weight: bold;
  margin-right: 4px;
  min-width: 40px;
}
.detail-info {
  color: #606266;
}
.detail-section {
  margin-bottom: 20px;
}
.detail-section h4 {
  margin-bottom: 10px;
  color: #303133;
  border-left: 4px solid #409eff;
  padding-left: 8px;
}
.no-data {
  color: #909399;
  font-style: italic;
}
:deep(.el-calendar__header) {
  padding: 10px;
  border-bottom: 1px solid #ebeef5;
}
:deep(.el-calendar-day) {
  padding: 0 !important;
  height: 80px;
}
:deep(.el-calendar-table:not(.is-range) td) {
  border: 1px solid #f0f0f0;
}
:deep(.el-calendar-table .el-calendar-day) {
  height: 80px !important;
  padding: 0 !important;
}
</style>
src/views/OfficeRelated/checkingIn/components/AttendanceStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,277 @@
<template>
  <div class="attendance-statistics">
    <div class="statistics-controls">
      <el-date-picker
        v-model="selectedMonth"
        type="month"
        placeholder="选择统计月份"
        value-format="yyyy-MM"
        @change="handleMonthChange"
      />
      <el-button type="primary" icon="el-icon-refresh" @click="refreshData">
        åˆ·æ–°æ•°æ®
      </el-button>
    </div>
    <el-row :gutter="20" class="stats-cards">
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-icon primary">
              <i class="el-icon-user-solid"></i>
            </div>
            <div class="stat-content">
              <div class="stat-value">{{ stats.totalEmployees }}</div>
              <div class="stat-label">总员工数</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-icon success">
              <i class="el-icon-success"></i>
            </div>
            <div class="stat-content">
              <div class="stat-value">{{ stats.attendanceRate }}%</div>
              <div class="stat-label">平均出勤率</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-icon warning">
              <i class="el-icon-warning"></i>
            </div>
            <div class="stat-content">
              <div class="stat-value">{{ stats.totalLate }}</div>
              <div class="stat-label">总迟到次数</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-icon danger">
              <i class="el-icon-error"></i>
            </div>
            <div class="stat-content">
              <div class="stat-value">{{ stats.totalAbsence }}</div>
              <div class="stat-label">总缺勤天数</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="20" class="charts-section">
      <el-col :span="12">
        <el-card header="出勤趋势分析" shadow="never">
          <div class="chart-container">
            <!-- è¿™é‡Œå¯ä»¥é›†æˆ ECharts å›¾è¡¨ -->
            <div class="chart-placeholder">
              <i class="el-icon-data-analysis"></i>
              <p>出勤趋势图表</p>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card header="考勤分布" shadow="never">
          <div class="chart-container">
            <div class="chart-placeholder">
              <i class="el-icon-pie-chart"></i>
              <p>考勤分布图表</p>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-card header="详细统计表" shadow="never" class="detail-table">
      <el-table :data="stats.detailData" border>
        <el-table-column prop="department" label="部门"  />
        <el-table-column prop="employeeCount" label="员工数"  />
        <el-table-column prop="attendanceDays" label="应出勤天数"  />
        <el-table-column prop="actualDays" label="实际出勤"  />
        <el-table-column prop="lateTimes" label="迟到次数" >
          <template #default="scope">
            <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small">
              {{ scope.row.lateTimes }}
            </el-tag>
            <span v-else>{{ scope.row.lateTimes }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="absenceDays" label="缺勤天数" >
          <template #default="scope">
            <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small">
              {{ scope.row.absenceDays }}
            </el-tag>
            <span v-else>{{ scope.row.absenceDays }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="attendanceRate" label="出勤率" >
          <template #default="scope">
            <el-progress
              :percentage="scope.row.attendanceRate"
              :show-text="false"
            />
            <span>{{ scope.row.attendanceRate }}%</span>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>
<script>
export default {
  name: 'attendance-statistics',
  props: {
    data: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      selectedMonth: new Date().toISOString().slice(0, 7), // å½“前年月
      stats: {
        totalEmployees: 0,
        attendanceRate: 0,
        totalLate: 0,
        totalAbsence: 0,
        detailData: []
      }
    }
  },
  watch: {
    data: {
      handler(newData) {
        this.calculateStats(newData)
      },
      immediate: true
    }
  },
  methods: {
    calculateStats(data) {
      // æ¨¡æ‹Ÿç»Ÿè®¡è®¡ç®—
      this.stats = {
        totalEmployees: 156,
        attendanceRate: 92.5,
        totalLate: 24,
        totalAbsence: 12,
        detailData: [
          { department: '技术部', employeeCount: 45, attendanceDays: 22,
            actualDays: 41, lateTimes: 8, absenceDays: 2, attendanceRate: 93.2 },
          { department: '市场部', employeeCount: 32, attendanceDays: 22,
            actualDays: 30, lateTimes: 5, absenceDays: 1, attendanceRate: 95.5 },
          { department: '人事部', employeeCount: 18, attendanceDays: 22,
            actualDays: 17, lateTimes: 3, absenceDays: 4, attendanceRate: 89.3 },
          { department: '财务部', employeeCount: 15, attendanceDays: 22,
            actualDays: 14, lateTimes: 2, absenceDays: 1, attendanceRate: 93.8 }
        ]
      }
    },
    handleMonthChange(month) {
      this.$message.info(`切换统计月份: ${month}`)
      this.refreshData()
    },
    refreshData() {
      this.$emit('refresh')
    }
  }
}
</script>
<style scoped>
.attendance-statistics {
  padding: 20px;
}
.statistics-controls {
  margin-bottom: 24px;
  display: flex;
  gap: 16px;
  align-items: center;
}
.stats-cards {
  margin-bottom: 24px;
}
.stat-card {
  display: flex;
  align-items: center;
  padding: 16px;
}
.stat-icon {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 16px;
  font-size: 28px;
  color: white;
}
.stat-icon.primary { background: #409EFF; }
.stat-icon.success { background: #67C23A; }
.stat-icon.warning { background: #E6A23C; }
.stat-icon.danger { background: #F56C6C; }
.stat-content {
  flex: 1;
}
.stat-value {
  font-size: 28px;
  font-weight: bold;
  color: #303133;
  margin-bottom: 4px;
}
.stat-label {
  color: #909399;
  font-size: 14px;
}
.charts-section {
  margin-bottom: 24px;
}
.chart-container {
  height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.chart-placeholder {
  text-align: center;
  color: #909399;
}
.chart-placeholder i {
  font-size: 48px;
  margin-bottom: 16px;
  display: block;
}
.detail-table {
  margin-top: 24px;
}
</style>
src/views/OfficeRelated/checkingIn/components/AttendanceTable.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
<template>
  <div class="attendance-table">
    <div class="table-actions">
      <el-button type="primary" size="small" icon="el-icon-download" @click="exportData">
        å¯¼å‡ºæ•°æ®
      </el-button>
      <el-button size="small" icon="el-icon-refresh" @click="$emit('refresh')">
        åˆ·æ–°
      </el-button>
    </div>
    <el-table
      :data="tableData"
      border
      v-loading="loading"
      style="width: 100%"
      @sort-change="handleSortChange"
    >
      <el-table-column prop="id" label="ID" width="80" sortable />
      <el-table-column prop="employeeName" label="员工姓名"  sortable />
      <el-table-column prop="date" label="日期"  sortable />
      <el-table-column prop="checkIn" label="签到时间"  />
      <el-table-column prop="checkOut" label="签退时间"  />
      <el-table-column prop="workHours" label="工作时长"  sortable>
        <template #default="scope">
          <el-tag :type="getHoursType(scope.row.workHours)" size="small">
            {{ scope.row.workHours }}h
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="status" label="状态" >
        <template #default="scope">
          <el-tag
            :type="getStatusType(scope.row.status)"
            effect="light"
          >
            {{ scope.row.status }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150" fixed="right">
        <template #default="scope">
          <el-button
            size="mini"
            type="text"
            @click="handleViewDetail(scope.row)"
            icon="el-icon-view"
          >
            è¯¦æƒ…
          </el-button>
          <el-button
            size="mini"
            type="text"
            @click="handleEdit(scope.row)"
            icon="el-icon-edit"
          >
            ç¼–辑
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <div class="pagination-container">
      <el-pagination
        :current-page="currentPage"
        :page-size="pageSize"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>
<script>
export default {
  name: 'AttendanceTable',
  props: {
    data: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      currentPage: 1,
      pageSize: 10,
      sortProp: '',
      sortOrder: ''
    }
  },
  computed: {
    total() {
      return this.data.length
    },
    tableData() {
      let data = [...this.data]
      // æŽ’序处理
      if (this.sortProp) {
        data.sort((a, b) => {
          let aVal = a[this.sortProp]
          let bVal = b[this.sortProp]
          if (this.sortOrder === 'ascending') {
            return aVal > bVal ? 1 : -1
          } else {
            return aVal < bVal ? 1 : -1
          }
        })
      }
      // åˆ†é¡µå¤„理
      const start = (this.currentPage - 1) * this.pageSize
      const end = start + this.pageSize
      return data.slice(start, end)
    }
  },
  methods: {
    getStatusType(status) {
      const typeMap = {
        '正常': 'success',
        '迟到': 'warning',
        '早退': 'warning',
        '缺勤': 'danger'
      }
      return typeMap[status] || 'info'
    },
    getHoursType(hours) {
      const numHours = parseFloat(hours)
      if (numHours >= 8) return 'success'
      if (numHours >= 6) return 'warning'
      return 'danger'
    },
    handleViewDetail(row) {
      this.$emit('view-detail', row)
    },
    handleEdit(row) {
      this.$message.info(`编辑出勤记录: ${row.employeeName} - ${row.date}`)
    },
    exportData() {
      this.$message.success('导出功能开发中')
    },
    handleSortChange({ prop, order }) {
      this.sortProp = prop
      this.sortOrder = order
    },
    handleSizeChange(size) {
      this.pageSize = size
      this.currentPage = 1
    },
    handleCurrentChange(page) {
      this.currentPage = page
    }
  }
}
</script>
<style scoped>
.attendance-table {
  padding: 20px;
}
.table-actions {
  margin-bottom: 16px;
  display: flex;
  gap: 10px;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
</style>
src/views/OfficeRelated/checkingIn/components/BusinessTripTable.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,234 @@
<template>
  <div class="business-trip-table">
    <div class="table-header">
      <div class="header-actions">
        <el-button type="primary" size="small" icon="el-icon-plus" @click="handleAddTrip">
          æ–°å¢žå‡ºå·®
        </el-button>
        <el-button size="small" icon="el-icon-download" @click="exportData">
          å¯¼å‡ºæ•°æ®
        </el-button>
      </div>
      <div class="header-filters">
        <el-input
          v-model="filters.employeeName"
          placeholder="搜索员工姓名"
          prefix-icon="el-icon-search"
          style="width: 200px"
          clearable
        />
        <el-select v-model="filters.status" placeholder="状态筛选" clearable>
          <el-option label="进行中" value="进行中" />
          <el-option label="已完成" value="已完成" />
        </el-select>
      </div>
    </div>
    <el-table
      :data="filteredData"
      border
      v-loading="loading"
      style="width: 100%"
    >
      <el-table-column prop="tripNumber" label="出差单号" width="140" />
      <el-table-column prop="employeeName" label="员工姓名"  />
      <el-table-column prop="startCity" label="出发城市"  />
      <el-table-column prop="endCity" label="目的城市"  />
      <el-table-column prop="startDate" label="开始日期"  sortable />
      <el-table-column prop="endDate" label="结束日期"  sortable />
      <el-table-column prop="duration" label="出差天数" >
        <template #default="scope">
          <el-tag size="small">{{ calculateDuration(scope.row) }}天</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="distance" label="里程(km)"  sortable />
      <el-table-column prop="status" label="状态" >
        <template #default="scope">
          <el-tag
            :type="scope.row.status === '已完成' ? 'success' : 'primary'"
            effect="light"
          >
            {{ scope.row.status }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="180" fixed="right">
        <template #default="scope">
          <el-button
            size="mini"
            type="text"
            @click="handleViewDetail(scope.row)"
            icon="el-icon-view"
          >
            è¯¦æƒ…
          </el-button>
          <el-button
            size="mini"
            type="text"
            @click="handleEdit(scope.row)"
            icon="el-icon-edit"
          >
            ç¼–辑
          </el-button>
          <el-button
            size="mini"
            type="text"
            @click="handleDelete(scope.row)"
            icon="el-icon-delete"
            style="color: #f56c6c;"
          >
            åˆ é™¤
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <div class="pagination-container">
      <el-pagination
        :current-page="currentPage"
        :page-size="pageSize"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>
<script>
export default {
  name: 'BusinessTripTable',
  props: {
    data: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      currentPage: 1,
      pageSize: 10,
      filters: {
        employeeName: '',
        status: ''
      }
    }
  },
  computed: {
    filteredData() {
      let data = this.data.filter(item => {
        const nameMatch = !this.filters.employeeName ||
          item.employeeName.includes(this.filters.employeeName)
        const statusMatch = !this.filters.status ||
          item.status === this.filters.status
        return nameMatch && statusMatch
      })
      const start = (this.currentPage - 1) * this.pageSize
      const end = start + this.pageSize
      return data.slice(start, end)
    },
    total() {
      return this.data.filter(item => {
        const nameMatch = !this.filters.employeeName ||
          item.employeeName.includes(this.filters.employeeName)
        const statusMatch = !this.filters.status ||
          item.status === this.filters.status
        return nameMatch && statusMatch
      }).length
    }
  },
  methods: {
    calculateDuration(row) {
      const start = new Date(row.startDate)
      const end = new Date(row.endDate)
      const duration = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
      return duration
    },
    handleViewDetail(row) {
      this.$emit('view-detail', row)
    },
    handleAddTrip() {
      this.$message.info('打开新增出差对话框')
    },
    handleEdit(row) {
      this.$message.info(`编辑出差记录: ${row.tripNumber}`)
    },
    handleDelete(row) {
      this.$confirm('确定要删除这条出差记录吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$message.success('删除成功')
      }).catch(() => {})
    },
    exportData() {
      this.$message.success('导出功能开发中')
    },
    handleSizeChange(size) {
      this.pageSize = size
      this.currentPage = 1
    },
    handleCurrentChange(page) {
      this.currentPage = page
    }
  }
}
</script>
<style scoped>
.business-trip-table {
  padding: 20px;
}
.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  flex-wrap: wrap;
  gap: 16px;
}
.header-actions {
  display: flex;
  gap: 10px;
}
.header-filters {
  display: flex;
  gap: 10px;
  align-items: center;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
@media (max-width: 768px) {
  .table-header {
    flex-direction: column;
    align-items: stretch;
  }
  .header-actions {
    justify-content: space-between;
  }
}
</style>
src/views/OfficeRelated/checkingIn/components/MileageCalculation.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,325 @@
<template>
  <div class="mileage-calculation">
    <div class="calculation-header">
      <h3>出差里程核算</h3>
      <div class="header-actions">
        <el-button type="primary" icon="el-icon-calculator" @click="recalculateAll">
          é‡æ–°è®¡ç®—里程
        </el-button>
        <el-button icon="el-icon-download" @click="exportReport">
          å¯¼å‡ºæ ¸ç®—报告
        </el-button>
      </div>
    </div>
    <div class="filters-section">
      <el-form :model="filters" inline>
        <el-form-item label="员工姓名">
          <el-input
            v-model="filters.employeeName"
            placeholder="输入员工姓名"
            clearable
          />
        </el-form-item>
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="filters.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleFilter">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
    <el-table
      :data="filteredMileageData"
      border
      v-loading="loading"
      style="width: 100%"
    >
      <el-table-column prop="tripNumber" label="出差单号" width="140" />
      <el-table-column prop="employeeName" label="员工姓名"  />
      <el-table-column prop="startCity" label="出发城市"  />
      <el-table-column prop="endCity" label="目的城市"  />
      <el-table-column prop="startDate" label="开始日期"  />
      <el-table-column prop="endDate" label="结束日期"  />
      <el-table-column prop="calculatedDistance" label="核算里程(km)"  sortable>
        <template #default="scope">
          <el-tag type="info" size="small">
            {{ scope.row.calculatedDistance }}km
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="fuelCost" label="燃油费用(元)"  sortable>
        <template #default="scope">
          <span class="amount">Â¥{{ scope.row.fuelCost }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="allowance" label="出差补贴(元)"  sortable>
        <template #default="scope">
          <span class="amount">Â¥{{ scope.row.allowance }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="totalCost" label="总费用(元)"  sortable>
        <template #default="scope">
          <span class="amount total">Â¥{{ scope.row.totalCost }}</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150" fixed="right">
        <template #default="scope">
          <el-button
            size="mini"
            type="text"
            @click="handleRecalculate(scope.row)"
            icon="el-icon-refresh"
          >
            é‡æ–°è®¡ç®—
          </el-button>
          <el-button
            size="mini"
            type="text"
            @click="handleDetail(scope.row)"
            icon="el-icon-document"
          >
            æ˜Žç»†
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <div class="summary-section">
      <el-card shadow="never">
        <h4>费用汇总</h4>
        <el-row :gutter="20">
          <el-col :span="6">
            <div class="summary-item">
              <span class="label">总里程:</span>
              <span class="value">{{ totalMileage }}公里</span>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="summary-item">
              <span class="label">燃油费用:</span>
              <span class="value">Â¥{{ totalFuelCost }}</span>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="summary-item">
              <span class="label">出差补贴:</span>
              <span class="value">Â¥{{ totalAllowance }}</span>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="summary-item">
              <span class="label">总费用:</span>
              <span class="value total">Â¥{{ totalCost }}</span>
            </div>
          </el-col>
        </el-row>
      </el-card>
    </div>
    <div class="pagination-container">
      <el-pagination
        :current-page="currentPage"
        :page-size="pageSize"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>
<script>
export default {
  name: 'MileageCalculation',
  props: {
    data: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      currentPage: 1,
      pageSize: 10,
      filters: {
        employeeName: '',
        dateRange: []
      }
    }
  },
  computed: {
    filteredMileageData() {
      let data = this.data.filter(item => {
        const nameMatch = !this.filters.employeeName ||
          item.employeeName.includes(this.filters.employeeName)
        const dateMatch = !this.filters.dateRange || this.filters.dateRange.length !== 2 ||
          (item.startDate >= this.filters.dateRange[0] && item.endDate <= this.filters.dateRange[1])
        return nameMatch && dateMatch
      })
      const start = (this.currentPage - 1) * this.pageSize
      const end = start + this.pageSize
      return data.slice(start, end)
    },
    total() {
      return this.data.filter(item => {
        const nameMatch = !this.filters.employeeName ||
          item.employeeName.includes(this.filters.employeeName)
        const dateMatch = !this.filters.dateRange || this.filters.dateRange.length !== 2 ||
          (item.startDate >= this.filters.dateRange[0] && item.endDate <= this.filters.dateRange[1])
        return nameMatch && dateMatch
      }).length
    },
    totalMileage() {
      return this.filteredMileageData.reduce((sum, item) =>
        sum + parseFloat(item.calculatedDistance), 0
      ).toFixed(2)
    },
    totalFuelCost() {
      return this.filteredMileageData.reduce((sum, item) =>
        sum + parseFloat(item.fuelCost), 0
      ).toFixed(2)
    },
    totalAllowance() {
      return this.filteredMileageData.reduce((sum, item) =>
        sum + parseFloat(item.allowance), 0
      ).toFixed(2)
    },
    totalCost() {
      return this.filteredMileageData.reduce((sum, item) =>
        sum + parseFloat(item.totalCost), 0
      ).toFixed(2)
    }
  },
  methods: {
    handleFilter() {
      this.currentPage = 1
    },
    handleReset() {
      this.filters = {
        employeeName: '',
        dateRange: []
      }
      this.currentPage = 1
    },
    recalculateAll() {
      this.$confirm('确定要重新计算所有出差记录的里程吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$message.success('里程重新计算完成')
      }).catch(() => {})
    },
    handleRecalculate(row) {
      this.$message.info(`重新计算 ${row.employeeName} çš„里程`)
    },
    handleDetail(row) {
      this.$message.info(`查看 ${row.tripNumber} çš„详细费用明细`)
    },
    exportReport() {
      this.$message.success('导出功能开发中')
    },
    handleSizeChange(size) {
      this.pageSize = size
      this.currentPage = 1
    },
    handleCurrentChange(page) {
      this.currentPage = page
    }
  }
}
</script>
<style scoped>
.mileage-calculation {
  padding: 20px;
}
.calculation-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
}
.calculation-header h3 {
  margin: 0;
  color: #303133;
}
.header-actions {
  display: flex;
  gap: 10px;
}
.filters-section {
  margin-bottom: 20px;
  padding: 16px;
  background: #f8f9fa;
  border-radius: 4px;
}
.amount {
  font-weight: 600;
  color: #409EFF;
}
.amount.total {
  color: #F56C6C;
  font-size: 14px;
}
.summary-section {
  margin: 24px 0;
}
.summary-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
}
.summary-item .label {
  color: #606266;
}
.summary-item .value {
  font-weight: 600;
  color: #303133;
}
.summary-item .value.total {
  color: #F56C6C;
  font-size: 16px;
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  margin-top: 20px;
}
</style>
src/views/OfficeRelated/checkingIn/components/PersonBusiness.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,500 @@
<template>
  <div class="personal-business-trip-table">
    <div class="table-header">
      <h4>出差记录</h4>
      <div class="header-actions">
        <el-button
          size="small"
          type="primary"
          @click="handleAddTrip"
          icon="el-icon-plus"
        >
          æ–°å¢žå‡ºå·®
        </el-button>
        <el-button size="small" @click="exportToExcel" icon="el-icon-download">
          å¯¼å‡ºExcel
        </el-button>
      </div>
    </div>
    <!-- ç­›é€‰æ¡ä»¶ -->
    <el-card class="filter-card" shadow="never">
      <el-form :model="filterForm" inline>
        <el-form-item label="出差日期">
          <el-date-picker
            v-model="filterForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
          />
        </el-form-item>
        <el-form-item label="目的地">
          <el-input
            v-model="filterForm.destination"
            placeholder="输入目的地"
            clearable
          />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="filterForm.status" clearable>
            <el-option label="全部" value="" />
            <el-option label="进行中" value="进行中" />
            <el-option label="已完成" value="已完成" />
            <el-option label="已取消" value="已取消" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleFilter">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <el-table
      :data="filteredTrips"
      border
      style="width: 100%"
      v-loading="loading"
      class="business-trip-table"
    >
      <el-table-column prop="tripNumber" label="出差单号" width="140" fixed>
        <template #default="scope">
          <el-tag type="info" size="small">{{ scope.row.tripNumber }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="startCity" label="出发城市" >
        <template #default="scope">
          <span class="city-cell">{{ scope.row.startCity }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="endCity" label="目的城市" >
        <template #default="scope">
          <span class="city-cell">{{ scope.row.endCity }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="startDate" label="开始日期"  sortable>
        <template #default="scope">
          <span class="date-cell">{{ scope.row.startDate }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="endDate" label="结束日期"  sortable>
        <template #default="scope">
          <span class="date-cell">{{ scope.row.endDate }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="duration" label="出差天数" >
        <template #default="scope">
          <el-tag>{{ scope.row.duration }}天</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="distance" label="里程(km)"  sortable>
        <template #default="scope">
          <span class="distance-cell">{{ scope.row.distance }}km</span>
        </template>
      </el-table-column>
      <el-table-column
        prop="purpose"
        label="出差目的"
        min-width="150"
        show-overflow-tooltip
      >
        <template #default="scope">
          <span class="purpose-cell">{{ scope.row.purpose }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="status" label="状态"  fixed="right">
        <template #default="scope">
          <el-tag :type="getStatusType(scope.row.status)" effect="light">
            {{ scope.row.status }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150" fixed="right">
        <template #default="scope">
          <el-button
            size="mini"
            type="text"
            @click="viewTripDetails(scope.row)"
            icon="el-icon-view"
          >
            è¯¦æƒ…
          </el-button>
          <el-button
            size="mini"
            type="text"
            @click="editTrip(scope.row)"
            icon="el-icon-edit"
            :disabled="scope.row.status === '已完成'"
          >
            ç¼–辑
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <div class="pagination-container">
      <el-pagination
        :current-page="currentPage"
        :page-size="pageSize"
        :total="totalRecords"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
    <!-- å‡ºå·®è¯¦æƒ…对话框 -->
    <el-dialog
    :title="`出差详情 - ${currentTrip ? currentTrip.tripNumber : ''}`"
      :visible.sync="detailDialogVisible"
      width="600px"
    >
      <div v-if="currentTrip" class="trip-details">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="出差单号">
            {{ currentTrip.tripNumber }}
          </el-descriptions-item>
          <el-descriptions-item label="状态">
            <el-tag :type="getStatusType(currentTrip.status)">
              {{ currentTrip.status }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="出发城市">
            {{ currentTrip.startCity }}
          </el-descriptions-item>
          <el-descriptions-item label="目的城市">
            {{ currentTrip.endCity }}
          </el-descriptions-item>
          <el-descriptions-item label="开始日期">
            {{ currentTrip.startDate }}
          </el-descriptions-item>
          <el-descriptions-item label="结束日期">
            {{ currentTrip.endDate }}
          </el-descriptions-item>
          <el-descriptions-item label="出差天数">
            {{ currentTrip.duration }}天
          </el-descriptions-item>
          <el-descriptions-item label="里程距离">
            {{ currentTrip.distance }}公里
          </el-descriptions-item>
          <el-descriptions-item label="出差目的" :span="2">
            {{ currentTrip.purpose }}
          </el-descriptions-item>
          <el-descriptions-item label="备注" :span="2">
            {{ currentTrip.remarks || "无" }}
          </el-descriptions-item>
        </el-descriptions>
        <div
          v-if="currentTrip.expenses && currentTrip.expenses.length > 0"
          class="expenses-section"
        >
          <h5>费用明细</h5>
          <el-table :data="currentTrip.expenses" size="small">
            <el-table-column prop="item" label="费用项目" />
            <el-table-column prop="amount" label="金额" />
            <el-table-column prop="date" label="日期" />
          </el-table>
        </div>
      </div>
    </el-dialog>
    <!-- æ–°å¢ž/编辑出差对话框 -->
    <el-dialog
      :title="isEditing ? '编辑出差记录' : '新增出差记录'"
      :visible.sync="editDialogVisible"
      width="500px"
    >
      <el-form
        :model="tripForm"
        :rules="tripRules"
        ref="tripForm"
        label-width="100px"
      >
        <el-form-item label="目的城市" prop="endCity">
          <el-input v-model="tripForm.endCity" placeholder="请输入目的城市" />
        </el-form-item>
        <el-form-item label="开始日期" prop="startDate">
          <el-date-picker
            v-model="tripForm.startDate"
            type="date"
            placeholder="选择开始日期"
            value-format="yyyy-MM-dd"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="结束日期" prop="endDate">
          <el-date-picker
            v-model="tripForm.endDate"
            type="date"
            placeholder="选择结束日期"
            value-format="yyyy-MM-dd"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="出差目的" prop="purpose">
          <el-input
            v-model="tripForm.purpose"
            type="textarea"
            :rows="3"
            placeholder="请输入出差目的"
          />
        </el-form-item>
      </el-form>
      <span slot="footer">
        <el-button @click="editDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="saveTrip">保存</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
export default {
  name: "PersonBusiness",
  props: {
    data: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      currentPage: 1,
      pageSize: 10,
      filterForm: {
        dateRange: [],
        destination: "",
        status: ""
      },
      detailDialogVisible: false,
      editDialogVisible: false,
      isEditing: false,
      currentTrip: null,
      tripForm: {
        endCity: "",
        startDate: "",
        endDate: "",
        purpose: ""
      },
      tripRules: {
        endCity: [
          { required: true, message: "请输入目的城市", trigger: "blur" }
        ],
        startDate: [
          { required: true, message: "请选择开始日期", trigger: "change" }
        ],
        endDate: [
          { required: true, message: "请选择结束日期", trigger: "change" }
        ],
        purpose: [
          { required: true, message: "请输入出差目的", trigger: "blur" }
        ]
      }
    };
  },
  computed: {
    totalRecords() {
      return this.filteredTrips.length;
    },
    filteredTrips() {
      let filtered = this.data;
      // æŒ‰æ—¥æœŸèŒƒå›´è¿‡æ»¤
      if (this.filterForm.dateRange && this.filterForm.dateRange.length === 2) {
        const [start, end] = this.filterForm.dateRange;
        filtered = filtered.filter(item => {
          const itemStart = new Date(item.startDate);
          return itemStart >= new Date(start) && itemStart <= new Date(end);
        });
      }
      // æŒ‰ç›®çš„地过滤
      if (this.filterForm.destination) {
        filtered = filtered.filter(item =>
          item.endCity.includes(this.filterForm.destination)
        );
      }
      // æŒ‰çŠ¶æ€è¿‡æ»¤
      if (this.filterForm.status) {
        filtered = filtered.filter(
          item => item.status === this.filterForm.status
        );
      }
      return filtered;
    }
  },
  methods: {
    getStatusType(status) {
      const typeMap = {
        è¿›è¡Œä¸­: "primary",
        å·²å®Œæˆ: "success",
        å·²å–消: "danger"
      };
      return typeMap[status] || "info";
    },
    viewTripDetails(trip) {
      this.currentTrip = trip;
      this.detailDialogVisible = true;
    },
    handleAddTrip() {
      this.isEditing = false;
      this.tripForm = {
        endCity: "",
        startDate: "",
        endDate: "",
        purpose: ""
      };
      this.editDialogVisible = true;
    },
    editTrip(trip) {
      this.isEditing = true;
      this.currentTrip = trip;
      this.tripForm = { ...trip };
      this.editDialogVisible = true;
    },
    saveTrip() {
      this.$refs.tripForm.validate(valid => {
        if (valid) {
          // è¿™é‡Œåº”该调用API保存数据
          this.$message.success(this.isEditing ? "修改成功" : "新增成功");
          this.editDialogVisible = false;
          this.$emit("refresh");
        }
      });
    },
    handleFilter() {
      this.currentPage = 1;
    },
    handleReset() {
      this.filterForm = {
        dateRange: [],
        destination: "",
        status: ""
      };
      this.currentPage = 1;
    },
    exportToExcel() {
      // ç®€åŒ–版导出逻辑
      this.$message.success("导出功能开发中");
    },
    handleSizeChange(size) {
      this.pageSize = size;
      this.currentPage = 1;
    },
    handleCurrentChange(page) {
      this.currentPage = page;
    }
  }
};
</script>
<style scoped>
.personal-business-trip-table {
  padding: 20px;
}
.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.table-header h4 {
  margin: 0;
  color: #303133;
  font-size: 16px;
}
.header-actions {
  display: flex;
  gap: 10px;
}
.filter-card {
  margin-bottom: 20px;
}
.business-trip-table {
  margin-bottom: 20px;
}
.city-cell {
  font-weight: 500;
}
.date-cell {
  color: #606266;
}
.distance-cell {
  font-weight: 600;
  color: #409eff;
}
.purpose-cell {
  color: #606266;
  font-size: 13px;
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  margin-top: 20px;
}
.trip-details {
  padding: 10px;
}
.expenses-section {
  margin-top: 20px;
}
.expenses-section h5 {
  margin-bottom: 10px;
  color: #303133;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .table-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
  .header-actions {
    width: 100%;
    justify-content: flex-end;
  }
}
</style>
src/views/OfficeRelated/checkingIn/components/PersonalAttendanceReport.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,682 @@
<template>
  <div class="personal-attendance-report">
    <div class="report-header">
      <h4>考勤统计报表</h4>
      <div class="header-actions">
        <el-select v-model="reportPeriod" @change="handlePeriodChange" size="small">
          <el-option label="本月" value="month" />
          <el-option label="本季度" value="quarter" />
          <el-option label="本年度" value="year" />
        </el-select>
        <el-button
          size="small"
          @click="exportReport"
          icon="el-icon-download"
        >
          å¯¼å‡ºæŠ¥è¡¨
        </el-button>
      </div>
    </div>
    <!-- ç»Ÿè®¡æ¦‚览 -->
    <el-row :gutter="20" class="stats-overview">
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon attendance-icon">
              <i class="el-icon-date"></i>
            </div>
            <div class="stat-info">
              <div class="stat-value">{{ overview.totalDays }}</div>
              <div class="stat-label">总出勤天数</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon present-icon">
              <i class="el-icon-success"></i>
            </div>
            <div class="stat-info">
              <div class="stat-value">{{ overview.presentDays }}</div>
              <div class="stat-label">正常出勤</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon abnormal-icon">
              <i class="el-icon-warning"></i>
            </div>
            <div class="stat-info">
              <div class="stat-value">{{ overview.abnormalDays }}</div>
              <div class="stat-label">异常天数</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-content">
            <div class="stat-icon rate-icon">
              <i class="el-icon-data-analysis"></i>
            </div>
            <div class="stat-info">
              <div class="stat-value">{{ overview.attendanceRate }}%</div>
              <div class="stat-label">出勤率</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <el-row :gutter="20" class="charts-section">
      <el-col :span="12">
        <el-card header="出勤趋势" shadow="never">
          <div id="attendanceTrendChart" class="chart-container"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card header="考勤分布" shadow="never">
          <div id="attendanceDistributionChart" class="chart-container"></div>
        </el-card>
      </el-col>
    </el-row>
    <!-- è¯¦ç»†ç»Ÿè®¡è¡¨æ ¼ -->
    <el-card header="详细统计" class="detail-table-card" shadow="never">
      <el-table :data="detailedStats" border style="width: 100%">
        <el-table-column prop="month" label="月份"  />
        <el-table-column prop="workDays" label="应出勤天数"  />
        <el-table-column prop="actualDays" label="实际出勤"  />
        <el-table-column prop="lateTimes" label="迟到次数" >
          <template #default="scope">
            <el-tag v-if="scope.row.lateTimes > 0" type="warning" size="small">
              {{ scope.row.lateTimes }}
            </el-tag>
            <span v-else>{{ scope.row.lateTimes }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="leaveEarlyTimes" label="早退次数" >
          <template #default="scope">
            <el-tag v-if="scope.row.leaveEarlyTimes > 0" type="warning" size="small">
              {{ scope.row.leaveEarlyTimes }}
            </el-tag>
            <span v-else>{{ scope.row.leaveEarlyTimes }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="absenceDays" label="缺勤天数" >
          <template #default="scope">
            <el-tag v-if="scope.row.absenceDays > 0" type="danger" size="small">
              {{ scope.row.absenceDays }}
            </el-tag>
            <span v-else>{{ scope.row.absenceDays }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="businessTripDays" label="出差天数" >
          <template #default="scope">
            <el-tag v-if="scope.row.businessTripDays > 0" type="primary" size="small">
              {{ scope.row.businessTripDays }}
            </el-tag>
            <span v-else>{{ scope.row.businessTripDays }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="attendanceRate" label="出勤率" >
          <template #default="scope">
            <el-progress
              :percentage="scope.row.attendanceRate"
              :show-text="false"
              :color="getProgressColor(scope.row.attendanceRate)"
            />
            <span>{{ scope.row.attendanceRate }}%</span>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- å¼‚常记录 -->
    <el-card header="异常记录" class="abnormal-records-card" shadow="never">
      <el-table :data="abnormalRecords" border style="width: 100%">
        <el-table-column prop="date" label="日期"  />
        <el-table-column prop="type" label="异常类型" >
          <template #default="scope">
            <el-tag :type="getAbnormalType(scope.row.type)" size="small">
              {{ scope.row.type }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="description" label="描述" min-width="200" />
        <el-table-column prop="duration" label="时长"  />
        <el-table-column prop="status" label="状态" >
          <template #default="scope">
            <el-tag
              :type="scope.row.status === '已处理' ? 'success' : 'warning'"
              size="small"
            >
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" >
          <template #default="scope">
            <el-button type="text" size="mini" @click="viewAbnormalDetail(scope.row)">
              æŸ¥çœ‹
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'PersonalAttendanceReport',
  props: {
    stats: {
      type: Object,
      default: () => ({})
    },
    attendanceData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      reportPeriod: 'month',
      overview: {
        totalDays: 0,
        presentDays: 0,
        abnormalDays: 0,
        attendanceRate: 0
      },
      detailedStats: [],
      abnormalRecords: [],
      trendChart: null,
      distributionChart: null
    }
  },
  mounted() {
    this.initData()
    this.$nextTick(() => {
      this.initCharts()
    })
  },
  beforeDestroy() {
    // é”€æ¯å›¾è¡¨å®žä¾‹
    if (this.trendChart) {
      this.trendChart.dispose()
    }
    if (this.distributionChart) {
      this.distributionChart.dispose()
    }
  },
  methods: {
    initData() {
      // åˆå§‹åŒ–概览数据
      this.overview = {
        totalDays: this.stats.totalDays || 22,
        presentDays: this.stats.presentDays || 20,
        abnormalDays: this.stats.abnormalDays || 2,
        attendanceRate: this.stats.attendanceRate || 90.9
      }
      // åˆå§‹åŒ–详细统计
      this.detailedStats = [
        { month: '2024-12', workDays: 22, actualDays: 20, lateTimes: 2,
          leaveEarlyTimes: 1, absenceDays: 0, businessTripDays: 3, attendanceRate: 90.9 },
        { month: '2024-11', workDays: 21, actualDays: 19, lateTimes: 1,
          leaveEarlyTimes: 0, absenceDays: 1, businessTripDays: 2, attendanceRate: 90.5 },
        { month: '2024-10', workDays: 23, actualDays: 22, lateTimes: 0,
          leaveEarlyTimes: 0, absenceDays: 0, businessTripDays: 1, attendanceRate: 95.7 }
      ]
      // åˆå§‹åŒ–异常记录
      this.abnormalRecords = [
        { date: '2024-12-15', type: '迟到', description: '早上迟到30分钟', duration: '30分钟', status: '已处理' },
        { date: '2024-12-08', type: '早退', description: '下午提前1小时离开', duration: '1小时', status: '已处理' },
        { date: '2024-11-20', type: '缺勤', description: '病假', duration: '1天', status: '已处理' }
      ]
    },
    initCharts() {
      this.initTrendChart()
      this.initDistributionChart()
      this.setupChartResize()
    },
    initTrendChart() {
      const chartDom = document.getElementById('attendanceTrendChart')
      if (!chartDom) return
      this.trendChart = echarts.init(chartDom)
      const option = {
        tooltip: {
          trigger: 'axis',
          formatter: function(params) {
            let result = params[0].axisValue + '<br/>'
            params.forEach(param => {
              result += `${param.seriesName}: ${param.value}<br/>`
            })
            return result
          }
        },
        legend: {
          data: ['出勤天数', '异常天数', '出勤率'],
          bottom: 10
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '15%',
          top: '10%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: ['10月', '11月', '12月']
        },
        yAxis: [
          {
            type: 'value',
            name: '天数',
            min: 0,
            max: 30
          },
          {
            type: 'value',
            name: '出勤率(%)',
            min: 0,
            max: 100,
            axisLabel: {
              formatter: '{value}%'
            }
          }
        ],
        series: [
          {
            name: '出勤天数',
            type: 'bar',
            barWidth: '30%',
            data: [22, 19, 20],
            itemStyle: {
              color: '#409EFF'
            }
          },
          {
            name: '异常天数',
            type: 'bar',
            barWidth: '30%',
            data: [1, 2, 2],
            itemStyle: {
              color: '#F56C6C'
            }
          },
          {
            name: '出勤率',
            type: 'line',
            yAxisIndex: 1,
            data: [95.7, 90.5, 90.9],
            itemStyle: {
              color: '#67C23A'
            },
            lineStyle: {
              width: 3
            }
          }
        ]
      }
      this.trendChart.setOption(option)
    },
    initDistributionChart() {
      const chartDom = document.getElementById('attendanceDistributionChart')
      if (!chartDom) return
      this.distributionChart = echarts.init(chartDom)
      const option = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b}: {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          right: 10,
          top: 'center',
          data: ['正常出勤', '迟到', '早退', '缺勤', '出差']
        },
        series: [
          {
            name: '考勤分布',
            type: 'pie',
            radius: ['40%', '70%'],
            center: ['40%', '50%'],
            avoidLabelOverlap: false,
            itemStyle: {
              borderColor: '#fff',
              borderWidth: 2
            },
            label: {
              show: false,
              position: 'center'
            },
            emphasis: {
              label: {
                show: true,
                fontSize: 18,
                fontWeight: 'bold'
              }
            },
            labelLine: {
              show: false
            },
            data: [
              { value: 20, name: '正常出勤', itemStyle: { color: '#67C23A' } },
              { value: 2, name: '迟到', itemStyle: { color: '#E6A23C' } },
              { value: 1, name: '早退', itemStyle: { color: '#F56C6C' } },
              { value: 0, name: '缺勤', itemStyle: { color: '#909399' } },
              { value: 3, name: '出差', itemStyle: { color: '#409EFF' } }
            ]
          }
        ]
      }
      this.distributionChart.setOption(option)
    },
    setupChartResize() {
      // ç›‘听窗口变化,重新渲染图表
      const handleResize = () => {
        if (this.trendChart) {
          this.trendChart.resize()
        }
        if (this.distributionChart) {
          this.distributionChart.resize()
        }
      }
      window.addEventListener('resize', handleResize)
      this.$once('hook:beforeDestroy', () => {
        window.removeEventListener('resize', handleResize)
      })
    },
    handlePeriodChange(period) {
      this.reportPeriod = period
      this.updateChartData()
    },
    updateChartData() {
      // æ ¹æ®é€‰æ‹©çš„周期更新图表数据
      let data
      switch (this.reportPeriod) {
        case 'month':
          data = this.getMonthlyData()
          break
        case 'quarter':
          data = this.getQuarterlyData()
          break
        case 'year':
          data = this.getYearlyData()
          break
        default:
          data = this.getMonthlyData()
      }
      this.updateCharts(data)
    },
    getMonthlyData() {
      // æ¨¡æ‹Ÿæœˆåº¦æ•°æ®
      return {
        xAxis: ['10月', '11月', '12月'],
        attendance: [22, 19, 20],
        abnormal: [1, 2, 2],
        rate: [95.7, 90.5, 90.9]
      }
    },
    getQuarterlyData() {
      // æ¨¡æ‹Ÿå­£åº¦æ•°æ®
      return {
        xAxis: ['Q1', 'Q2', 'Q3', 'Q4'],
        attendance: [65, 62, 58, 61],
        abnormal: [5, 8, 6, 4],
        rate: [92.8, 88.6, 90.2, 93.5]
      }
    },
    getYearlyData() {
      // æ¨¡æ‹Ÿå¹´åº¦æ•°æ®
      return {
        xAxis: ['2022', '2023', '2024'],
        attendance: [240, 248, 252],
        abnormal: [25, 18, 15],
        rate: [90.2, 92.5, 94.1]
      }
    },
    updateCharts(data) {
      if (this.trendChart) {
        const option = this.trendChart.getOption()
        option.xAxis[0].data = data.xAxis
        option.series[0].data = data.attendance
        option.series[1].data = data.abnormal
        option.series[2].data = data.rate
        this.trendChart.setOption(option)
      }
    },
    getProgressColor(rate) {
      if (rate >= 95) return '#67C23A'
      if (rate >= 90) return '#E6A23C'
      if (rate >= 80) return '#F56C6C'
      return '#909399'
    },
    getAbnormalType(type) {
      const typeMap = {
        '迟到': 'warning',
        '早退': 'warning',
        '缺勤': 'danger',
        '请假': 'info'
      }
      return typeMap[type] || 'info'
    },
    viewAbnormalDetail(record) {
      this.$message.info(`查看异常记录: ${record.date} - ${record.type}`)
      // è¿™é‡Œå¯ä»¥æ‰“开详情对话框
    },
    exportReport() {
      this.$message.success('报表导出功能开发中')
      // è¿™é‡Œå¯ä»¥å®žçŽ°å¯¼å‡ºPDF或Excel功能
    }
  }
}
</script>
<style scoped>
.personal-attendance-report {
  padding: 20px;
  background: #fff;
  border-radius: 8px;
}
.report-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid #ebeef5;
}
.report-header h4 {
  margin: 0;
  color: #303133;
  font-size: 20px;
  font-weight: 600;
}
.header-actions {
  display: flex;
  gap: 12px;
  align-items: center;
}
.stats-overview {
  margin-bottom: 24px;
}
.stat-card {
  border-radius: 8px;
  transition: all 0.3s ease;
}
.stat-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-content {
  display: flex;
  align-items: center;
  padding: 16px;
}
.stat-icon {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 16px;
  font-size: 24px;
  color: white;
}
.attendance-icon {
  background: linear-gradient(135deg, #409EFF, #79BBFF);
}
.present-icon {
  background: linear-gradient(135deg, #67C23A, #95D475);
}
.abnormal-icon {
  background: linear-gradient(135deg, #E6A23C, #EEBD6D);
}
.rate-icon {
  background: linear-gradient(135deg, #F56C6C, #F89898);
}
.stat-info {
  flex: 1;
}
.stat-value {
  font-size: 28px;
  font-weight: bold;
  color: #303133;
  margin-bottom: 4px;
}
.stat-label {
  color: #909399;
  font-size: 14px;
}
.charts-section {
  margin-bottom: 24px;
}
.chart-container {
  width: 100%;
  height: 300px;
}
.detail-table-card,
.abnormal-records-card {
  margin-bottom: 20px;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
  .stats-overview .el-col {
    margin-bottom: 16px;
  }
}
@media (max-width: 768px) {
  .personal-attendance-report {
    padding: 12px;
  }
  .report-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  .header-actions {
    width: 100%;
    justify-content: space-between;
  }
  .stat-content {
    padding: 12px;
  }
  .stat-icon {
    width: 50px;
    height: 50px;
    font-size: 20px;
    margin-right: 12px;
  }
  .stat-value {
    font-size: 24px;
  }
  .chart-container {
    height: 250px;
  }
}
/* åŠ¨ç”»æ•ˆæžœ */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
/* è¡¨æ ¼æ ·å¼ä¼˜åŒ– */
.el-table {
  border-radius: 4px;
  overflow: hidden;
}
.el-table::before {
  display: none;
}
/* å¡ç‰‡æ ‡é¢˜æ ·å¼ */
.el-card__header {
  background: #f8f9fa;
  border-bottom: 1px solid #ebeef5;
  font-weight: 600;
  color: #303133;
}
</style>
src/views/OfficeRelated/checkingIn/components/PersonalAttendanceTable.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
<template>
  <div class="personal-attendance-table">
    <div class="table-header">
      <h4>出勤记录详情</h4>
      <div class="header-actions">
        <el-button size="small" @click="exportToCSV" icon="el-icon-download">
          å¯¼å‡ºCSV
        </el-button>
        <el-tooltip content="刷新数据" placement="top">
          <el-button size="small" @click="refreshData" icon="el-icon-refresh">
            åˆ·æ–°
          </el-button>
        </el-tooltip>
      </div>
    </div>
    <el-table
      :data="filteredData"
      border
      style="width: 100%"
      v-loading="loading"
      class="attendance-table"
    >
      <el-table-column prop="date" label="日期"  sortable>
        <template #default="scope">
          <span class="date-cell">{{ scope.row.date }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="checkIn" label="签到时间" >
        <template #default="scope">
          <span :class="getTimeClass(scope.row.checkIn, 'checkIn')">
            {{ scope.row.checkIn || '-' }}
          </span>
        </template>
      </el-table-column>
      <el-table-column prop="checkOut" label="签退时间" >
        <template #default="scope">
          <span :class="getTimeClass(scope.row.checkOut, 'checkOut')">
            {{ scope.row.checkOut || '-' }}
          </span>
        </template>
      </el-table-column>
      <el-table-column prop="workHours" label="工作时长"  sortable>
        <template #default="scope">
          <el-tag
            :type="getWorkHoursType(scope.row.workHours)"
            size="small"
          >
            {{ scope.row.workHours }}h
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="status" label="状态" >
        <template #default="scope">
          <el-tag
            :type="getStatusType(scope.row.status)"
            effect="plain"
          >
            {{ scope.row.status }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="remarks" label="备注" min-width="150" show-overflow-tooltip>
        <template #default="scope">
          <span class="remarks-cell">{{ scope.row.remarks || '无' }}</span>
        </template>
      </el-table-column>
      <el-table-column label="操作"  fixed="right">
        <template #default="scope">
          <el-button
            size="mini"
            type="text"
            @click="viewDetails(scope.row)"
            icon="el-icon-view"
          >
            è¯¦æƒ…
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <div class="pagination-container">
      <el-pagination
        :current-page="currentPage"
        :page-size="pageSize"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
    <!-- è¯¦æƒ…对话框 -->
    <el-dialog
      title="出勤记录详情"
      :visible.sync="detailDialogVisible"
      width="500px"
    >
      <div v-if="currentRecord" class="record-details">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="日期">
            {{ currentRecord.date }}
          </el-descriptions-item>
          <el-descriptions-item label="签到时间">
            <el-tag :type="getTimeClass(currentRecord.checkIn, 'checkIn')">
              {{ currentRecord.checkIn }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="签退时间">
            <el-tag :type="getTimeClass(currentRecord.checkOut, 'checkOut')">
              {{ currentRecord.checkOut }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="工作时长">
            <el-tag :type="getWorkHoursType(currentRecord.workHours)">
              {{ currentRecord.workHours }} å°æ—¶
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="状态">
            <el-tag :type="getStatusType(currentRecord.status)">
              {{ currentRecord.status }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="备注">
            {{ currentRecord.remarks || '无' }}
          </el-descriptions-item>
        </el-descriptions>
      </div>
    </el-dialog>
  </div>
</template>
<script>
export default {
  name: 'PersonalAttendanceTable',
  props: {
    data: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      currentPage: 1,
      pageSize: 10,
      detailDialogVisible: false,
      currentRecord: null,
      filterForm: {
        dateRange: [],
        status: ''
      }
    }
  },
  computed: {
    total() {
      return this.data.length
    },
    filteredData() {
      let filtered = this.data
      // æŒ‰æ—¥æœŸèŒƒå›´è¿‡æ»¤
      if (this.filterForm.dateRange && this.filterForm.dateRange.length === 2) {
        const [start, end] = this.filterForm.dateRange
        filtered = filtered.filter(item => {
          const itemDate = new Date(item.date)
          return itemDate >= new Date(start) && itemDate <= new Date(end)
        })
      }
      // æŒ‰çŠ¶æ€è¿‡æ»¤
      if (this.filterForm.status) {
        filtered = filtered.filter(item => item.status === this.filterForm.status)
      }
      // åˆ†é¡µ
      const start = (this.currentPage - 1) * this.pageSize
      const end = start + this.pageSize
      return filtered.slice(start, end)
    }
  },
  methods: {
    getTimeClass(time, type) {
      if (!time) return 'text-muted'
      const hour = parseInt(time.split(':')[0])
      if (type === 'checkIn') {
        return hour > 9 ? 'text-danger' : 'text-success'
      } else {
        return hour < 18 ? 'text-warning' : 'text-success'
      }
    },
    getWorkHoursType(hours) {
      const numHours = parseFloat(hours)
      if (numHours >= 8) return 'success'
      if (numHours >= 6) return 'warning'
      return 'danger'
    },
    getStatusType(status) {
      const typeMap = {
        '正常': 'success',
        '迟到': 'warning',
        '早退': 'warning',
        '缺勤': 'danger',
        '出差': 'primary'
      }
      return typeMap[status] || 'info'
    },
    viewDetails(record) {
      this.currentRecord = record
      this.detailDialogVisible = true
    },
    exportToCSV() {
      // ç®€åŒ–版CSV导出逻辑
      const headers = ['日期', '签到时间', '签退时间', '工作时长', '状态', '备注']
      const csvData = this.data.map(item => [
        item.date,
        item.checkIn,
        item.checkOut,
        item.workHours,
        item.status,
        item.remarks || ''
      ])
      const csvContent = [headers, ...csvData]
        .map(row => row.map(field => `"${field}"`).join(','))
        .join('\n')
      const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' })
      const link = document.createElement('a')
      link.href = URL.createObjectURL(blob)
      link.download = `出勤记录_${new Date().toISOString().split('T')[0]}.csv`
      link.click()
    },
    refreshData() {
      this.$emit('refresh')
    },
    handleSizeChange(size) {
      this.pageSize = size
      this.currentPage = 1
    },
    handleCurrentChange(page) {
      this.currentPage = page
    }
  }
}
</script>
<style scoped>
.personal-attendance-table {
  padding: 20px;
}
.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.table-header h4 {
  margin: 0;
  color: #303133;
  font-size: 16px;
}
.header-actions {
  display: flex;
  gap: 10px;
}
.attendance-table {
  margin-bottom: 20px;
}
.date-cell {
  font-weight: 500;
}
.text-success { color: #67c23a; }
.text-warning { color: #e6a23c; }
.text-danger { color: #f56c6c; }
.text-muted { color: #909399; }
.text-primary { color: #409eff; }
.remarks-cell {
  color: #606266;
  font-size: 13px;
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  margin-top: 20px;
}
.record-details {
  padding: 10px;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .table-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
  .header-actions {
    width: 100%;
    justify-content: flex-end;
  }
}
</style>
src/views/OfficeRelated/checkingIn/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,317 @@
<template>
  <div class="attendance-management">
    <!-- é¡¶éƒ¨ç»Ÿè®¡ä¿¡æ¯ -->
    <el-card class="statistics-card">
      <div class="statistics-header">
        <h3>考勤统计概览</h3>
        <span class="statistics-date">{{ currentDate }}</span>
      </div>
      <el-row :gutter="20" class="statistics-content">
        <el-col :span="6">
          <div class="stat-item">
            <div class="stat-value">{{ statistics.totalEmployees }}</div>
            <div class="stat-label">总员工数</div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="stat-item">
            <div class="stat-value text-success">{{ statistics.onDuty }}</div>
            <div class="stat-label">今日出勤</div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="stat-item">
            <div class="stat-value text-warning">{{ statistics.onBusinessTrip }}</div>
            <div class="stat-label">出差中</div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="stat-item">
            <div class="stat-value text-danger">{{ statistics.absent }}</div>
            <div class="stat-label">缺勤</div>
          </div>
        </el-col>
      </el-row>
    </el-card>
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <el-card class="filter-card">
      <el-form :model="queryParams" inline>
        <el-form-item label="年月">
          <el-date-picker
            v-model="queryParams.month"
            type="month"
            placeholder="选择年月"
            value-format="yyyy-MM"
          />
        </el-form-item>
        <el-form-item label="员工姓名">
          <el-input
            v-model="queryParams.employeeName"
            placeholder="请输入员工姓名"
            clearable
          />
        </el-form-item>
        <el-form-item label="考勤类型">
          <el-select v-model="queryParams.attendanceType" clearable>
            <el-option label="全部" value="" />
            <el-option label="出勤" value="attendance" />
            <el-option label="出差" value="business_trip" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- é€‰é¡¹å¡ -->
    <el-card>
      <el-tabs v-model="activeTab" @tab-click="handleTabChange">
        <el-tab-pane label="出勤记录" name="attendance">
          <attendance-table
            :data="attendanceData"
            :loading="loading"
            @view-detail="handleViewDetail"
          />
        </el-tab-pane>
        <el-tab-pane label="出差记录" name="businessTrip">
          <business-trip-table
            :data="businessTripData"
            :loading="loading"
            @view-detail="handleViewDetail"
          />
        </el-tab-pane>
        <el-tab-pane label="统计出勤" name="statistics">
          <attendance-statistics
            :data="statisticsData"
            :loading="loading"
          />
        </el-tab-pane>
        <el-tab-pane label="出差里程核算" name="mileage">
          <mileage-calculation
            :data="mileageData"
            :loading="loading"
          />
        </el-tab-pane>
      </el-tabs>
    </el-card>
  </div>
</template>
<script>
import AttendanceTable from './components/AttendanceTable.vue'
import BusinessTripTable from './components/BusinessTripTable.vue'
import AttendanceStatistics from './components/AttendanceStatistics.vue'
import MileageCalculation from './components/MileageCalculation.vue'
export default {
  name: 'AttendanceList',
  components: {
    AttendanceTable,
    BusinessTripTable,
    AttendanceStatistics,
    MileageCalculation
  },
  data() {
    return {
      currentDate: new Date().toLocaleDateString('zh-CN'),
      statistics: {
        totalEmployees: 156,
        onDuty: 142,
        onBusinessTrip: 8,
        absent: 6
      },
      queryParams: {
        month: '',
        employeeName: '',
        attendanceType: ''
      },
      activeTab: 'attendance',
      loading: false,
      attendanceData: [],
      businessTripData: [],
      statisticsData: [],
      mileageData: []
    }
  },
  created() {
    this.loadData()
  },
  methods: {
    // åŠ è½½æ¨¡æ‹Ÿæ•°æ®
    async loadData() {
      this.loading = true
      try {
        // æ¨¡æ‹ŸAPI调用延迟
        await new Promise(resolve => setTimeout(resolve, 500))
        // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
        this.attendanceData = this.generateAttendanceData()
        this.businessTripData = this.generateBusinessTripData()
        this.statisticsData = this.generateStatisticsData()
        this.mileageData = this.generateMileageData()
      } catch (error) {
        console.error('加载数据失败:', error)
      } finally {
        this.loading = false
      }
    },
    // ç”Ÿæˆå‡ºå‹¤æ¨¡æ‹Ÿæ•°æ®
    generateAttendanceData() {
      const employees = ['张三', '李四', '王五', '赵六', '钱七', '孙八']
      const statuses = ['正常', '迟到', '早退', '缺勤']
      const data = []
      for (let i = 0; i < 20; i++) {
        data.push({
          id: i + 1,
          employeeName: employees[Math.floor(Math.random() * employees.length)],
          date: `2024-12-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
          checkIn: `08:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
          checkOut: `18:${String(Math.floor(Math.random() * 30)).padStart(2, '0')}`,
          status: statuses[Math.floor(Math.random() * statuses.length)],
          workHours: (8 + Math.random() * 2).toFixed(1)
        })
      }
      return data
    },
    // ç”Ÿæˆå‡ºå·®æ¨¡æ‹Ÿæ•°æ®
    generateBusinessTripData() {
      const cities = ['北京', '上海', '广州', '深圳', '杭州', '成都']
      const data = []
      for (let i = 0; i < 15; i++) {
        const startDate = new Date(2024, 11, Math.floor(Math.random() * 28) + 1)
        const endDate = new Date(startDate.getTime() + Math.random() * 5 * 24 * 60 * 60 * 1000)
        data.push({
          id: i + 1,
          employeeName: `员工${String(i + 1).padStart(3, '0')}`,
          tripNumber: `BT202412${String(i + 1).padStart(3, '0')}`,
          startCity: cities[Math.floor(Math.random() * cities.length)],
          endCity: cities[Math.floor(Math.random() * cities.length)],
          startDate: startDate.toISOString().split('T')[0],
          endDate: endDate.toISOString().split('T')[0],
          distance: Math.floor(Math.random() * 1000) + 200,
          status: ['进行中', '已完成'][Math.floor(Math.random() * 2)]
        })
      }
      return data
    },
    // ç”Ÿæˆç»Ÿè®¡æ¨¡æ‹Ÿæ•°æ®
    generateStatisticsData() {
      const months = ['2024-01', '2024-02', '2024-03', '2024-04', '2024-05', '2024-06']
      return months.map(month => ({
        month,
        attendanceDays: Math.floor(Math.random() * 20) + 15,
        lateTimes: Math.floor(Math.random() * 5),
        leaveEarlyTimes: Math.floor(Math.random() * 3),
        absenceDays: Math.floor(Math.random() * 3)
      }))
    },
    // ç”Ÿæˆé‡Œç¨‹æ¨¡æ‹Ÿæ•°æ®
    generateMileageData() {
      return this.businessTripData.map(item => ({
        ...item,
        calculatedDistance: item.distance,
        fuelCost: (item.distance * 0.8).toFixed(2),
        allowance: (item.distance * 0.5).toFixed(2)
      }))
    },
    handleQuery() {
      this.loadData()
    },
    handleReset() {
      this.queryParams = {
        month: '',
        employeeName: '',
        attendanceType: ''
      }
      this.loadData()
    },
    handleTabChange(tab) {
      this.activeTab = tab.name
    },
    handleViewDetail(employee) {
      this.$router.push({
        path: '/office/checkingInInfo',
        query: {
          employeeId: employee.id,
          employeeName: employee.employeeName
        }
      })
    }
  }
}
</script>
<style scoped>
.attendance-management {
  padding: 20px;
}
.statistics-card {
  margin-bottom: 20px;
}
.statistics-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.statistics-header h3 {
  margin: 0;
  color: #303133;
}
.statistics-date {
  color: #909399;
  font-size: 14px;
}
.statistics-content {
  text-align: center;
}
.stat-item {
  padding: 20px;
  border-radius: 8px;
  background: #f8f9fa;
}
.stat-value {
  font-size: 32px;
  font-weight: bold;
  color: #409eff;
  margin-bottom: 8px;
}
.stat-label {
  color: #606266;
  font-size: 14px;
}
.text-success { color: #67c23a; }
.text-warning { color: #e6a23c; }
.text-danger { color: #f56c6c; }
.filter-card {
  margin-bottom: 20px;
}
</style>
src/views/OfficeRelated/checkingIn/mockData.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
// åœ¨çˆ¶ç»„ä»¶ data() ä¸­æˆ–单独创建 mockData.js æ–‡ä»¶
export const generateMockData = () => {
  return {
    // å‘˜å·¥è€ƒå‹¤æ•°æ®
    attendanceData: [
      {
        id: 1,
        date: '2024-12-01',
        checkIn: '08:30',
        checkOut: '18:00',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 2,
        date: '2024-12-02',
        checkIn: '09:15',
        checkOut: '18:00',
        status: 'late',
        workHours: 8.75
      },
      {
        id: 3,
        date: '2024-12-03',
        checkIn: '08:45',
        checkOut: '17:30',
        status: 'present',
        workHours: 8.75
      },
      {
        id: 4,
        date: '2024-12-04',
        checkIn: '08:25',
        checkOut: '18:10',
        status: 'present',
        workHours: 9.75
      },
      {
        id: 5,
        date: '2024-12-05',
        checkIn: null,
        checkOut: null,
        status: 'absent',
        workHours: 0
      },
      {
        id: 6,
        date: '2024-12-08',
        checkIn: '08:40',
        checkOut: '17:45',
        status: 'present',
        workHours: 9.0
      },
      {
        id: 7,
        date: '2024-12-09',
        checkIn: '08:35',
        checkOut: '18:05',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 8,
        date: '2024-12-10',
        checkIn: '09:05',
        checkOut: '17:50',
        status: 'late',
        workHours: 8.75
      },
      {
        id: 9,
        date: '2024-12-11',
        checkIn: '08:50',
        checkOut: '18:15',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 10,
        date: '2024-12-12',
        checkIn: '08:30',
        checkOut: '17:40',
        status: 'present',
        workHours: 9.0
      },
      {
        id: 11,
        date: '2024-12-15',
        checkIn: '08:45',
        checkOut: '18:00',
        status: 'present',
        workHours: 9.25
      },
      {
        id: 12,
        date: '2024-12-16',
        checkIn: '08:55',
        checkOut: '17:55',
        status: 'present',
        workHours: 9.0
      },
      {
        id: 13,
        date: '2024-12-17',
        checkIn: '08:40',
        checkOut: '18:10',
        status: 'present',
        workHours: 9.5
      },
      {
        id: 14,
        date: '2024-12-18',
        checkIn: '09:20',
        checkOut: '17:30',
        status: 'late',
        workHours: 8.0
      },
      {
        id: 15,
        date: '2024-12-19',
        checkIn: '08:35',
        checkOut: '18:05',
        status: 'present',
        workHours: 9.5
      }
    ],
    // å‡ºå·®æ•°æ®
    businessTripData: [
      {
        id: 1,
        tripNumber: 'BT202412001',
        startCity: '北京',
        endCity: '上海',
        startDate: '2024-12-05',
        endDate: '2024-12-08',
        distance: 1200,
        purpose: '客户会议',
        status: 'completed'
      },
      {
        id: 2,
        tripNumber: 'BT202412002',
        startCity: '北京',
        endCity: '广州',
        startDate: '2024-12-15',
        endDate: '2024-12-18',
        distance: 1900,
        purpose: '项目调研',
        status: 'completed'
      },
      {
        id: 3,
        tripNumber: 'BT202412003',
        startCity: '北京',
        endCity: '深圳',
        startDate: '2024-12-22',
        endDate: '2024-12-24',
        distance: 1950,
        purpose: '技术交流',
        status: 'completed'
      }
    ]
  }
}
src/views/business/GetWitness/GetWitnessInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1577 @@
<template>
  <div class="organ-procurement-detail">
    <!-- åŸºæœ¬ä¿¡æ¯ -->
    <el-card class="detail-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">器官获取基本信息</span>
        <div style="float: right;">
          <el-button type="primary" @click="handleSave" :loading="saveLoading">
            ä¿å­˜
          </el-button>
          <el-button
            type="success"
            @click="handleProcure"
            :disabled="form.procurementStatus === 'procured'"
          >
            ç¡®è®¤èŽ·å–
          </el-button>
        </div>
      </div>
      <el-form :model="form" ref="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="住院号" prop="hospitalNo">
              <el-input v-model="form.hospitalNo" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="案例编号" prop="caseNo">
              <el-input v-model="form.caseNo" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="捐献者姓名" prop="donorName">
              <el-input v-model="form.donorName" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="性别" prop="gender">
              <el-select v-model="form.gender" style="width: 100%">
                <el-option label="男" value="0" />
                <el-option label="女" value="1" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="年龄" prop="age">
              <el-input v-model="form.age" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="出生日期" prop="birthDate">
              <el-date-picker
                v-model="form.birthDate"
                type="date"
                value-format="yyyy-MM-dd"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="疾病诊断" prop="diagnosis">
              <el-input
                type="textarea"
                :rows="2"
                v-model="form.diagnosis"
                placeholder="请输入疾病诊断信息"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="获取时间" prop="procurementTime">
              <el-date-picker
                v-model="form.procurementTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
                :disabled="form.procurementStatus !== 'procured'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="手术名称" prop="surgeryName">
              <el-input v-model="form.surgeryName" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="手术开始时间" prop="surgeryStartTime">
              <el-date-picker
                v-model="form.surgeryStartTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="供体死亡时间" prop="donorDeathTime">
              <el-date-picker
                v-model="form.donorDeathTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="腹主动脉插管时间" prop="abdominalAortaCannulationTime">
              <el-date-picker
                v-model="form.abdominalAortaCannulationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="下腔静脉插管时间" prop="inferiorVenaCavaCannulationTime">
              <el-date-picker
                v-model="form.inferiorVenaCavaCannulationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="肠系膜上静脉插管时间" prop="superiorMesentericVeinCannulationTime">
              <el-date-picker
                v-model="form.superiorMesentericVeinCannulationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="登记人" prop="registrant">
              <el-input v-model="form.registrant" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="登记时间" prop="registrationTime">
              <el-date-picker
                v-model="form.registrationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
                readonly
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </el-card>
    <!-- å™¨å®˜èŽ·å–è®°å½•éƒ¨åˆ† -->
    <el-card class="procurement-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">器官获取记录</span>
        <div style="float: right;">
          <el-tag :type="form.procurementStatus === 'procured' ? 'success' : 'warning'">
            {{ form.procurementStatus === 'procured' ? '已获取' : '待获取' }}
          </el-tag>
        </div>
      </div>
      <el-form
        ref="procurementForm"
        :rules="procurementRules"
        :model="procurementData"
        label-position="right"
      >
        <el-row>
          <el-col>
            <el-form-item label-width="100px" label="获取器官">
              <el-checkbox-group v-model="selectedOrgans" @change="handleOrganSelectionChange">
                <el-checkbox
                  v-for="dict in dict.type.sys_Organ || []"
                  :key="dict.value"
                  :label="dict.value"
                  :disabled="form.procurementStatus === 'procured'"
                >
                  {{ dict.label }}
                </el-checkbox>
              </el-checkbox-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col>
            <el-form-item>
              <el-table
                :data="procurementData.records"
                v-loading="loading"
                border
                style="width: 100%"
                :row-class-name="getOrganRowClassName"
              >
                <el-table-column
                  label="器官名称"
                  align="center"
                  width="120"
                  prop="organName"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.organName"
                      placeholder="器官名称"
                      :disabled="true"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="获取开始时间"
                  align="center"
                  width="180"
                  prop="organStartTime"
                >
                  <template slot-scope="scope">
                    <el-date-picker
                      clearable
                      size="small"
                      style="width: 100%"
                      v-model="scope.row.organStartTime"
                      type="datetime"
                      value-format="yyyy-MM-dd HH:mm:ss"
                      placeholder="选择获取开始时间"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="器官离体时间"
                  align="center"
                  width="180"
                  prop="organGetTime"
                >
                  <template slot-scope="scope">
                    <el-date-picker
                      clearable
                      size="small"
                      style="width: 100%"
                      v-model="scope.row.organGetTime"
                      type="datetime"
                      value-format="yyyy-MM-dd HH:mm:ss"
                      placeholder="选择器官离体时间"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="获取医院"
                  align="center"
                  width="200"
                  prop="gainHospitalNo"
                >
                  <template slot-scope="scope">
                    <el-select
                      v-model="scope.row.gainHospitalNo"
                      placeholder="请选择获取医院"
                      style="width: 100%"
                      :disabled="form.procurementStatus === 'procured'"
                      @change="handleHospitalChange(scope.row, $event)"
                    >
                      <el-option
                        v-for="hospital in hospitalList"
                        :key="hospital.hospitalNo"
                        :label="hospital.hospitalName"
                        :value="hospital.hospitalNo"
                      />
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column
                  label="获取医师"
                  align="center"
                  width="120"
                  prop="organGetDoctor"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.organGetDoctor"
                      placeholder="获取医师"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="助手"
                  align="center"
                  width="120"
                  prop="assistant"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.assistant"
                      placeholder="助手"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="获取护士"
                  align="center"
                  width="120"
                  prop="procurementNurse"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.procurementNurse"
                      placeholder="获取护士"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="手术室护士"
                  align="center"
                  width="120"
                  prop="operatingRoomNurse"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.operatingRoomNurse"
                      placeholder="手术室护士"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="麻醉医生"
                  align="center"
                  width="120"
                  prop="anesthesiologist"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.anesthesiologist"
                      placeholder="麻醉医生"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="获取状态"
                  align="center"
                  width="120"
                  prop="organState"
                >
                  <template slot-scope="scope">
                    <el-select
                      v-model="scope.row.organState"
                      placeholder="请选择获取状态"
                      style="width: 100%"
                      :disabled="form.procurementStatus === 'procured'"
                    >
                      <el-option
                        v-for="dict in organStateList"
                        :key="dict.value"
                        :label="dict.label"
                        :value="dict.value"
                      />
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column
                  label="说明"
                  align="center"
                  prop="notGetReason"
                  min-width="200"
                >
                  <template slot-scope="scope">
                    <el-input
                      type="textarea"
                      clearable
                      v-model="scope.row.notGetReason"
                      placeholder="请输入未获取说明"
                      :disabled="form.procurementStatus === 'procured'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="操作"
                  align="center"
                  width="120"
                  class-name="small-padding fixed-width"
                  v-if="form.procurementStatus !== 'procured'"
                >
                  <template slot-scope="scope">
                    <el-button
                      size="mini"
                      type="text"
                      icon="el-icon-edit"
                      @click="handleEditProcurement(scope.row)"
                    >
                      ç¼–辑
                    </el-button>
                  </template>
                </el-table-column>
              </el-table>
            </el-form-item>
          </el-col>
        </el-row>
        <!-- èŽ·å–ç»Ÿè®¡ä¿¡æ¯ -->
        <div class="procurement-stats" v-if="procurementData.records.length > 0">
          <el-row :gutter="20">
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">已获取器官:</span>
                <span class="stat-value">{{ procurementData.records.length }} ä¸ª</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">待完善信息:</span>
                <span class="stat-value">{{ incompleteRecords }} ä¸ª</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">涉及医院:</span>
                <span class="stat-value">{{ uniqueHospitals }} å®¶</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">获取状态:</span>
                <span class="stat-value">
                  <el-tag :type="form.procurementStatus === 'procured' ? 'success' : 'warning'">
                    {{ form.procurementStatus === 'procured' ? '已完成' : '进行中' }}
                  </el-tag>
                </span>
              </div>
            </el-col>
          </el-row>
        </div>
        <div v-else class="empty-procurement">
          <el-empty description="暂无获取记录" :image-size="80">
            <span>请先选择要获取的器官</span>
          </el-empty>
        </div>
      </el-form>
      <div class="dialog-footer" v-if="form.procurementStatus !== 'procured'">
        <el-button
          type="primary"
          @click="handleSaveProcurement"
          :loading="saveLoading"
          :disabled="procurementData.records.length === 0"
        >
          ä¿å­˜èŽ·å–è®°å½•
        </el-button>
        <el-button
          type="success"
          @click="handleConfirmProcurement"
          :loading="confirmLoading"
          :disabled="incompleteRecords > 0"
        >
          ç¡®è®¤å®ŒæˆèŽ·å–
        </el-button>
      </div>
    </el-card>
    <!-- é™„件管理部分 -->
    <el-card class="attachment-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">相关附件</span>
        <el-button
          type="primary"
          size="mini"
          icon="el-icon-upload"
          @click="handleUploadAttachment"
        >
          ä¸Šä¼ é™„ä»¶
        </el-button>
      </div>
      <div class="attachment-list">
        <el-table :data="attachments" style="width: 100%">
          <el-table-column label="文件名称" min-width="200">
            <template slot-scope="scope">
              <div class="file-info">
                <i :class="getFileIcon(scope.row.fileName)" style="margin-right: 8px; color: #409EFF;"></i>
                <span>{{ scope.row.fileName }}</span>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="文件类型" width="100" align="center">
            <template slot-scope="scope">
              <el-tag size="small">{{ getFileType(scope.row.fileName) }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column label="文件大小" width="100" align="center">
            <template slot-scope="scope">
              <span>{{ formatFileSize(scope.row.fileSize) }}</span>
            </template>
          </el-table-column>
          <el-table-column label="上传时间" width="160" align="center">
            <template slot-scope="scope">
              <span>{{ parseTime(scope.row.uploadTime) }}</span>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="150" align="center">
            <template slot-scope="scope">
              <el-button
                size="mini"
                type="text"
                icon="el-icon-view"
                @click="handlePreviewAttachment(scope.row)"
              >预览</el-button>
              <el-button
                size="mini"
                type="text"
                icon="el-icon-download"
                @click="handleDownloadAttachment(scope.row)"
              >下载</el-button>
              <el-button
                size="mini"
                type="text"
                icon="el-icon-delete"
                style="color: #F56C6C;"
                @click="handleRemoveAttachment(scope.row)"
              >删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-card>
    <!-- ç¼–辑获取记录对话框 -->
    <el-dialog
      title="编辑器官获取记录"
      :visible.sync="editDialogVisible"
      width="600px"
    >
      <el-form :model="currentRecord" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="器官名称">
              <el-input v-model="currentRecord.organName" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="获取状态">
              <el-select v-model="currentRecord.organState" style="width: 100%">
                <el-option
                  v-for="dict in organStateList"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="获取医师">
              <el-input v-model="currentRecord.organGetDoctor" placeholder="请输入获取医师" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="助手">
              <el-input v-model="currentRecord.assistant" placeholder="请输入助手姓名" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="获取护士">
              <el-input v-model="currentRecord.procurementNurse" placeholder="请输入获取护士" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="手术室护士">
              <el-input v-model="currentRecord.operatingRoomNurse" placeholder="请输入手术室护士" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="麻醉医生">
          <el-input v-model="currentRecord.anesthesiologist" placeholder="请输入麻醉医生" />
        </el-form-item>
        <el-form-item label="未获取说明" v-if="currentRecord.organState === '0'">
          <el-input
            type="textarea"
            :rows="3"
            v-model="currentRecord.notGetReason"
            placeholder="请输入未获取的原因说明"
          />
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="editDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleEditConfirm">确认</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import {
  getOrganProcurementDetail,
  updateOrganProcurement,
  saveProcurementRecords,
  getHospitalList,
  getCoordinatorList
} from "./organProcurement";
export default {
  name: "OrganProcurementDetail",
  dicts: ["sys_user_sex", "sys_Organ", "sys_0_1", "sys_DonationCategory"],
  data() {
    return {
      // è¡¨å•数据
      form: {
        id: undefined,
        hospitalNo: "",
        caseNo: "",
        donorName: "",
        gender: "",
        age: "",
        birthDate: "",
        diagnosis: "",
        procurementStatus: "pending",
        procurementTime: "",
        registrant: "",
        registrationTime: "",
        surgeryName: "",
        surgeryStartTime: "",
        donorDeathTime: "",
        abdominalAortaCannulationTime: "",
        inferiorVenaCavaCannulationTime: "",
        superiorMesentericVeinCannulationTime: "",
        donationCategory: "1",
        deathJudgeDoctor1: "",
        deathJudgeDoctor2: "",
        deathReason: "",
        operationEndTime: "",
        coordinatorInOperating: "",
        coordinatorOutOperating: "",
        coordinatorSignTime: "",
        responsibleUserName: "",
        coordinatedUserId1: "",
        coordinatedUserId2: "",
        isSpendRemember: 1,
        isRestoreRemains: 1
      },
      // è¡¨å•验证规则
      rules: {
        donorName: [
          { required: true, message: "捐献者姓名不能为空", trigger: "blur" }
        ],
        diagnosis: [
          { required: true, message: "疾病诊断不能为空", trigger: "blur" }
        ],
        surgeryName: [
          { required: true, message: "手术名称不能为空", trigger: "blur" }
        ]
      },
      // èŽ·å–è®°å½•éªŒè¯è§„åˆ™
      procurementRules: {},
      // ä¿å­˜åŠ è½½çŠ¶æ€
      saveLoading: false,
      confirmLoading: false,
      // åŠ è½½çŠ¶æ€
      loading: false,
      // é€‰ä¸­çš„器官
      selectedOrgans: [],
      // åŒ»é™¢åˆ—表
      hospitalList: [],
      // åè°ƒå‘˜åˆ—表
      coordinatorList: [],
      // å™¨å®˜çŠ¶æ€åˆ—è¡¨
      organStateList: [
        { value: "1", label: "已获取" },
        { value: "0", label: "未获取" },
        { value: "2", label: "部分获取" }
      ],
      // èŽ·å–è®°å½•æ•°æ®
      procurementData: {
        records: []
      },
      // é™„件数据
      attachments: [],
      // ç¼–辑对话框
      editDialogVisible: false,
      currentRecord: {},
      currentEditIndex: -1
    };
  },
  computed: {
    // å½“前用户信息
    currentUser() {
      return JSON.parse(sessionStorage.getItem("user") || "{}");
    },
    // ä¸å®Œæ•´çš„记录数量
    incompleteRecords() {
      return this.procurementData.records.filter(
        record =>
          !record.organStartTime ||
          !record.organGetTime ||
          !record.gainHospitalNo ||
          !record.organGetDoctor
      ).length;
    },
    // å”¯ä¸€åŒ»é™¢æ•°é‡
    uniqueHospitals() {
      const hospitals = this.procurementData.records
        .map(record => record.gainHospitalNo)
        .filter(Boolean);
      return new Set(hospitals).size;
    }
  },
  created() {
    const id = this.$route.query.id;
    if (id) {
      this.getDetail(id);
    } else {
      this.generateCaseNo();
      this.form.registrant = this.currentUser.username || "当前用户";
      this.form.registrationTime = new Date()
        .toISOString()
        .replace("T", " ")
        .substring(0, 19);
    }
    this.getHospitalData();
    this.getCoordinatorData();
  },
  methods: {
    // ç”Ÿæˆæ¡ˆä¾‹ç¼–号
    generateCaseNo() {
      const timestamp = Date.now().toString();
      this.form.hospitalNo = "D" + timestamp.slice(-6);
      this.form.caseNo = "C" + timestamp.slice(-6);
    },
    // èŽ·å–è¯¦æƒ…
    getDetail(id) {
      this.loading = true;
      getOrganProcurementDetail(id)
        .then(response => {
          if (response.code === 200) {
            this.form = response.data;
            if (response.data.procurementRecords) {
              this.procurementData.records = response.data.procurementRecords;
              this.selectedOrgans = response.data.procurementRecords.map(
                item => item.organNo
              );
            }
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取器官获取详情失败:", error);
          this.loading = false;
          this.$message.error("获取详情失败");
        });
    },
    // èŽ·å–åŒ»é™¢æ•°æ®
    getHospitalData() {
      getHospitalList().then(response => {
        if (response.code === 200) {
          this.hospitalList = response.data;
        }
      });
    },
    // èŽ·å–åè°ƒå‘˜æ•°æ®
    getCoordinatorData() {
      getCoordinatorList().then(response => {
        if (response.code === 200) {
          this.coordinatorList = response.data;
        }
      });
    },
    // å™¨å®˜é€‰æ‹©çŠ¶æ€å˜åŒ–
    handleOrganSelectionChange(selectedValues) {
      const currentOrganNos = this.procurementData.records.map(
        item => item.organNo
      );
      // æ–°å¢žé€‰æ‹©çš„器官
      selectedValues.forEach(organValue => {
        if (!currentOrganNos.includes(organValue)) {
          const organInfo = this.dict.type.sys_Organ.find(
            item => item.value === organValue
          );
          if (organInfo) {
            this.procurementData.records.push({
              organName: organInfo.label,
              organNo: organValue,
              id: null,
              procurementId: this.form.id,
              organStartTime: "",
              organGetTime: "",
              gainHospitalNo: "",
              gainHospitalName: "",
              organGetDoctor: "",
              assistant: "",
              procurementNurse: "",
              operatingRoomNurse: "",
              anesthesiologist: "",
              organState: "1",
              notGetReason: ""
            });
          }
        }
      });
      // ç§»é™¤å–消选择的器官
      this.procurementData.records = this.procurementData.records.filter(
        record => {
          if (selectedValues.includes(record.organNo)) {
            return true;
          } else {
            if (record.id) {
              this.$confirm(
                "删除器官获取数据后将无法恢复,您确认删除该条记录吗?",
                "提示",
                {
                  confirmButtonText: "确定",
                  cancelButtonText: "取消",
                  type: "warning"
                }
              )
                .then(() => {
                  this.procurementData.records = this.procurementData.records.filter(
                    r => r.organNo !== record.organNo
                  );
                  this.$message.success("删除成功");
                })
                .catch(() => {
                  this.selectedOrgans.push(record.organNo);
                });
              return true;
            } else {
              return false;
            }
          }
        }
      );
    },
    // åŒ»é™¢é€‰æ‹©å˜åŒ–
    handleHospitalChange(row, hospitalNo) {
      const hospital = this.hospitalList.find(
        item => item.hospitalNo === hospitalNo
      );
      if (hospital) {
        row.gainHospitalName = hospital.hospitalName;
      }
    },
    // ç¼–辑获取记录
    handleEditProcurement(row) {
      const index = this.procurementData.records.findIndex(
        item => item.organNo === row.organNo
      );
      if (index !== -1) {
        this.currentRecord = { ...row };
        this.currentEditIndex = index;
        this.editDialogVisible = true;
      }
    },
    // ç¡®è®¤ç¼–辑
    handleEditConfirm() {
      if (this.currentEditIndex !== -1) {
        this.procurementData.records[this.currentEditIndex] = {
          ...this.currentRecord
        };
        this.$message.success("获取记录更新成功");
        this.editDialogVisible = false;
      }
    },
    // å™¨å®˜è¡Œæ ·å¼
    getOrganRowClassName({ row }) {
      if (
        !row.organStartTime ||
        !row.organGetTime ||
        !row.gainHospitalNo ||
        !row.organGetDoctor
      ) {
        return "warning-row";
      }
      return "";
    },
    // ä¿å­˜åŸºæœ¬ä¿¡æ¯
    handleSave() {
      this.$refs.form.validate(valid => {
        if (valid) {
          this.saveLoading = true;
          const apiMethod = this.form.id
            ? updateOrganProcurement
            : addOrganProcurement;
          apiMethod(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("保存成功");
                if (!this.form.id) {
                  this.form.id = response.data.id;
                  this.$router.replace({
                    query: { ...this.$route.query, id: this.form.id }
                  });
                }
              }
            })
            .catch(error => {
              console.error("保存失败:", error);
              this.$message.error("保存失败");
            })
            .finally(() => {
              this.saveLoading = false;
            });
        }
      });
    },
    // ä¿å­˜èŽ·å–è®°å½•
    handleSaveProcurement() {
      if (!this.form.id) {
        this.$message.warning("请先保存基本信息");
        return;
      }
      this.saveLoading = true;
      saveProcurementRecords(this.form.id, this.procurementData.records)
        .then(response => {
          if (response.code === 200) {
            this.$message.success("获取记录保存成功");
          }
        })
        .catch(error => {
          console.error("保存获取记录失败:", error);
          this.$message.error("保存获取记录失败");
        })
        .finally(() => {
          this.saveLoading = false;
        });
    },
    // ç¡®è®¤å®ŒæˆèŽ·å–
    handleConfirmProcurement() {
      if (this.incompleteRecords > 0) {
        this.$message.warning("请先完善所有获取记录的信息");
        return;
      }
      this.$confirm("确认完成器官获取吗?完成后将无法修改获取信息。", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.confirmLoading = true;
          this.form.procurementStatus = "procured";
          this.form.procurementTime = new Date()
            .toISOString()
            .replace("T", " ")
            .substring(0, 19);
          updateOrganProcurement(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("器官获取已完成");
              }
            })
            .catch(error => {
              console.error("确认获取失败:", error);
              this.$message.error("确认获取失败");
            })
            .finally(() => {
              this.confirmLoading = false;
            });
        })
        .catch(() => {});
    },
    // ä¸Šä¼ é™„ä»¶
    handleUploadAttachment() {
      this.$message.info("附件上传功能");
    },
    // é¢„览附件
    handlePreviewAttachment(attachment) {
      this.$message.info("附件预览功能");
    },
    // ä¸‹è½½é™„ä»¶
    handleDownloadAttachment(attachment) {
      this.$message.info("附件下载功能");
    },
    // åˆ é™¤é™„ä»¶
    handleRemoveAttachment(attachment) {
      this.$confirm("确定要删除这个附件吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$message.success("附件删除成功");
        })
        .catch(() => {});
    },
    // èŽ·å–æ–‡ä»¶å›¾æ ‡
    getFileIcon(fileName) {
      const ext = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const iconMap = {
        pdf: "el-icon-document",
        doc: "el-icon-document",
        docx: "el-icon-document",
        xls: "el-icon-document",
        xlsx: "el-icon-document",
        jpg: "el-icon-picture",
        jpeg: "el-icon-picture",
        png: "el-icon-picture"
      };
      return iconMap[ext] || "el-icon-document";
    },
    // èŽ·å–æ–‡ä»¶ç±»åž‹
    getFileType(fileName) {
      const ext = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const typeMap = {
        pdf: "PDF",
        doc: "DOC",
        docx: "DOCX",
        xls: "XLS",
        xlsx: "XLSX",
        jpg: "JPG",
        jpeg: "JPEG",
        png: "PNG"
      };
      return typeMap[ext] || ext.toUpperCase();
    },
    // æ–‡ä»¶å¤§å°æ ¼å¼åŒ–
    formatFileSize(size) {
      if (size === 0) return "0 B";
      const k = 1024;
      const sizes = ["B", "KB", "MB", "GB"];
      const i = Math.floor(Math.log(size) / Math.log(k));
      return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time) {
      if (!time) return "";
      const date = new Date(time);
      return `${date.getFullYear()}-${(date.getMonth() + 1)
        .toString()
        .padStart(2, "0")}-${date
        .getDate()
        .toString()
        .padStart(2, "0")} ${date
        .getHours()
        .toString()
        .padStart(2, "0")}:${date
        .getMinutes()
        .toString()
        .padStart(2, "0")}`;
    }
  }
};
</script>
<style scoped>
.organ-procurement-detail {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.detail-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  border: 1px solid #e4e7ed;
}
.procurement-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  border: 1px solid #e4e7ed;
}
.attachment-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  border: 1px solid #e4e7ed;
}
.detail-title {
  font-size: 18px;
  font-weight: 600;
  color: #303133;
  line-height: 1.4;
}
/* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
.procurement-stats {
  margin-top: 20px;
  padding: 15px;
  background: linear-gradient(135deg, #a6b2e7 0%, #8a66ad 100%);
  border-radius: 8px;
  color: white;
}
.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px;
  text-align: center;
}
.stat-label {
  font-size: 12px;
  opacity: 0.9;
  margin-bottom: 5px;
  color: rgba(255, 255, 255, 0.9);
}
.stat-value {
  font-size: 18px;
  font-weight: bold;
  color: white;
}
/* ç©ºçŠ¶æ€æ ·å¼ */
.empty-procurement {
  text-align: center;
  padding: 40px 0;
  color: #909399;
  background: #fafafa;
  border-radius: 4px;
  margin: 20px 0;
}
/* å¯¹è¯æ¡†åº•部按钮 */
.dialog-footer {
  margin-top: 20px;
  text-align: center;
  padding-top: 20px;
  border-top: 1px solid #e4e7ed;
}
.dialog-footer .el-button {
  margin: 0 10px;
  min-width: 120px;
}
/* è¡¨æ ¼è¡Œæ ·å¼ */
:deep(.warning-row) {
  background-color: #fff7e6 !important;
}
:deep(.warning-row:hover) {
  background-color: #ffecc2 !important;
}
:deep(.el-table .cell) {
  padding: 8px 12px;
  line-height: 1.5;
}
:deep(.el-table th) {
  background-color: #f5f7fa;
  color: #606266;
  font-weight: 600;
}
:deep(.el-table--border) {
  border: 1px solid #e4e7ed;
  border-radius: 4px;
}
:deep(.el-table--border th) {
  border-right: 1px solid #e4e7ed;
}
:deep(.el-table--border td) {
  border-right: 1px solid #e4e7ed;
}
/* è¡¨å•样式优化 */
:deep(.el-form-item__label) {
  font-weight: 500;
  color: #606266;
}
:deep(.el-input__inner) {
  border-radius: 4px;
  border: 1px solid #dcdfe6;
  transition: border-color 0.3s ease;
}
:deep(.el-input__inner:focus) {
  border-color: #409EFF;
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
:deep(.el-textarea__inner) {
  border-radius: 4px;
  resize: vertical;
  min-height: 60px;
}
:deep(.el-select) {
  width: 100%;
}
/* æŒ‰é’®æ ·å¼ä¼˜åŒ– */
:deep(.el-button--primary) {
  background: linear-gradient(135deg, #409EFF 0%, #3375e0 100%);
  border: none;
  border-radius: 4px;
  transition: all 0.3s ease;
}
:deep(.el-button--primary:hover) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
}
:deep(.el-button--success) {
  background: linear-gradient(135deg, #67C23A 0%, #529b2f 100%);
  border: none;
  border-radius: 4px;
  transition: all 0.3s ease;
}
:deep(.el-button--success:hover) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(103, 194, 58, 0.4);
}
:deep(.el-button--warning) {
  background: linear-gradient(135deg, #E6A23C 0%, #d18c2a 100%);
  border: none;
  border-radius: 4px;
  transition: all 0.3s ease;
}
:deep(.el-button--warning:hover) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(230, 162, 60, 0.4);
}
:deep(.el-button--danger) {
  background: linear-gradient(135deg, #F56C6C 0%, #e05b5b 100%);
  border: none;
  border-radius: 4px;
  transition: all 0.3s ease;
}
:deep(.el-button--danger:hover) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(245, 108, 108, 0.4);
}
/* æ ‡ç­¾æ ·å¼ */
:deep(.el-tag) {
  border-radius: 12px;
  border: none;
  font-weight: 500;
  padding: 4px 12px;
}
:deep(.el-tag--success) {
  background: linear-gradient(135deg, #67C23A 0%, #529b2f 100%);
  color: white;
}
:deep(.el-tag--warning) {
  background: linear-gradient(135deg, #E6A23C 0%, #d18c2a 100%);
  color: white;
}
/* å¤é€‰æ¡†ç»„样式 */
:deep(.el-checkbox-group) {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
  margin-top: 10px;
}
:deep(.el-checkbox) {
  margin-right: 0;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
  background: linear-gradient(135deg, #409EFF 0%, #3375e0 100%);
  border-color: #409EFF;
}
:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
  color: #409EFF;
  font-weight: 500;
}
/* æ—¥æœŸé€‰æ‹©å™¨æ ·å¼ */
:deep(.el-date-editor) {
  width: 100%;
}
:deep(.el-picker-panel) {
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
/* åŠ è½½çŠ¶æ€ */
:deep(.el-loading-mask) {
  border-radius: 4px;
}
/* å¡ç‰‡å¤´éƒ¨æ ·å¼ä¼˜åŒ– */
:deep(.el-card__header) {
  background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
  border-bottom: 1px solid #e4e7ed;
  padding: 15px 20px;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
  .organ-procurement-detail {
    padding: 15px;
  }
  :deep(.el-col) {
    margin-bottom: 10px;
  }
}
@media (max-width: 768px) {
  .organ-procurement-detail {
    padding: 10px;
  }
  .detail-title {
    font-size: 16px;
  }
  .stat-item {
    padding: 5px;
  }
  .stat-label {
    font-size: 10px;
  }
  .stat-value {
    font-size: 14px;
  }
  :deep(.el-table .cell) {
    padding: 4px 8px;
    font-size: 12px;
  }
  :deep(.el-checkbox-group) {
    gap: 8px;
  }
  .dialog-footer .el-button {
    margin: 5px;
    min-width: 100px;
  }
}
@media (max-width: 480px) {
  .organ-procurement-detail {
    padding: 5px;
  }
  :deep(.el-card__header) {
    padding: 10px 15px;
  }
  :deep(.el-form-item__label) {
    font-size: 12px;
  }
  :deep(.el-table) {
    font-size: 11px;
  }
}
/* åŠ¨ç”»æ•ˆæžœ */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
/* è‡ªå®šä¹‰æ»šåŠ¨æ¡ */
:deep(::-webkit-scrollbar) {
  width: 6px;
  height: 6px;
}
:deep(::-webkit-scrollbar-track) {
  background: #f1f1f1;
  border-radius: 3px;
}
:deep(::-webkit-scrollbar-thumb) {
  background: #c1c1c1;
  border-radius: 3px;
}
:deep(::-webkit-scrollbar-thumb:hover) {
  background: #a8a8a8;
}
/* æ–‡ä»¶ä¿¡æ¯æ ·å¼ */
.file-info {
  display: flex;
  align-items: center;
  padding: 5px 0;
}
.file-info i {
  font-size: 18px;
  margin-right: 10px;
}
/* æ“ä½œæŒ‰é’®ç»„样式 */
:deep(.small-padding .el-button) {
  margin: 0 2px;
  padding: 4px 8px;
}
/* è¡¨å•行间距优化 */
:deep(.el-form-item) {
  margin-bottom: 18px;
}
:deep(.el-row) {
  margin-bottom: 10px;
}
/* å¯¹è¯æ¡†æ ·å¼ä¼˜åŒ– */
:deep(.el-dialog) {
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
:deep(.el-dialog__header) {
  background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
  border-bottom: 1px solid #e4e7ed;
  padding: 15px 20px;
  border-radius: 8px 8px 0 0;
}
:deep(.el-dialog__title) {
  font-weight: 600;
  color: #303133;
}
:deep(.el-dialog__body) {
  padding: 20px;
}
:deep(.el-dialog__footer) {
  padding: 15px 20px;
  border-top: 1px solid #e4e7ed;
}
/* ç‰¹æ®ŠçŠ¶æ€æç¤º */
.procurement-warning {
  background-color: #fff7e6;
  border: 1px solid #ffecc2;
  border-radius: 4px;
  padding: 10px 15px;
  margin: 10px 0;
  color: #e6a23c;
  font-size: 14px;
}
.procurement-success {
  background-color: #f0f9ff;
  border: 1px solid #b3e0ff;
  border-radius: 4px;
  padding: 10px 15px;
  margin: 10px 0;
  color: #409EFF;
  font-size: 14px;
}
/* æ—¶é—´çº¿æ ·å¼ï¼ˆç”¨äºŽæ‰‹æœ¯æ—¶é—´å±•示) */
.procurement-timeline {
  margin: 20px 0;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 4px;
}
.timeline-item {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
  padding: 8px 12px;
  background: white;
  border-radius: 4px;
  border-left: 4px solid #409EFF;
}
.timeline-label {
  font-weight: 500;
  min-width: 120px;
  color: #606266;
}
.timeline-value {
  color: #303133;
  font-weight: 500;
}
/* æ‰“印样式 */
@media print {
  .organ-procurement-detail {
    padding: 0;
    background: white;
  }
  .detail-card,
  .procurement-card,
  .attachment-card {
    box-shadow: none;
    border: 1px solid #ddd;
    margin-bottom: 15px;
    page-break-inside: avoid;
  }
  .dialog-footer,
  .el-button {
    display: none;
  }
}
</style>
src/views/business/GetWitness/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,372 @@
<template>
  <div class="organ-procurement-list">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-card class="search-card">
      <el-form
        :model="queryParams"
        ref="queryForm"
        :inline="true"
        label-width="100px"
      >
        <el-form-item label="住院号" prop="hospitalNo">
          <el-input
            v-model="queryParams.hospitalNo"
            placeholder="请输入住院号"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="捐献者姓名" prop="donorName">
          <el-input
            v-model="queryParams.donorName"
            placeholder="请输入捐献者姓名"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="获取状态" prop="procurementStatus">
          <el-select
            v-model="queryParams.procurementStatus"
            placeholder="请选择获取状态"
            clearable
            style="width: 200px"
          >
            <el-option label="已获取" value="procured" />
            <el-option label="待获取" value="pending" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="handleQuery"
            >搜索</el-button
          >
          <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- æ“ä½œæŒ‰é’® -->
    <el-card class="tool-card">
      <el-row :gutter="10">
        <el-col :span="16">
          <el-button type="primary" icon="el-icon-plus" @click="handleCreate"
            >新建获取</el-button
          >
          <el-button
            type="success"
            icon="el-icon-edit"
            :disabled="single"
            @click="handleUpdate"
            >修改</el-button
          >
          <el-button
            type="danger"
            icon="el-icon-delete"
            :disabled="multiple"
            @click="handleDelete"
            >删除</el-button
          >
        </el-col>
        <el-col :span="8" style="text-align: right">
          <el-tooltip content="刷新" placement="top">
            <el-button icon="el-icon-refresh" circle @click="getList" />
          </el-tooltip>
        </el-col>
      </el-row>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <el-table
        v-loading="loading"
        :data="organProcurementList"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column
          label="住院号"
          align="center"
          prop="hospitalNo"
          width="120"
        />
        <el-table-column
          label="捐献者姓名"
          align="center"
          prop="donorName"
          width="120"
        />
        <el-table-column label="性别" align="center" prop="gender" width="80">
          <template slot-scope="scope">
            <dict-tag
              :options="dict.type.sys_user_sex"
              :value="parseInt(scope.row.gender)"
            />
          </template>
        </el-table-column>
        <el-table-column label="年龄" align="center" prop="age" width="80" />
        <el-table-column
          label="疾病诊断"
          align="center"
          prop="diagnosis"
          min-width="180"
          show-overflow-tooltip
        />
        <el-table-column
          label="获取状态"
          align="center"
          prop="procurementStatus"
          width="100"
        >
          <template slot-scope="scope">
            <el-tag :type="scope.row.procurementStatus === 'procured' ? 'success' : 'warning'">
              {{ scope.row.procurementStatus === 'procured' ? '已获取' : '待获取' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="获取时间"
          align="center"
          prop="procurementTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.procurementTime
                ? parseTime(scope.row.procurementTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="登记人"
          align="center"
          prop="registrant"
          width="100"
        />
        <el-table-column
          label="登记时间"
          align="center"
          prop="registrationTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.registrationTime
                ? parseTime(scope.row.registrationTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          align="center"
          width="150"
          class-name="small-padding fixed-width"
        >
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleView(scope.row)"
              >详情</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-edit"
              @click="handleUpdate(scope.row)"
              >修改</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              style="color: #F56C6C"
              @click="handleDelete(scope.row)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="queryParams.pageNum"
        :limit.sync="queryParams.pageSize"
        @pagination="getList"
      />
    </el-card>
  </div>
</template>
<script>
import { listOrganProcurement, delOrganProcurement } from "./organProcurement";
import Pagination from "@/components/Pagination";
export default {
  name: "OrganProcurementList",
  components: { Pagination },
  dicts: ["sys_user_sex"],
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ€»æ¡æ•°
      total: 0,
      // å™¨å®˜èŽ·å–è¡¨æ ¼æ•°æ®
      organProcurementList: [],
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        hospitalNo: undefined,
        donorName: undefined,
        procurementStatus: undefined
      }
    };
  },
  created() {
    this.getList();
  },
  methods: {
    // æŸ¥è¯¢å™¨å®˜èŽ·å–åˆ—è¡¨
    getList() {
      this.loading = true;
      listOrganProcurement(this.queryParams)
        .then(response => {
          if (response.code === 200) {
            this.organProcurementList = response.data.rows;
            this.total = response.data.total;
          } else {
            this.$message.error("获取数据失败");
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取器官获取列表失败:", error);
          this.loading = false;
          this.$message.error("获取数据失败");
        });
    },
    // æœç´¢æŒ‰é’®æ“ä½œ
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŒ‰é’®æ“ä½œ
    resetQuery() {
      this.$refs.queryForm.resetFields();
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleView(row) {
      this.$router.push({
        path: "/case/GetWitnessInfo",
        query: { id: row.id }
      });
    },
    // æ–°å¢žæŒ‰é’®æ“ä½œ
    handleCreate() {
      this.$router.push("/case/GetWitnessInfo");
    },
    // ä¿®æ”¹æŒ‰é’®æ“ä½œ
    handleUpdate(row) {
      const id = row.id || this.ids[0];
      this.$router.push({
        path: "/case/GetWitnessInfo",
        query: { id: id }
      });
    },
    // åˆ é™¤æŒ‰é’®æ“ä½œ
    handleDelete(row) {
      const ids = row.id ? [row.id] : this.ids;
      this.$confirm("是否确认删除选中的数据项?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          return delOrganProcurement(ids);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("删除成功");
            this.getList();
          }
        })
        .catch(() => {});
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time, pattern) {
      if (!time) return "";
      const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
      let date;
      if (typeof time === "object") {
        date = time;
      } else {
        if (typeof time === "string" && /^[0-9]+$/.test(time)) {
          time = parseInt(time);
        }
        if (typeof time === "number" && time.toString().length === 10) {
          time = time * 1000;
        }
        date = new Date(time);
      }
      const formatObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
      };
      const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
        let value = formatObj[key];
        if (key === "a") {
          return ["日", "一", "二", "三", "四", "五", "六"][value];
        }
        if (result.length > 0 && value < 10) {
          value = "0" + value;
        }
        return value || 0;
      });
      return time_str;
    }
  }
};
</script>
<style scoped>
.organ-procurement-list {
  padding: 20px;
}
.search-card {
  margin-bottom: 20px;
}
.tool-card {
  margin-bottom: 20px;
}
.fixed-width .el-button {
  margin: 0 5px;
}
</style>
src/views/business/GetWitness/organProcurement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
// æ¨¡æ‹Ÿå™¨å®˜èŽ·å–æ•°æ®
const mockOrganProcurementData = [
  {
    id: 1,
    hospitalNo: "D202312001",
    caseNo: "C202312001",
    donorName: "张三",
    gender: "0",
    age: 45,
    birthDate: "1978-05-15",
    diagnosis: "脑外伤",
    procurementStatus: "procured",
    procurementTime: "2023-12-01 16:30:00",
    registrant: "李协调员",
    registrationTime: "2023-12-01 15:00:00",
    createTime: "2023-12-01 10:00:00",
    surgeryName: "多器官获取手术",
    surgeryStartTime: "2023-12-01 14:00:00",
    donorDeathTime: "2023-12-01 13:30:00",
    abdominalAortaCannulationTime: "2023-12-01 14:30:00",
    inferiorVenaCavaCannulationTime: "2023-12-01 14:35:00",
    superiorMesentericVeinCannulationTime: "2023-12-01 14:40:00"
  },
  {
    id: 2,
    hospitalNo: "D202312002",
    caseNo: "C202312002",
    donorName: "李四",
    gender: "1",
    age: 38,
    birthDate: "1985-08-22",
    diagnosis: "心脏骤停",
    procurementStatus: "procured",
    procurementTime: "2023-12-02 11:20:00",
    registrant: "张协调员",
    registrationTime: "2023-12-02 10:00:00",
    createTime: "2023-12-02 08:30:00",
    surgeryName: "心脏获取手术",
    surgeryStartTime: "2023-12-02 10:30:00",
    donorDeathTime: "2023-12-02 10:00:00",
    abdominalAortaCannulationTime: "2023-12-02 10:45:00",
    inferiorVenaCavaCannulationTime: "2023-12-02 10:50:00",
    superiorMesentericVeinCannulationTime: "2023-12-02 10:55:00"
  },
  {
    id: 3,
    hospitalNo: "D202312003",
    caseNo: "C202312003",
    donorName: "王五",
    gender: "0",
    age: 52,
    birthDate: "1971-03-10",
    diagnosis: "脑梗死",
    procurementStatus: "pending",
    procurementTime: "",
    registrant: "赵协调员",
    registrationTime: "2023-12-03 17:20:00",
    createTime: "2023-12-03 14:00:00",
    surgeryName: "",
    surgeryStartTime: "",
    donorDeathTime: "",
    abdominalAortaCannulationTime: "",
    inferiorVenaCavaCannulationTime: "",
    superiorMesentericVeinCannulationTime: ""
  }
];
// æ¨¡æ‹Ÿå™¨å®˜èŽ·å–è®°å½•æ•°æ®
const mockProcurementRecordData = [
  {
    id: 1,
    procurementId: 1,
    organName: "肝脏",
    organNo: "L001",
    organStartTime: "2023-12-01 15:00:00",
    organGetTime: "2023-12-01 15:45:00",
    gainHospitalNo: "H1001",
    gainHospitalName: "北京协和医院",
    organGetDoctor: "王医生",
    assistant: "李医生",
    procurementNurse: "张护士",
    operatingRoomNurse: "刘护士",
    anesthesiologist: "陈麻醉师",
    organState: "1",
    notGetReason: ""
  },
  {
    id: 2,
    procurementId: 1,
    organName: "肾脏",
    organNo: "K001",
    organStartTime: "2023-12-01 15:10:00",
    organGetTime: "2023-12-01 15:50:00",
    gainHospitalNo: "H1002",
    gainHospitalName: "上海瑞金医院",
    organGetDoctor: "赵医生",
    assistant: "钱医生",
    procurementNurse: "孙护士",
    operatingRoomNurse: "周护士",
    anesthesiologist: "吴麻醉师",
    organState: "1",
    notGetReason: ""
  },
  {
    id: 3,
    procurementId: 1,
    organName: "心脏",
    organNo: "H001",
    organStartTime: "2023-12-01 15:20:00",
    organGetTime: "2023-12-01 16:00:00",
    gainHospitalNo: "H1003",
    gainHospitalName: "广州中山医院",
    organGetDoctor: "郑医生",
    assistant: "王医生",
    procurementNurse: "林护士",
    operatingRoomNurse: "黄护士",
    anesthesiologist: "杨麻醉师",
    organState: "1",
    notGetReason: ""
  }
];
// æ¨¡æ‹ŸåŒ»é™¢æ•°æ®
const mockHospitalData = [
  { id: 1, hospitalNo: "H1001", hospitalName: "北京协和医院", type: "4" },
  { id: 2, hospitalNo: "H1002", hospitalName: "上海瑞金医院", type: "4" },
  { id: 3, hospitalNo: "H1003", hospitalName: "广州中山医院", type: "4" },
  { id: 4, hospitalNo: "H1004", hospitalName: "武汉同济医院", type: "4" },
  { id: 5, hospitalNo: "H1005", hospitalName: "成都华西医院", type: "4" }
];
// æ¨¡æ‹Ÿåè°ƒå‘˜æ•°æ®
const mockCoordinatorData = [
  { reportNo: "C001", reportName: "张协调员" },
  { reportNo: "C002", reportName: "李协调员" },
  { reportNo: "C003", reportName: "王协调员" },
  { reportNo: "C004", reportName: "赵协调员" }
];
// æ¨¡æ‹ŸAPI响应延迟
const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms));
// æŸ¥è¯¢å™¨å®˜èŽ·å–åˆ—è¡¨
export const listOrganProcurement = async (queryParams = {}) => {
  await delay();
  const {
    pageNum = 1,
    pageSize = 10,
    hospitalNo,
    donorName,
    procurementStatus
  } = queryParams;
  // è¿‡æ»¤æ•°æ®
  let filteredData = mockOrganProcurementData.filter(item => {
    let match = true;
    if (hospitalNo && !item.hospitalNo.includes(hospitalNo)) {
      match = false;
    }
    if (donorName && !item.donorName.includes(donorName)) {
      match = false;
    }
    if (procurementStatus && item.procurementStatus !== procurementStatus) {
      match = false;
    }
    return match;
  });
  // åˆ†é¡µ
  const startIndex = (pageNum - 1) * pageSize;
  const endIndex = startIndex + parseInt(pageSize);
  const paginatedData = filteredData.slice(startIndex, endIndex);
  return {
    code: 200,
    message: "success",
    data: {
      rows: paginatedData,
      total: filteredData.length,
      pageNum: parseInt(pageNum),
      pageSize: parseInt(pageSize)
    }
  };
};
// èŽ·å–å™¨å®˜èŽ·å–è¯¦ç»†ä¿¡æ¯
export const getOrganProcurementDetail = async (id) => {
  await delay();
  const detail = mockOrganProcurementData.find(item => item.id == id);
  if (detail) {
    // èŽ·å–èŽ·å–è®°å½•
    const procurementRecords = mockProcurementRecordData.filter(item => item.procurementId == id);
    return {
      code: 200,
      message: "success",
      data: {
        ...detail,
        procurementRecords
      }
    };
  } else {
    return {
      code: 404,
      message: "器官获取记录不存在"
    };
  }
};
// æ–°å¢žå™¨å®˜èŽ·å–
export const addOrganProcurement = async (data) => {
  await delay();
  const newId = Math.max(...mockOrganProcurementData.map(item => item.id), 0) + 1;
  const hospitalNo = `D${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const caseNo = `C${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const newRecord = {
    ...data,
    id: newId,
    hospitalNo,
    caseNo,
    registrationTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
    createTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
  };
  mockOrganProcurementData.unshift(newRecord);
  return {
    code: 200,
    message: "新增成功",
    data: newRecord
  };
};
// ä¿®æ”¹å™¨å®˜èŽ·å–
export const updateOrganProcurement = async (data) => {
  await delay();
  const index = mockOrganProcurementData.findIndex(item => item.id == data.id);
  if (index !== -1) {
    mockOrganProcurementData[index] = {
      ...mockOrganProcurementData[index],
      ...data,
      updateTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
    };
    return {
      code: 200,
      message: "修改成功",
      data: mockOrganProcurementData[index]
    };
  } else {
    return {
      code: 404,
      message: "器官获取记录不存在"
    };
  }
};
// åˆ é™¤å™¨å®˜èŽ·å–
export const delOrganProcurement = async (ids) => {
  await delay();
  const idArray = Array.isArray(ids) ? ids : [ids];
  idArray.forEach(id => {
    const index = mockOrganProcurementData.findIndex(item => item.id == id);
    if (index !== -1) {
      mockOrganProcurementData.splice(index, 1);
    }
  });
  return {
    code: 200,
    message: "删除成功"
  };
};
// ä¿å­˜å™¨å®˜èŽ·å–è®°å½•
export const saveProcurementRecords = async (procurementId, records) => {
  await delay();
  // åˆ é™¤è¯¥èŽ·å–ID的所有记录
  const existingIndexes = [];
  mockProcurementRecordData.forEach((item, index) => {
    if (item.procurementId == procurementId) {
      existingIndexes.push(index);
    }
  });
  // ä»ŽåŽå¾€å‰åˆ é™¤é¿å…ç´¢å¼•问题
  existingIndexes.reverse().forEach(index => {
    mockProcurementRecordData.splice(index, 1);
  });
  // æ·»åŠ æ–°è®°å½•
  records.forEach(record => {
    const newId = Math.max(...mockProcurementRecordData.map(item => item.id), 0) + 1;
    mockProcurementRecordData.push({
      ...record,
      id: newId,
      procurementId: procurementId
    });
  });
  return {
    code: 200,
    message: "保存成功",
    data: records
  };
};
// èŽ·å–åŒ»é™¢åˆ—è¡¨
export const getHospitalList = async () => {
  await delay();
  return {
    code: 200,
    message: "success",
    data: mockHospitalData
  };
};
// èŽ·å–åè°ƒå‘˜åˆ—è¡¨
export const getCoordinatorList = async () => {
  await delay();
  return {
    code: 200,
    message: "success",
    data: mockCoordinatorData
  };
};
export default {
  listOrganProcurement,
  getOrganProcurementDetail,
  addOrganProcurement,
  updateOrganProcurement,
  delOrganProcurement,
  saveProcurementRecords,
  getHospitalList,
  getCoordinatorList
};
src/views/business/OrganUtilization/OrganUtilizationInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1656 @@
<template>
  <div class="organ-utilization-detail">
    <!-- åŸºæœ¬ä¿¡æ¯ -->
    <el-card class="detail-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">器官利用基本信息</span>
        <div style="float: right;">
          <el-button type="primary" @click="handleSave" :loading="saveLoading">
            ä¿å­˜
          </el-button>
          <el-button
            type="success"
            @click="handleComplete"
            :disabled="form.utilizationStatus === 'completed'"
          >
            å®Œæˆåˆ©ç”¨
          </el-button>
        </div>
      </div>
      <el-form :model="form" ref="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="住院号" prop="hospitalNo">
              <el-input v-model="form.hospitalNo" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="案例编号" prop="caseNo">
              <el-input v-model="form.caseNo" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="捐献者姓名" prop="donorName">
              <el-input v-model="form.donorName" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="性别" prop="gender">
              <el-select v-model="form.gender" style="width: 100%">
                <el-option label="男" value="0" />
                <el-option label="女" value="1" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="年龄" prop="age">
              <el-input v-model="form.age" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="出生日期" prop="birthDate">
              <el-date-picker
                v-model="form.birthDate"
                type="date"
                value-format="yyyy-MM-dd"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="疾病诊断" prop="diagnosis">
              <el-input
                type="textarea"
                :rows="2"
                v-model="form.diagnosis"
                placeholder="请输入疾病诊断信息"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="分配时间" prop="allocationTime">
              <el-date-picker
                v-model="form.allocationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="6">
            <el-form-item align="left" label="遗体捐献" prop="isBodyDonation">
              <el-radio-group v-model="form.isBodyDonation">
                <el-radio
                  v-for="dict in dict.type.sys_0_1 || []"
                  :key="dict.value"
                  :label="dict.value"
                  >{{ dict.label }}</el-radio
                >
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="18">
            <el-form-item align="left" label="接收单位" prop="receivingUnit">
              <el-input
                v-model="form.receivingUnit"
                placeholder="请输入接收单位"
                :disabled="form.isBodyDonation !== '1'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="6">
            <el-form-item label="负责人" prop="responsibleUserId">
              <el-select
                v-model="form.responsibleUserId"
                placeholder="请选择负责人"
                style="width: 100%"
              >
                <el-option
                  v-for="item in leaderList"
                  :key="item.reportNo"
                  :label="item.reportName"
                  :value="item.reportNo"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="协调员一" prop="coordinatedUserId1">
              <el-select
                v-model="form.coordinatedUserId1"
                placeholder="请选择协调员"
                style="width: 100%"
              >
                <el-option
                  v-for="item in coordinatorList"
                  :key="item.reportNo"
                  :label="item.reportName"
                  :value="item.reportNo"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="协调员二" prop="coordinatedUserId2">
              <el-select
                v-model="form.coordinatedUserId2"
                placeholder="请选择协调员"
                style="width: 100%"
              >
                <el-option
                  v-for="item in coordinatorList"
                  :key="item.reportNo"
                  :label="item.reportName"
                  :value="item.reportNo"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="完成时间" prop="completionTime">
              <el-date-picker
                v-model="form.completionTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
                :disabled="form.utilizationStatus !== 'completed'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="登记人" prop="registrant">
              <el-input v-model="form.registrant" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="登记时间" prop="registrationTime">
              <el-date-picker
                v-model="form.registrationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
                readonly
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </el-card>
    <!-- å™¨å®˜åˆ©ç”¨è®°å½•部分 -->
    <el-card class="utilization-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">器官利用记录</span>
        <div style="float: right;">
          <el-tag :type="getStatusTagType(form.utilizationStatus)">
            {{ getStatusText(form.utilizationStatus) }}
          </el-tag>
        </div>
      </div>
      <el-form
        ref="utilizationForm"
        :rules="utilizationRules"
        :model="utilizationData"
        label-position="right"
      >
        <el-row>
          <el-col>
            <el-form-item label-width="100px" label="移植器官">
              <el-checkbox-group v-model="selectedOrgans" @change="handleOrganSelectionChange">
                <el-checkbox
                  v-for="dict in dict.type.sys_Organ || []"
                  :key="dict.value"
                  :label="dict.value"
                  :disabled="form.utilizationStatus === 'completed'"
                >
                  {{ dict.label }}
                </el-checkbox>
              </el-checkbox-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col>
            <el-form-item>
              <el-table
                :data="utilizationData.records"
                v-loading="loading"
                border
                style="width: 100%"
                :row-class-name="getOrganRowClassName"
              >
                <el-table-column
                  label="器官名称"
                  align="center"
                  width="120"
                  prop="organName"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.organName"
                      placeholder="器官名称"
                      :disabled="true"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="系统编号"
                  align="center"
                  width="120"
                  prop="caseNo"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.caseNo"
                      placeholder="系统编号"
                      :disabled="form.utilizationStatus === 'completed'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="移植医院"
                  align="center"
                  width="200"
                  prop="hospitalNo"
                >
                  <template slot-scope="scope">
                    <el-select
                      v-model="scope.row.hospitalNo"
                      placeholder="请选择移植医院"
                      style="width: 100%"
                      :disabled="form.utilizationStatus === 'completed'"
                      @change="handleHospitalChange(scope.row, $event)"
                    >
                      <el-option
                        v-for="hospital in hospitalList"
                        :key="hospital.hospitalNo"
                        :label="hospital.hospitalName"
                        :value="hospital.hospitalNo"
                      />
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column
                  label="受体姓氏"
                  align="center"
                  width="120"
                  prop="recipientName"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.recipientName"
                      placeholder="受体姓氏"
                      :disabled="form.utilizationStatus === 'completed'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="移植负责人"
                  align="center"
                  width="120"
                  prop="transplantDoctor"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.transplantDoctor"
                      placeholder="医师姓名"
                      :disabled="form.utilizationStatus === 'completed'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="移植时间"
                  align="center"
                  width="150"
                  prop="transplantTime"
                >
                  <template slot-scope="scope">
                    <el-date-picker
                      clearable
                      size="small"
                      style="width: 100%"
                      v-model="scope.row.transplantTime"
                      type="date"
                      value-format="yyyy-MM-dd"
                      placeholder="选择移植时间"
                      :disabled="form.utilizationStatus === 'completed'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="移植状态"
                  align="center"
                  width="120"
                  prop="transplantStatus"
                >
                  <template slot-scope="scope">
                    <el-select
                      v-model="scope.row.transplantStatus"
                      placeholder="请选择移植状态"
                      style="width: 100%"
                      :disabled="form.utilizationStatus === 'completed'"
                    >
                      <el-option
                        v-for="dict in transplantStatusList"
                        :key="dict.value"
                        :label="dict.label"
                        :value="dict.value"
                      />
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column
                  label="说明"
                  align="center"
                  prop="abandonReason"
                  min-width="200"
                >
                  <template slot-scope="scope">
                    <el-input
                      type="textarea"
                      clearable
                      v-model="scope.row.abandonReason"
                      placeholder="请输入弃用说明"
                      :disabled="form.utilizationStatus === 'completed'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="操作"
                  align="center"
                  width="120"
                  class-name="small-padding fixed-width"
                  v-if="form.utilizationStatus !== 'completed'"
                >
                  <template slot-scope="scope">
                    <el-button
                      size="mini"
                      type="text"
                      icon="el-icon-edit"
                      @click="handleEditUtilization(scope.row)"
                    >
                      ç¼–辑
                    </el-button>
                  </template>
                </el-table-column>
              </el-table>
            </el-form-item>
          </el-col>
        </el-row>
        <!-- åˆ©ç”¨ç»Ÿè®¡ä¿¡æ¯ -->
        <div class="utilization-stats" v-if="utilizationData.records.length > 0">
          <el-row :gutter="20">
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">已利用器官:</span>
                <span class="stat-value">{{ utilizationData.records.length }} ä¸ª</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">待完善信息:</span>
                <span class="stat-value">{{ incompleteRecords }} ä¸ª</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">涉及医院:</span>
                <span class="stat-value">{{ uniqueHospitals }} å®¶</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">利用状态:</span>
                <span class="stat-value">
                  <el-tag :type="getStatusTagType(form.utilizationStatus)">
                    {{ getStatusText(form.utilizationStatus) }}
                  </el-tag>
                </span>
              </div>
            </el-col>
          </el-row>
        </div>
        <div v-else class="empty-utilization">
          <el-empty description="暂无利用记录" :image-size="80">
            <span>请先选择要利用的器官</span>
          </el-empty>
        </div>
      </el-form>
      <div class="dialog-footer" v-if="form.utilizationStatus !== 'completed'">
        <el-button
          type="primary"
          @click="handleSaveUtilization"
          :loading="saveLoading"
          :disabled="utilizationData.records.length === 0"
        >
          ä¿å­˜åˆ©ç”¨è®°å½•
        </el-button>
        <el-button
          type="success"
          @click="handleConfirmUtilization"
          :loading="confirmLoading"
          :disabled="incompleteRecords > 0"
        >
          ç¡®è®¤å®Œæˆåˆ©ç”¨
        </el-button>
      </div>
    </el-card>
    <!-- å—者详细信息部分 -->
    <el-card class="recipient-card" v-if="utilizationData.records.length > 0">
      <div slot="header" class="clearfix">
        <span class="detail-title">受者详细信息</span>
      </div>
      <el-tabs v-model="activeRecipientTab" type="card">
        <el-tab-pane
          v-for="record in utilizationData.records"
          :key="record.organNo"
          :label="record.organName"
          :name="record.organNo"
        >
          <el-form :model="record" label-width="140px">
            <el-row :gutter="20">
              <el-col :span="8">
                <el-form-item label="受者姓名">
                  <el-input v-model="record.recipientName" placeholder="请输入受者姓名" />
                </el-form-item>
              </el-col>
              <el-col :span="8">
                <el-form-item label="出生年月">
                  <el-date-picker
                    v-model="record.recipientBirthDate"
                    type="month"
                    value-format="yyyy-MM"
                    placeholder="选择出生年月"
                    style="width: 100%"
                  />
                </el-form-item>
              </el-col>
              <el-col :span="8">
                <el-form-item label="性别">
                  <el-select v-model="record.recipientGender" placeholder="请选择性别" style="width: 100%">
                    <el-option label="男" value="0" />
                    <el-option label="女" value="1" />
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="20">
              <el-col :span="12">
                <el-form-item label="移植中心名称">
                  <el-input v-model="record.transplantCenter" placeholder="请输入移植中心名称" />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="所在地">
                  <el-input v-model="record.location" placeholder="请输入所在地" />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="20">
              <el-col :span="12">
                <el-form-item label="移植日期">
                  <el-date-picker
                    v-model="record.transplantTime"
                    type="date"
                    value-format="yyyy-MM-dd"
                    placeholder="选择移植日期"
                    style="width: 100%"
                  />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="原发病">
                  <el-input v-model="record.originalDisease" placeholder="请输入原发病" />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="20">
              <el-col :span="24">
                <el-form-item label="检测指标">
                  <el-input
                    type="textarea"
                    :rows="3"
                    v-model="record.testIndicators"
                    placeholder="请输入各类必要的检测指标"
                  />
                </el-form-item>
              </el-col>
            </el-row>
          </el-form>
        </el-tab-pane>
      </el-tabs>
    </el-card>
    <!-- éšè®¿è®°å½•部分 -->
    <el-card class="followup-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">随访记录</span>
        <el-button
          type="primary"
          size="mini"
          icon="el-icon-plus"
          @click="handleAddFollowup"
          style="float: right;"
        >
          æ–°å¢žéšè®¿
        </el-button>
      </div>
      <el-table :data="followupData.records" v-loading="loading" border>
        <el-table-column label="器官名称" align="center" width="120" prop="organName" />
        <el-table-column label="随访时间" align="center" width="160" prop="followupTime">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.followupTime) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="随访类型" align="center" width="100" prop="followupType">
          <template slot-scope="scope">
            <el-tag :type="getFollowupTypeTag(scope.row.followupType)">
              {{ getFollowupTypeText(scope.row.followupType) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="受者情况" align="center" prop="recipientCondition" min-width="200" show-overflow-tooltip />
        <el-table-column label="随访医生" align="center" width="120" prop="followupDoctor" />
        <el-table-column label="下次随访时间" align="center" width="160" prop="nextFollowupTime">
          <template slot-scope="scope">
            <span>{{ scope.row.nextFollowupTime || '-' }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="150">
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleViewFollowup(scope.row)"
            >查看</el-button>
            <el-button
              size="mini"
              type="text"
              icon="el-icon-edit"
              @click="handleEditFollowup(scope.row)"
            >编辑</el-button>
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              style="color: #F56C6C;"
              @click="handleDeleteFollowup(scope.row)"
            >删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- é™„件管理部分 -->
    <el-card class="attachment-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">相关附件</span>
        <el-button
          type="primary"
          size="mini"
          icon="el-icon-upload"
          @click="handleUploadAttachment"
        >
          ä¸Šä¼ é™„ä»¶
        </el-button>
      </div>
      <div class="attachment-list">
        <el-table :data="attachments" style="width: 100%">
          <el-table-column label="文件名称" min-width="200">
            <template slot-scope="scope">
              <div class="file-info">
                <i :class="getFileIcon(scope.row.fileName)" style="margin-right: 8px; color: #409EFF;"></i>
                <span>{{ scope.row.fileName }}</span>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="文件类型" width="100" align="center">
            <template slot-scope="scope">
              <el-tag size="small">{{ getFileType(scope.row.fileName) }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column label="文件大小" width="100" align="center">
            <template slot-scope="scope">
              <span>{{ formatFileSize(scope.row.fileSize) }}</span>
            </template>
          </el-table-column>
          <el-table-column label="上传时间" width="160" align="center">
            <template slot-scope="scope">
              <span>{{ parseTime(scope.row.uploadTime) }}</span>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="150" align="center">
            <template slot-scope="scope">
              <el-button
                size="mini"
                type="text"
                icon="el-icon-view"
                @click="handlePreviewAttachment(scope.row)"
              >预览</el-button>
              <el-button
                size="mini"
                type="text"
                icon="el-icon-download"
                @click="handleDownloadAttachment(scope.row)"
              >下载</el-button>
              <el-button
                size="mini"
                type="text"
                icon="el-icon-delete"
                style="color: #F56C6C;"
                @click="handleRemoveAttachment(scope.row)"
              >删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-card>
    <!-- ç¼–辑利用记录对话框 -->
    <el-dialog
      title="编辑器官利用记录"
      :visible.sync="editDialogVisible"
      width="600px"
    >
      <el-form :model="currentRecord" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="器官名称">
              <el-input v-model="currentRecord.organName" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="移植状态">
              <el-select v-model="currentRecord.transplantStatus" style="width: 100%">
                <el-option
                  v-for="dict in transplantStatusList"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="弃用说明" v-if="currentRecord.transplantStatus === '0'">
          <el-input
            type="textarea"
            :rows="3"
            v-model="currentRecord.abandonReason"
            placeholder="请输入弃用的原因说明"
          />
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="editDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleEditConfirm">确认</el-button>
      </div>
    </el-dialog>
    <!-- éšè®¿è®°å½•对话框 -->
    <el-dialog
      :title="followupDialogTitle"
      :visible.sync="followupDialogVisible"
      width="700px"
    >
      <el-form :model="currentFollowup" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="器官名称">
              <el-select v-model="currentFollowup.organNo" style="width: 100%">
                <el-option
                  v-for="organ in utilizationData.records"
                  :key="organ.organNo"
                  :label="organ.organName"
                  :value="organ.organNo"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="随访类型">
              <el-select v-model="currentFollowup.followupType" style="width: 100%">
                <el-option label="常规随访" value="routine" />
                <el-option label="紧急随访" value="emergency" />
                <el-option label="特殊随访" value="special" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="随访时间">
              <el-date-picker
                v-model="currentFollowup.followupTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="随访医生">
              <el-input v-model="currentFollowup.followupDoctor" placeholder="请输入随访医生" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="受者情况">
          <el-input
            type="textarea"
            :rows="3"
            v-model="currentFollowup.recipientCondition"
            placeholder="请输入受者当前情况"
          />
        </el-form-item>
        <el-form-item label="用药情况">
          <el-input
            type="textarea"
            :rows="2"
            v-model="currentFollowup.medicationSituation"
            placeholder="请输入用药情况"
          />
        </el-form-item>
        <el-form-item label="检查结果">
          <el-input
            type="textarea"
            :rows="2"
            v-model="currentFollowup.testResults"
            placeholder="请输入检查结果"
          />
        </el-form-item>
        <el-form-item label="下次随访时间">
          <el-date-picker
            v-model="currentFollowup.nextFollowupTime"
            type="date"
            value-format="yyyy-MM-dd"
            style="width: 100%"
          />
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="followupDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSaveFollowup">保存</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import {
  getOrganUtilizationDetail,
  updateOrganUtilization,
  addOrganUtilization,
  saveUtilizationRecords,
  saveFollowupRecord,
  getHospitalList,
  getLeaderList,
  getCoordinatorList
} from "./organUtilization";
export default {
  name: "OrganUtilizationDetail",
  dicts: ["sys_user_sex", "sys_Organ", "sys_0_1"],
  data() {
    return {
      // è¡¨å•数据
      form: {
        id: undefined,
        hospitalNo: "",
        caseNo: "",
        donorName: "",
        gender: "",
        age: "",
        birthDate: "",
        diagnosis: "",
        utilizationStatus: "pending",
        allocationTime: "",
        registrant: "",
        registrationTime: "",
        isBodyDonation: "0",
        receivingUnit: "",
        responsibleUserId: "",
        coordinatedUserId1: "",
        coordinatedUserId2: "",
        completionTime: ""
      },
      // è¡¨å•验证规则
      rules: {
        donorName: [
          { required: true, message: "捐献者姓名不能为空", trigger: "blur" }
        ],
        diagnosis: [
          { required: true, message: "疾病诊断不能为空", trigger: "blur" }
        ]
      },
      // åˆ©ç”¨è®°å½•验证规则
      utilizationRules: {},
      // ä¿å­˜åŠ è½½çŠ¶æ€
      saveLoading: false,
      confirmLoading: false,
      // åŠ è½½çŠ¶æ€
      loading: false,
      // é€‰ä¸­çš„器官
      selectedOrgans: [],
      // åŒ»é™¢åˆ—表
      hospitalList: [],
      // è´Ÿè´£äººåˆ—表
      leaderList: [],
      // åè°ƒå‘˜åˆ—表
      coordinatorList: [],
      // ç§»æ¤çŠ¶æ€åˆ—è¡¨
      transplantStatusList: [
        { value: "1", label: "已移植" },
        { value: "0", label: "未移植" },
        { value: "2", label: "移植中" }
      ],
      // åˆ©ç”¨è®°å½•数据
      utilizationData: {
        records: []
      },
      // éšè®¿è®°å½•数据
      followupData: {
        records: []
      },
      // é™„件数据
      attachments: [],
      // å½“前激活的受者标签
      activeRecipientTab: "",
      // ç¼–辑对话框
      editDialogVisible: false,
      currentRecord: {},
      currentEditIndex: -1,
      // éšè®¿å¯¹è¯æ¡†
      followupDialogVisible: false,
      followupDialogTitle: "新增随访记录",
      currentFollowup: {},
      isEditingFollowup: false
    };
  },
  computed: {
    // å½“前用户信息
    currentUser() {
      return JSON.parse(sessionStorage.getItem("user") || "{}");
    },
    // ä¸å®Œæ•´çš„记录数量
    incompleteRecords() {
      return this.utilizationData.records.filter(
        record =>
          !record.caseNo ||
          !record.hospitalNo ||
          !record.recipientName ||
          !record.transplantTime
      ).length;
    },
    // å”¯ä¸€åŒ»é™¢æ•°é‡
    uniqueHospitals() {
      const hospitals = this.utilizationData.records
        .map(record => record.hospitalNo)
        .filter(Boolean);
      return new Set(hospitals).size;
    }
  },
  created() {
    const id = this.$route.query.id;
    if (id) {
      this.getDetail(id);
    } else {
      this.generateCaseNo();
      this.form.registrant = this.currentUser.username || "当前用户";
      this.form.registrationTime = new Date()
        .toISOString()
        .replace("T", " ")
        .substring(0, 19);
    }
    this.getHospitalData();
    this.getLeaderData();
    this.getCoordinatorData();
  },
  methods: {
    // ç”Ÿæˆæ¡ˆä¾‹ç¼–号
    generateCaseNo() {
      const timestamp = Date.now().toString();
      this.form.hospitalNo = "D" + timestamp.slice(-6);
      this.form.caseNo = "C" + timestamp.slice(-6);
    },
    // èŽ·å–è¯¦æƒ…
    getDetail(id) {
      this.loading = true;
      getOrganUtilizationDetail(id)
        .then(response => {
          if (response.code === 200) {
            this.form = response.data;
            if (response.data.utilizationRecords) {
              this.utilizationData.records = response.data.utilizationRecords;
              this.selectedOrgans = response.data.utilizationRecords.map(
                item => item.organNo
              );
              if (this.utilizationData.records.length > 0) {
                this.activeRecipientTab = this.utilizationData.records[0].organNo;
              }
            }
            if (response.data.followupRecords) {
              this.followupData.records = response.data.followupRecords;
            }
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取器官利用详情失败:", error);
          this.loading = false;
          this.$message.error("获取详情失败");
        });
    },
    // èŽ·å–åŒ»é™¢æ•°æ®
    getHospitalData() {
      getHospitalList().then(response => {
        if (response.code === 200) {
          this.hospitalList = response.data;
        }
      });
    },
    // èŽ·å–è´Ÿè´£äººæ•°æ®
    getLeaderData() {
      getLeaderList().then(response => {
        if (response.code === 200) {
          this.leaderList = response.data;
        }
      });
    },
    // èŽ·å–åè°ƒå‘˜æ•°æ®
    getCoordinatorData() {
      getCoordinatorList().then(response => {
        if (response.code === 200) {
          this.coordinatorList = response.data;
        }
      });
    },
    // å™¨å®˜é€‰æ‹©çŠ¶æ€å˜åŒ–
    handleOrganSelectionChange(selectedValues) {
      const currentOrganNos = this.utilizationData.records.map(
        item => item.organNo
      );
      // æ–°å¢žé€‰æ‹©çš„器官
      selectedValues.forEach(organValue => {
        if (!currentOrganNos.includes(organValue)) {
          const organInfo = this.dict.type.sys_Organ.find(
            item => item.value === organValue
          );
          if (organInfo) {
            this.utilizationData.records.push({
              organName: organInfo.label,
              organNo: organValue,
              id: null,
              utilizationId: this.form.id,
              caseNo: "",
              hospitalNo: "",
              hospitalName: "",
              recipientName: "",
              transplantDoctor: "",
              transplantTime: "",
              transplantStatus: "1",
              abandonReason: "",
              recipientBirthDate: "",
              recipientGender: "",
              transplantCenter: "",
              location: "",
              originalDisease: "",
              testIndicators: ""
            });
          }
        }
      });
      // ç§»é™¤å–消选择的器官
      this.utilizationData.records = this.utilizationData.records.filter(
        record => {
          if (selectedValues.includes(record.organNo)) {
            return true;
          } else {
            if (record.id) {
              this.$confirm(
                "删除器官利用数据后将无法恢复,您确认删除该条记录吗?",
                "提示",
                {
                  confirmButtonText: "确定",
                  cancelButtonText: "取消",
                  type: "warning"
                }
              )
                .then(() => {
                  this.utilizationData.records = this.utilizationData.records.filter(
                    r => r.organNo !== record.organNo
                  );
                  this.$message.success("删除成功");
                })
                .catch(() => {
                  this.selectedOrgans.push(record.organNo);
                });
              return true;
            } else {
              return false;
            }
          }
        }
      );
    },
    // åŒ»é™¢é€‰æ‹©å˜åŒ–
    handleHospitalChange(row, hospitalNo) {
      const hospital = this.hospitalList.find(
        item => item.hospitalNo === hospitalNo
      );
      if (hospital) {
        row.hospitalName = hospital.hospitalName;
      }
    },
    // ç¼–辑利用记录
    handleEditUtilization(row) {
      const index = this.utilizationData.records.findIndex(
        item => item.organNo === row.organNo
      );
      if (index !== -1) {
        this.currentRecord = { ...row };
        this.currentEditIndex = index;
        this.editDialogVisible = true;
      }
    },
    // ç¡®è®¤ç¼–辑
    handleEditConfirm() {
      if (this.currentEditIndex !== -1) {
        this.utilizationData.records[this.currentEditIndex] = {
          ...this.currentRecord
        };
        this.$message.success("利用记录更新成功");
        this.editDialogVisible = false;
      }
    },
    // å™¨å®˜è¡Œæ ·å¼
    getOrganRowClassName({ row }) {
      if (
        !row.caseNo ||
        !row.hospitalNo ||
        !row.recipientName ||
        !row.transplantTime
      ) {
        return "warning-row";
      }
      return "";
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTagType(status) {
      const typeMap = {
        completed: "success",
        in_progress: "warning",
        pending: "info"
      };
      return typeMap[status] || "info";
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      const textMap = {
        completed: "已完成",
        in_progress: "进行中",
        pending: "待处理"
      };
      return textMap[status] || "未知";
    },
    // èŽ·å–éšè®¿ç±»åž‹æ ‡ç­¾
    getFollowupTypeTag(type) {
      const typeMap = {
        routine: "success",
        emergency: "danger",
        special: "warning"
      };
      return typeMap[type] || "info";
    },
    // èŽ·å–éšè®¿ç±»åž‹æ–‡æœ¬
    getFollowupTypeText(type) {
      const textMap = {
        routine: "常规随访",
        emergency: "紧急随访",
        special: "特殊随访"
      };
      return textMap[type] || "未知";
    },
    // ä¿å­˜åŸºæœ¬ä¿¡æ¯
    handleSave() {
      this.$refs.form.validate(valid => {
        if (valid) {
          this.saveLoading = true;
          const apiMethod = this.form.id
            ? updateOrganUtilization
            : addOrganUtilization;
          apiMethod(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("保存成功");
                if (!this.form.id) {
                  this.form.id = response.data.id;
                  this.$router.replace({
                    query: { ...this.$route.query, id: this.form.id }
                  });
                }
              }
            })
            .catch(error => {
              console.error("保存失败:", error);
              this.$message.error("保存失败");
            })
            .finally(() => {
              this.saveLoading = false;
            });
        }
      });
    },
    // ä¿å­˜åˆ©ç”¨è®°å½•
    handleSaveUtilization() {
      if (!this.form.id) {
        this.$message.warning("请先保存基本信息");
        return;
      }
      this.saveLoading = true;
      saveUtilizationRecords(this.form.id, this.utilizationData.records)
        .then(response => {
          if (response.code === 200) {
            this.$message.success("利用记录保存成功");
          }
        })
        .catch(error => {
          console.error("保存利用记录失败:", error);
          this.$message.error("保存利用记录失败");
        })
        .finally(() => {
          this.saveLoading = false;
        });
    },
    // ç¡®è®¤å®Œæˆåˆ©ç”¨
    handleConfirmUtilization() {
      if (this.incompleteRecords > 0) {
        this.$message.warning("请先完善所有利用记录的信息");
        return;
      }
      this.$confirm("确认完成器官利用吗?完成后将无法修改利用信息。", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.confirmLoading = true;
          this.form.utilizationStatus = "completed";
          this.form.completionTime = new Date()
            .toISOString()
            .replace("T", " ")
            .substring(0, 19);
          updateOrganUtilization(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("器官利用已完成");
              }
            })
            .catch(error => {
              console.error("确认利用失败:", error);
              this.$message.error("确认利用失败");
            })
            .finally(() => {
              this.confirmLoading = false;
            });
        })
        .catch(() => {});
    },
    // å®Œæˆåˆ©ç”¨
    handleComplete() {
      this.handleConfirmUtilization();
    },
    // æ–°å¢žéšè®¿è®°å½•
    handleAddFollowup() {
      this.followupDialogTitle = "新增随访记录";
      this.isEditingFollowup = false;
      this.currentFollowup = {
        organNo: this.utilizationData.records.length > 0 ? this.utilizationData.records[0].organNo : "",
        followupTime: new Date().toISOString().replace("T", " ").substring(0, 19),
        followupType: "routine",
        recipientCondition: "",
        medicationSituation: "",
        testResults: "",
        nextFollowupTime: "",
        followupDoctor: ""
      };
      this.followupDialogVisible = true;
    },
    // æŸ¥çœ‹éšè®¿è®°å½•
    handleViewFollowup(record) {
      this.currentFollowup = { ...record };
      this.followupDialogTitle = "查看随访记录";
      this.followupDialogVisible = true;
    },
    // ç¼–辑随访记录
    handleEditFollowup(record) {
      this.followupDialogTitle = "编辑随访记录";
      this.isEditingFollowup = true;
      this.currentFollowup = { ...record };
      this.followupDialogVisible = true;
    },
    // åˆ é™¤éšè®¿è®°å½•
    handleDeleteFollowup(record) {
      this.$confirm("确定要删除这条随访记录吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          const index = this.followupData.records.findIndex(
            item => item.id === record.id
          );
          if (index !== -1) {
            this.followupData.records.splice(index, 1);
            this.$message.success("随访记录删除成功");
          }
        })
        .catch(() => {});
    },
    // ä¿å­˜éšè®¿è®°å½•
    handleSaveFollowup() {
      if (!this.currentFollowup.organNo) {
        this.$message.warning("请选择器官");
        return;
      }
      if (!this.currentFollowup.followupTime) {
        this.$message.warning("请选择随访时间");
        return;
      }
      this.saveLoading = true;
      // èŽ·å–å™¨å®˜åç§°
      const organRecord = this.utilizationData.records.find(
        item => item.organNo === this.currentFollowup.organNo
      );
      const organName = organRecord ? organRecord.organName : "";
      const followupData = {
        ...this.currentFollowup,
        organName: organName,
        utilizationId: this.form.id
      };
      saveFollowupRecord(followupData)
        .then(response => {
          if (response.code === 200) {
            if (this.isEditingFollowup) {
              // æ›´æ–°çŽ°æœ‰è®°å½•
              const index = this.followupData.records.findIndex(
                item => item.id === this.currentFollowup.id
              );
              if (index !== -1) {
                this.followupData.records[index] = response.data;
              }
            } else {
              // æ·»åŠ æ–°è®°å½•
              this.followupData.records.push(response.data);
            }
            this.$message.success("随访记录保存成功");
            this.followupDialogVisible = false;
          }
        })
        .catch(error => {
          console.error("保存随访记录失败:", error);
          this.$message.error("保存随访记录失败");
        })
        .finally(() => {
          this.saveLoading = false;
        });
    },
    // ä¸Šä¼ é™„ä»¶
    handleUploadAttachment() {
      this.$message.info("附件上传功能");
    },
    // é¢„览附件
    handlePreviewAttachment(attachment) {
      this.$message.info("附件预览功能");
    },
    // ä¸‹è½½é™„ä»¶
    handleDownloadAttachment(attachment) {
      this.$message.info("附件下载功能");
    },
    // åˆ é™¤é™„ä»¶
    handleRemoveAttachment(attachment) {
      this.$confirm("确定要删除这个附件吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$message.success("附件删除成功");
        })
        .catch(() => {});
    },
    // èŽ·å–æ–‡ä»¶å›¾æ ‡
    getFileIcon(fileName) {
      const ext = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const iconMap = {
        pdf: "el-icon-document",
        doc: "el-icon-document",
        docx: "el-icon-document",
        xls: "el-icon-document",
        xlsx: "el-icon-document",
        jpg: "el-icon-picture",
        jpeg: "el-icon-picture",
        png: "el-icon-picture"
      };
      return iconMap[ext] || "el-icon-document";
    },
    // èŽ·å–æ–‡ä»¶ç±»åž‹
    getFileType(fileName) {
      const ext = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const typeMap = {
        pdf: "PDF",
        doc: "DOC",
        docx: "DOCX",
        xls: "XLS",
        xlsx: "XLSX",
        jpg: "JPG",
        jpeg: "JPEG",
        png: "PNG"
      };
      return typeMap[ext] || ext.toUpperCase();
    },
    // æ–‡ä»¶å¤§å°æ ¼å¼åŒ–
    formatFileSize(size) {
      if (size === 0) return "0 B";
      const k = 1024;
      const sizes = ["B", "KB", "MB", "GB"];
      const i = Math.floor(Math.log(size) / Math.log(k));
      return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time) {
      if (!time) return "";
      const date = new Date(time);
      return `${date.getFullYear()}-${(date.getMonth() + 1)
        .toString()
        .padStart(2, "0")}-${date
        .getDate()
        .toString()
        .padStart(2, "0")} ${date
        .getHours()
        .toString()
        .padStart(2, "0")}:${date
        .getMinutes()
        .toString()
        .padStart(2, "0")}`;
    },
    // æäº¤å½’æ¡£
    handleSubmitArchive() {
      this.$confirm("确认提交归档吗?归档后将无法修改数据。", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$message.success("提交归档成功");
        })
        .catch(() => {});
    },
    // æ’¤é”€å½’æ¡£
    handleRevokeArchive() {
      this.$confirm("确认撤销归档吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$message.success("撤销归档成功");
        })
        .catch(() => {});
    },
    // ç»ˆæ­¢æ¡ˆä¾‹
    handleTerminateCase() {
      this.$confirm("确认终止案例吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$message.success("案例已终止");
        })
        .catch(() => {});
    },
    // æ¢å¤æ¡ˆä¾‹
    handleRestoreCase() {
      this.$confirm("确认恢复案例吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$message.success("案例已恢复");
        })
        .catch(() => {});
    }
  }
};
</script>
<style lang="scss" scoped>
.organ-utilization-detail {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.detail-card, .utilization-card, .recipient-card, .followup-card, .attachment-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  border: 1px solid #e4e7ed;
}
.detail-title {
  font-size: 18px;
  font-weight: 600;
  color: #303133;
  line-height: 1.4;
}
/* è¡¨æ ¼æ•´ä½“样式 */
:deep(.el-table) {
  border-radius: 8px;
  overflow: hidden;
}
:deep(.el-table th) {
  background-color: #f5f7fa;
  color: #606266;
  font-weight: 600;
}
:deep(.el-table .cell) {
  padding: 12px 8px;
  line-height: 1.5;
}
/* æ–‘马纹表格行 */
:deep(.el-table__row.warning-row) {
  background-color: #fdf6ec;
}
:deep(.el-table__row.default-row) {
  background-color: #f0f9ff;
}
/* é¼ æ ‡æ‚¬åœæ•ˆæžœ */
:deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
  background-color: #f5f7fa !important;
}
/* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
.utilization-stats {
  margin-top: 20px;
  padding: 15px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 8px;
  color: white;
}
.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px;
  text-align: center;
}
.stat-label {
  font-size: 18px;
  opacity: 0.9;
  margin-bottom: 5px;
}
.stat-value {
  font-size: 20px;
  font-weight: bold;
}
/* è¡¨å•标签和输入框样式 */
:deep(.el-form-item__label) {
  font-weight: 500;
  color: #606266;
}
:deep(.el-input__inner) {
  border-radius: 4px;
  transition: border-color 0.3s ease;
}
:deep(.el-input__inner:focus) {
  border-color: #409EFF;
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
/* æŒ‰é’®æ ·å¼ä¼˜åŒ– */
:deep(.el-button--primary) {
  background: linear-gradient(135deg, #409EFF 0%, #3375e0 100%);
  border: none;
  border-radius: 4px;
  transition: all 0.3s ease;
}
:deep(.el-button--primary:hover) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
}
/* æ ‡ç­¾é¡µæ ·å¼ */
:deep(.el-tabs__item) {
  font-weight: 500;
}
:deep(.el-tabs__active-bar) {
  background: linear-gradient(135deg, #409EFF 0%, #3375e0 100%);
}
/* å¹³æ¿è®¾å¤‡é€‚配 */
@media (max-width: 1024px) {
  .organ-utilization-detail {
    padding: 15px;
  }
  :deep(.el-col) {
    margin-bottom: 10px;
  }
}
/* æ‰‹æœºè®¾å¤‡é€‚配 */
@media (max-width: 768px) {
  .organ-utilization-detail {
    padding: 10px;
  }
  .detail-title {
    font-size: 16px;
  }
  :deep(.el-table .cell) {
    padding: 8px 4px;
    font-size: 12px;
  }
  :deep(.el-form-item__label) {
    font-size: 12px;
  }
}
/* è¶…小屏幕设备 */
@media (max-width: 480px) {
  .organ-utilization-detail {
    padding: 5px;
  }
  :deep(.el-card__header) {
    padding: 10px 15px;
  }
}
/* ç©ºçŠ¶æ€æ ·å¼ */
.empty-utilization {
  text-align: center;
  padding: 40px 0;
  color: #909399;
  background: #fafafa;
  border-radius: 4px;
  margin: 20px 0;
}
/* åŠ è½½çŠ¶æ€ */
:deep(.el-loading-mask) {
  border-radius: 4px;
}
/* æ–‡ä»¶ä¿¡æ¯æ ·å¼ */
.file-info {
  display: flex;
  align-items: center;
  padding: 5px 0;
}
.file-info i {
  font-size: 18px;
  margin-right: 10px;
}
/* åŠ¨ç”»æ•ˆæžœ */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>
src/views/business/OrganUtilization/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,377 @@
<template>
  <div class="organ-utilization-list">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-card class="search-card">
      <el-form
        :model="queryParams"
        ref="queryForm"
        :inline="true"
        label-width="100px"
      >
        <el-form-item label="住院号" prop="hospitalNo">
          <el-input
            v-model="queryParams.hospitalNo"
            placeholder="请输入住院号"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="捐献者姓名" prop="donorName">
          <el-input
            v-model="queryParams.donorName"
            placeholder="请输入捐献者姓名"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="利用状态" prop="utilizationStatus">
          <el-select
            v-model="queryParams.utilizationStatus"
            placeholder="请选择利用状态"
            clearable
            style="width: 200px"
          >
            <el-option label="已完成" value="completed" />
            <el-option label="进行中" value="in_progress" />
            <el-option label="待处理" value="pending" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="handleQuery"
            >搜索</el-button
          >
          <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- æ“ä½œæŒ‰é’® -->
    <el-card class="tool-card">
      <el-row :gutter="10">
        <el-col :span="16">
          <el-button type="primary" icon="el-icon-plus" @click="handleCreate"
            >新建利用</el-button
          >
          <el-button
            type="success"
            icon="el-icon-edit"
            :disabled="single"
            @click="handleUpdate"
            >修改</el-button
          >
          <el-button
            type="danger"
            icon="el-icon-delete"
            :disabled="multiple"
            @click="handleDelete"
            >删除</el-button
          >
        </el-col>
        <el-col :span="8" style="text-align: right">
          <el-tooltip content="刷新" placement="top">
            <el-button icon="el-icon-refresh" circle @click="getList" />
          </el-tooltip>
        </el-col>
      </el-row>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <el-table
        v-loading="loading"
        :data="organUtilizationList"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column
          label="住院号"
          align="center"
          prop="hospitalNo"
          width="120"
        />
        <el-table-column
          label="捐献者姓名"
          align="center"
          prop="donorName"
          width="120"
        />
        <el-table-column label="性别" align="center" prop="gender" width="80">
          <template slot-scope="scope">
            <dict-tag
              :options="dict.type.sys_user_sex"
              :value="parseInt(scope.row.gender)"
            />
          </template>
        </el-table-column>
        <el-table-column label="年龄" align="center" prop="age" width="80" />
        <el-table-column
          label="疾病诊断"
          align="center"
          prop="diagnosis"
          min-width="180"
          show-overflow-tooltip
        />
        <el-table-column
          label="利用状态"
          align="center"
          prop="utilizationStatus"
          width="100"
        >
          <template slot-scope="scope">
            <el-tag :type="getStatusTagType(scope.row.utilizationStatus)">
              {{ getStatusText(scope.row.utilizationStatus) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="登记人"
          align="center"
          prop="registrant"
          width="100"
        />
        <el-table-column
          label="登记时间"
          align="center"
          prop="registrationTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.registrationTime
                ? parseTime(scope.row.registrationTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          align="center"
          width="150"
          class-name="small-padding fixed-width"
        >
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleView(scope.row)"
              >详情</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-edit"
              @click="handleUpdate(scope.row)"
              >修改</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              style="color: #F56C6C"
              @click="handleDelete(scope.row)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="queryParams.pageNum"
        :limit.sync="queryParams.pageSize"
        @pagination="getList"
      />
    </el-card>
  </div>
</template>
<script>
import { listOrganUtilization, delOrganUtilization } from "./organUtilization";
import Pagination from "@/components/Pagination";
export default {
  name: "OrganUtilizationList",
  components: { Pagination },
  dicts: ["sys_user_sex"],
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ€»æ¡æ•°
      total: 0,
      // å™¨å®˜åˆ©ç”¨è¡¨æ ¼æ•°æ®
      organUtilizationList: [],
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        hospitalNo: undefined,
        donorName: undefined,
        utilizationStatus: undefined
      }
    };
  },
  created() {
    this.getList();
  },
  methods: {
    // æŸ¥è¯¢å™¨å®˜åˆ©ç”¨åˆ—表
    getList() {
      this.loading = true;
      listOrganUtilization(this.queryParams)
        .then(response => {
          if (response.code === 200) {
            this.organUtilizationList = response.data.rows;
            this.total = response.data.total;
          } else {
            this.$message.error("获取数据失败");
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取器官利用列表失败:", error);
          this.loading = false;
          this.$message.error("获取数据失败");
        });
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTagType(status) {
      const typeMap = {
        completed: "success",
        in_progress: "warning",
        pending: "info"
      };
      return typeMap[status] || "info";
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      const textMap = {
        completed: "已完成",
        in_progress: "进行中",
        pending: "待处理"
      };
      return textMap[status] || "未知";
    },
    // æœç´¢æŒ‰é’®æ“ä½œ
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŒ‰é’®æ“ä½œ
    resetQuery() {
      this.$refs.queryForm.resetFields();
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleView(row) {
      this.$router.push({
        path: "/case/organUtilizationInfo",
        query: { id: row.id }
      });
    },
    // æ–°å¢žæŒ‰é’®æ“ä½œ
    handleCreate() {
      this.$router.push("/case/organUtilizationInfo");
    },
    // ä¿®æ”¹æŒ‰é’®æ“ä½œ
    handleUpdate(row) {
      const id = row.id || this.ids[0];
      this.$router.push({
        path: "/case/organUtilizationInfo",
        query: { id: id }
      });
    },
    // åˆ é™¤æŒ‰é’®æ“ä½œ
    handleDelete(row) {
      const ids = row.id ? [row.id] : this.ids;
      this.$confirm("是否确认删除选中的数据项?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          return delOrganUtilization(ids);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("删除成功");
            this.getList();
          }
        })
        .catch(() => {});
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time, pattern) {
      if (!time) return "";
      const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
      let date;
      if (typeof time === "object") {
        date = time;
      } else {
        if (typeof time === "string" && /^[0-9]+$/.test(time)) {
          time = parseInt(time);
        }
        if (typeof time === "number" && time.toString().length === 10) {
          time = time * 1000;
        }
        date = new Date(time);
      }
      const formatObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
      };
      const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
        let value = formatObj[key];
        if (key === "a") {
          return ["日", "一", "二", "三", "四", "五", "六"][value];
        }
        if (result.length > 0 && value < 10) {
          value = "0" + value;
        }
        return value || 0;
      });
      return time_str;
    }
  }
};
</script>
<style scoped>
.organ-utilization-list {
  padding: 20px;
}
.search-card {
  margin-bottom: 20px;
}
.tool-card {
  margin-bottom: 20px;
}
.fixed-width .el-button {
  margin: 0 5px;
}
</style>
src/views/business/OrganUtilization/organUtilization.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,439 @@
// æ¨¡æ‹Ÿå™¨å®˜åˆ©ç”¨æ•°æ®
const mockOrganUtilizationData = [
  {
    id: 1,
    hospitalNo: "D202312001",
    caseNo: "C202312001",
    donorName: "张三",
    gender: "0",
    age: 45,
    diagnosis: "脑外伤",
    registrant: "李协调员",
    registrationTime: "2023-12-01 15:00:00",
    createTime: "2023-12-01 10:00:00",
    utilizationStatus: "completed",
    completionTime: "2023-12-01 16:30:00",
    isBodyDonation: "1",
    receivingUnit: "医科大学解剖教研室",
    responsibleUserId: "L001",
    coordinatedUserId1: "C001",
    coordinatedUserId2: "C002"
  },
  {
    id: 2,
    hospitalNo: "D202312002",
    caseNo: "C202312002",
    donorName: "李四",
    gender: "1",
    age: 38,
    diagnosis: "心脏骤停",
    registrant: "张协调员",
    registrationTime: "2023-12-02 10:00:00",
    createTime: "2023-12-02 08:30:00",
    utilizationStatus: "in_progress",
    completionTime: "",
    isBodyDonation: "0",
    receivingUnit: "",
    responsibleUserId: "",
    coordinatedUserId1: "",
    coordinatedUserId2: ""
  },
  {
    id: 3,
    hospitalNo: "D202312003",
    caseNo: "C202312003",
    donorName: "王五",
    gender: "0",
    age: 52,
    diagnosis: "脑梗死",
    registrant: "赵协调员",
    registrationTime: "2023-12-03 17:20:00",
    createTime: "2023-12-03 14:00:00",
    utilizationStatus: "pending",
    completionTime: "",
    isBodyDonation: "0",
    receivingUnit: "",
    responsibleUserId: "",
    coordinatedUserId1: "",
    coordinatedUserId2: ""
  }
];
// æ¨¡æ‹Ÿå™¨å®˜åˆ©ç”¨è®°å½•数据
const mockUtilizationRecordData = [
  {
    id: 1,
    utilizationId: 1,
    organName: "肝脏",
    organNo: "L001",
    caseNo: "C202312001",
    hospitalNo: "H1001",
    hospitalName: "北京协和医院",
    recipientName: "王",
    transplantDoctor: "张医生",
    transplantTime: "2023-12-01 16:00:00",
    transplantStatus: "1",
    abandonReason: "",
    recipientBirthDate: "1980-05-15",
    recipientGender: "0",
    transplantCenter: "北京协和医院移植中心",
    location: "北京市",
    originalDisease: "肝硬化",
    testIndicators: "肝功能正常,血型匹配"
  },
  {
    id: 2,
    utilizationId: 1,
    organName: "肾脏",
    organNo: "K001",
    caseNo: "C202312001",
    hospitalNo: "H1002",
    hospitalName: "上海瑞金医院",
    recipientName: "李",
    transplantDoctor: "王医生",
    transplantTime: "2023-12-01 16:30:00",
    transplantStatus: "1",
    abandonReason: "",
    recipientBirthDate: "1975-08-20",
    recipientGender: "1",
    transplantCenter: "上海瑞金医院移植中心",
    location: "上海市",
    originalDisease: "尿毒症",
    testIndicators: "肾功能正常,免疫匹配"
  },
  {
    id: 3,
    utilizationId: 1,
    organName: "心脏",
    organNo: "H001",
    caseNo: "C202312001",
    hospitalNo: "H1003",
    hospitalName: "广州中山医院",
    recipientName: "陈",
    transplantDoctor: "刘医生",
    transplantTime: "2023-12-01 17:00:00",
    transplantStatus: "1",
    abandonReason: "",
    recipientBirthDate: "1982-03-10",
    recipientGender: "0",
    transplantCenter: "广州中山医院心脏中心",
    location: "广州市",
    originalDisease: "心肌病",
    testIndicators: "心功能正常,血型匹配"
  }
];
// æ¨¡æ‹Ÿéšè®¿è®°å½•数据
const mockFollowupRecordData = [
  {
    id: 1,
    utilizationId: 1,
    organNo: "L001",
    followupTime: "2024-01-01 10:00:00",
    followupType: "routine",
    recipientCondition: "恢复良好,肝功能正常",
    medicationSituation: "免疫抑制剂规律服用",
    testResults: "肝功能指标正常,血药浓度达标",
    nextFollowupTime: "2024-04-01",
    followupDoctor: "张医生",
    attachment: "肝功能检查报告.pdf"
  },
  {
    id: 2,
    utilizationId: 1,
    organNo: "K001",
    followupTime: "2024-01-02 09:30:00",
    followupType: "routine",
    recipientCondition: "肾功能稳定,无排斥反应",
    medicationSituation: "抗排斥药物按时服用",
    testResults: "肌酐水平正常,尿常规无异常",
    nextFollowupTime: "2024-04-02",
    followupDoctor: "王医生",
    attachment: "肾功能随访报告.pdf"
  },
  {
    id: 3,
    utilizationId: 1,
    organNo: "H001",
    followupTime: "2024-01-03 11:00:00",
    followupType: "emergency",
    recipientCondition: "出现轻微排斥反应,已处理",
    medicationSituation: "调整免疫抑制剂剂量",
    testResults: "心功能指标基本正常,需密切观察",
    nextFollowupTime: "2024-01-10",
    followupDoctor: "刘医生",
    attachment: "心脏移植随访记录.pdf"
  }
];
// æ¨¡æ‹ŸåŒ»é™¢æ•°æ®
const mockHospitalData = [
  { id: 1, hospitalNo: "H1001", hospitalName: "北京协和医院", type: "4" },
  { id: 2, hospitalNo: "H1002", hospitalName: "上海瑞金医院", type: "4" },
  { id: 3, hospitalNo: "H1003", hospitalName: "广州中山医院", type: "4" }
];
// æ¨¡æ‹Ÿè´Ÿè´£äººæ•°æ®
const mockLeaderData = [
  { reportNo: "L001", reportName: "张主任" },
  { reportNo: "L002", reportName: "王主任" },
  { reportNo: "L003", reportName: "李主任" }
];
// æ¨¡æ‹Ÿåè°ƒå‘˜æ•°æ®
const mockCoordinatorData = [
  { reportNo: "C001", reportName: "张协调员" },
  { reportNo: "C002", reportName: "李协调员" },
  { reportNo: "C003", reportName: "王协调员" },
  { reportNo: "C004", reportName: "赵协调员" }
];
// æ¨¡æ‹ŸAPI响应延迟
const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms));
// æŸ¥è¯¢å™¨å®˜åˆ©ç”¨åˆ—表
export const listOrganUtilization = async (queryParams = {}) => {
  await delay();
  const {
    pageNum = 1,
    pageSize = 10,
    hospitalNo,
    donorName,
    utilizationStatus
  } = queryParams;
  // è¿‡æ»¤æ•°æ®
  let filteredData = mockOrganUtilizationData.filter(item => {
    let match = true;
    if (hospitalNo && !item.hospitalNo.includes(hospitalNo)) {
      match = false;
    }
    if (donorName && !item.donorName.includes(donorName)) {
      match = false;
    }
    if (utilizationStatus && item.utilizationStatus !== utilizationStatus) {
      match = false;
    }
    return match;
  });
  // åˆ†é¡µ
  const startIndex = (pageNum - 1) * pageSize;
  const endIndex = startIndex + parseInt(pageSize);
  const paginatedData = filteredData.slice(startIndex, endIndex);
  return {
    code: 200,
    message: "success",
    data: {
      rows: paginatedData,
      total: filteredData.length,
      pageNum: parseInt(pageNum),
      pageSize: parseInt(pageSize)
    }
  };
};
// èŽ·å–å™¨å®˜åˆ©ç”¨è¯¦ç»†ä¿¡æ¯
export const getOrganUtilizationDetail = async (id) => {
  await delay();
  const detail = mockOrganUtilizationData.find(item => item.id == id);
  if (detail) {
    // èŽ·å–åˆ©ç”¨è®°å½•
    const utilizationRecords = mockUtilizationRecordData.filter(item => item.utilizationId == id);
    // èŽ·å–éšè®¿è®°å½•
    const followupRecords = mockFollowupRecordData.filter(item => item.utilizationId == id);
    return {
      code: 200,
      message: "success",
      data: {
        ...detail,
        utilizationRecords,
        followupRecords
      }
    };
  } else {
    return {
      code: 404,
      message: "器官利用记录不存在"
    };
  }
};
// æ–°å¢žå™¨å®˜åˆ©ç”¨
export const addOrganUtilization = async (data) => {
  await delay();
  const newId = Math.max(...mockOrganUtilizationData.map(item => item.id), 0) + 1;
  const hospitalNo = `D${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const caseNo = `C${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const newRecord = {
    ...data,
    id: newId,
    hospitalNo,
    caseNo,
    registrationTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
    createTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
  };
  mockOrganUtilizationData.unshift(newRecord);
  return {
    code: 200,
    message: "新增成功",
    data: newRecord
  };
};
// ä¿®æ”¹å™¨å®˜åˆ©ç”¨
export const updateOrganUtilization = async (data) => {
  await delay();
  const index = mockOrganUtilizationData.findIndex(item => item.id == data.id);
  if (index !== -1) {
    mockOrganUtilizationData[index] = {
      ...mockOrganUtilizationData[index],
      ...data,
      updateTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
    };
    return {
      code: 200,
      message: "修改成功",
      data: mockOrganUtilizationData[index]
    };
  } else {
    return {
      code: 404,
      message: "器官利用记录不存在"
    };
  }
};
// åˆ é™¤å™¨å®˜åˆ©ç”¨
export const delOrganUtilization = async (ids) => {
  await delay();
  const idArray = Array.isArray(ids) ? ids : [ids];
  idArray.forEach(id => {
    const index = mockOrganUtilizationData.findIndex(item => item.id == id);
    if (index !== -1) {
      mockOrganUtilizationData.splice(index, 1);
    }
  });
  return {
    code: 200,
    message: "删除成功"
  };
};
// ä¿å­˜å™¨å®˜åˆ©ç”¨è®°å½•
export const saveUtilizationRecords = async (utilizationId, records) => {
  await delay();
  // åˆ é™¤è¯¥åˆ©ç”¨ID的所有记录
  const existingIndexes = [];
  mockUtilizationRecordData.forEach((item, index) => {
    if (item.utilizationId == utilizationId) {
      existingIndexes.push(index);
    }
  });
  // ä»ŽåŽå¾€å‰åˆ é™¤é¿å…ç´¢å¼•问题
  existingIndexes.reverse().forEach(index => {
    mockUtilizationRecordData.splice(index, 1);
  });
  // æ·»åŠ æ–°è®°å½•
  records.forEach(record => {
    const newId = Math.max(...mockUtilizationRecordData.map(item => item.id), 0) + 1;
    mockUtilizationRecordData.push({
      ...record,
      id: newId,
      utilizationId: utilizationId
    });
  });
  return {
    code: 200,
    message: "保存成功",
    data: records
  };
};
// ä¿å­˜éšè®¿è®°å½•
export const saveFollowupRecord = async (record) => {
  await delay();
  const newId = Math.max(...mockFollowupRecordData.map(item => item.id), 0) + 1;
  const newRecord = {
    ...record,
    id: newId
  };
  mockFollowupRecordData.push(newRecord);
  return {
    code: 200,
    message: "随访记录保存成功",
    data: newRecord
  };
};
// èŽ·å–åŒ»é™¢åˆ—è¡¨
export const getHospitalList = async () => {
  await delay();
  return {
    code: 200,
    message: "success",
    data: mockHospitalData
  };
};
// èŽ·å–è´Ÿè´£äººåˆ—è¡¨
export const getLeaderList = async () => {
  await delay();
  return {
    code: 200,
    message: "success",
    data: mockLeaderData
  };
};
// èŽ·å–åè°ƒå‘˜åˆ—è¡¨
export const getCoordinatorList = async () => {
  await delay();
  return {
    code: 200,
    message: "success",
    data: mockCoordinatorData
  };
};
export default {
  listOrganUtilization,
  getOrganUtilizationDetail,
  addOrganUtilization,
  updateOrganUtilization,
  delOrganUtilization,
  saveUtilizationRecords,
  saveFollowupRecord,
  getHospitalList,
  getLeaderList,
  getCoordinatorList
};
src/views/business/affirm/mockConfirmationApi.js
@@ -116,7 +116,18 @@
    }, 300);
  });
}
// æ›´æ–°æçŒ®ç¡®è®¤ä¿¡æ¯
export function updateDeathJudgment(data) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const index = confirmationData.rows.findIndex(item => item.id === data.id);
      if (index !== -1) {
        confirmationData.rows[index] = { ...confirmationData.rows[index], ...data };
      }
      resolve({ code: 200, message: '更新成功' });
    }, 300);
  });
}
// åˆ é™¤æçŒ®ç¡®è®¤
export function delConfirmation(ids) {
  return new Promise((resolve) => {
src/views/business/allocation/allocationInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1014 @@
<template>
  <div class="organ-allocation-detail">
    <!-- åŸºæœ¬ä¿¡æ¯éƒ¨åˆ† -->
    <el-card class="detail-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">器官分配基本信息</span>
        <div style="float: right;">
          <el-button type="primary" @click="handleSave" :loading="saveLoading">
            ä¿å­˜
          </el-button>
          <el-button
            type="success"
            @click="handleAllocate"
            :disabled="form.allocationStatus === 'allocated'"
          >
            ç¡®è®¤åˆ†é…
          </el-button>
        </div>
      </div>
      <el-form :model="form" ref="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="住院号" prop="hospitalNo">
              <el-input v-model="form.hospitalNo" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="案例编号" prop="caseNo">
              <el-input v-model="form.caseNo" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="捐献者姓名" prop="donorName">
              <el-input v-model="form.donorName" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="性别" prop="gender">
              <el-select v-model="form.gender" style="width: 100%">
                <el-option label="男" value="0" />
                <el-option label="女" value="1" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="年龄" prop="age">
              <el-input v-model="form.age" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="出生日期" prop="birthDate">
              <el-date-picker
                v-model="form.birthDate"
                type="date"
                value-format="yyyy-MM-dd"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="疾病诊断" prop="diagnosis">
              <el-input
                type="textarea"
                :rows="2"
                v-model="form.diagnosis"
                placeholder="请输入疾病诊断信息"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="分配时间" prop="allocationTime">
              <el-date-picker
                v-model="form.allocationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
                :disabled="form.allocationStatus !== 'allocated'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="登记人" prop="registrant">
              <el-input v-model="form.registrant" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="登记时间" prop="registrationTime">
              <el-date-picker
                v-model="form.registrationTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
                readonly
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </el-card>
    <!-- å™¨å®˜åˆ†é…è®°å½•部分 -->
    <el-card class="allocation-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">器官分配记录</span>
        <div style="float: right;">
          <el-tag
            :type="
              form.allocationStatus === 'allocated' ? 'success' : 'warning'
            "
          >
            {{ form.allocationStatus === "allocated" ? "已分配" : "待分配" }}
          </el-tag>
        </div>
      </div>
      <el-form
        ref="allocationForm"
        :rules="allocationRules"
        :model="allocationData"
        label-position="right"
      >
        <el-row>
          <el-col>
            <el-form-item label-width="100px" label="分配器官">
              <el-checkbox-group
                v-model="selectedOrgans"
                @change="handleOrganSelectionChange"
              >
                <el-checkbox
                  v-for="organ in organDict"
                  :key="organ.value"
                  :label="organ.value"
                  :disabled="form.allocationStatus === 'allocated'"
                >
                  {{ organ.label }}
                </el-checkbox>
              </el-checkbox-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col>
            <el-form-item>
              <el-table
                :data="allocationData.records"
                v-loading="loading"
                border
                style="width: 100%"
                :row-class-name="getOrganRowClassName"
              >
                <el-table-column
                  label="器官名称"
                  align="center"
                  width="120"
                  prop="organName"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.organName"
                      placeholder="器官名称"
                      :disabled="true"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="分配系统编号"
                  align="center"
                  width="150"
                  prop="systemNo"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.systemNo"
                      placeholder="分配系统编号"
                      :disabled="form.allocationStatus === 'allocated'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="分配接收时间"
                  align="center"
                  width="180"
                  prop="applicantTime"
                >
                  <template slot-scope="scope">
                    <el-date-picker
                      clearable
                      size="small"
                      style="width: 100%"
                      v-model="scope.row.applicantTime"
                      type="datetime"
                      value-format="yyyy-MM-dd HH:mm:ss"
                      placeholder="选择分配接收时间"
                      :disabled="form.allocationStatus === 'allocated'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="受体姓氏"
                  align="center"
                  width="120"
                  prop="recipientName"
                >
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.recipientName"
                      placeholder="受体姓氏"
                      :disabled="form.allocationStatus === 'allocated'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="移植医院"
                  align="center"
                  width="200"
                  prop="transplantHospitalNo"
                >
                  <template slot-scope="scope">
                    <el-select
                      v-model="scope.row.transplantHospitalNo"
                      placeholder="请选择移植医院"
                      style="width: 100%"
                      :disabled="form.allocationStatus === 'allocated'"
                      @change="handleHospitalChange(scope.row, $event)"
                    >
                      <el-option
                        v-for="hospital in hospitalList"
                        :key="hospital.hospitalNo"
                        :label="hospital.hospitalName"
                        :value="hospital.hospitalNo"
                      />
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column
                  label="说明"
                  align="center"
                  prop="reallocationReason"
                  min-width="200"
                >
                  <template slot-scope="scope">
                    <el-input
                      type="textarea"
                      clearable
                      v-model="scope.row.reallocationReason"
                      placeholder="请输入说明"
                      :disabled="form.allocationStatus === 'allocated'"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="操作"
                  align="center"
                  width="120"
                  class-name="small-padding fixed-width"
                  v-if="form.allocationStatus !== 'allocated'"
                >
                  <template slot-scope="scope">
                    <el-button
                      size="mini"
                      type="text"
                      icon="el-icon-copy-document"
                      @click="handleRedistribution(scope.row)"
                      :disabled="!scope.row.systemNo"
                    >
                      é‡åˆ†é…
                    </el-button>
                  </template>
                </el-table-column>
              </el-table>
            </el-form-item>
          </el-col>
        </el-row>
        <!-- åˆ†é…ç»Ÿè®¡ä¿¡æ¯ -->
        <div class="allocation-stats" v-if="allocationData.records.length > 0">
          <el-row :gutter="20">
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">已分配器官:</span>
                <span class="stat-value"
                  >{{ allocationData.records.length }} ä¸ª</span
                >
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">待完善信息:</span>
                <span class="stat-value">{{ incompleteRecords }} ä¸ª</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">涉及医院:</span>
                <span class="stat-value">{{ uniqueHospitals }} å®¶</span>
              </div>
            </el-col>
            <el-col :span="6">
              <div class="stat-item">
                <span class="stat-label">分配状态:</span>
                <span class="stat-value">
                  <el-tag
                    :type="
                      form.allocationStatus === 'allocated'
                        ? 'success'
                        : 'warning'
                    "
                  >
                    {{
                      form.allocationStatus === "allocated"
                        ? "已完成"
                        : "进行中"
                    }}
                  </el-tag>
                </span>
              </div>
            </el-col>
          </el-row>
        </div>
        <div v-else class="empty-allocation">
          <el-empty description="暂无分配记录" :image-size="80">
            <span>请先选择要分配的器官</span>
          </el-empty>
        </div>
      </el-form>
      <div class="dialog-footer" v-if="form.allocationStatus !== 'allocated'">
        <el-button
          type="primary"
          @click="handleSaveAllocation"
          :loading="saveLoading"
          :disabled="allocationData.records.length === 0"
        >
          ä¿å­˜åˆ†é…è®°å½•
        </el-button>
        <el-button
          type="success"
          @click="handleConfirmAllocation"
          :loading="confirmLoading"
          :disabled="incompleteRecords > 0"
        >
          ç¡®è®¤å®Œæˆåˆ†é…
        </el-button>
      </div>
    </el-card>
    <!-- é™„件管理部分 -->
    <el-card class="attachment-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">相关附件</span>
        <upload-attachment
          :file-list="attachments"
          @change="handleAttachmentChange"
          :limit="10"
          :accept="'.pdf,.jpg,.jpeg,.png,.doc,.docx'"
        />
      </div>
      <div class="attachment-list">
        <el-table :data="attachments" style="width: 100%">
          <el-table-column label="文件名称" min-width="200">
            <template slot-scope="scope">
              <div class="file-info">
                <i
                  :class="getFileIcon(scope.row.fileName)"
                  style="margin-right: 8px; color: #409EFF;"
                ></i>
                <span>{{ scope.row.fileName }}</span>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="文件类型" width="100" align="center">
            <template slot-scope="scope">
              <el-tag size="small">{{
                getFileType(scope.row.fileName)
              }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column label="文件大小" width="100" align="center">
            <template slot-scope="scope">
              <span>{{ formatFileSize(scope.row.fileSize) }}</span>
            </template>
          </el-table-column>
          <el-table-column label="上传时间" width="160" align="center">
            <template slot-scope="scope">
              <span>{{ parseTime(scope.row.uploadTime) }}</span>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="150" align="center">
            <template slot-scope="scope">
              <el-button
                size="mini"
                type="text"
                icon="el-icon-view"
                @click="handlePreviewAttachment(scope.row)"
                >预览</el-button
              >
              <el-button
                size="mini"
                type="text"
                icon="el-icon-download"
                @click="handleDownloadAttachment(scope.row)"
                >下载</el-button
              >
              <el-button
                size="mini"
                type="text"
                icon="el-icon-delete"
                style="color: #F56C6C;"
                @click="handleRemoveAttachment(scope.row)"
                >删除</el-button
              >
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-card>
    <!-- é™„件预览对话框 -->
    <attachment-preview
      :visible="attachmentPreviewVisible"
      :file-list="currentAttachmentList"
      :title="attachmentPreviewTitle"
      @close="attachmentPreviewVisible = false"
    />
    <!-- é‡åˆ†é…å¯¹è¯æ¡† -->
    <el-dialog
      title="器官重分配"
      :visible.sync="redistributionDialogVisible"
      width="500px"
    >
      <el-form :model="redistributionForm" label-width="100px">
        <el-form-item label="原器官信息">
          <el-input v-model="redistributionForm.organName" readonly />
        </el-form-item>
        <el-form-item label="重分配原因" prop="reason">
          <el-input
            type="textarea"
            :rows="4"
            v-model="redistributionForm.reason"
            placeholder="请输入重分配原因"
          />
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="redistributionDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleRedistributionConfirm"
          >确认重分配</el-button
        >
      </div>
    </el-dialog>
  </div>
</template>
<script>
import {
  getOrganAllocationDetail,
  updateOrganAllocation,
  saveAllocationRecords,
  getHospitalList,
  getOrganDict
} from "./organAllocation";
import UploadAttachment from "@/components/UploadAttachment";
import AttachmentPreview from "@/components/AttachmentPreview";
export default {
  name: "OrganAllocationDetail",
  components: {
    UploadAttachment,
    AttachmentPreview,
  },
  data() {
    return {
      // è¡¨å•数据
      form: {
        id: undefined,
        hospitalNo: "",
        caseNo: "",
        donorName: "",
        gender: "",
        age: "",
        birthDate: "",
        diagnosis: "",
        allocationStatus: "pending",
        allocationTime: "",
        registrant: "",
        registrationTime: ""
      },
      // è¡¨å•验证规则
      rules: {
        donorName: [
          { required: true, message: "捐献者姓名不能为空", trigger: "blur" }
        ],
        diagnosis: [
          { required: true, message: "疾病诊断不能为空", trigger: "blur" }
        ]
      },
      // åˆ†é…è®°å½•验证规则
      allocationRules: {},
      // ä¿å­˜åŠ è½½çŠ¶æ€
      saveLoading: false,
      confirmLoading: false,
      // åŠ è½½çŠ¶æ€
      loading: false,
      // é€‰ä¸­çš„器官
      selectedOrgans: [],
      // å™¨å®˜å­—å…¸
      organDict: [],
      // åŒ»é™¢åˆ—表
      hospitalList: [],
      // åˆ†é…è®°å½•数据
      allocationData: {
        records: []
      },
      // é™„件数据
      attachments: [],
      // é‡åˆ†é…å¯¹è¯æ¡†
      redistributionDialogVisible: false,
      redistributionForm: {
        organName: "",
        reason: ""
      },
      currentRedistributeRecord: null
    };
  },
  computed: {
    // å½“前用户信息
    currentUser() {
      return JSON.parse(sessionStorage.getItem("user") || "{}");
    },
    // ä¸å®Œæ•´çš„记录数量
    incompleteRecords() {
      return this.allocationData.records.filter(
        record =>
          !record.systemNo ||
          !record.applicantTime ||
          !record.recipientName ||
          !record.transplantHospitalNo
      ).length;
    },
    // å”¯ä¸€åŒ»é™¢æ•°é‡
    uniqueHospitals() {
      const hospitals = this.allocationData.records
        .map(record => record.transplantHospitalNo)
        .filter(Boolean);
      return new Set(hospitals).size;
    }
  },
  created() {
    const id = this.$route.query.id;
    if (id) {
      this.getDetail(id);
    } else {
      this.generateCaseNo();
      this.form.registrant = this.currentUser.username || "当前用户";
      this.form.registrationTime = new Date()
        .toISOString()
        .replace("T", " ")
        .substring(0, 19);
    }
    this.getOrganDictionary();
    this.getHospitalData();
  },
  methods: {
    // ç”Ÿæˆæ¡ˆä¾‹ç¼–号
    generateCaseNo() {
      const timestamp = Date.now().toString();
      this.form.hospitalNo = "D" + timestamp.slice(-6);
      this.form.caseNo = "C" + timestamp.slice(-6);
    },
    // èŽ·å–è¯¦æƒ…
    getDetail(id) {
      this.loading = true;
      getOrganAllocationDetail(id)
        .then(response => {
          if (response.code === 200) {
            this.form = response.data;
            if (response.data.allocationRecords) {
              this.allocationData.records = response.data.allocationRecords;
              this.selectedOrgans = response.data.allocationRecords.map(
                item => item.organNo
              );
            }
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取器官分配详情失败:", error);
          this.loading = false;
          this.$message.error("获取详情失败");
        });
    },
    // èŽ·å–å™¨å®˜å­—å…¸
    getOrganDictionary() {
      getOrganDict().then(response => {
        if (response.code === 200) {
          this.organDict = response.data;
        }
      });
    },
    // èŽ·å–åŒ»é™¢æ•°æ®
    getHospitalData() {
      getHospitalList().then(response => {
        if (response.code === 200) {
          this.hospitalList = response.data;
        }
      });
    },
      handleAttachmentChange(fileList) {
console.log(fileList,'测试');
    },
    // å™¨å®˜é€‰æ‹©çŠ¶æ€å˜åŒ–
    handleOrganSelectionChange(selectedValues) {
      const currentOrganNos = this.allocationData.records.map(
        item => item.organNo
      );
      // æ–°å¢žé€‰æ‹©çš„器官
      selectedValues.forEach(organValue => {
        if (!currentOrganNos.includes(organValue)) {
          const organInfo = this.organDict.find(
            item => item.value === organValue
          );
          if (organInfo) {
            this.allocationData.records.push({
              organName: organInfo.label,
              organNo: organValue,
              id: null,
              allocationId: this.form.id,
              systemNo: "",
              applicantTime: "",
              recipientName: "",
              transplantHospitalNo: "",
              transplantHospitalName: "",
              reallocationReason: "",
              organState: 1
            });
          }
        }
      });
      // ç§»é™¤å–消选择的器官
      this.allocationData.records = this.allocationData.records.filter(
        record => {
          if (selectedValues.includes(record.organNo)) {
            return true;
          } else {
            if (record.id) {
              this.$confirm(
                "删除器官分配数据后将无法恢复,您确认删除该条记录吗?",
                "提示",
                {
                  confirmButtonText: "确定",
                  cancelButtonText: "取消",
                  type: "warning"
                }
              )
                .then(() => {
                  // å®žé™…项目中这里应该调用删除API
                  this.allocationData.records = this.allocationData.records.filter(
                    r => r.organNo !== record.organNo
                  );
                  this.$message.success("删除成功");
                })
                .catch(() => {
                  this.selectedOrgans.push(record.organNo);
                });
              return true; // ç­‰å¾…用户确认
            } else {
              return false; // ç›´æŽ¥åˆ é™¤æ–°è®°å½•
            }
          }
        }
      );
    },
    // åŒ»é™¢é€‰æ‹©å˜åŒ–
    handleHospitalChange(row, hospitalNo) {
      const hospital = this.hospitalList.find(
        item => item.hospitalNo === hospitalNo
      );
      if (hospital) {
        row.transplantHospitalName = hospital.hospitalName;
      }
    },
    // é‡åˆ†é…æ“ä½œ
    handleRedistribution(row) {
      this.currentRedistributeRecord = row;
      this.redistributionForm.organName = row.organName;
      this.redistributionForm.reason = row.reallocationReason || "";
      this.redistributionDialogVisible = true;
    },
    // ç¡®è®¤é‡åˆ†é…
    handleRedistributionConfirm() {
      if (!this.redistributionForm.reason) {
        this.$message.warning("请输入重分配原因");
        return;
      }
      if (this.currentRedistributeRecord) {
        this.currentRedistributeRecord.reallocationReason = this.redistributionForm.reason;
        this.$message.success("重分配原因已更新");
        this.redistributionDialogVisible = false;
      }
    },
    // å™¨å®˜è¡Œæ ·å¼
    getOrganRowClassName({ row }) {
      if (
        !row.systemNo ||
        !row.applicantTime ||
        !row.recipientName ||
        !row.transplantHospitalNo
      ) {
        return "warning-row";
      }
      return "";
    },
    // ä¿å­˜åŸºæœ¬ä¿¡æ¯
    handleSave() {
      this.$refs.form.validate(valid => {
        if (valid) {
          this.saveLoading = true;
          const apiMethod = this.form.id
            ? updateOrganAllocation
            : addOrganAllocation;
          apiMethod(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("保存成功");
                if (!this.form.id) {
                  this.form.id = response.data.id;
                  this.$router.replace({
                    query: { ...this.$route.query, id: this.form.id }
                  });
                }
              }
            })
            .catch(error => {
              console.error("保存失败:", error);
              this.$message.error("保存失败");
            })
            .finally(() => {
              this.saveLoading = false;
            });
        }
      });
    },
    // ä¿å­˜åˆ†é…è®°å½•
    handleSaveAllocation() {
      if (!this.form.id) {
        this.$message.warning("请先保存基本信息");
        return;
      }
      this.saveLoading = true;
      saveAllocationRecords(this.form.id, this.allocationData.records)
        .then(response => {
          if (response.code === 200) {
            this.$message.success("分配记录保存成功");
          }
        })
        .catch(error => {
          console.error("保存分配记录失败:", error);
          this.$message.error("保存分配记录失败");
        })
        .finally(() => {
          this.saveLoading = false;
        });
    },
    // ç¡®è®¤å®Œæˆåˆ†é…
    handleConfirmAllocation() {
      if (this.incompleteRecords > 0) {
        this.$message.warning("请先完善所有分配记录的信息");
        return;
      }
      this.$confirm("确认完成器官分配吗?完成后将无法修改分配信息。", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.confirmLoading = true;
          this.form.allocationStatus = "allocated";
          this.form.allocationTime = new Date()
            .toISOString()
            .replace("T", " ")
            .substring(0, 19);
          updateOrganAllocation(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("器官分配已完成");
              }
            })
            .catch(error => {
              console.error("确认分配失败:", error);
              this.$message.error("确认分配失败");
            })
            .finally(() => {
              this.confirmLoading = false;
            });
        })
        .catch(() => {});
    },
    // ä¸Šä¼ é™„ä»¶
    handleUploadAttachment() {
      // é™„件上传逻辑
      this.$message.info("附件上传功能");
    },
    // é¢„览附件
    handlePreviewAttachment(attachment) {
      // é™„件预览逻辑
      this.$message.info("附件预览功能");
    },
    // ä¸‹è½½é™„ä»¶
    handleDownloadAttachment(attachment) {
      // é™„件下载逻辑
      this.$message.info("附件下载功能");
    },
    // åˆ é™¤é™„ä»¶
    handleRemoveAttachment(attachment) {
      this.$confirm("确定要删除这个附件吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$message.success("附件删除成功");
        })
        .catch(() => {});
    },
    // èŽ·å–æ–‡ä»¶å›¾æ ‡
    getFileIcon(fileName) {
      const ext = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const iconMap = {
        pdf: "el-icon-document",
        doc: "el-icon-document",
        docx: "el-icon-document",
        xls: "el-icon-document",
        xlsx: "el-icon-document",
        jpg: "el-icon-picture",
        jpeg: "el-icon-picture",
        png: "el-icon-picture"
      };
      return iconMap[ext] || "el-icon-document";
    },
    // èŽ·å–æ–‡ä»¶ç±»åž‹
    getFileType(fileName) {
      const ext = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const typeMap = {
        pdf: "PDF",
        doc: "DOC",
        docx: "DOCX",
        xls: "XLS",
        xlsx: "XLSX",
        jpg: "JPG",
        jpeg: "JPEG",
        png: "PNG"
      };
      return typeMap[ext] || ext.toUpperCase();
    },
    // æ–‡ä»¶å¤§å°æ ¼å¼åŒ–
    formatFileSize(size) {
      if (size === 0) return "0 B";
      const k = 1024;
      const sizes = ["B", "KB", "MB", "GB"];
      const i = Math.floor(Math.log(size) / Math.log(k));
      return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time) {
      if (!time) return "";
      const date = new Date(time);
      return `${date.getFullYear()}-${(date.getMonth() + 1)
        .toString()
        .padStart(2, "0")}-${date
        .getDate()
        .toString()
        .padStart(2, "0")} ${date
        .getHours()
        .toString()
        .padStart(2, "0")}:${date
        .getMinutes()
        .toString()
        .padStart(2, "0")}`;
    }
  }
};
</script>
<style scoped>
.organ-allocation-detail {
  padding: 20px;
  background-color: #f5f7fa;
}
.detail-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.allocation-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.attachment-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.detail-title {
  font-size: 18px;
  font-weight: 600;
  color: #303133;
}
/* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
.allocation-stats {
  margin-top: 20px;
  padding: 15px;
  background: linear-gradient(135deg, #9eb7e5 0%, #53519c 100%);
  border-radius: 8px;
  color: white;
  font-size: 18px;
}
.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px;
}
.stat-label {
  /* font-size: 12px; */
  opacity: 0.9;
  /* color: #606266; */
  margin-bottom: 5px;
}
.stat-value {
  font-size: 20px;
  font-weight: bold;
  /* color: #303133; */
}
/* ç©ºçŠ¶æ€æ ·å¼ */
.empty-allocation {
  text-align: center;
  padding: 40px 0;
  color: #909399;
}
/* å¯¹è¯æ¡†åº•部按钮 */
.dialog-footer {
  margin-top: 20px;
  text-align: center;
  padding-top: 20px;
  border-top: 1px solid #e4e7ed;
}
/* è¡¨æ ¼è¡Œæ ·å¼ */
:deep(.warning-row) {
  background-color: #fff7e6;
}
:deep(.warning-row:hover) {
  background-color: #ffecc2;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .organ-allocation-detail {
    padding: 10px;
  }
  .allocation-stats .el-col {
    margin-bottom: 10px;
  }
}
</style>
src/views/business/allocation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,372 @@
<template>
  <div class="organ-allocation-list">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-card class="search-card">
      <el-form
        :model="queryParams"
        ref="queryForm"
        :inline="true"
        label-width="100px"
      >
        <el-form-item label="住院号" prop="hospitalNo">
          <el-input
            v-model="queryParams.hospitalNo"
            placeholder="请输入住院号"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="捐献者姓名" prop="donorName">
          <el-input
            v-model="queryParams.donorName"
            placeholder="请输入捐献者姓名"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="分配状态" prop="allocationStatus">
          <el-select
            v-model="queryParams.allocationStatus"
            placeholder="请选择分配状态"
            clearable
            style="width: 200px"
          >
            <el-option label="已分配" value="allocated" />
            <el-option label="待分配" value="pending" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="handleQuery"
            >搜索</el-button
          >
          <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- æ“ä½œæŒ‰é’® -->
    <el-card class="tool-card">
      <el-row :gutter="10">
        <el-col :span="16">
          <el-button type="primary" icon="el-icon-plus" @click="handleCreate"
            >新建分配</el-button
          >
          <el-button
            type="success"
            icon="el-icon-edit"
            :disabled="single"
            @click="handleUpdate"
            >修改</el-button
          >
          <el-button
            type="danger"
            icon="el-icon-delete"
            :disabled="multiple"
            @click="handleDelete"
            >删除</el-button
          >
        </el-col>
        <el-col :span="8" style="text-align: right">
          <el-tooltip content="刷新" placement="top">
            <el-button icon="el-icon-refresh" circle @click="getList" />
          </el-tooltip>
        </el-col>
      </el-row>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <el-table
        v-loading="loading"
        :data="organAllocationList"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column
          label="住院号"
          align="center"
          prop="hospitalNo"
          width="120"
        />
        <el-table-column
          label="捐献者姓名"
          align="center"
          prop="donorName"
          width="120"
        />
        <el-table-column label="性别" align="center" prop="gender" width="80">
          <template slot-scope="scope">
            <dict-tag
              :options="dict.type.sys_user_sex"
              :value="parseInt(scope.row.gender)"
            />
          </template>
        </el-table-column>
        <el-table-column label="年龄" align="center" prop="age" width="80" />
        <el-table-column
          label="疾病诊断"
          align="center"
          prop="diagnosis"
          min-width="180"
          show-overflow-tooltip
        />
        <el-table-column
          label="分配状态"
          align="center"
          prop="allocationStatus"
          width="100"
        >
          <template slot-scope="scope">
            <el-tag :type="scope.row.allocationStatus === 'allocated' ? 'success' : 'warning'">
              {{ scope.row.allocationStatus === 'allocated' ? '已分配' : '待分配' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="分配时间"
          align="center"
          prop="allocationTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.allocationTime
                ? parseTime(scope.row.allocationTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="登记人"
          align="center"
          prop="registrant"
          width="100"
        />
        <el-table-column
          label="登记时间"
          align="center"
          prop="registrationTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.registrationTime
                ? parseTime(scope.row.registrationTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          align="center"
          width="150"
          class-name="small-padding fixed-width"
        >
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleView(scope.row)"
              >详情</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-edit"
              @click="handleUpdate(scope.row)"
              >修改</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              style="color: #F56C6C"
              @click="handleDelete(scope.row)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="queryParams.pageNum"
        :limit.sync="queryParams.pageSize"
        @pagination="getList"
      />
    </el-card>
  </div>
</template>
<script>
import { listOrganAllocation, delOrganAllocation } from "./organAllocation";
import Pagination from "@/components/Pagination";
export default {
  name: "OrganAllocationList",
  components: { Pagination },
  dicts: ["sys_user_sex"],
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ€»æ¡æ•°
      total: 0,
      // å™¨å®˜åˆ†é…è¡¨æ ¼æ•°æ®
      organAllocationList: [],
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        hospitalNo: undefined,
        donorName: undefined,
        allocationStatus: undefined
      }
    };
  },
  created() {
    this.getList();
  },
  methods: {
    // æŸ¥è¯¢å™¨å®˜åˆ†é…åˆ—表
    getList() {
      this.loading = true;
      listOrganAllocation(this.queryParams)
        .then(response => {
          if (response.code === 200) {
            this.organAllocationList = response.data.rows;
            this.total = response.data.total;
          } else {
            this.$message.error("获取数据失败");
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取器官分配列表失败:", error);
          this.loading = false;
          this.$message.error("获取数据失败");
        });
    },
    // æœç´¢æŒ‰é’®æ“ä½œ
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŒ‰é’®æ“ä½œ
    resetQuery() {
      this.$refs.queryForm.resetFields();
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleView(row) {
      this.$router.push({
        path: "/case/allocationInfo",
        query: { id: row.id }
      });
    },
    // æ–°å¢žæŒ‰é’®æ“ä½œ
    handleCreate() {
      this.$router.push("/case/allocationInfo");
    },
    // ä¿®æ”¹æŒ‰é’®æ“ä½œ
    handleUpdate(row) {
      const id = row.id || this.ids[0];
      this.$router.push({
        path: "/case/allocationInfo",
        query: { id: id }
      });
    },
    // åˆ é™¤æŒ‰é’®æ“ä½œ
    handleDelete(row) {
      const ids = row.id ? [row.id] : this.ids;
      this.$confirm("是否确认删除选中的数据项?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          return delOrganAllocation(ids);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("删除成功");
            this.getList();
          }
        })
        .catch(() => {});
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time, pattern) {
      if (!time) return "";
      const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
      let date;
      if (typeof time === "object") {
        date = time;
      } else {
        if (typeof time === "string" && /^[0-9]+$/.test(time)) {
          time = parseInt(time);
        }
        if (typeof time === "number" && time.toString().length === 10) {
          time = time * 1000;
        }
        date = new Date(time);
      }
      const formatObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
      };
      const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
        let value = formatObj[key];
        if (key === "a") {
          return ["日", "一", "二", "三", "四", "五", "六"][value];
        }
        if (result.length > 0 && value < 10) {
          value = "0" + value;
        }
        return value || 0;
      });
      return time_str;
    }
  }
};
</script>
<style scoped>
.organ-allocation-list {
  padding: 20px;
}
.search-card {
  margin-bottom: 20px;
}
.tool-card {
  margin-bottom: 20px;
}
.fixed-width .el-button {
  margin: 0 5px;
}
</style>
src/views/business/allocation/organAllocation.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,329 @@
// æ¨¡æ‹Ÿå™¨å®˜åˆ†é…æ•°æ®
const mockOrganAllocationData = [
  {
    id: 1,
    hospitalNo: "D202312001",
    caseNo: "C202312001",
    donorName: "张三",
    gender: "0",
    age: 45,
    birthDate: "1978-05-15",
    diagnosis: "脑外伤",
    allocationStatus: "allocated",
    allocationTime: "2023-12-01 16:30:00",
    registrant: "李协调员",
    registrationTime: "2023-12-01 15:00:00",
    createTime: "2023-12-01 10:00:00"
  },
  {
    id: 2,
    hospitalNo: "D202312002",
    caseNo: "C202312002",
    donorName: "李四",
    gender: "1",
    age: 38,
    birthDate: "1985-08-22",
    diagnosis: "心脏骤停",
    allocationStatus: "allocated",
    allocationTime: "2023-12-02 11:20:00",
    registrant: "张协调员",
    registrationTime: "2023-12-02 10:00:00",
    createTime: "2023-12-02 08:30:00"
  },
  {
    id: 3,
    hospitalNo: "D202312003",
    caseNo: "C202312003",
    donorName: "王五",
    gender: "0",
    age: 52,
    birthDate: "1971-03-10",
    diagnosis: "脑梗死",
    allocationStatus: "pending",
    allocationTime: "",
    registrant: "赵协调员",
    registrationTime: "2023-12-03 17:20:00",
    createTime: "2023-12-03 14:00:00"
  }
];
// æ¨¡æ‹Ÿå™¨å®˜åˆ†é…è®°å½•数据
const mockAllocationRecordData = [
  {
    id: 1,
    allocationId: 1,
    organName: "肝脏",
    organNo: "L001",
    caseNo: "C202312001",
    systemNo: "AL202312001",
    applicantTime: "2023-12-01 17:00:00",
    recipientName: "王",
    transplantHospitalNo: "H1001",
    transplantHospitalName: "北京协和医院",
    reallocationReason: "",
    organState: 1
  },
  {
    id: 2,
    allocationId: 1,
    organName: "肾脏",
    organNo: "K001",
    caseNo: "C202312001",
    systemNo: "AL202312002",
    applicantTime: "2023-12-01 17:30:00",
    recipientName: "李",
    transplantHospitalNo: "H1002",
    transplantHospitalName: "上海瑞金医院",
    reallocationReason: "",
    organState: 1
  },
  {
    id: 3,
    allocationId: 1,
    organName: "心脏",
    organNo: "H001",
    caseNo: "C202312001",
    systemNo: "AL202312003",
    applicantTime: "2023-12-01 18:00:00",
    recipientName: "å¼ ",
    transplantHospitalNo: "H1003",
    transplantHospitalName: "广州中山医院",
    reallocationReason: "",
    organState: 1
  }
];
// æ¨¡æ‹ŸåŒ»é™¢æ•°æ®
const mockHospitalData = [
  { id: 1, hospitalNo: "H1001", hospitalName: "北京协和医院", type: "4" },
  { id: 2, hospitalNo: "H1002", hospitalName: "上海瑞金医院", type: "4" },
  { id: 3, hospitalNo: "H1003", hospitalName: "广州中山医院", type: "4" },
  { id: 4, hospitalNo: "H1004", hospitalName: "武汉同济医院", type: "4" },
  { id: 5, hospitalNo: "H1005", hospitalName: "成都华西医院", type: "4" }
];
// æ¨¡æ‹Ÿå™¨å®˜ç±»åž‹å­—å…¸
const mockOrganDict = [
  { value: "L001", label: "肝脏" },
  { value: "K001", label: "肾脏" },
  { value: "H001", label: "心脏" },
  { value: "L002", label: "肺脏" },
  { value: "P001", label: "胰腺" },
  { value: "I001", label: "小肠" },
  { value: "C001", label: "角膜" }
];
// æ¨¡æ‹ŸAPI响应延迟
const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms));
// æŸ¥è¯¢å™¨å®˜åˆ†é…åˆ—表
export const listOrganAllocation = async (queryParams = {}) => {
  await delay();
  const {
    pageNum = 1,
    pageSize = 10,
    hospitalNo,
    donorName,
    allocationStatus
  } = queryParams;
  // è¿‡æ»¤æ•°æ®
  let filteredData = mockOrganAllocationData.filter(item => {
    let match = true;
    if (hospitalNo && !item.hospitalNo.includes(hospitalNo)) {
      match = false;
    }
    if (donorName && !item.donorName.includes(donorName)) {
      match = false;
    }
    if (allocationStatus && item.allocationStatus !== allocationStatus) {
      match = false;
    }
    return match;
  });
  // åˆ†é¡µ
  const startIndex = (pageNum - 1) * pageSize;
  const endIndex = startIndex + parseInt(pageSize);
  const paginatedData = filteredData.slice(startIndex, endIndex);
  return {
    code: 200,
    message: "success",
    data: {
      rows: paginatedData,
      total: filteredData.length,
      pageNum: parseInt(pageNum),
      pageSize: parseInt(pageSize)
    }
  };
};
// èŽ·å–å™¨å®˜åˆ†é…è¯¦ç»†ä¿¡æ¯
export const getOrganAllocationDetail = async (id) => {
  await delay();
  const detail = mockOrganAllocationData.find(item => item.id == id);
  if (detail) {
    // èŽ·å–åˆ†é…è®°å½•
    const allocationRecords = mockAllocationRecordData.filter(item => item.allocationId == id);
    return {
      code: 200,
      message: "success",
      data: {
        ...detail,
        allocationRecords
      }
    };
  } else {
    return {
      code: 404,
      message: "器官分配记录不存在"
    };
  }
};
// æ–°å¢žå™¨å®˜åˆ†é…
export const addOrganAllocation = async (data) => {
  await delay();
  const newId = Math.max(...mockOrganAllocationData.map(item => item.id), 0) + 1;
  const hospitalNo = `D${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const caseNo = `C${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const newRecord = {
    ...data,
    id: newId,
    hospitalNo,
    caseNo,
    registrationTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
    createTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
  };
  mockOrganAllocationData.unshift(newRecord);
  return {
    code: 200,
    message: "新增成功",
    data: newRecord
  };
};
// ä¿®æ”¹å™¨å®˜åˆ†é…
export const updateOrganAllocation = async (data) => {
  await delay();
  const index = mockOrganAllocationData.findIndex(item => item.id == data.id);
  if (index !== -1) {
    mockOrganAllocationData[index] = {
      ...mockOrganAllocationData[index],
      ...data,
      updateTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
    };
    return {
      code: 200,
      message: "修改成功",
      data: mockOrganAllocationData[index]
    };
  } else {
    return {
      code: 404,
      message: "器官分配记录不存在"
    };
  }
};
// åˆ é™¤å™¨å®˜åˆ†é…
export const delOrganAllocation = async (ids) => {
  await delay();
  const idArray = Array.isArray(ids) ? ids : [ids];
  idArray.forEach(id => {
    const index = mockOrganAllocationData.findIndex(item => item.id == id);
    if (index !== -1) {
      mockOrganAllocationData.splice(index, 1);
    }
  });
  return {
    code: 200,
    message: "删除成功"
  };
};
// ä¿å­˜å™¨å®˜åˆ†é…è®°å½•
export const saveAllocationRecords = async (allocationId, records) => {
  await delay();
  // åˆ é™¤è¯¥åˆ†é…ID的所有记录
  const existingIndexes = [];
  mockAllocationRecordData.forEach((item, index) => {
    if (item.allocationId == allocationId) {
      existingIndexes.push(index);
    }
  });
  // ä»ŽåŽå¾€å‰åˆ é™¤é¿å…ç´¢å¼•问题
  existingIndexes.reverse().forEach(index => {
    mockAllocationRecordData.splice(index, 1);
  });
  // æ·»åŠ æ–°è®°å½•
  records.forEach(record => {
    const newId = Math.max(...mockAllocationRecordData.map(item => item.id), 0) + 1;
    mockAllocationRecordData.push({
      ...record,
      id: newId,
      allocationId: allocationId
    });
  });
  return {
    code: 200,
    message: "保存成功",
    data: records
  };
};
// èŽ·å–åŒ»é™¢åˆ—è¡¨
export const getHospitalList = async () => {
  await delay();
  return {
    code: 200,
    message: "success",
    data: mockHospitalData
  };
};
// èŽ·å–å™¨å®˜å­—å…¸
export const getOrganDict = async () => {
  await delay();
  return {
    code: 200,
    message: "success",
    data: mockOrganDict
  };
};
export default {
  listOrganAllocation,
  getOrganAllocationDetail,
  addOrganAllocation,
  updateOrganAllocation,
  delOrganAllocation,
  saveAllocationRecords,
  getHospitalList,
  getOrganDict
};
src/views/business/appear/caseDetail.vue
@@ -3,40 +3,76 @@
    <el-tabs v-model="activeTab">
      <el-tab-pane label="基本信息" name="basic">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="捐献编号">{{ caseData.donorNo }}</el-descriptions-item>
          <el-descriptions-item label="捐献者姓名">{{ caseData.donorName }}</el-descriptions-item>
          <el-descriptions-item label="捐献编号">{{
            caseData.donorNo
          }}</el-descriptions-item>
          <el-descriptions-item label="捐献者姓名">{{
            caseData.donorName
          }}</el-descriptions-item>
          <el-descriptions-item label="性别">
            <dict-tag :options="genderOptions" :value="caseData.gender"/>
            <dict-tag :options="genderOptions" :value="caseData.gender" />
          </el-descriptions-item>
          <el-descriptions-item label="年龄">{{ caseData.age }}岁</el-descriptions-item>
          <el-descriptions-item label="年龄"
            >{{ caseData.age }}岁</el-descriptions-item
          >
          <el-descriptions-item label="血型">
            <dict-tag :options="bloodTypeOptions" :value="caseData.bloodType"/>
            <dict-tag :options="bloodTypeOptions" :value="caseData.bloodType" />
          </el-descriptions-item>
          <el-descriptions-item label="证件号码">{{ caseData.idCardNo }}</el-descriptions-item>
          <el-descriptions-item label="民族">{{ caseData.nation }}</el-descriptions-item>
          <el-descriptions-item label="联系电话">{{ caseData.phone }}</el-descriptions-item>
          <el-descriptions-item label="住址" :span="2">{{ caseData.address }}</el-descriptions-item>
          <el-descriptions-item label="证件号码">{{
            caseData.idCardNo
          }}</el-descriptions-item>
          <el-descriptions-item label="民族">{{
            caseData.nation
          }}</el-descriptions-item>
          <el-descriptions-item label="联系电话">{{
            caseData.phone
          }}</el-descriptions-item>
          <el-descriptions-item label="住址" :span="2">{{
            caseData.address
          }}</el-descriptions-item>
        </el-descriptions>
      </el-tab-pane>
      <el-tab-pane label="医疗信息" name="medical">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="疾病诊断">{{ caseData.diagnosis }}</el-descriptions-item>
          <el-descriptions-item label="住院号">{{ caseData.inpatientNo }}</el-descriptions-item>
          <el-descriptions-item label="所在科室">{{ caseData.departmentName }}</el-descriptions-item>
          <el-descriptions-item label="主治医生">{{ caseData.doctorName }}</el-descriptions-item>
          <el-descriptions-item label="传染病情况">{{ caseData.infectiousDisease || '无' }}</el-descriptions-item>
          <el-descriptions-item label="医疗记录">{{ caseData.medicalRecord }}</el-descriptions-item>
          <el-descriptions-item label="疾病诊断">{{
            caseData.diagnosis
          }}</el-descriptions-item>
          <el-descriptions-item label="住院号">{{
            caseData.inpatientNo
          }}</el-descriptions-item>
          <el-descriptions-item label="所在科室">{{
            caseData.departmentName
          }}</el-descriptions-item>
          <el-descriptions-item label="主治医生">{{
            caseData.doctorName
          }}</el-descriptions-item>
          <el-descriptions-item label="传染病情况">{{
            caseData.infectiousDisease || "无"
          }}</el-descriptions-item>
          <el-descriptions-item label="医疗记录">{{
            caseData.medicalRecord
          }}</el-descriptions-item>
        </el-descriptions>
      </el-tab-pane>
      <el-tab-pane label="医院信息" name="hospital">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="医院名称">{{ caseData.hospitalName }}</el-descriptions-item>
          <el-descriptions-item label="医院级别">{{ caseData.hospitalLevel }}</el-descriptions-item>
          <el-descriptions-item label="联系人">{{ caseData.contactPerson }}</el-descriptions-item>
          <el-descriptions-item label="联系电话">{{ caseData.contactPhone }}</el-descriptions-item>
          <el-descriptions-item label="医院地址" :span="2">{{ caseData.hospitalAddress }}</el-descriptions-item>
          <el-descriptions-item label="医院名称">{{
            caseData.hospitalName
          }}</el-descriptions-item>
          <el-descriptions-item label="医院级别">{{
            caseData.hospitalLevel
          }}</el-descriptions-item>
          <el-descriptions-item label="联系人">{{
            caseData.contactPerson
          }}</el-descriptions-item>
          <el-descriptions-item label="联系电话">{{
            caseData.contactPhone
          }}</el-descriptions-item>
          <el-descriptions-item label="医院地址" :span="2">{{
            caseData.hospitalAddress
          }}</el-descriptions-item>
        </el-descriptions>
      </el-tab-pane>
@@ -99,16 +135,26 @@
        </el-card>
      </el-tab-pane>
      <el-tab-pane label="审批信息" name="approval" v-if="caseData.status !== '0'">
      <el-tab-pane
        label="审批信息"
        name="approval"
        v-if="caseData.status !== '0'"
      >
        <el-descriptions :column="1" border>
          <el-descriptions-item label="审批结果">
            <el-tag :type="caseData.status | statusFilter">
              {{ caseData.status | statusTextFilter }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="审批时间">{{ caseData.approveTime }}</el-descriptions-item>
          <el-descriptions-item label="审批人">{{ caseData.approverName }}</el-descriptions-item>
          <el-descriptions-item label="审批意见">{{ caseData.approveOpinion }}</el-descriptions-item>
          <el-descriptions-item label="审批时间">{{
            caseData.approveTime
          }}</el-descriptions-item>
          <el-descriptions-item label="审批人">{{
            caseData.approverName
          }}</el-descriptions-item>
          <el-descriptions-item label="审批意见">{{
            caseData.approveOpinion
          }}</el-descriptions-item>
        </el-descriptions>
      </el-tab-pane>
    </el-tabs>
@@ -252,24 +298,24 @@
  filters: {
    statusFilter(status) {
      const statusMap = {
        '0': 'warning',
        '1': 'success',
        '2': 'danger'
        "0": "warning",
        "1": "success",
        "2": "danger"
      };
      return statusMap[status];
    },
    statusTextFilter(status) {
      const statusMap = {
        '0': '待审批',
        '1': '已通过',
        '2': '已驳回'
        "0": "待审批",
        "1": "已通过",
        "2": "已驳回"
      };
      return statusMap[status];
    }
  },
  data() {
    return {
      activeTab: 'basic',
      activeTab: "basic",
      genderOptions: [
        { value: "0", label: "男" },
        { value: "1", label: "女" }
@@ -333,12 +379,15 @@
  },
  methods: {
    handleClose() {
      this.$emit('close');
      this.$emit("close");
    },
    // èŽ·å–æ–‡ä»¶ç±»åž‹
    getFileType(fileName) {
      const extension = fileName.split('.').pop().toLowerCase();
      const extension = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const imageTypes = ["jpg", "jpeg", "png", "gif", "bmp", "webp"];
      const pdfTypes = ["pdf"];
src/views/business/course/components/BaseStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
<template>
  <div class="base-stage">
    <slot name="header"></slot>
    <div class="stage-content">
      <slot></slot>
    </div>
    <slot name="footer"></slot>
  </div>
</template>
<script>
export default {
  name: 'BaseStage',
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  methods: {
    // æ ¼å¼åŒ–æ—¶é—´
    formatTime(time) {
      if (!time) return '-';
      return this.$dayjs(time).format('YYYY-MM-DD HH:mm');
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTag(status) {
      const map = {
        'completed': 'success',
        'in_progress': 'warning',
        'pending': 'info'
      };
      return map[status] || 'info';
    }
  }
};
</script>
<style scoped>
.base-stage {
  padding: 0;
}
.stage-content {
  min-height: 200px;
}
</style>
src/views/business/course/components/DeathJudgmentStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,206 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <template #header>
      <el-alert
        title="死亡判定阶段"
        :type="stageData.status === 'completed' ? 'success' : 'warning'"
        :description="getAlertDescription()"
        show-icon
        :closable="false"
      />
    </template>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>判定基本信息</span>
          </div>
          <el-descriptions :column="1" border>
            <el-descriptions-item label="判定类型">
              <el-tag type="primary">脑死亡判定</el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="判定时间">
              {{ formatTime(judgmentDetails.judgmentTime) }}
            </el-descriptions-item>
            <el-descriptions-item label="判定医生一">
              {{ judgmentDetails.doctor1 }}
            </el-descriptions-item>
            <el-descriptions-item label="判定医生二">
              {{ judgmentDetails.doctor2 }}
            </el-descriptions-item>
            <el-descriptions-item label="判定结果">
              <el-tag :type="judgmentDetails.result ? 'success' : 'warning'">
                {{ judgmentDetails.result ? '脑死亡确认' : '判定中' }}
              </el-tag>
            </el-descriptions-item>
          </el-descriptions>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>判定流程记录</span>
          </div>
          <el-steps direction="vertical" :active="judgmentSteps.active" space="100px">
            <el-step
              v-for="step in judgmentSteps.steps"
              :key="step.title"
              :title="step.title"
              :description="step.description"
              :status="step.status"
            />
          </el-steps>
        </el-card>
      </el-col>
    </el-row>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>判定详细记录</span>
      </div>
      <el-table :data="judgmentRecords" border>
        <el-table-column label="检查项目" prop="item" width="180" />
        <el-table-column label="检查方法" prop="method" width="150" />
        <el-table-column label="检查结果" prop="result" width="120">
          <template slot-scope="scope">
            <el-tag :type="scope.row.result === '符合' ? 'success' : 'warning'">
              {{ scope.row.result }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="检查时间" width="160">
          <template slot-scope="scope">
            {{ formatTime(scope.row.time) }}
          </template>
        </el-table-column>
        <el-table-column label="检查医生" prop="doctor" width="120" />
        <el-table-column label="备注" prop="remark" min-width="200" />
      </el-table>
    </el-card>
    <template #footer>
      <div class="action-buttons" style="margin-top: 20px; text-align: center;">
        <el-button type="primary" @click="handleViewCertificate">
          æŸ¥çœ‹æ­»äº¡è¯æ˜Ž
        </el-button>
        <el-button type="success" @click="handleConfirmJudgment">
          ç¡®è®¤åˆ¤å®šç»“æžœ
        </el-button>
      </div>
    </template>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
export default {
  name: 'DeathJudgmentStage',
  components: { BaseStage },
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      judgmentDetails: {
        judgmentTime: '2023-12-03 09:15:00',
        doctor1: '张主任',
        doctor2: '王医生',
        result: true,
        certificateNo: 'SW20231203001'
      },
      judgmentSteps: {
        active: 4,
        steps: [
          {
            title: '初步临床检查',
            description: '完成自主呼吸测试',
            status: 'finish'
          },
          {
            title: '脑干反射测试',
            description: '各项反射测试完成',
            status: 'finish'
          },
          {
            title: '确认性检查',
            description: '脑电图检查完成',
            status: 'finish'
          },
          {
            title: '最终判定',
            description: '两位医生独立判定',
            status: 'finish'
          }
        ]
      },
      judgmentRecords: [
        {
          item: '自主呼吸测试',
          method: '呼吸暂停试验',
          result: '符合',
          time: '2023-12-03 08:30:00',
          doctor: '张主任',
          remark: '无自主呼吸'
        },
        {
          item: '瞳孔对光反射',
          method: '光刺激测试',
          result: '符合',
          time: '2023-12-03 08:45:00',
          doctor: '王医生',
          remark: '瞳孔固定,对光反射消失'
        },
        {
          item: '脑干听觉诱发电位',
          method: 'BAEP检查',
          result: '符合',
          time: '2023-12-03 09:00:00',
          doctor: '李医生',
          remark: '脑干功能丧失'
        }
      ]
    };
  },
  methods: {
    getAlertDescription() {
      if (this.stageData.status === 'completed') {
        return '脑死亡判定已完成,符合器官捐献条件';
      } else if (this.stageData.status === 'in_progress') {
        return '死亡判定流程正在进行中';
      }
      return '等待开始死亡判定流程';
    },
    handleViewCertificate() {
      this.$message.info('查看死亡证明功能');
    },
    handleConfirmJudgment() {
      this.$confirm('确认死亡判定结果吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$message.success('死亡判定结果已确认');
      });
    }
  }
};
</script>
<style scoped>
.action-buttons {
  display: flex;
  justify-content: center;
  gap: 15px;
  margin-top: 20px;
}
</style>
src/views/business/course/components/DonationConfirmStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,217 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <template #header>
      <el-alert
        :title="`捐献确认 - ${getStatusText()}`"
        :type="getAlertType()"
        :description="getAlertDescription()"
        show-icon
        :closable="false"
      />
    </template>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>确认信息概览</span>
          </div>
          <div class="confirm-overview">
            <div class="overview-item">
              <span class="label">确认状态:</span>
              <el-tag :type="getStatusTag(confirmationDetails.status)">
                {{ getStatusText(confirmationDetails.status) }}
              </el-tag>
            </div>
            <div class="overview-item">
              <span class="label">确认时间:</span>
              <span>{{ formatTime(confirmationDetails.confirmTime) }}</span>
            </div>
            <div class="overview-item">
              <span class="label">确认方式:</span>
              <span>{{ confirmationDetails.method }}</span>
            </div>
            <div class="overview-item">
              <span class="label">协调员:</span>
              <span>{{ confirmationDetails.coordinator }}</span>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>家属同意情况</span>
          </div>
          <el-descriptions :column="1" size="small">
            <el-descriptions-item label="主要家属">
              {{ familyConsent.mainRelative }}
            </el-descriptions-item>
            <el-descriptions-item label="同意状态">
              <el-tag :type="familyConsent.consented ? 'success' : 'warning'">
                {{ familyConsent.consented ? '已同意' : '待确认' }}
              </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="签字时间">
              {{ formatTime(familyConsent.signTime) }}
            </el-descriptions-item>
            <el-descriptions-item label="关系证明">
              {{ familyConsent.relationshipProof }}
            </el-descriptions-item>
          </el-descriptions>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>法律文件</span>
          </div>
          <div class="document-list">
            <div v-for="doc in legalDocuments" :key="doc.name" class="document-item">
              <div class="doc-info">
                <i :class="doc.icon" style="color: #409EFF; margin-right: 8px;"></i>
                <span>{{ doc.name }}</span>
              </div>
              <el-tag :type="doc.status === 'completed' ? 'success' : 'warning'" size="small">
                {{ doc.status === 'completed' ? '已签署' : '待签署' }}
              </el-tag>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>捐献意愿确认书</span>
      </div>
      <div class="consent-content">
        <p>本人/家属确认,在充分了解器官捐献的相关信息后,自愿同意进行器官捐献。</p>
        <el-divider />
        <el-descriptions :column="2" border>
          <el-descriptions-item label="捐献者姓名">{{ caseInfo.donorName }}</el-descriptions-item>
          <el-descriptions-item label="捐献者身份证号">{{ confirmationDetails.idNumber }}</el-descriptions-item>
          <el-descriptions-item label="捐献器官类型">{{ confirmationDetails.organs }}</el-descriptions-item>
          <el-descriptions-item label="捐献用途">{{ confirmationDetails.purpose }}</el-descriptions-item>
          <el-descriptions-item label="签字人">{{ familyConsent.mainRelative }}</el-descriptions-item>
          <el-descriptions-item label="签字日期">{{ formatTime(familyConsent.signTime) }}</el-descriptions-item>
        </el-descriptions>
      </div>
    </el-card>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
export default {
  name: 'DonationConfirmStage',
  components: { BaseStage },
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      confirmationDetails: {
        status: 'completed',
        confirmTime: '2023-12-03 11:00:00',
        method: '家属书面同意',
        coordinator: '赵协调员',
        idNumber: '110101199001011234',
        organs: '心脏、肝脏、肾脏、角膜',
        purpose: '临床移植'
      },
      familyConsent: {
        mainRelative: '张三父亲',
        consented: true,
        signTime: '2023-12-03 10:45:00',
        relationshipProof: '户口本关系证明'
      },
      legalDocuments: [
        { name: '器官捐献同意书', icon: 'el-icon-document', status: 'completed' },
        { name: '家属关系证明', icon: 'el-icon-document', status: 'completed' },
        { name: '医疗免责声明', icon: 'el-icon-document', status: 'completed' },
        { name: '隐私保护协议', icon: 'el-icon-document', status: 'completed' }
      ]
    };
  },
  methods: {
    getStatusText() {
      const status = this.stageData.status;
      return status === 'completed' ? '已完成' :
             status === 'in_progress' ? '进行中' : '未开始';
    },
    getAlertType() {
      const status = this.stageData.status;
      return status === 'completed' ? 'success' :
             status === 'in_progress' ? 'warning' : 'info';
    },
    getAlertDescription() {
      const status = this.stageData.status;
      return status === 'completed' ? '捐献确认流程已完成,所有法律文件已签署' :
             status === 'in_progress' ? '捐献确认流程正在进行中' : '等待开始捐献确认流程';
    },
    getStatusTag(status) {
      const map = {
        'completed': 'success',
        'in_progress': 'warning',
        'pending': 'info'
      };
      return map[status] || 'info';
    }
  }
};
</script>
<style scoped>
.confirm-overview {
  padding: 10px 0;
}
.overview-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  padding: 8px 0;
  border-bottom: 1px solid #f0f0f0;
}
.overview-item .label {
  color: #606266;
  font-weight: 500;
}
.document-list {
  padding: 10px 0;
}
.document-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
  padding: 8px 12px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
}
.doc-info {
  display: flex;
  align-items: center;
}
.consent-content {
  padding: 20px;
  line-height: 1.6;
}
</style>
src/views/business/course/components/DonorMaintenanceStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,156 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <template #header>
      <el-alert
        title="供者维护阶段"
        type="success"
        description="供者信息维护已完成,所有基本信息已确认无误"
        show-icon
        :closable="false"
      />
    </template>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="12">
        <el-card class="info-card">
          <div slot="header" class="card-header">
            <span>供者基本信息</span>
          </div>
          <el-descriptions :column="1" border size="small">
            <el-descriptions-item label="住院号">
              {{ caseInfo.hospitalNo }}
            </el-descriptions-item>
            <el-descriptions-item label="捐献者姓名">
              {{ caseInfo.donorName }}
            </el-descriptions-item>
            <el-descriptions-item label="性别">
              <dict-tag :options="dict.type.sys_user_sex" :value="parseInt(caseInfo.gender)" />
            </el-descriptions-item>
            <el-descriptions-item label="年龄">
              {{ caseInfo.age }} å²
            </el-descriptions-item>
            <el-descriptions-item label="疾病诊断">
              {{ caseInfo.diagnosis }}
            </el-descriptions-item>
          </el-descriptions>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card class="timeline-card">
          <div slot="header" class="card-header">
            <span>维护时间线</span>
          </div>
          <el-timeline>
            <el-timeline-item
              v-for="event in maintenanceEvents"
              :key="event.time"
              :timestamp="formatTime(event.time)"
              :type="event.type"
            >
              {{ event.content }}
            </el-timeline-item>
          </el-timeline>
        </el-card>
      </el-col>
    </el-row>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>维护记录详情</span>
      </div>
      <el-table :data="maintenanceRecords" border>
        <el-table-column label="维护项目" prop="item" width="150" />
        <el-table-column label="维护内容" prop="content" min-width="200" />
        <el-table-column label="维护人" prop="operator" width="120" />
        <el-table-column label="维护时间" width="160">
          <template slot-scope="scope">
            {{ formatTime(scope.row.time) }}
          </template>
        </el-table-column>
        <el-table-column label="状态" width="100">
          <template slot-scope="scope">
            <el-tag :type="scope.row.status === 'completed' ? 'success' : 'warning'">
              {{ scope.row.status === 'completed' ? '已完成' : '进行中' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
export default {
  name: 'DonorMaintenanceStage',
  components: { BaseStage },
  dicts: ['sys_user_sex'],
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      maintenanceEvents: [
        {
          time: '2023-12-01 08:30:00',
          content: '供者基本信息录入',
          type: 'primary'
        },
        {
          time: '2023-12-01 09:15:00',
          content: '医疗档案建立',
          type: 'success'
        },
        {
          time: '2023-12-01 10:00:00',
          content: '初步评估完成',
          type: 'success'
        }
      ],
      maintenanceRecords: [
        {
          item: '基本信息',
          content: '供者身份信息确认与录入',
          operator: '张医生',
          time: '2023-12-01 08:30:00',
          status: 'completed'
        },
        {
          item: '医疗档案',
          content: '病史资料收集与整理',
          operator: '李护士',
          time: '2023-12-01 09:15:00',
          status: 'completed'
        },
        {
          item: '初步评估',
          content: '捐献适宜性初步评估',
          operator: '王主任',
          time: '2023-12-01 10:00:00',
          status: 'completed'
        }
      ]
    };
  }
};
</script>
<style scoped>
.card-header {
  font-weight: 600;
  color: #303133;
}
.info-card, .timeline-card {
  height: 100%;
}
</style>
src/views/business/course/components/EthicalReviewStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,206 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <template #header>
      <el-alert
        title="伦理审查阶段"
        :type="getAlertType()"
        :description="getAlertDescription()"
        show-icon
        :closable="false"
      />
    </template>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>审查委员会信息</span>
          </div>
          <el-descriptions :column="1" border>
            <el-descriptions-item label="委员会名称">
              {{ reviewCommittee.name }}
            </el-descriptions-item>
            <el-descriptions-item label="审查会议时间">
              {{ formatTime(reviewCommittee.meetingTime) }}
            </el-descriptions-item>
            <el-descriptions-item label="参会委员">
              {{ reviewCommittee.members.length }} äºº
            </el-descriptions-item>
            <el-descriptions-item label="审查结论">
              <el-tag :type="reviewCommittee.conclusion ? 'success' : 'warning'">
                {{ reviewCommittee.conclusion ? '审查通过' : '审查中' }}
              </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="主席签字">
              {{ reviewCommittee.chairman }}
            </el-descriptions-item>
          </el-descriptions>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>审查流程进度</span>
          </div>
          <el-steps direction="vertical" :active="reviewProgress.active" space="80px">
            <el-step
              v-for="step in reviewProgress.steps"
              :key="step.title"
              :title="step.title"
              :description="step.description"
              :status="step.status"
            />
          </el-steps>
        </el-card>
      </el-col>
    </el-row>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>委员审查意见</span>
      </div>
      <el-table :data="reviewComments" border>
        <el-table-column label="委员姓名" prop="memberName" width="120" />
        <el-table-column label="专业领域" prop="specialty" width="120" />
        <el-table-column label="审查意见" prop="comment" min-width="200" />
        <el-table-column label="投票结果" width="100">
          <template slot-scope="scope">
            <el-tag :type="scope.row.vote === '同意' ? 'success' : 'danger'">
              {{ scope.row.vote }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="审查时间" width="160">
          <template slot-scope="scope">
            {{ formatTime(scope.row.reviewTime) }}
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>审查决议文件</span>
        <el-button type="primary" size="small" @click="handleViewResolution">
          æŸ¥çœ‹å†³è®®æ–‡ä»¶
        </el-button>
      </div>
      <div class="resolution-content">
        <p><strong>伦理审查决议:</strong></p>
        <p>{{ resolutionContent }}</p>
        <el-divider />
        <div class="signature-area">
          <p>伦理委员会主席:{{ reviewCommittee.chairman }}</p>
          <p>日期:{{ formatTime(reviewCommittee.meetingTime) }}</p>
        </div>
      </div>
    </el-card>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
export default {
  name: 'EthicalReviewStage',
  components: { BaseStage },
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      reviewCommittee: {
        name: '医院伦理审查委员会',
        meetingTime: '2023-12-03 15:20:00',
        members: ['张教授', '李主任', '王医生', '赵委员', '钱专家'],
        conclusion: true,
        chairman: '张教授'
      },
      reviewProgress: {
        active: 4,
        steps: [
          {
            title: '材料初审',
            description: '申请材料完整性审查',
            status: 'finish'
          },
          {
            title: '委员评审',
            description: '各委员独立审查',
            status: 'finish'
          },
          {
            title: '会议讨论',
            description: '委员会集体讨论',
            status: 'finish'
          },
          {
            title: '形成决议',
            description: '投票形成最终决议',
            status: 'finish'
          }
        ]
      },
      reviewComments: [
        {
          memberName: '张教授',
          specialty: '医学伦理',
          comment: '捐献程序符合伦理规范,同意通过',
          vote: '同意',
          reviewTime: '2023-12-03 14:30:00'
        },
        {
          memberName: '李主任',
          specialty: '临床医学',
          comment: '医疗程序规范,无伦理问题',
          vote: '同意',
          reviewTime: '2023-12-03 14:45:00'
        },
        {
          memberName: '王医生',
          specialty: '法律医学',
          comment: '法律文件齐全,程序合法',
          vote: '同意',
          reviewTime: '2023-12-03 15:00:00'
        }
      ],
      resolutionContent: '经伦理审查委员会审查,该器官捐献案例符合医学伦理要求,捐献程序规范,家属意愿真实有效,同意进行器官捐献。'
    };
  },
  methods: {
    getAlertType() {
      const status = this.stageData.status;
      return status === 'completed' ? 'success' :
             status === 'in_progress' ? 'warning' : 'info';
    },
    getAlertDescription() {
      const status = this.stageData.status;
      return status === 'completed' ? '伦理审查已通过,可以进行器官分配' :
             status === 'in_progress' ? '伦理审查流程正在进行中' : '等待开始伦理审查流程';
    },
    handleViewResolution() {
      this.$message.info('查看伦理审查决议文件功能');
    }
  }
};
</script>
<style scoped>
.resolution-content {
  padding: 20px;
  line-height: 1.8;
}
.signature-area {
  text-align: right;
  margin-top: 30px;
}
</style>
src/views/business/course/components/MedicalAssessmentStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,208 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <template #header>
      <el-alert
        :title="alertTitle"
        :type="alertType"
        :description="alertDescription"
        show-icon
        :closable="false"
      />
    </template>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>评估概况</span>
          </div>
          <div class="assessment-stats">
            <div class="stat-item">
              <span class="stat-label">评估状态:</span>
              <el-tag :type="getStatusTag(stageData.status)">
                {{ getStatusText(stageData.status) }}
              </el-tag>
            </div>
            <div class="stat-item">
              <span class="stat-label">评估医生:</span>
              <span>{{ stageData.operator || '待分配' }}</span>
            </div>
            <div class="stat-item">
              <span class="stat-label">开始时间:</span>
              <span>{{ formatTime(stageData.updateTime) }}</span>
            </div>
            <div class="stat-item">
              <span class="stat-label">完成时间:</span>
              <span>{{ formatTime(stageData.completeTime) || '-' }}</span>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="16">
        <el-card>
          <div slot="header" class="card-header">
            <span>评估项目进度</span>
          </div>
          <div class="progress-list">
            <div v-for="item in assessmentItems" :key="item.name" class="progress-item">
              <div class="progress-info">
                <span class="item-name">{{ item.name }}</span>
                <span class="item-status">
                  <el-tag :type="item.status === 'completed' ? 'success' : 'warning'" size="small">
                    {{ item.status === 'completed' ? '已完成' : '待评估' }}
                  </el-tag>
                </span>
              </div>
              <el-progress
                :percentage="item.status === 'completed' ? 100 : 0"
                :show-text="false"
                :status="item.status === 'completed' ? 'success' : 'exception'"
              />
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>评估详情记录</span>
        <el-button type="primary" size="small" @click="handleViewReport">
          æŸ¥çœ‹è¯„估报告
        </el-button>
      </div>
      <el-descriptions :column="2" border>
        <el-descriptions-item label="生理指标评估">
          {{ assessmentDetails.physiological || '待评估' }}
        </el-descriptions-item>
        <el-descriptions-item label="器官功能评估">
          {{ assessmentDetails.organFunction || '待评估' }}
        </el-descriptions-item>
        <el-descriptions-item label="感染性疾病筛查">
          {{ assessmentDetails.infectionScreening || '待筛查' }}
        </el-descriptions-item>
        <el-descriptions-item label="恶性肿瘤筛查">
          {{ assessmentDetails.cancerScreening || '待筛查' }}
        </el-descriptions-item>
        <el-descriptions-item label="评估结论">
          <el-tag :type="assessmentDetails.conclusion ? 'success' : 'warning'">
            {{ assessmentDetails.conclusion || '评估中' }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="评估医生意见">
          {{ assessmentDetails.doctorOpinion || '待填写' }}
        </el-descriptions-item>
      </el-descriptions>
    </el-card>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
export default {
  name: 'MedicalAssessmentStage',
  components: { BaseStage },
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  computed: {
    alertTitle() {
      const status = this.stageData.status;
      return status === 'completed' ? '医学评估完成' :
             status === 'in_progress' ? '医学评估进行中' : '待开始医学评估';
    },
    alertType() {
      const status = this.stageData.status;
      return status === 'completed' ? 'success' :
             status === 'in_progress' ? 'warning' : 'info';
    },
    alertDescription() {
      const status = this.stageData.status;
      return status === 'completed' ? '所有医学评估项目已完成,供者符合捐献条件' :
             status === 'in_progress' ? '医学评估正在进行中,请关注评估进度' : '等待开始医学评估流程';
    }
  },
  data() {
    return {
      assessmentItems: [
        { name: '生理指标评估', status: 'completed' },
        { name: '器官功能评估', status: 'completed' },
        { name: '感染性疾病筛查', status: 'completed' },
        { name: '恶性肿瘤筛查', status: 'completed' },
        { name: '遗传性疾病筛查', status: 'completed' },
        { name: '心理状态评估', status: 'completed' }
      ],
      assessmentDetails: {
        physiological: '各项生理指标正常,符合捐献要求',
        organFunction: '主要器官功能良好,无禁忌症',
        infectionScreening: '传染病筛查均为阴性',
        cancerScreening: '无恶性肿瘤迹象',
        conclusion: '适合器官捐献',
        doctorOpinion: '供者身体状况良好,符合捐献医学标准'
      }
    };
  },
  methods: {
    getStatusText(status) {
      const map = {
        'completed': '已完成',
        'in_progress': '进行中',
        'pending': '未开始'
      };
      return map[status] || '未知';
    },
    handleViewReport() {
      this.$message.info('查看医学评估报告功能');
    }
  }
};
</script>
<style scoped>
.assessment-stats {
  padding: 10px 0;
}
.stat-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  padding: 8px 0;
  border-bottom: 1px solid #f0f0f0;
}
.stat-label {
  color: #606266;
  font-weight: 500;
}
.progress-list {
  padding: 10px 0;
}
.progress-item {
  margin-bottom: 15px;
}
.progress-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}
.item-name {
  color: #606266;
  font-size: 14px;
}
</style>
src/views/business/course/components/OrganAllocationStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,447 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <template #header>
      <el-alert
        :title="`器官分配 - ${getStatusText()}`"
        :type="getAlertType()"
        :description="getAlertDescription()"
        show-icon
        :closable="false"
      />
    </template>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>分配概况</span>
          </div>
          <div class="allocation-stats">
            <div class="stat-item">
              <span class="label">待分配器官:</span>
              <span class="value">{{ allocationStats.totalOrgans }} ä¸ª</span>
            </div>
            <div class="stat-item">
              <span class="label">已分配器官:</span>
              <span class="value">{{ allocationStats.allocatedOrgans }} ä¸ª</span>
            </div>
            <div class="stat-item">
              <span class="label">分配系统:</span>
              <span class="value">{{ allocationStats.system }}</span>
            </div>
            <div class="stat-item">
              <span class="label">匹配成功率:</span>
              <el-progress
                :percentage="allocationStats.matchRate"
                :status="allocationStats.matchRate > 80 ? 'success' : 'warning'"
              />
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>分配时间窗口</span>
          </div>
          <div class="time-window">
            <div class="time-item">
              <span class="label">分配开始:</span>
              <span>{{ formatTime(allocationDetails.startTime) }}</span>
            </div>
            <div class="time-item">
              <span class="label">预计完成:</span>
              <span>{{ formatTime(allocationDetails.estimatedEndTime) }}</span>
            </div>
            <div class="time-item">
              <span class="label">器官耐受时间:</span>
              <span>{{ allocationDetails.toleranceTime }}</span>
            </div>
            <div class="time-item">
              <span class="label">剩余时间:</span>
              <el-tag :type="getTimeRemainingType()">
                {{ allocationDetails.timeRemaining }}
              </el-tag>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>分配优先级</span>
          </div>
          <el-steps direction="vertical" :active="prioritySteps.active">
            <el-step
              v-for="step in prioritySteps.steps"
              :key="step.title"
              :title="step.title"
              :description="step.description"
              :status="step.status"
            />
          </el-steps>
        </el-card>
      </el-col>
    </el-row>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>器官分配详情</span>
        <el-button type="primary" size="small" @click="handleStartAllocation">
          å¯åŠ¨è‡ªåŠ¨åˆ†é…
        </el-button>
      </div>
      <el-table :data="organAllocationData" border>
        <el-table-column label="器官名称" prop="organName" width="120" align="center" />
        <el-table-column label="器官状态" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="getOrganStatusTag(scope.row.status)" size="small">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="匹配受体" prop="recipient" width="150" />
        <el-table-column label="血型匹配" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="scope.row.bloodMatch ? 'success' : 'warning'">
              {{ scope.row.bloodMatch ? '匹配' : '待匹配' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="组织配型" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="scope.row.tissueMatch ? 'success' : 'warning'">
              {{ scope.row.tissueMatch ? '合格' : '待检测' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="移植医院" prop="hospital" min-width="200" />
        <el-table-column label="分配时间" width="160" align="center">
          <template slot-scope="scope">
            {{ scope.row.allocationTime || '待分配' }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="120" align="center">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="handleViewMatch(scope.row)">
              æŸ¥çœ‹åŒ¹é…
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>分配算法结果</span>
      </div>
      <div class="algorithm-results">
        <el-row :gutter="20">
          <el-col :span="12">
            <div class="result-item">
              <h4>匹配评分排名</h4>
              <el-table :data="matchRanking" border size="small">
                <el-table-column label="排名" width="60" align="center">
                  <template slot-scope="scope">
                    {{ scope.$index + 1 }}
                  </template>
                </el-table-column>
                <el-table-column label="受体编号" prop="recipientNo" width="100" />
                <el-table-column label="匹配分数" prop="matchScore" width="100">
                  <template slot-scope="scope">
                    <el-rate
                      v-model="scope.row.matchScore"
                      disabled
                      show-score
                      text-color="#ff9900"
                      score-template="{value} åˆ†"
                    />
                  </template>
                </el-table-column>
                <el-table-column label="推荐器官" prop="recommendedOrgan" />
              </el-table>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="result-item">
              <h4>分配因素权重</h4>
              <div class="weight-distribution">
                <div v-for="factor in allocationFactors" :key="factor.name" class="factor-item">
                  <span class="factor-name">{{ factor.name }}</span>
                  <el-progress
                    :percentage="factor.weight"
                    :show-text="false"
                    :color="factor.color"
                  />
                  <span class="factor-percent">{{ factor.weight }}%</span>
                </div>
              </div>
            </div>
          </el-col>
        </el-row>
      </div>
    </el-card>
    <template #footer>
      <div class="action-buttons" style="margin-top: 20px; text-align: center;">
        <el-button type="primary" @click="handleAutoAllocation">
          æ™ºèƒ½åˆ†é…
        </el-button>
        <el-button type="success" @click="handleConfirmAllocation">
          ç¡®è®¤åˆ†é…ç»“æžœ
        </el-button>
        <el-button type="warning" @click="handleManualAdjust">
          æ‰‹åŠ¨è°ƒæ•´
        </el-button>
      </div>
    </template>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
export default {
  name: 'OrganAllocationStage',
  components: { BaseStage },
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      allocationStats: {
        totalOrgans: 3,
        allocatedOrgans: 0,
        system: '中国人体器官分配与共享计算机系统',
        matchRate: 0
      },
      allocationDetails: {
        startTime: '2023-12-04 10:00:00',
        estimatedEndTime: '2023-12-04 12:00:00',
        toleranceTime: '肝脏:12小时,肾脏:24小时,心脏:8小时,肺脏:12小时',
        timeRemaining: '2小时'
      },
      prioritySteps: {
        active: 3,
        steps: [
          {
            title: '病情危重优先',
            description: '根据患者病情危重程度分配[1](@ref)',
            status: 'finish'
          },
          {
            title: '组织配型优先',
            description: '组织配型匹配度高的患者优先',
            status: 'finish'
          },
          {
            title: '血型相同优先',
            description: '血型匹配的患者优先考虑',
            status: 'finish'
          },
          {
            title: '儿童匹配优先',
            description: '儿童患者享有优先分配权',
            status: 'wait'
          }
        ]
      },
      organAllocationData: [
        {
          organName: '肝脏',
          status: '待分配',
          recipient: '',
          bloodMatch: false,
          tissueMatch: false,
          hospital: '',
          allocationTime: ''
        },
        {
          organName: '肾脏',
          status: '待分配',
          recipient: '',
          bloodMatch: false,
          tissueMatch: false,
          hospital: '',
          allocationTime: ''
        },
        {
          organName: '心脏',
          status: '待分配',
          recipient: '',
          bloodMatch: false,
          tissueMatch: false,
          hospital: '',
          allocationTime: ''
        }
      ],
      matchRanking: [
        {
          recipientNo: 'R202312001',
          matchScore: 4.8,
          recommendedOrgan: '肝脏'
        },
        {
          recipientNo: 'R202312002',
          matchScore: 4.5,
          recommendedOrgan: '肾脏'
        },
        {
          recipientNo: 'R202312003',
          matchScore: 4.3,
          recommendedOrgan: '心脏'
        }
      ],
      allocationFactors: [
        { name: '病情危重程度', weight: 35, color: '#f56c6c' },
        { name: '组织配型匹配', weight: 25, color: '#e6a23c' },
        { name: '等待时间', weight: 15, color: '#5cb87a' },
        { name: '地理因素', weight: 10, color: '#6f7ad3' },
        { name: '年龄因素', weight: 15, color: '#8e44ad' }
      ]
    };
  },
  methods: {
    getStatusText() {
      const status = this.stageData.status;
      return status === 'completed' ? '已完成' :
             status === 'in_progress' ? '进行中' : '未开始';
    },
    getAlertType() {
      const status = this.stageData.status;
      return status === 'completed' ? 'success' :
             status === 'in_progress' ? 'warning' : 'info';
    },
    getAlertDescription() {
      const status = this.stageData.status;
      if (status === 'completed') {
        return '器官分配已完成,所有器官均已成功匹配受体';
      } else if (status === 'in_progress') {
        return '器官分配进行中,系统正在自动匹配最佳受体';
      }
      return '等待开始器官分配流程';
    },
    getOrganStatusTag(status) {
      const map = {
        '已分配': 'success',
        '分配中': 'warning',
        '待分配': 'info'
      };
      return map[status] || 'info';
    },
    getTimeRemainingType() {
      return this.allocationDetails.timeRemaining.includes('小时') ? 'success' : 'danger';
    },
    handleStartAllocation() {
      this.$confirm('确认启动自动器官分配吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$message.success('已启动自动分配算法');
        // æ¨¡æ‹Ÿåˆ†é…è¿‡ç¨‹
        this.allocationStats.allocatedOrgans = 3;
        this.allocationStats.matchRate = 95;
        this.organAllocationData.forEach(organ => {
          organ.status = '已分配';
          organ.allocationTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
        });
      });
    },
    handleAutoAllocation() {
      this.$message.info('执行智能分配算法');
    },
    handleConfirmAllocation() {
      this.$confirm('确认最终分配结果吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'success'
      }).then(() => {
        this.$message.success('器官分配结果已确认');
      });
    },
    handleManualAdjust() {
      this.$message.info('进入手动调整模式');
    },
    handleViewMatch(row) {
      this.$message.info(`查看${row.organName}的匹配详情`);
    }
  }
};
</script>
<style scoped>
.allocation-stats, .time-window {
  padding: 10px 0;
}
.stat-item, .time-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  padding: 8px 0;
  border-bottom: 1px solid #f0f0f0;
}
.stat-item .label, .time-item .label {
  color: #606266;
  font-weight: 500;
  min-width: 100px;
}
.stat-item .value {
  font-weight: 600;
  color: #409EFF;
}
.algorithm-results {
  padding: 15px 0;
}
.result-item {
  margin-bottom: 20px;
}
.result-item h4 {
  margin-bottom: 15px;
  color: #303133;
}
.weight-distribution {
  padding: 10px 0;
}
.factor-item {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
}
.factor-name {
  width: 120px;
  color: #606266;
  font-size: 14px;
}
.factor-item .el-progress {
  flex: 1;
  margin: 0 15px;
}
.factor-percent {
  width: 40px;
  text-align: right;
  color: #909399;
  font-size: 12px;
}
</style>
src/views/business/course/components/OrganProcurementStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,499 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <template #header>
      <el-alert
        :title="`器官获取 - ${getStatusText()}`"
        :type="getAlertType()"
        :description="getAlertDescription()"
        show-icon
        :closable="false"
      />
    </template>
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>获取手术信息</span>
          </div>
          <el-descriptions :column="1" border size="small">
            <el-descriptions-item label="手术时间">
              {{ formatTime(procurementDetails.surgeryTime) }}
            </el-descriptions-item>
            <el-descriptions-item label="手术地点">
              {{ procurementDetails.location }}
            </el-descriptions-item>
            <el-descriptions-item label="主刀医生">
              {{ procurementDetails.surgeon }}
            </el-descriptions-item>
            <el-descriptions-item label="麻醉医生">
              {{ procurementDetails.anesthesiologist }}
            </el-descriptions-item>
            <el-descriptions-item label="手术状态">
              <el-tag :type="procurementDetails.status === 'completed' ? 'success' : 'warning'">
                {{ procurementDetails.status === 'completed' ? '已完成' : '进行中' }}
              </el-tag>
            </el-descriptions-item>
          </el-descriptions>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>器官获取统计</span>
          </div>
          <div class="procurement-stats">
            <div class="stat-item">
              <span class="label">计划获取:</span>
              <span class="value">{{ procurementStats.planned }} ä¸ª</span>
            </div>
            <div class="stat-item">
              <span class="label">实际获取:</span>
              <span class="value">{{ procurementStats.actual }} ä¸ª</span>
            </div>
            <div class="stat-item">
              <span class="label">获取成功率:</span>
              <el-progress
                :percentage="procurementStats.successRate"
                :status="procurementStats.successRate > 90 ? 'success' : 'warning'"
              />
            </div>
            <div class="stat-item">
              <span class="label">质量评估:</span>
              <el-rate
                v-model="procurementStats.qualityRating"
                disabled
                show-score
                text-color="#ff9900"
                score-template="{value} æ˜Ÿ"
              />
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card>
          <div slot="header" class="card-header">
            <span>保存与运输</span>
          </div>
          <div class="preservation-info">
            <div class="info-item">
              <span class="label">保存方式:</span>
              <span>{{ preservationDetails.method }}</span>
            </div>
            <div class="info-item">
              <span class="label">保存温度:</span>
              <span>{{ preservationDetails.temperature }}</span>
            </div>
            <div class="info-item">
              <span class="label">灌注液:</span>
              <span>{{ preservationDetails.perfusionSolution }}</span>
            </div>
            <div class="info-item">
              <span class="label">预计存活时间:</span>
              <el-tag :type="getSurvivalTimeType()">
                {{ preservationDetails.survivalTime }}
              </el-tag>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>器官获取详情</span>
        <el-button type="primary" size="small" @click="handleStartProcurement">
          å¼€å§‹èŽ·å–æ‰‹æœ¯
        </el-button>
      </div>
      <el-table :data="organProcurementData" border>
        <el-table-column label="器官名称" prop="organName" width="120" align="center" />
        <el-table-column label="获取状态" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="getProcurementStatusTag(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="获取时间" width="160" align="center">
          <template slot-scope="scope">
            {{ formatTime(scope.row.procurementTime) || '-' }}
          </template>
        </el-table-column>
        <el-table-column label="重量(g)" prop="weight" width="100" align="center" />
        <el-table-column label="质量评估" width="120" align="center">
          <template slot-scope="scope">
            <el-rate
              v-model="scope.row.qualityScore"
              disabled
              show-score
              text-color="#ff9900"
              score-template="{value}"
            />
          </template>
        </el-table-column>
        <el-table-column label="灌注情况" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="scope.row.perfusionStatus ? 'success' : 'warning'">
              {{ scope.row.perfusionStatus ? '已完成' : '待灌注' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="保存方式" prop="preservationMethod" width="120" />
        <el-table-column label="操作" width="150" align="center">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="handleViewOrgan(scope.row)">
              æŸ¥çœ‹è¯¦æƒ…
            </el-button>
            <el-button
              v-if="scope.row.status !== '已获取'"
              type="text"
              size="small"
              @click="handleRecordProcurement(scope.row)"
            >
              è®°å½•获取
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>手术记录与影像</span>
      </div>
      <el-tabs v-model="activeMedicalTab">
        <el-tab-pane label="手术记录" name="surgeryRecord">
          <div class="surgery-record">
            <el-timeline>
              <el-timeline-item
                v-for="record in surgeryRecords"
                :key="record.time"
                :timestamp="formatTime(record.time)"
                :type="record.type"
                :icon="record.icon"
              >
                <p>{{ record.content }}</p>
                <div v-if="record.images && record.images.length > 0" class="record-images">
                  <el-image
                    v-for="(img, index) in record.images"
                    :key="index"
                    :src="img"
                    :preview-src-list="record.images"
                    style="width: 100px; height: 100px; margin-right: 10px;"
                  />
                </div>
              </el-timeline-item>
            </el-timeline>
          </div>
        </el-tab-pane>
        <el-tab-pane label="器官影像" name="organImages">
          <div class="organ-images">
            <el-row :gutter="15">
              <el-col v-for="organ in organImages" :key="organ.name" :span="8">
                <el-card shadow="hover">
                  <div slot="header" class="image-header">
                    <span>{{ organ.name }}</span>
                  </div>
                  <el-image
                    :src="organ.image"
                    :preview-src-list="[organ.image]"
                    fit="cover"
                    style="width: 100%; height: 200px;"
                  />
                  <div style="padding: 10px;">
                    <p>{{ organ.description }}</p>
                    <el-tag size="small">{{ organ.status }}</el-tag>
                  </div>
                </el-card>
              </el-col>
            </el-row>
          </div>
        </el-tab-pane>
        <el-tab-pane label="质量检测报告" name="qualityReport">
          <div class="quality-report">
            <el-table :data="qualityReports" border>
              <el-table-column label="检测项目" prop="item" width="150" />
              <el-table-column label="检测结果" prop="result" width="120">
                <template slot-scope="scope">
                  <el-tag :type="scope.row.pass ? 'success' : 'danger'">
                    {{ scope.row.pass ? '合格' : '不合格' }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="参考范围" prop="reference" width="120" />
              <el-table-column label="检测值" prop="value" width="100" />
              <el-table-column label="检测时间" width="160">
                <template slot-scope="scope">
                  {{ formatTime(scope.row.testTime) }}
                </template>
              </el-table-column>
              <el-table-column label="检测医生" prop="doctor" width="120" />
            </el-table>
          </div>
        </el-tab-pane>
      </el-tabs>
    </el-card>
    <template #footer>
      <div class="action-buttons" style="margin-top: 20px; text-align: center;">
        <el-button type="primary" @click="handleCompleteProcurement">
          å®Œæˆå™¨å®˜èŽ·å–
        </el-button>
        <el-button type="success" @click="handleGenerateReport">
          ç”ŸæˆèŽ·å–æŠ¥å‘Š
        </el-button>
        <el-button type="warning" @click="handleUploadEvidence">
          ä¸Šä¼ æ‰‹æœ¯è¯æ®
        </el-button>
      </div>
    </template>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
export default {
  name: 'OrganProcurementStage',
  components: { BaseStage },
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      activeMedicalTab: 'surgeryRecord',
      procurementDetails: {
        surgeryTime: '2023-12-04 14:00:00',
        location: '手术室一号',
        surgeon: '张主任',
        anesthesiologist: '王医生',
        status: 'in_progress'
      },
      procurementStats: {
        planned: 3,
        actual: 0,
        successRate: 0,
        qualityRating: 0
      },
      preservationDetails: {
        method: '低温机械灌注[9](@ref)',
        temperature: '4°C',
        perfusionSolution: 'UW保存液',
        survivalTime: '肝脏12小时[1](@ref)'
      },
      organProcurementData: [
        {
          organName: '肝脏',
          status: '获取中',
          procurementTime: '',
          weight: 1500,
          qualityScore: 0,
          perfusionStatus: false,
          preservationMethod: '低温保存'
        },
        {
          organName: '肾脏',
          status: '待获取',
          procurementTime: '',
          weight: 0,
          qualityScore: 0,
          perfusionStatus: false,
          preservationMethod: ''
        },
        {
          organName: '心脏',
          status: '待获取',
          procurementTime: '',
          weight: 0,
          qualityScore: 0,
          perfusionStatus: false,
          preservationMethod: ''
        }
      ],
      surgeryRecords: [
        {
          time: '2023-12-04 14:00:00',
          content: '手术开始,患者体位摆放,消毒铺巾',
          type: 'primary',
          icon: 'el-icon-video-play'
        },
        {
          time: '2023-12-04 14:30:00',
          content: '开腹手术,暴露腹腔器官',
          type: 'success',
          icon: 'el-icon-scissors'
        },
        {
          time: '2023-12-04 15:00:00',
          content: '肝脏游离,血管解剖分离',
          type: 'warning',
          icon: 'el-icon-medal'
        }
      ],
      organImages: [
        {
          name: '肝脏',
          image: '/images/liver-procurement.jpg',
          description: '获取的肝脏器官,形态完整',
          status: '质量良好'
        }
      ],
      qualityReports: [
        {
          item: '肝功能酶学',
          result: true,
          reference: '<40 U/L',
          value: '35 U/L',
          testTime: '2023-12-04 16:00:00',
          doctor: '李检验师'
        },
        {
          item: '组织完整性',
          result: true,
          reference: '完整',
          value: '完整',
          testTime: '2023-12-04 16:15:00',
          doctor: '张病理师'
        }
      ]
    };
  },
  methods: {
    getStatusText() {
      const status = this.stageData.status;
      return status === 'completed' ? '已完成' :
             status === 'in_progress' ? '进行中' : '未开始';
    },
    getAlertType() {
      const status = this.stageData.status;
      return status === 'completed' ? 'success' :
             status === 'in_progress' ? 'warning' : 'info';
    },
    getAlertDescription() {
      const status = this.stageData.status;
      if (status === 'completed') {
        return '器官获取手术已完成,所有器官均已成功获取并保存';
      } else if (status === 'in_progress') {
        return '器官获取手术进行中,请密切关注手术进展';
      }
      return '等待开始器官获取手术';
    },
    getProcurementStatusTag(status) {
      const map = {
        '已获取': 'success',
        '获取中': 'warning',
        '待获取': 'info'
      };
      return map[status] || 'info';
    },
    getSurvivalTimeType() {
      return this.preservationDetails.survivalTime.includes('小时') ? 'success' : 'danger';
    },
    handleStartProcurement() {
      this.$confirm('确认开始器官获取手术吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$message.success('器官获取手术已开始');
      });
    },
    handleRecordProcurement(row) {
      this.$prompt('请输入器官重量(g)', '记录器官获取', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        inputPattern: /^\d+$/,
        inputErrorMessage: '请输入有效的重量数字'
      }).then(({ value }) => {
        row.status = '已获取';
        row.procurementTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
        row.weight = parseInt(value);
        row.qualityScore = 4.5;
        row.perfusionStatus = true;
        this.procurementStats.actual++;
        this.procurementStats.successRate = Math.round(
          (this.procurementStats.actual / this.procurementStats.planned) * 100
        );
        this.procurementStats.qualityRating = 4.2;
        this.$message.success(`${row.organName}获取记录已保存`);
      });
    },
    handleViewOrgan(row) {
      this.$message.info(`查看${row.organName}的详细信息`);
    },
    handleCompleteProcurement() {
      this.$confirm('确认完成所有器官获取吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'success'
      }).then(() => {
        this.$message.success('器官获取阶段已完成');
      });
    },
    handleGenerateReport() {
      this.$message.info('生成器官获取报告');
    },
    handleUploadEvidence() {
      this.$message.info('上传手术证据材料');
    }
  }
};
</script>
<style scoped>
.procurement-stats, .preservation-info {
  padding: 10px 0;
}
.stat-item, .info-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  padding: 8px 0;
  border-bottom: 1px solid #f0f0f0;
}
.stat-item .label, .info-item .label {
  color: #606266;
  font-weight: 500;
  min-width: 100px;
}
.stat-item .value {
  font-weight: 600;
  color: #409EFF;
}
.surgery-record {
  padding: 20px;
}
.record-images {
  margin-top: 10px;
}
.image-header {
  font-weight: 600;
  color: #303133;
}
.quality-report {
  padding: 10px 0;
}
</style>
src/views/business/course/components/OrganUtilizationStage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,812 @@
<template>
  <base-stage :stage-data="stageData" :case-info="caseInfo">
    <!-- å¤´éƒ¨è­¦å‘Šä¿¡æ¯ -->
    <template #header>
      <el-alert
        :title="`器官利用 - ${getStatusText()}`"
        :type="getAlertType()"
        :description="getAlertDescription()"
        show-icon
        :closable="false"
      />
    </template>
    <!-- ç»Ÿè®¡æ¦‚览行 -->
    <el-row :gutter="20" style="margin-top: 20px;">
      <el-col :span="6">
        <el-card>
          <div slot="header" class="card-header">
            <span>利用概况</span>
          </div>
          <div class="utilization-overview">
            <div class="overview-item">
              <div class="overview-icon" style="color: #67C23A;">
                <i class="el-icon-success"></i>
              </div>
              <div class="overview-content">
                <div class="value">{{ utilizationStats.transplanted }}</div>
                <div class="label">已移植器官</div>
              </div>
            </div>
            <div class="overview-item">
              <div class="overview-icon" style="color: #E6A23C;">
                <i class="el-icon-time"></i>
              </div>
              <div class="overview-content">
                <div class="value">{{ utilizationStats.inProgress }}</div>
                <div class="label">移植中</div>
              </div>
            </div>
            <div class="overview-item">
              <div class="overview-icon" style="color: #F56C6C;">
                <i class="el-icon-warning"></i>
              </div>
              <div class="overview-content">
                <div class="value">{{ utilizationStats.failed }}</div>
                <div class="label">移植失败</div>
              </div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <div slot="header" class="card-header">
            <span>成功率统计</span>
          </div>
          <div class="success-stats">
            <div class="success-item">
              <span class="label">移植成功率:</span>
              <el-progress
                :percentage="utilizationStats.successRate"
                :status="utilizationStats.successRate > 85 ? 'success' : 'warning'"
              />
            </div>
            <div class="success-item">
              <span class="label">器官利用率:</span>
              <el-progress
                :percentage="utilizationStats.utilizationRate"
                status="success"
              />
            </div>
            <div class="success-item">
              <span class="label">患者存活率:</span>
              <el-progress
                :percentage="utilizationStats.survivalRate"
                status="success"
              />
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <div slot="header" class="card-header">
            <span>时间跟踪</span>
          </div>
          <div class="time-tracking">
            <div class="time-item">
              <span class="label">获取到移植:</span>
              <span class="value">{{ timeTracking.procurementToTransplant }}</span>
            </div>
            <div class="time-item">
              <span class="label">冷缺血时间:</span>
              <span class="value">{{ timeTracking.coldIschemiaTime }}</span>
            </div>
            <div class="time-item">
              <span class="label">手术时长:</span>
              <span class="value">{{ timeTracking.surgeryDuration }}</span>
            </div>
            <div class="time-item">
              <span class="label">ICU停留:</span>
              <span class="value">{{ timeTracking.icuStay }}</span>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <div slot="header" class="card-header">
            <span>质量评估</span>
          </div>
          <div class="quality-assessment">
            <div class="quality-item">
              <span class="label">器官质量评分:</span>
              <el-rate
                v-model="qualityStats.organQuality"
                disabled
                show-score
                text-color="#ff9900"
                score-template="{value}"
              />
            </div>
            <div class="quality-item">
              <span class="label">手术质量:</span>
              <el-rate
                v-model="qualityStats.surgeryQuality"
                disabled
                show-score
                text-color="#ff9900"
                score-template="{value}"
              />
            </div>
            <div class="quality-item">
              <span class="label">随访完成率:</span>
              <el-progress
                :percentage="qualityStats.followupCompletionRate"
                status="success"
              />
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æ•°æ®å¯è§†åŒ–部分 -->
    <el-row :gutter="20" style="margin-top: 20px;">
      <!-- å™¨å®˜åˆ©ç”¨åˆ†å¸ƒå›¾ -->
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>器官利用分布</span>
            <el-radio-group v-model="chartView" size="small" @change="updateCharts">
              <el-radio-button label="bar">柱状图</el-radio-button>
              <el-radio-button label="pie">饼图</el-radio-button>
            </el-radio-group>
          </div>
          <div class="chart-container">
            <div ref="organDistributionChart" style="width: 100%; height: 300px;"></div>
          </div>
        </el-card>
      </el-col>
      <!-- æˆåŠŸçŽ‡è¶‹åŠ¿å›¾ -->
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>移植成功率趋势</span>
          </div>
          <div class="chart-container">
            <div ref="successTrendChart" style="width: 100%; height: 300px;"></div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="20" style="margin-top: 20px;">
      <!-- éšè®¿æ•°æ®ç»Ÿè®¡ -->
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>随访数据统计</span>
          </div>
          <div class="chart-container">
            <div ref="followupStatsChart" style="width: 100%; height: 300px;"></div>
          </div>
        </el-card>
      </el-col>
      <!-- å¹¶å‘症分析 -->
      <el-col :span="12">
        <el-card>
          <div slot="header" class="card-header">
            <span>并发症分析</span>
          </div>
          <div class="chart-container">
            <div ref="complicationChart" style="width: 100%; height: 300px;"></div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- è¯¦ç»†æ•°æ®è¡¨æ ¼ -->
    <el-card style="margin-top: 20px;">
      <div slot="header" class="card-header">
        <span>器官利用详情</span>
        <el-button type="primary" size="small" @click="exportData">
          å¯¼å‡ºæ•°æ®
        </el-button>
      </div>
      <el-table :data="organUtilizationData" v-loading="loading" border>
        <el-table-column label="器官名称" prop="organName" width="120" align="center" />
        <el-table-column label="移植状态" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="getTransplantStatusTag(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="受体信息" width="150">
          <template slot-scope="scope">
            <div>{{ scope.row.recipientName }}</div>
            <div style="font-size: 12px; color: #909399;">{{ scope.row.recipientAge }}岁/{{ scope.row.recipientGender }}</div>
          </template>
        </el-table-column>
        <el-table-column label="移植医院" prop="hospital" min-width="180" />
        <el-table-column label="移植时间" width="160" align="center">
          <template slot-scope="scope">
            {{ formatTime(scope.row.transplantTime) }}
          </template>
        </el-table-column>
        <el-table-column label="随访次数" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="scope.row.followupCount > 0 ? 'success' : 'info'">
              {{ scope.row.followupCount }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="当前状态" width="120" align="center">
          <template slot-scope="scope">
            <el-tag :type="getRecipientStatusTag(scope.row.recipientStatus)">
              {{ scope.row.recipientStatus }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150" align="center">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="handleViewDetails(scope.row)">
              æŸ¥çœ‹è¯¦æƒ…
            </el-button>
            <el-button type="text" size="small" @click="handleAddFollowup(scope.row)">
              æ·»åŠ éšè®¿
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- è¡ŒåŠ¨æŒ‰é’® -->
    <template #footer>
      <div class="action-buttons" style="margin-top: 20px; text-align: center;">
        <el-button type="primary" @click="handleGenerateReport">
          ç”Ÿæˆåˆ©ç”¨æŠ¥å‘Š
        </el-button>
        <el-button type="success" @click="handleCompleteUtilization">
          å®Œæˆå™¨å®˜åˆ©ç”¨
        </el-button>
        <el-button type="warning" @click="handleStatistics">
          ç»Ÿè®¡æ•°æ®åˆ†æž
        </el-button>
      </div>
    </template>
  </base-stage>
</template>
<script>
import BaseStage from './BaseStage.vue';
import * as echarts from 'echarts';
export default {
  name: 'OrganUtilizationStage',
  components: { BaseStage },
  props: {
    stageData: {
      type: Object,
      default: () => ({})
    },
    caseInfo: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      chartView: 'bar',
      loading: false,
      utilizationStats: {
        transplanted: 3,
        inProgress: 0,
        failed: 0,
        successRate: 95,
        utilizationRate: 100,
        survivalRate: 92
      },
      timeTracking: {
        procurementToTransplant: '4.5小时',
        coldIschemiaTime: '肝脏:6h,肾脏:8h,心脏:3h',
        surgeryDuration: '肝脏:4h,肾脏:3h,心脏:5h',
        icuStay: '肝脏:3天,肾脏:2天,心脏:5天'
      },
      qualityStats: {
        organQuality: 4.5,
        surgeryQuality: 4.8,
        followupCompletionRate: 85
      },
      organUtilizationData: [
        {
          organName: '肝脏',
          status: '移植成功',
          recipientName: '王先生',
          recipientAge: 45,
          recipientGender: '男',
          hospital: '北京协和医院移植中心',
          transplantTime: '2023-12-04 16:00:00',
          followupCount: 3,
          recipientStatus: '恢复良好'
        },
        {
          organName: '肾脏',
          status: '移植成功',
          recipientName: '李女士',
          recipientAge: 38,
          recipientGender: '女',
          hospital: '上海瑞金医院移植中心',
          transplantTime: '2023-12-04 17:30:00',
          followupCount: 2,
          recipientStatus: '稳定恢复'
        },
        {
          organName: '心脏',
          status: '移植成功',
          recipientName: '陈先生',
          recipientAge: 52,
          recipientGender: '男',
          hospital: '广州中山医院心脏中心',
          transplantTime: '2023-12-04 18:15:00',
          followupCount: 1,
          recipientStatus: '密切观察'
        }
      ],
      // å›¾è¡¨å®žä¾‹
      organDistributionChart: null,
      successTrendChart: null,
      followupStatsChart: null,
      complicationChart: null
    };
  },
  mounted() {
    this.$nextTick(() => {
      this.initCharts();
    });
  },
  beforeDestroy() {
    // é”€æ¯å›¾è¡¨å®žä¾‹
    if (this.organDistributionChart) {
      this.organDistributionChart.dispose();
    }
    if (this.successTrendChart) {
      this.successTrendChart.dispose();
    }
    if (this.followupStatsChart) {
      this.followupStatsChart.dispose();
    }
    if (this.complicationChart) {
      this.complicationChart.dispose();
    }
  },
  methods: {
    // åˆå§‹åŒ–图表
    initCharts() {
      this.initOrganDistributionChart();
      this.initSuccessTrendChart();
      this.initFollowupStatsChart();
      this.initComplicationChart();
    },
    // åˆå§‹åŒ–器官分布图表
    initOrganDistributionChart() {
      this.organDistributionChart = echarts.init(this.$refs.organDistributionChart);
      const option = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b}: {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          right: 10,
          top: 'center',
          data: ['肝脏', '肾脏', '心脏', '肺脏', '角膜', '其他']
        },
        series: [
          {
            name: '器官利用',
            type: 'pie',
            radius: ['50%', '70%'],
            avoidLabelOverlap: false,
            label: {
              show: false,
              position: 'center'
            },
            emphasis: {
              label: {
                show: true,
                fontSize: '18',
                fontWeight: 'bold'
              }
            },
            labelLine: {
              show: false
            },
            data: [
              { value: 35, name: '肝脏' },
              { value: 30, name: '肾脏' },
              { value: 15, name: '心脏' },
              { value: 10, name: '肺脏' },
              { value: 8, name: '角膜' },
              { value: 2, name: '其他' }
            ]
          }
        ]
      };
      this.organDistributionChart.setOption(option);
    },
    // åˆå§‹åŒ–成功率趋势图表
    initSuccessTrendChart() {
      this.successTrendChart = echarts.init(this.$refs.successTrendChart);
      const option = {
        tooltip: {
          trigger: 'axis'
        },
        legend: {
          data: ['肝脏移植', '肾脏移植', '心脏移植', '平均成功率']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          boundaryGap: false,
          data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月']
        },
        yAxis: {
          type: 'value',
          min: 80,
          max: 100
        },
        series: [
          {
            name: '肝脏移植',
            type: 'line',
            smooth: true,
            data: [92, 93, 94, 95, 96, 95, 96]
          },
          {
            name: '肾脏移植',
            type: 'line',
            smooth: true,
            data: [94, 95, 95, 96, 95, 96, 97]
          },
          {
            name: '心脏移植',
            type: 'line',
            smooth: true,
            data: [88, 89, 90, 91, 92, 91, 92]
          },
          {
            name: '平均成功率',
            type: 'line',
            smooth: true,
            lineStyle: {
              type: 'dashed'
            },
            data: [91.3, 92.3, 93, 94, 94.3, 94, 95]
          }
        ]
      };
      this.successTrendChart.setOption(option);
    },
    // åˆå§‹åŒ–随访统计图表
    initFollowupStatsChart() {
      this.followupStatsChart = echarts.init(this.$refs.followupStatsChart);
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        legend: {
          data: ['计划随访', '已完成', '逾期未完成']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'value'
        },
        yAxis: {
          type: 'category',
          data: ['1个月', '3个月', '6个月', '1å¹´', '2å¹´', '5å¹´']
        },
        series: [
          {
            name: '计划随访',
            type: 'bar',
            stack: 'total',
            label: {
              show: true
            },
            emphasis: {
              focus: 'series'
            },
            data: [120, 132, 101, 134, 90, 60]
          },
          {
            name: '已完成',
            type: 'bar',
            stack: 'total',
            label: {
              show: true
            },
            emphasis: {
              focus: 'series'
            },
            data: [115, 125, 95, 120, 85, 55]
          },
          {
            name: '逾期未完成',
            type: 'bar',
            stack: 'total',
            label: {
              show: true
            },
            emphasis: {
              focus: 'series'
            },
            data: [5, 7, 6, 14, 5, 5]
          }
        ]
      };
      this.followupStatsChart.setOption(option);
    },
    // åˆå§‹åŒ–并发症分析图表
    initComplicationChart() {
      this.complicationChart = echarts.init(this.$refs.complicationChart);
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        radar: {
          indicator: [
            { name: '感染风险', max: 100 },
            { name: '排斥反应', max: 100 },
            { name: '血管并发症', max: 100 },
            { name: '胆道并发症', max: 100 },
            { name: '代谢异常', max: 100 },
            { name: '其他并发症', max: 100 }
          ]
        },
        series: [
          {
            type: 'radar',
            data: [
              {
                value: [85, 90, 78, 82, 75, 70],
                name: '肝脏移植',
                areaStyle: {}
              },
              {
                value: [78, 85, 72, 65, 80, 68],
                name: '肾脏移植',
                areaStyle: {}
              },
              {
                value: [90, 88, 85, 60, 82, 75],
                name: '心脏移植',
                areaStyle: {}
              }
            ]
          }
        ]
      };
      this.complicationChart.setOption(option);
    },
    // æ›´æ–°å›¾è¡¨è§†å›¾
    updateCharts() {
      if (this.chartView === 'bar') {
        this.updateToBarChart();
      } else {
        this.initOrganDistributionChart(); // åˆ‡å›žé¥¼å›¾
      }
    },
    // æ›´æ–°ä¸ºæŸ±çж图
    updateToBarChart() {
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: ['肝脏', '肾脏', '心脏', '肺脏', '角膜', '其他']
        },
        yAxis: {
          type: 'value',
          name: '数量'
        },
        series: [
          {
            name: '器官利用数量',
            type: 'bar',
            data: [35, 30, 15, 10, 8, 2],
            itemStyle: {
              color: function(params) {
                const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272'];
                return colorList[params.dataIndex];
              }
            }
          }
        ]
      };
      this.organDistributionChart.setOption(option);
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText() {
      const status = this.stageData.status;
      return status === 'completed' ? '已完成' :
             status === 'in_progress' ? '进行中' : '未开始';
    },
    // èŽ·å–è­¦å‘Šç±»åž‹
    getAlertType() {
      const status = this.stageData.status;
      return status === 'completed' ? 'success' :
             status === 'in_progress' ? 'warning' : 'info';
    },
    // èŽ·å–è­¦å‘Šæè¿°
    getAlertDescription() {
      const status = this.stageData.status;
      if (status === 'completed') {
        return '器官利用阶段已完成,所有器官均已成功移植并开始随访';
      } else if (status === 'in_progress') {
        return '器官利用进行中,移植手术已完成,正在进行术后随访';
      }
      return '等待开始器官利用流程';
    },
    // èŽ·å–ç§»æ¤çŠ¶æ€æ ‡ç­¾
    getTransplantStatusTag(status) {
      const map = {
        '移植成功': 'success',
        '移植中': 'warning',
        '移植失败': 'danger'
      };
      return map[status] || 'info';
    },
    // èŽ·å–å—ä½“çŠ¶æ€æ ‡ç­¾
    getRecipientStatusTag(status) {
      const map = {
        '恢复良好': 'success',
        '稳定恢复': 'warning',
        '密切观察': 'danger'
      };
      return map[status] || 'info';
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleViewDetails(row) {
      this.$message.info(`查看${row.organName}移植详情`);
    },
    // æ·»åŠ éšè®¿
    handleAddFollowup(row) {
      this.$message.info(`为${row.recipientName}添加随访记录`);
    },
    // ç”ŸæˆæŠ¥å‘Š
    handleGenerateReport() {
      this.$message.info('生成器官利用分析报告');
    },
    // å®Œæˆåˆ©ç”¨
    handleCompleteUtilization() {
      this.$confirm('确认完成器官利用阶段吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'success'
      }).then(() => {
        this.$message.success('器官利用阶段已完成');
      });
    },
    // ç»Ÿè®¡æ•°æ®åˆ†æž
    handleStatistics() {
      this.$message.info('打开统计分析面板');
    },
    // å¯¼å‡ºæ•°æ®
    exportData() {
      this.$message.info('导出器官利用数据');
    }
  }
};
</script>
<style scoped>
.utilization-overview {
  padding: 10px 0;
}
.overview-item {
  display: flex;
  align-items: center;
  margin-bottom: 15px;
  padding: 8px 0;
  border-bottom: 1px solid #f0f0f0;
}
.overview-icon {
  font-size: 24px;
  margin-right: 15px;
}
.overview-content .value {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
}
.overview-content .label {
  font-size: 12px;
  color: #909399;
}
.success-stats, .time-tracking, .quality-assessment {
  padding: 10px 0;
}
.success-item, .time-item, .quality-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.success-item .label, .time-item .label, .quality-item .label {
  color: #606266;
  font-size: 14px;
  min-width: 80px;
}
.time-item .value {
  font-weight: 600;
  color: #409EFF;
}
.chart-container {
  position: relative;
  min-height: 300px;
}
.action-buttons {
  display: flex;
  justify-content: center;
  gap: 15px;
  margin-top: 20px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>
src/views/business/course/donationProcess.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,453 @@
// æ¨¡æ‹ŸæçŒ®è¿›ç¨‹æ•°æ®
const mockDonationProcessData = {
  caseInfo: {
    id: '202312001',
    caseNo: 'C202312001',
    hospitalNo: 'D202312001',
    donorName: '张三',
    gender: '0',
    age: 45,
    bloodType: 'A',
    diagnosis: '脑外伤',
    status: 'in_progress',
    createTime: '2023-12-01 08:00:00',
    registrant: '李协调员',
    currentStage: 'organ_allocation',
    // æ–°å¢žåŸºæœ¬ä¿¡æ¯
    height: 175,
    weight: 70,
    bloodPressure: '120/80',
    contactPerson: '张父',
    contactPhone: '13800138000',
    hospital: '北京协和医院',
    department: '神经外科',
    attendingDoctor: '王主任'
  },
  processStages: [
    {
      key: 'donor_maintenance',
      name: '供者维护',
      status: 'completed',
      completeTime: '2023-12-01 10:00:00',
      updateTime: '2023-12-01 10:00:00',
      operator: '张医生',
      details: {
        maintenanceRecords: 5,
        lastCheckup: '2023-12-01 09:30:00',
        vitalSigns: {
          heartRate: 75,
          bloodPressure: '118/76',
          temperature: 36.5,
          oxygenSaturation: 98
        },
        medications: [
          { name: '多巴胺', dosage: '5μg/kg/min', time: '2023-12-01 08:00:00' },
          { name: '甘露醇', dosage: '125ml', time: '2023-12-01 09:00:00' }
        ],
        labResults: {
          wbc: 8.5,
          hgb: 12.5,
          plt: 250,
          na: 140,
          k: 4.0
        }
      }
    },
    {
      key: 'medical_assessment',
      name: '医学评估',
      status: 'completed',
      completeTime: '2023-12-02 14:30:00',
      updateTime: '2023-12-02 14:30:00',
      operator: '李主任',
      details: {
        assessmentItems: [
          { name: '神经系统评估', result: '脑死亡确认', status: 'completed' },
          { name: '心血管系统评估', result: '功能正常', status: 'completed' },
          { name: '呼吸系统评估', result: '呼吸机维持', status: 'completed' },
          { name: '肝肾功能评估', result: '功能良好', status: 'completed' },
          { name: '感染性疾病筛查', result: '阴性', status: 'completed' }
        ],
        imagingResults: {
          ctBrain: '脑水肿,脑干反射消失',
          chestXRay: '双肺清晰',
          abdominalUS: '肝胆胰脾未见异常'
        },
        conclusion: '符合器官捐献医学标准',
        contraindications: '无绝对禁忌症'
      }
    },
    {
      key: 'death_judgment',
      name: '死亡判定',
      status: 'completed',
      completeTime: '2023-12-03 09:15:00',
      updateTime: '2023-12-03 09:15:00',
      operator: '王医生',
      details: {
        judgmentType: '脑死亡判定',
        judgmentTime: '2023-12-03 09:00:00',
        doctors: ['张主任', '王医生'],
        testResults: [
          { test: '自主呼吸测试', result: '无自主呼吸', time: '2023-12-03 08:30:00' },
          { test: '瞳孔对光反射', result: '反射消失', time: '2023-12-03 08:45:00' },
          { test: '脑干听觉诱发电位', result: '脑干功能丧失', time: '2023-12-03 09:00:00' }
        ],
        certificateNo: 'SW20231203001',
        legalDocuments: ['死亡证明书', '脑死亡判定书']
      }
    },
    {
      key: 'donation_confirm',
      name: '捐献确认',
      status: 'completed',
      completeTime: '2023-12-03 11:00:00',
      updateTime: '2023-12-03 11:00:00',
      operator: '赵协调员',
      details: {
        familyConsent: {
          mainRelative: '张父',
          relationship: '父子',
          consentTime: '2023-12-03 10:45:00',
          consentForm: '已签署',
          witness: '李护士'
        },
        donationType: '多器官捐献',
        organs: ['肝脏', '肾脏', '心脏', '角膜'],
        legalDocuments: [
          '器官捐献同意书',
          '家属关系证明',
          '医疗免责声明'
        ],
        coordinator: '赵协调员',
        confirmationTime: '2023-12-03 11:00:00'
      }
    },
    {
      key: 'ethical_review',
      name: '伦理审查',
      status: 'completed',
      completeTime: '2023-12-03 15:20:00',
      updateTime: '2023-12-03 15:20:00',
      operator: '伦理委员会',
      details: {
        committee: '医院伦理审查委员会',
        meetingTime: '2023-12-03 14:00:00',
        members: ['张教授', '李主任', '王医生', '赵委员', '钱专家'],
        reviewItems: [
          { item: '捐献意愿真实性', result: '确认真实', vote: '全票通过' },
          { item: '医学评估准确性', result: '确认准确', vote: '全票通过' },
          { item: '法律文件完整性', result: '确认完整', vote: '全票通过' }
        ],
        conclusion: '符合伦理要求,同意进行器官捐献',
        resolutionNo: 'LL20231203001'
      }
    },
    {
      key: 'organ_allocation',
      name: '器官分配',
      status: 'in_progress',
      updateTime: '2023-12-04 10:00:00',
      operator: '分配系统',
      details: {
        allocationStartTime: '2023-12-04 09:00:00',
        allocationSystem: '中国人体器官分配与共享计算机系统',
        organs: [
          {
            organ: '肝脏',
            status: '分配中',
            matchScore: 95,
            recommendedRecipient: '王先生',
            recipientAge: 45,
            recipientBloodType: 'A',
            hospital: '北京协和医院',
            urgency: '紧急'
          },
          {
            organ: '肾脏',
            status: '匹配完成',
            matchScore: 92,
            recommendedRecipient: '李女士',
            recipientAge: 38,
            recipientBloodType: 'A',
            hospital: '上海瑞金医院',
            urgency: '高'
          },
          {
            organ: '心脏',
            status: '待分配',
            matchScore: 88,
            recommendedRecipient: '陈先生',
            recipientAge: 52,
            recipientBloodType: 'O',
            hospital: '广州中山医院',
            urgency: '紧急'
          }
        ],
        allocationFactors: [
          { factor: '病情危重程度', weight: 35 },
          { factor: '组织配型匹配', weight: 25 },
          { factor: '等待时间', weight: 15 },
          { factor: '地理因素', weight: 10 },
          { factor: '年龄因素', weight: 15 }
        ]
      }
    },
    {
      key: 'organ_procurement',
      name: '器官获取',
      status: 'pending',
      updateTime: '2023-12-03 16:00:00',
      operator: '待分配',
      details: {
        scheduledTime: '2023-12-04 14:00:00',
        operationRoom: '手术室一号',
        surgicalTeam: {
          surgeon: '待分配',
          assistant: '待分配',
          anesthesiologist: '待分配',
          nurse: '待分配'
        },
        preservationPlan: {
          method: '低温机械灌注',
          solution: 'UW保存液',
          temperature: '4°C'
        },
        organs: [
          {
            organ: '肝脏',
            planned: true,
            preservation: '待准备',
            estimatedTime: '4小时'
          },
          {
            organ: '肾脏',
            planned: true,
            preservation: '待准备',
            estimatedTime: '3小时'
          },
          {
            organ: '心脏',
            planned: true,
            preservation: '待准备',
            estimatedTime: '5小时'
          }
        ]
      }
    },
    {
      key: 'organ_utilization',
      name: '器官利用',
      status: 'pending',
      updateTime: '2023-12-03 16:00:00',
      operator: '待分配',
      details: {
        transplantCenters: [
          {
            hospital: '北京协和医院',
            organ: '肝脏',
            recipient: '王先生',
            scheduledTime: '2023-12-04 18:00:00',
            surgicalTeam: '待确认'
          },
          {
            hospital: '上海瑞金医院',
            organ: '肾脏',
            recipient: '李女士',
            scheduledTime: '2023-12-04 19:00:00',
            surgicalTeam: '待确认'
          }
        ],
        followupPlan: {
          frequency: '术后1个月、3个月、6个月、1å¹´',
          items: ['肝功能检查', '免疫抑制剂浓度', '影像学检查'],
          coordinator: '待分配'
        },
        qualityMetrics: {
          expectedSurvivalRate: 92,
          complicationRisk: '中等',
          successRate: 95
        }
      }
    }
  ],
  // æ–°å¢žæ—¶é—´çº¿äº‹ä»¶
  timelineEvents: [
    {
      time: '2023-12-01 08:00:00',
      event: '案例登记',
      operator: '李协调员',
      description: '捐献案例正式登记启动'
    },
    {
      time: '2023-12-01 10:00:00',
      event: '供者维护完成',
      operator: '张医生',
      description: '完成供者生命体征维护和医疗管理'
    },
    {
      time: '2023-12-02 14:30:00',
      event: '医学评估完成',
      operator: '李主任',
      description: '全面医学评估确认符合捐献标准'
    },
    {
      time: '2023-12-03 09:15:00',
      event: '死亡判定完成',
      operator: '王医生',
      description: '脑死亡判定程序完成'
    },
    {
      time: '2023-12-03 11:00:00',
      event: '捐献确认完成',
      operator: '赵协调员',
      description: '家属签署捐献同意书'
    },
    {
      time: '2023-12-03 15:20:00',
      event: '伦理审查通过',
      operator: '伦理委员会',
      description: '伦理审查委员会全票通过'
    }
  ],
  // æ–°å¢žç»Ÿè®¡ä¿¡æ¯
  statistics: {
    totalStages: 8,
    completedStages: 5,
    completionRate: 62.5,
    timeElapsed: '2天6小时',
    estimatedCompletion: '2023-12-04 20:00:00',
    organsToDonate: 4,
    potentialRecipients: 3
  }
};
// èŽ·å–æçŒ®è¿›ç¨‹è¯¦æƒ…
export const getDonationProcessDetail = async (caseId) => {
  await new Promise(resolve => setTimeout(resolve, 500));
  // æ¨¡æ‹Ÿæ ¹æ®caseId返回不同数据
  const data = JSON.parse(JSON.stringify(mockDonationProcessData));
  data.caseInfo.id = caseId;
  return {
    code: 200,
    message: 'success',
    data: data
  };
};
// æ›´æ–°é˜¶æ®µçŠ¶æ€
export const updateStageStatus = async (caseId, stageKey, status) => {
  await new Promise(resolve => setTimeout(resolve, 300));
  // æ¨¡æ‹Ÿæ›´æ–°é€»è¾‘
  const stage = mockDonationProcessData.processStages.find(s => s.key === stageKey);
  if (stage) {
    stage.status = status;
    stage.updateTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
    if (status === 'completed') {
      stage.completeTime = stage.updateTime;
    }
  }
  return {
    code: 200,
    message: '阶段状态更新成功',
    data: {
      caseId,
      stageKey,
      status,
      updateTime: stage.updateTime
    }
  };
};
// èŽ·å–é˜¶æ®µè¯¦æƒ…
export const getStageDetail = async (caseId, stageKey) => {
  await new Promise(resolve => setTimeout(resolve, 200));
  const stage = mockDonationProcessData.processStages.find(s => s.key === stageKey);
  if (!stage) {
    return {
      code: 404,
      message: '阶段不存在',
      data: null
    };
  }
  return {
    code: 200,
    message: 'success',
    data: stage
  };
};
// èŽ·å–æ—¶é—´çº¿äº‹ä»¶
export const getTimelineEvents = async (caseId) => {
  await new Promise(resolve => setTimeout(resolve, 150));
  return {
    code: 200,
    message: 'success',
    data: mockDonationProcessData.timelineEvents
  };
};
// èŽ·å–æ¡ˆä¾‹ç»Ÿè®¡ä¿¡æ¯
export const getCaseStatistics = async (caseId) => {
  await new Promise(resolve => setTimeout(resolve, 100));
  return {
    code: 200,
    message: 'success',
    data: mockDonationProcessData.statistics
  };
};
// æäº¤é˜¶æ®µå®¡æ ¸
export const submitStageReview = async (caseId, stageKey, reviewData) => {
  await new Promise(resolve => setTimeout(resolve, 400));
  return {
    code: 200,
    message: '审核提交成功',
    data: {
      caseId,
      stageKey,
      reviewId: `REV${Date.now()}`,
      submitTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
      ...reviewData
    }
  };
};
// ä¸Šä¼ é˜¶æ®µæ–‡ä»¶
export const uploadStageFile = async (caseId, stageKey, fileInfo) => {
  await new Promise(resolve => setTimeout(resolve, 500));
  return {
    code: 200,
    message: '文件上传成功',
    data: {
      caseId,
      stageKey,
      fileId: `FILE${Date.now()}`,
      fileName: fileInfo.name,
      fileSize: fileInfo.size,
      uploadTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
      url: `/files/${caseId}/${stageKey}/${fileInfo.name}`
    }
  };
};
export default {
  getDonationProcessDetail,
  updateStageStatus,
  getStageDetail,
  getTimelineEvents,
  getCaseStatistics,
  submitStageReview,
  uploadStageFile
};
src/views/business/course/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,677 @@
<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>
            <el-tag :type="getOverallStatusTag(caseInfo.status)">
              {{ getStatusText(caseInfo.status) }}
            </el-tag>
          </div>
          <div class="timeline-container">
            <div
              v-for="stage in processStages"
              :key="stage.key"
              class="timeline-item"
              :class="{
                'active': activeStage === stage.key,
                'completed': stage.status === 'completed',
                'in-progress': stage.status === 'in_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 === 'in_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>
                  <el-tag
                    size="small"
                    :type="getStageStatusTag(stage.status)"
                  >
                    {{ getStageStatusText(stage.status) }}
                  </el-tag>
                </div>
                <div class="stage-info">
                  <div v-if="stage.completeTime" class="time-info">
                    <span>完成时间: {{ formatTime(stage.completeTime) }}</span>
                  </div>
                  <div v-if="stage.updateTime" class="time-info">
                    <span>最近更新: {{ formatTime(stage.updateTime) }}</span>
                  </div>
                  <div v-if="stage.operator" class="operator-info">
                    <span>负责人: {{ stage.operator }}</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <!-- å³ä¾§å†…容区域 -->
        <div class="content-section">
          <!-- æ¡ˆä¾‹åŸºæœ¬ä¿¡æ¯ -->
          <div class="basic-info-section">
            <div class="section-header">
              <h3>案例基本信息</h3>
              <el-button
                type="primary"
                size="small"
                @click="handleEditBasicInfo"
              >
                ç¼–辑信息
              </el-button>
            </div>
            <el-descriptions :column="2" border>
              <el-descriptions-item label="案例编号">
                {{ caseInfo.caseNo }}
              </el-descriptions-item>
              <el-descriptions-item label="住院号">
                {{ caseInfo.hospitalNo }}
              </el-descriptions-item>
              <el-descriptions-item label="捐献者姓名">
                {{ caseInfo.donorName }}
              </el-descriptions-item>
              <el-descriptions-item label="性别">
                <dict-tag
                  :options="dict.type.sys_user_sex"
                  :value="parseInt(caseInfo.gender)"
                />
              </el-descriptions-item>
              <el-descriptions-item label="年龄">
                {{ caseInfo.age }} å²
              </el-descriptions-item>
              <el-descriptions-item label="血型">
                <dict-tag
                  :options="dict.type.sys_BloodType"
                  :value="caseInfo.bloodType"
                />
              </el-descriptions-item>
              <el-descriptions-item label="疾病诊断">
                {{ caseInfo.diagnosis }}
              </el-descriptions-item>
              <el-descriptions-item label="案例状态">
                <el-tag :type="getOverallStatusTag(caseInfo.status)">
                  {{ getStatusText(caseInfo.status) }}
                </el-tag>
              </el-descriptions-item>
              <el-descriptions-item label="创建时间">
                {{ formatTime(caseInfo.createTime) }}
              </el-descriptions-item>
              <el-descriptions-item label="登记人">
                {{ caseInfo.registrant }}
              </el-descriptions-item>
              <el-descriptions-item label="当前阶段">
                {{ getCurrentStageName() }}
              </el-descriptions-item>
            </el-descriptions>
          </div>
          <!-- é˜¶æ®µè¯¦æƒ…内容 -->
          <div class="stage-detail-section">
            <div class="section-header">
              <h3>{{ activeStageName }} - é˜¶æ®µè¯¦æƒ…</h3>
              <div class="stage-actions">
                <el-button
                  v-if="activeStageData.status !== 'completed'"
                  type="success"
                  size="small"
                  @click="handleCompleteStage"
                >
                  å®Œæˆé˜¶æ®µ
                </el-button>
                <el-button
                  type="primary"
                  size="small"
                  @click="handleViewDetail"
                >
                  æŸ¥çœ‹è¯¦æƒ…
                </el-button>
                <el-button
                  v-if="activeStageData.status === 'completed'"
                  type="warning"
                  size="small"
                  @click="handleModifyStage"
                >
                  ä¿®æ”¹ä¿¡æ¯
                </el-button>
              </div>
            </div>
            <!-- åŠ¨æ€é˜¶æ®µå†…å®¹ -->
            <div class="stage-content">
              <component
                :is="getStageComponent()"
                :stageData="activeStageData"
                :caseInfo="caseInfo"
              />
            </div>
          </div>
        </div>
      </div>
    </el-card>
  </div>
</template>
<script>
import { getDonationProcessDetail } from './donationProcess';
import DonorMaintenanceStage from './components/DonorMaintenanceStage';
import MedicalAssessmentStage from './components/MedicalAssessmentStage';
import DeathJudgmentStage from './components/DeathJudgmentStage';
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";
export default {
  name: 'DonationProcessDetail',
  components: {
    DonorMaintenanceStage,
    MedicalAssessmentStage,
    DeathJudgmentStage,
    DonationConfirmStage,
    EthicalReviewStage,
    OrganAllocationStage,
    OrganProcurementStage,
    OrganUtilizationStage
  },
  dicts: ['sys_user_sex', 'sys_BloodType', 'sys_0_1'],
  data() {
    return {
      caseId: null,
      caseInfo: {
        id: '',
        caseNo: '',
        hospitalNo: '',
        donorName: '',
        gender: '',
        age: '',
        bloodType: '',
        diagnosis: '',
        status: 'in_progress',
        createTime: '',
        registrant: '',
        currentStage: 'donor_maintenance'
      },
      processStages: [
        {
          key: 'donor_maintenance',
          name: '供者维护',
          status: 'completed',
          completeTime: '2023-12-01 10:00:00',
          updateTime: '2023-12-01 10:00:00',
          operator: '张医生'
        },
        {
          key: 'medical_assessment',
          name: '医学评估',
          status: 'completed',
          completeTime: '2023-12-02 14:30:00',
          updateTime: '2023-12-02 14:30:00',
          operator: '李主任'
        },
        {
          key: 'death_judgment',
          name: '死亡判定',
          status: 'completed',
          completeTime: '2023-12-03 09:15:00',
          updateTime: '2023-12-03 09:15:00',
          operator: '王医生'
        },
        {
          key: 'donation_confirm',
          name: '捐献确认',
          status: 'completed',
          completeTime: '2023-12-03 11:00:00',
          updateTime: '2023-12-03 11:00:00',
          operator: '赵协调员'
        },
        {
          key: 'ethical_review',
          name: '伦理审查',
          status: 'completed',
          completeTime: '2023-12-03 15:20:00',
          updateTime: '2023-12-03 15:20:00',
          operator: '伦理委员会'
        },
        {
          key: 'organ_allocation',
          name: '器官分配',
          status: 'in_progress',
          updateTime: '2023-12-04 10:00:00',
          operator: '分配系统'
        },
        {
          key: 'organ_procurement',
          name: '器官获取',
          status: 'pending',
          operator: '待分配'
        },
        {
          key: 'organ_utilization',
          name: '器官利用',
          status: 'pending',
          operator: '待分配'
        }
      ],
      activeStage: 'organ_allocation',
      activeStageName: '器官分配',
      activeStageData: {},
      loading: false
    };
  },
  computed: {
  },
  created() {
    this.caseId = this.$route.query.id;
    if (this.caseId) {
      this.getDetail();
    } else {
      this.generateMockData();
    }
    this.setActiveStage(this.activeStage);
  },
  methods: {
       getStageComponent() {
      const componentMap = {
        'donor_maintenance': 'DonorMaintenanceStage',
        'medical_assessment': 'MedicalAssessmentStage',
        'death_judgment': 'DeathJudgmentStage',
        'donation_confirm': 'DonationConfirmStage',
        'ethical_review': 'EthicalReviewStage',
        'organ_allocation': 'OrganAllocationStage',
        'organ_procurement': 'OrganProcurementStage',
        'organ_utilization': 'OrganUtilizationStage'
      };
      return componentMap[this.activeStage];
    },
    // èŽ·å–è¯¦æƒ…æ•°æ®
    async getDetail() {
      this.loading = true;
      try {
        const response = await getDonationProcessDetail(this.caseId);
        if (response.code === 200) {
          this.caseInfo = response.data.caseInfo;
          this.processStages = response.data.processStages;
          this.setActiveStage(response.data.currentStage);
        }
      } catch (error) {
        console.error('获取捐献进程详情失败:', error);
        this.$message.error('获取详情失败');
      } finally {
        this.loading = false;
      }
    },
    // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
    generateMockData() {
      this.caseInfo = {
        id: '202312001',
        caseNo: 'C202312001',
        hospitalNo: 'D202312001',
        donorName: '张三',
        gender: '0',
        age: 45,
        bloodType: 'A',
        diagnosis: '脑外伤',
        status: 'in_progress',
        createTime: '2023-12-01 08:00:00',
        registrant: '李协调员',
        currentStage: 'organ_allocation'
      };
    },
    // è®¾ç½®å½“前激活阶段
    setActiveStage(stageKey) {
      this.activeStage = stageKey;
      const stage = this.processStages.find(s => s.key === stageKey);
      if (stage) {
        this.activeStageName = stage.name;
        this.activeStageData = stage;
      }
    },
    // å¤„理阶段点击
    handleStageClick(stage) {
      if (stage.status !== 'pending') {
        this.setActiveStage(stage.key);
      } else {
        this.$message.warning('该阶段尚未开始,无法查看详情');
      }
    },
    // èŽ·å–é˜¶æ®µçŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStageStatusTag(status) {
      const map = {
        'completed': 'success',
        'in_progress': 'warning',
        'pending': 'info'
      };
      return map[status] || 'info';
    },
    // èŽ·å–é˜¶æ®µçŠ¶æ€æ–‡æœ¬
    getStageStatusText(status) {
      const map = {
        'completed': '已完成',
        'in_progress': '进行中',
        'pending': '未开始'
      };
      return map[status] || '未知';
    },
    // èŽ·å–æ•´ä½“çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getOverallStatusTag(status) {
      const map = {
        'completed': 'success',
        'in_progress': 'warning',
        'pending': 'info',
        'terminated': 'danger'
      };
      return map[status] || 'info';
    },
    // èŽ·å–æ•´ä½“çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      const map = {
        'completed': '已完成',
        'in_progress': '进行中',
        'pending': '未开始',
        'terminated': '已终止'
      };
      return map[status] || '未知';
    },
    // æ—¶é—´æ ¼å¼åŒ–
    formatTime(time) {
      if (!time) return '-';
      return dayjs(time).format('YYYY-MM-DD HH:mm');
    },
    // èŽ·å–å½“å‰é˜¶æ®µåç§°
    getCurrentStageName() {
      const currentStage = this.processStages.find(
        stage => stage.status === 'in_progress'
      );
      return currentStage ? currentStage.name : '已完成';
    },
    // ç¼–辑基本信息
    handleEditBasicInfo() {
      this.$message.info('编辑基本信息功能');
    },
    // å®Œæˆé˜¶æ®µ
    handleCompleteStage() {
      this.$confirm(`确定要完成【${this.activeStageName}】阶段吗?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        // æ›´æ–°å½“前阶段状态
        const currentIndex = this.processStages.findIndex(
          stage => stage.key === this.activeStage
        );
        if (currentIndex !== -1) {
          this.processStages[currentIndex].status = 'completed';
          this.processStages[currentIndex].completeTime = new Date().toISOString();
          // æ¿€æ´»ä¸‹ä¸€ä¸ªé˜¶æ®µ
          if (currentIndex < this.processStages.length - 1) {
            this.processStages[currentIndex + 1].status = 'in_progress';
            this.setActiveStage(this.processStages[currentIndex + 1].key);
          } else {
            this.caseInfo.status = 'completed';
          }
          this.$message.success('阶段已完成');
        }
      });
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleViewDetail() {
      const routeMap = {
        'donor_maintenance': '/case/donorMaintenance/detail',
        'medical_assessment': '/case/medicalAssessment/detail',
        'death_judgment': '/case/deathJudgment/detail',
        'donation_confirm': '/case/donationConfirm/detail',
        'ethical_review': '/case/ethicalReview/detail',
        'organ_allocation': '/case/organAllocation/detail',
        'organ_procurement': '/case/organProcurement/detail',
        'organ_utilization': '/case/organUtilization/detail'
      };
      const route = routeMap[this.activeStage];
      if (route) {
        this.$router.push({
          path: route,
          query: { id: this.caseId }
        });
      }
    },
    // ä¿®æ”¹é˜¶æ®µä¿¡æ¯
    handleModifyStage() {
      this.$message.info(`修改${this.activeStageName}信息功能`);
    }
  }
};
</script>
<style scoped>
.donation-process-detail {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.process-card {
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.process-container {
  display: flex;
  min-height: 800px;
  gap: 20px;
}
/* å·¦ä¾§æ—¶é—´çº¿æ ·å¼ */
.timeline-section {
  flex: 0 0 300px;
  background: white;
  border-radius: 6px;
  padding: 20px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 15px;
  border-bottom: 1px solid #e4e7ed;
}
.section-header h3 {
  margin: 0;
  color: #303133;
  font-size: 16px;
}
.timeline-container {
  display: flex;
  flex-direction: column;
  gap: 15px;
}
.timeline-item {
  display: flex;
  align-items: flex-start;
  padding: 15px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.3s ease;
  border: 1px solid #e4e7ed;
}
.timeline-item:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transform: translateY(-1px);
}
.timeline-item.active {
  border-color: #409EFF;
  background-color: #f0f9ff;
}
.timeline-item.completed {
  border-color: #67C23A;
  background-color: #f0f9e8;
}
.timeline-item.in-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.in-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;
}
.stage-info {
  font-size: 12px;
  color: #606266;
}
.time-info, .operator-info {
  margin-bottom: 4px;
}
/* å³ä¾§å†…容区域样式 */
.content-section {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.basic-info-section,
.stage-detail-section {
  background: white;
  border-radius: 6px;
  padding: 20px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.stage-actions {
  display: flex;
  gap: 10px;
}
.stage-content {
  margin-top: 20px;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
  .process-container {
    flex-direction: column;
  }
  .timeline-section {
    flex: none;
    margin-bottom: 20px;
  }
}
@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-actions {
    flex-wrap: wrap;
  }
}
/* åŠ¨ç”»æ•ˆæžœ */
.timeline-item {
  transition: all 0.3s ease;
}
.timeline-item:hover {
  transform: translateY(-2px);
}
/* è¿›åº¦æ¡æ ·å¼ä¼˜åŒ– */
:deep(.el-progress-bar) {
  padding-right: 0;
}
:deep(.el-progress__text) {
  font-size: 12px;
}
</style>
src/views/business/decide/DecideInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,653 @@
<template>
  <div class="death-judgment-detail">
    <el-card class="detail-card">
      <!-- åŸºç¡€ä¿¡æ¯ -->
      <div slot="header" class="clearfix">
        <span class="detail-title">死亡判定基本信息</span>
        <el-button
          v-if="isEdit"
          type="primary"
          style="float: right; padding: 3px 0"
          @click="handleSave"
          :loading="saveLoading"
        >
          ä¿å­˜ä¿¡æ¯
        </el-button>
      </div>
      <el-form :model="form" ref="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="案例编号" prop="hospitalNo">
              <el-input
                v-model="form.hospitalNo"
                :readonly="!isEdit"
                placeholder="自动生成 D+数字"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="捐献者姓名" prop="donorName">
              <el-input v-model="form.donorName" :readonly="!isEdit" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="性别" prop="gender">
              <el-select
                v-model="form.gender"
                :disabled="!isEdit"
                style="width: 100%"
              >
                <el-option label="男" value="0" />
                <el-option label="女" value="1" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="年龄" prop="age">
              <el-input v-model="form.age" :readonly="!isEdit" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="疾病诊断" prop="diagnosis">
              <el-input v-model="form.diagnosis" :readonly="!isEdit" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="死亡原因" prop="deathReason">
              <el-select
                v-model="form.deathReason"
                :disabled="!isEdit"
                style="width: 100%"
              >
                <el-option label="脑死亡" value="brain_death" />
                <el-option label="心死亡" value="heart_death" />
                <el-option label="其他" value="other" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="死亡时间" prop="deathTime">
              <el-date-picker
                v-model="form.deathTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
                :disabled="!isEdit"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="判定医生" prop="judgmentDoctor">
              <el-input v-model="form.judgmentDoctor" :readonly="!isEdit" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="登记人" prop="registrant">
              <el-input v-model="form.registrant" :readonly="!isEdit" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="死亡判定说明" prop="judgmentDescription">
          <el-input
            type="textarea"
            :rows="3"
            v-model="form.judgmentDescription"
            :readonly="!isEdit"
            placeholder="详细记录死亡判定过程和依据"
          />
        </el-form-item>
      </el-form>
    </el-card>
    <!-- è¯„估表附件 -->
    <el-card class="attachment-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">死亡判定评估表附件</span>
        <el-button
          v-if="isEdit"
          type="primary"
          size="mini"
          @click="openUploadDialog"
          :loading="uploadLoading"
        >
          ä¸Šä¼ é™„ä»¶
        </el-button>
      </div>
      <!-- é™„件类型选项卡 -->
      <el-tabs v-model="activeAttachmentType" type="card">
        <el-tab-pane
          v-for="type in attachmentTypes"
          :key="type.value"
          :label="type.label"
          :name="type.value"
        >
          <div class="attachment-upload-section">
            <div class="upload-header">
              <span class="upload-title">{{ type.label }}</span>
              <el-tooltip content="点击上传该类型评估表" placement="top">
                <el-button
                  size="mini"
                  type="primary"
                  icon="el-icon-plus"
                  @click="openUploadDialog(type.value)"
                  :disabled="!isEdit"
                >
                  æ·»åŠ è¯„ä¼°è¡¨
                </el-button>
              </el-tooltip>
            </div>
            <!-- é™„件列表 -->
            <el-table
              :data="getAttachmentsByType(type.value)"
              v-loading="attachmentLoading"
              style="width: 100%; margin-top: 15px;"
            >
              <el-table-column label="文件名称" min-width="200">
                <template slot-scope="scope">
                  <div class="file-info">
                    <i
                      class="el-icon-document"
                      style="margin-right: 8px; color: #409EFF;"
                    ></i>
                    <span>{{ scope.row.fileName }}</span>
                  </div>
                </template>
              </el-table-column>
              <el-table-column label="文件类型" width="100" align="center">
                <template slot-scope="scope">
                  <el-tag size="small">{{ getFileType(scope.row.fileName) }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="文件大小" width="100" align="center">
                <template slot-scope="scope">
                  <span>{{ formatFileSize(scope.row.fileSize) }}</span>
                </template>
              </el-table-column>
              <el-table-column label="上传时间" width="160" align="center">
                <template slot-scope="scope">
                  <span>{{ parseTime(scope.row.uploadTime) }}</span>
                </template>
              </el-table-column>
              <el-table-column label="上传人" width="100" align="center">
                <template slot-scope="scope">
                  <span>{{ scope.row.uploader }}</span>
                </template>
              </el-table-column>
              <el-table-column
                label="操作"
                width="180"
                align="center"
              >
                <template slot-scope="scope">
                  <el-button
                    size="mini"
                    type="text"
                    icon="el-icon-view"
                    @click="handlePreview(scope.row)"
                    >预览</el-button
                  >
                  <el-button
                    size="mini"
                    type="text"
                    icon="el-icon-download"
                    @click="handleDownload(scope.row)"
                    >下载</el-button
                  >
                  <el-button
                    v-if="isEdit"
                    size="mini"
                    type="text"
                    icon="el-icon-delete"
                    style="color: #F56C6C;"
                    @click="handleRemoveAttachment(scope.row)"
                    >删除</el-button
                  >
                </template>
              </el-table-column>
            </el-table>
            <div
              v-if="getAttachmentsByType(type.value).length === 0"
              class="empty-attachment"
            >
              <el-empty description="暂无评估表附件" :image-size="80"></el-empty>
            </div>
          </div>
        </el-tab-pane>
      </el-tabs>
    </el-card>
    <!-- ä¸Šä¼ å¯¹è¯æ¡† -->
    <el-dialog
      :title="`上传${getCurrentTypeLabel}评估表`"
      :visible.sync="uploadDialogVisible"
      width="500px"
      :close-on-click-modal="false"
    >
      <el-upload
        ref="uploadRef"
        class="upload-demo"
        drag
        action="#"
        multiple
        :file-list="tempFileList"
        :before-upload="beforeUpload"
        :on-change="handleFileChange"
        :on-remove="handleTempRemove"
        :auto-upload="false"
      >
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将评估表文件拖到此处,或<em>点击上传</em></div>
        <div class="el-upload__tip" slot="tip">
          æ”¯æŒä¸Šä¼ pdf、jpg、png、doc、docx、xls、xlsx格式文件,单个文件不超过10MB
        </div>
      </el-upload>
      <div slot="footer" class="dialog-footer">
        <el-button @click="uploadDialogVisible = false">取消</el-button>
        <el-button
          type="primary"
          @click="submitUpload"
          :loading="uploadLoading"
          :disabled="tempFileList.length === 0"
        >
          ç¡®è®¤ä¸Šä¼ 
        </el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import { getDeathJudgmentDetail, updateDeathJudgment } from "./mockDeathJudgmentApi";
export default {
  name: "DeathJudgmentDetail",
  data() {
    return {
      // æ˜¯å¦ç¼–辑模式
      isEdit: false,
      // ä¿å­˜åŠ è½½çŠ¶æ€
      saveLoading: false,
      // è¡¨å•数据
      form: {
        id: undefined,
        hospitalNo: "",
        donorName: "",
        gender: "",
        age: "",
        diagnosis: "",
        deathReason: "",
        deathTime: "",
        judgmentDoctor: "",
        judgmentDescription: "",
        registrant: "",
        registrationTime: ""
      },
      // è¡¨å•验证规则
      rules: {
        donorName: [
          { required: true, message: "捐献者姓名不能为空", trigger: "blur" }
        ],
        deathReason: [
          { required: true, message: "死亡原因不能为空", trigger: "change" }
        ],
        deathTime: [
          { required: true, message: "死亡时间不能为空", trigger: "change" }
        ],
        judgmentDoctor: [
          { required: true, message: "判定医生不能为空", trigger: "blur" }
        ]
      },
      // é™„件相关数据
      activeAttachmentType: "1",
      attachmentLoading: false,
      uploadDialogVisible: false,
      uploadLoading: false,
      tempFileList: [],
      currentUploadType: "",
      // è¯„估表类型定义
      attachmentTypes: [
        { value: "1", label: "脑死亡判定表" },
        { value: "2", label: "脑电图评估表" },
        { value: "3", label: "短潜伏期体感诱发电位评估表" },
        { value: "4", label: "经颅多普勒超声评估记录" },
        { value: "5", label: "卫健委脑损伤质控中心 - ä¸´åºŠç»¼åˆè¯„估表" },
        { value: "6", label: "UW评分表" },
        { value: "7", label: "心死亡判定表" }
      ],
      // é™„件列表数据
      attachmentList: []
    };
  },
  computed: {
    getCurrentTypeLabel() {
      const type = this.attachmentTypes.find(
        t => t.value === this.currentUploadType
      );
      return type ? type.label : "";
    }
  },
  created() {
    const id = this.$route.query.id;
    this.isEdit = this.$route.path.includes('/edit') || this.$route.path.includes('/add');
    if (id && !this.$route.path.includes('/add')) {
      this.getDetail(id);
    } else if (this.$route.path.includes('/add')) {
      this.generateHospitalNo();
    }
    this.getAttachmentList();
  },
  methods: {
    // ç”Ÿæˆæ¡ˆä¾‹ç¼–号
    generateHospitalNo() {
      // æ¨¡æ‹Ÿç”Ÿæˆæ¡ˆä¾‹ç¼–号:D + æ—¶é—´æˆ³åŽ6位
      const timestamp = Date.now().toString();
      this.form.hospitalNo = 'D' + timestamp.slice(-6);
    },
    // èŽ·å–è¯¦æƒ…
    getDetail(id) {
      getDeathJudgmentDetail(id).then(response => {
        if (response.code === 200) {
          this.form = response.data;
        }
      });
    },
    // èŽ·å–é™„ä»¶åˆ—è¡¨
    getAttachmentList() {
      this.attachmentLoading = true;
      // æ¨¡æ‹Ÿé™„件数据 - å®žé™…项目中从接口获取
      setTimeout(() => {
        this.attachmentList = [
          {
            id: 1,
            type: "1",
            typeName: "脑死亡判定表",
            fileName: "脑死亡判定表_202312001.pdf",
            fileSize: 2548321,
            uploadTime: "2023-12-01 10:30:00",
            uploader: "张医生",
            fileUrl: "/attachments/brain_death_1.pdf"
          },
          {
            id: 2,
            type: "2",
            typeName: "脑电图评估表",
            fileName: "脑电图评估表_202312001.docx",
            fileSize: 512345,
            uploadTime: "2023-12-01 14:20:00",
            uploader: "李医生",
            fileUrl: "/attachments/eeg_1.docx"
          }
        ];
        this.attachmentLoading = false;
      }, 500);
    },
    // æ ¹æ®ç±»åž‹èŽ·å–é™„ä»¶
    getAttachmentsByType(type) {
      return this.attachmentList.filter(item => item.type === type);
    },
    // èŽ·å–æ–‡ä»¶ç±»åž‹
    getFileType(fileName) {
      const ext = fileName.split('.').pop().toLowerCase();
      const typeMap = {
        'pdf': 'PDF',
        'doc': 'DOC',
        'docx': 'DOCX',
        'xls': 'XLS',
        'xlsx': 'XLSX',
        'jpg': 'JPG',
        'jpeg': 'JPEG',
        'png': 'PNG'
      };
      return typeMap[ext] || ext.toUpperCase();
    },
    // æ‰“开上传对话框
    openUploadDialog(type = null) {
      this.currentUploadType = type || this.activeAttachmentType;
      this.tempFileList = [];
      this.uploadDialogVisible = true;
      this.$nextTick(() => {
        if (this.$refs.uploadRef) {
          this.$refs.uploadRef.clearFiles();
        }
      });
    },
    // ä¸Šä¼ å‰æ ¡éªŒ
    beforeUpload(file) {
      const allowedTypes = [
        'application/pdf',
        'image/jpeg',
        'image/png',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.ms-excel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
      ];
      const maxSize = 10 * 1024 * 1024; // 10MB
      // æ ¡éªŒæ–‡ä»¶ç±»åž‹
      const isTypeOk = allowedTypes.includes(file.type) ||
                      file.name.endsWith('.pdf') ||
                      file.name.endsWith('.jpg') ||
                      file.name.endsWith('.jpeg') ||
                      file.name.endsWith('.png') ||
                      file.name.endsWith('.doc') ||
                      file.name.endsWith('.docx') ||
                      file.name.endsWith('.xls') ||
                      file.name.endsWith('.xlsx');
      if (!isTypeOk) {
        this.$message.error('文件格式不支持,请上传pdf、jpg、png、doc、docx、xls或xlsx格式文件');
        return false;
      }
      // æ ¡éªŒæ–‡ä»¶å¤§å°
      if (file.size > maxSize) {
        this.$message.error('文件大小不能超过10MB');
        return false;
      }
      return true;
    },
    // æ–‡ä»¶é€‰æ‹©å˜åŒ–
    handleFileChange(file, fileList) {
      this.tempFileList = fileList;
    },
    // ç§»é™¤ä¸´æ—¶æ–‡ä»¶
    handleTempRemove(file, fileList) {
      this.tempFileList = fileList;
    },
    // æäº¤ä¸Šä¼ 
    async submitUpload() {
      if (this.tempFileList.length === 0) {
        this.$message.warning('请先选择要上传的文件');
        return;
      }
      this.uploadLoading = true;
      try {
        // æ¨¡æ‹Ÿä¸Šä¼ è¿‡ç¨‹ - å®žé™…项目中调用上传接口
        for (const file of this.tempFileList) {
          const newAttachment = {
            id: Date.now() + Math.random(),
            type: this.currentUploadType,
            typeName: this.getCurrentTypeLabel,
            fileName: file.name,
            fileSize: file.size,
            uploadTime: new Date().toISOString(),
            uploader: '当前用户',
            fileUrl: URL.createObjectURL(file.raw)
          };
          this.attachmentList.push(newAttachment);
        }
        this.$message.success('文件上传成功');
        this.uploadDialogVisible = false;
        this.tempFileList = [];
      } catch (error) {
        this.$message.error('文件上传失败');
        console.error('上传失败:', error);
      } finally {
        this.uploadLoading = false;
      }
    },
    // åˆ é™¤é™„ä»¶
    handleRemoveAttachment(attachment) {
      this.$confirm('确定要删除这个评估表附件吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        const index = this.attachmentList.findIndex(item => item.id === attachment.id);
        if (index !== -1) {
          this.attachmentList.splice(index, 1);
          this.$message.success('评估表删除成功');
        }
      }).catch(() => {});
    },
    // é¢„览附件
    handlePreview(attachment) {
      if (attachment.fileName.endsWith('.pdf')) {
        window.open(attachment.fileUrl, '_blank');
      } else if (attachment.fileName.match(/\.(jpg|jpeg|png)$/i)) {
        this.$alert(`<img src="${attachment.fileUrl}" style="max-width: 100%;" alt="${attachment.fileName}">`,
          '图片预览', {
            dangerouslyUseHTMLString: true,
            customClass: 'image-preview-dialog'
          });
      } else {
        this.$message.info('该文件类型暂不支持在线预览,请下载后查看');
      }
    },
    // ä¸‹è½½é™„ä»¶
    handleDownload(attachment) {
      // å®žé™…项目中调用下载接口
      const link = document.createElement('a');
      link.href = attachment.fileUrl;
      link.download = attachment.fileName;
      link.click();
      this.$message.success(`开始下载: ${attachment.fileName}`);
    },
    // ä¿å­˜ä¿¡æ¯
    handleSave() {
      this.$refs.form.validate(valid => {
        if (valid) {
          this.saveLoading = true;
          // æ¨¡æ‹Ÿä¿å­˜è¿‡ç¨‹
          updateDeathJudgment(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success('保存成功');
                if (this.$route.path.includes('/add')) {
                  this.$router.push('/case/deathJudgment');
                } else {
                  this.isEdit = false;
                }
              }
            })
            .catch(error => {
              console.error('保存失败:', error);
              this.$message.error('保存失败');
            })
            .finally(() => {
              this.saveLoading = false;
            });
        }
      });
    },
    // æ–‡ä»¶å¤§å°æ ¼å¼åŒ–
    formatFileSize(size) {
      if (size === 0) return '0 B';
      const k = 1024;
      const sizes = ['B', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(size) / Math.log(k));
      return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time) {
      if (!time) return '';
      const date = new Date(time);
      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    }
  }
};
</script>
<style scoped>
.death-judgment-detail {
  padding: 20px;
}
.detail-card {
  margin-bottom: 20px;
}
.attachment-card {
  margin-bottom: 20px;
}
.detail-title {
  font-size: 16px;
  font-weight: bold;
}
.attachment-upload-section {
  padding: 10px;
}
.upload-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.upload-title {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
}
.file-info {
  display: flex;
  align-items: center;
}
.empty-attachment {
  text-align: center;
  padding: 40px 0;
  color: #909399;
}
/* å›¾ç‰‡é¢„览对话框样式 */
:deep(.image-preview-dialog) {
  width: auto;
  max-width: 90vw;
}
:deep(.image-preview-dialog .el-message-box__content) {
  text-align: center;
}
</style>
src/views/business/decide/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,437 @@
<template>
  <div class="death-judgment-list">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-card class="search-card">
      <el-form
        :model="queryParams"
        ref="queryForm"
        :inline="true"
        label-width="100px"
      >
        <el-form-item label="住院号" prop="hospitalNo">
          <el-input
            v-model="queryParams.hospitalNo"
            placeholder="请输入住院号"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="捐献者姓名" prop="donorName">
          <el-input
            v-model="queryParams.donorName"
            placeholder="请输入捐献者姓名"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="死亡原因" prop="deathReason">
          <el-select
            v-model="queryParams.deathReason"
            placeholder="请选择死亡原因"
            clearable
            style="width: 200px"
          >
            <el-option label="脑死亡" value="brain_death" />
            <el-option label="心死亡" value="heart_death" />
            <el-option label="其他" value="other" />
          </el-select>
        </el-form-item>
        <el-form-item label="死亡时间范围" prop="deathTimeRange">
          <el-date-picker
            v-model="queryParams.deathTimeRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
            style="width: 240px"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="handleQuery"
            >搜索</el-button
          >
          <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- æ“ä½œæŒ‰é’® -->
    <el-card class="tool-card">
      <el-row :gutter="10">
        <el-col :span="16">
          <el-button type="primary" icon="el-icon-plus" @click="handleCreate"
            >新建死亡判定</el-button
          >
          <el-button
            type="success"
            icon="el-icon-edit"
            :disabled="single"
            @click="handleUpdate"
            >修改</el-button
          >
          <el-button
            type="danger"
            icon="el-icon-delete"
            :disabled="multiple"
            @click="handleDelete"
            >删除</el-button
          >
          <el-button
            type="warning"
            icon="el-icon-download"
            @click="handleExport"
            >导出</el-button
          >
        </el-col>
        <el-col :span="8" style="text-align: right">
          <el-tooltip content="刷新" placement="top">
            <el-button icon="el-icon-refresh" circle @click="getList" />
          </el-tooltip>
        </el-col>
      </el-row>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <el-table
        v-loading="loading"
        :data="deathJudgmentList"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column
          label="住院号"
          align="center"
          prop="hospitalNo"
          width="120"
        />
        <el-table-column
          label="捐献者姓名"
          align="center"
          prop="donorName"
          width="120"
        />
        <el-table-column label="性别" align="center" prop="gender" width="80">
          <template slot-scope="scope">
            <dict-tag
              :options="dict.type.sys_user_sex"
              :value="parseInt(scope.row.gender)"
            />
          </template>
        </el-table-column>
        <el-table-column label="年龄" align="center" prop="age" width="80" />
        <el-table-column
          label="疾病诊断"
          align="center"
          prop="diagnosis"
          min-width="180"
          show-overflow-tooltip
        />
        <el-table-column
          label="死亡原因"
          align="center"
          prop="deathReason"
          width="120"
        >
          <template slot-scope="scope">
            <el-tag :type="reasonFilter(scope.row.deathReason)">
              {{ reasonTextFilter(scope.row.deathReason) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="死亡时间"
          align="center"
          prop="deathTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.deathTime
                ? parseTime(scope.row.deathTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="判定医生"
          align="center"
          prop="judgmentDoctor"
          width="120"
        />
        <el-table-column
          label="登记时间"
          align="center"
          prop="registrationTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.registrationTime
                ? parseTime(scope.row.registrationTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="登记人"
          align="center"
          prop="registrant"
          width="100"
        />
        <el-table-column
          label="操作"
          align="center"
          width="180"
          class-name="small-padding fixed-width"
        >
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleView(scope.row)"
              >详情</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-edit"
              @click="handleUpdate(scope.row)"
              >修改</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              style="color: #F56C6C"
              @click="handleDelete(scope.row)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="queryParams.pageNum"
        :limit.sync="queryParams.pageSize"
        @pagination="getList"
      />
    </el-card>
  </div>
</template>
<script>
import { listDeathJudgment, delDeathJudgment, exportDeathJudgment } from "./mockDeathJudgmentApi";
import Pagination from "@/components/Pagination";
export default {
  name: "DeathJudgmentList",
  components: { Pagination },
  dicts: ["sys_user_sex"],
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ€»æ¡æ•°
      total: 0,
      // æ­»äº¡åˆ¤å®šè¡¨æ ¼æ•°æ®
      deathJudgmentList: [],
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        hospitalNo: undefined,
        donorName: undefined,
        deathReason: undefined,
        deathTimeRange: []
      }
    };
  },
  created() {
    this.getList();
  },
  methods: {
    // æ­»äº¡åŽŸå› è¿‡æ»¤å™¨
    reasonFilter(reason) {
      const reasonMap = {
        "brain_death": "primary", // è„‘死亡
        "heart_death": "danger", // å¿ƒæ­»äº¡
        "other": "info" // å…¶ä»–
      };
      return reasonMap[reason] || "info";
    },
    reasonTextFilter(reason) {
      const reasonMap = {
        "brain_death": "脑死亡",
        "heart_death": "心死亡",
        "other": "其他"
      };
      return reasonMap[reason] || "未知";
    },
    // æŸ¥è¯¢æ­»äº¡åˆ¤å®šåˆ—表
    getList() {
      this.loading = true;
      listDeathJudgment(this.queryParams)
        .then(response => {
          if (response.code === 200) {
            this.deathJudgmentList = response.data.rows;
            this.total = response.data.total;
          } else {
            this.$message.error("获取数据失败");
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取死亡判定列表失败:", error);
          this.loading = false;
          this.$message.error("获取数据失败");
        });
    },
    // æœç´¢æŒ‰é’®æ“ä½œ
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŒ‰é’®æ“ä½œ
    resetQuery() {
      this.$refs.queryForm.resetFields();
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleView(row) {
      this.$router.push({
        path: "/case/DecideInfo",
        query: { id: row.id }
      });
    },
    // æ–°å¢žæŒ‰é’®æ“ä½œ
    handleCreate() {
      this.$router.push("/case/DecideInfo");
    },
    // ä¿®æ”¹æŒ‰é’®æ“ä½œ
    handleUpdate(row) {
      const id = row.id || this.ids[0];
      this.$router.push({
        path: "/case/DecideInfo",
        query: { id: id }
      });
    },
    // åˆ é™¤æŒ‰é’®æ“ä½œ
    handleDelete(row) {
      const ids = row.id ? [row.id] : this.ids;
      this.$confirm("是否确认删除选中的数据项?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          return delDeathJudgment(ids);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("删除成功");
            this.getList();
          }
        })
        .catch(() => {});
    },
    // å¯¼å‡ºæŒ‰é’®æ“ä½œ
    handleExport() {
      const queryParams = this.queryParams;
      this.$confirm("是否确认导出所有死亡判定数据?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.loading = true;
          return exportDeathJudgment(queryParams);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("导出成功");
            // å®žé™…项目中这里处理文件下载
          }
          this.loading = false;
        })
        .catch(() => {
          this.loading = false;
        });
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time, pattern) {
      if (!time) return "";
      const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
      let date;
      if (typeof time === "object") {
        date = time;
      } else {
        if (typeof time === "string" && /^[0-9]+$/.test(time)) {
          time = parseInt(time);
        }
        if (typeof time === "number" && time.toString().length === 10) {
          time = time * 1000;
        }
        date = new Date(time);
      }
      const formatObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
      };
      const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
        let value = formatObj[key];
        if (key === "a") {
          return ["日", "一", "二", "三", "四", "五", "六"][value];
        }
        if (result.length > 0 && value < 10) {
          value = "0" + value;
        }
        return value || 0;
      });
      return time_str;
    }
  }
};
</script>
<style scoped>
.death-judgment-list {
  padding: 20px;
}
.search-card {
  margin-bottom: 20px;
}
.tool-card {
  margin-bottom: 20px;
}
.fixed-width .el-button {
  margin: 0 5px;
}
</style>
src/views/business/decide/mockDeathJudgmentApi.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,391 @@
// æ¨¡æ‹Ÿæ­»äº¡åˆ¤å®šæ•°æ®
const mockDeathJudgmentData = [
  {
    id: 1,
    hospitalNo: "D202312001",
    donorName: "张三",
    gender: "0",
    age: 45,
    diagnosis: "脑外伤",
    deathReason: "brain_death",
    deathTime: "2023-12-01 14:30:00",
    judgmentDoctor: "王医生",
    judgmentDescription: "经过多次脑死亡判定,符合脑死亡临床诊断标准",
    registrant: "李协调员",
    registrationTime: "2023-12-01 15:00:00",
    createTime: "2023-12-01 10:00:00"
  },
  {
    id: 2,
    hospitalNo: "D202312002",
    donorName: "李四",
    gender: "1",
    age: 38,
    diagnosis: "心脏骤停",
    deathReason: "heart_death",
    deathTime: "2023-12-02 09:15:00",
    judgmentDoctor: "刘医生",
    judgmentDescription: "心死亡判定,心电图呈直线,无自主呼吸",
    registrant: "张协调员",
    registrationTime: "2023-12-02 10:00:00",
    createTime: "2023-12-02 08:30:00"
  },
  {
    id: 3,
    hospitalNo: "D202312003",
    donorName: "王五",
    gender: "0",
    age: 52,
    diagnosis: "脑梗死",
    deathReason: "brain_death",
    deathTime: "2023-12-03 16:45:00",
    judgmentDoctor: "陈医生",
    judgmentDescription: "脑干功能完全丧失,符合脑死亡标准",
    registrant: "赵协调员",
    registrationTime: "2023-12-03 17:20:00",
    createTime: "2023-12-03 14:00:00"
  },
  {
    id: 4,
    hospitalNo: "D202312004",
    donorName: "赵六",
    gender: "1",
    age: 29,
    diagnosis: "多发伤",
    deathReason: "other",
    deathTime: "2023-12-04 11:20:00",
    judgmentDoctor: "孙医生",
    judgmentDescription: "创伤性休克导致多器官功能衰竭",
    registrant: "钱协调员",
    registrationTime: "2023-12-04 12:00:00",
    createTime: "2023-12-04 09:45:00"
  },
  {
    id: 5,
    hospitalNo: "D202312005",
    donorName: "孙七",
    gender: "0",
    age: 61,
    diagnosis: "脑肿瘤",
    deathReason: "brain_death",
    deathTime: "2023-12-05 13:10:00",
    judgmentDoctor: "周医生",
    judgmentDescription: "颅内压增高导致脑疝形成",
    registrant: "吴协调员",
    registrationTime: "2023-12-05 13:45:00",
    createTime: "2023-12-05 10:30:00"
  }
];
// æ¨¡æ‹Ÿé™„件数据
const mockAttachmentData = [
  {
    id: 1,
    judgmentId: 1,
    type: "1",
    typeName: "脑死亡判定表",
    fileName: "脑死亡判定表_202312001.pdf",
    fileSize: 2548321,
    uploadTime: "2023-12-01 14:35:00",
    uploader: "王医生",
    fileUrl: "/attachments/brain_death_1.pdf"
  },
  {
    id: 2,
    judgmentId: 1,
    type: "2",
    typeName: "脑电图评估表",
    fileName: "脑电图评估表_202312001.docx",
    fileSize: 512345,
    uploadTime: "2023-12-01 15:20:00",
    uploader: "李医生",
    fileUrl: "/attachments/eeg_1.docx"
  },
  {
    id: 3,
    judgmentId: 2,
    type: "7",
    typeName: "心死亡判定表",
    fileName: "心死亡判定表_202312002.pdf",
    fileSize: 1892345,
    uploadTime: "2023-12-02 09:30:00",
    uploader: "刘医生",
    fileUrl: "/attachments/heart_death_2.pdf"
  },
  {
    id: 4,
    judgmentId: 3,
    type: "1",
    typeName: "脑死亡判定表",
    fileName: "脑死亡判定表_202312003.pdf",
    fileSize: 3124567,
    uploadTime: "2023-12-03 17:00:00",
    uploader: "陈医生",
    fileUrl: "/attachments/brain_death_3.pdf"
  },
  {
    id: 5,
    judgmentId: 3,
    type: "4",
    typeName: "经颅多普勒超声评估记录",
    fileName: "经颅多普勒_202312003.xlsx",
    fileSize: 845672,
    uploadTime: "2023-12-03 17:15:00",
    uploader: "张技师",
    fileUrl: "/attachments/tcd_3.xlsx"
  }
];
// æ¨¡æ‹ŸAPI响应延迟
const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms));
// æŸ¥è¯¢æ­»äº¡åˆ¤å®šåˆ—表
export const listDeathJudgment = async (queryParams = {}) => {
  await delay();
  const {
    pageNum = 1,
    pageSize = 10,
    hospitalNo,
    donorName,
    deathReason,
    deathTimeRange = []
  } = queryParams;
  // è¿‡æ»¤æ•°æ®
  let filteredData = mockDeathJudgmentData.filter(item => {
    let match = true;
    if (hospitalNo && !item.hospitalNo.includes(hospitalNo)) {
      match = false;
    }
    if (donorName && !item.donorName.includes(donorName)) {
      match = false;
    }
    if (deathReason && item.deathReason !== deathReason) {
      match = false;
    }
    if (deathTimeRange.length === 2) {
      const deathTime = new Date(item.deathTime);
      const startTime = new Date(deathTimeRange[0]);
      const endTime = new Date(deathTimeRange[1]);
      endTime.setDate(endTime.getDate() + 1);
      if (deathTime < startTime || deathTime >= endTime) {
        match = false;
      }
    }
    return match;
  });
  // åˆ†é¡µ
  const startIndex = (pageNum - 1) * pageSize;
  const endIndex = startIndex + parseInt(pageSize);
  const paginatedData = filteredData.slice(startIndex, endIndex);
  return {
    code: 200,
    message: "success",
    data: {
      rows: paginatedData,
      total: filteredData.length,
      pageNum: parseInt(pageNum),
      pageSize: parseInt(pageSize)
    }
  };
};
// èŽ·å–æ­»äº¡åˆ¤å®šè¯¦ç»†ä¿¡æ¯
export const getDeathJudgmentDetail = async (id) => {
  await delay();
  const detail = mockDeathJudgmentData.find(item => item.id == id);
  if (detail) {
    // èŽ·å–è¯¥åˆ¤å®šå¯¹åº”çš„é™„ä»¶
    const attachments = mockAttachmentData.filter(item => item.judgmentId == id);
    return {
      code: 200,
      message: "success",
      data: {
        ...detail,
        attachments
      }
    };
  } else {
    return {
      code: 404,
      message: "死亡判定记录不存在"
    };
  }
};
// æ–°å¢žæ­»äº¡åˆ¤å®š
export const addDeathJudgment = async (data) => {
  await delay();
  const newId = Math.max(...mockDeathJudgmentData.map(item => item.id)) + 1;
  const hospitalNo = `D${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const newRecord = {
    ...data,
    id: newId,
    hospitalNo,
    registrationTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
    createTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
  };
  // æ¨¡æ‹Ÿæ·»åŠ åˆ°æ•°æ®
  mockDeathJudgmentData.unshift(newRecord);
  return {
    code: 200,
    message: "新增成功",
    data: newRecord
  };
};
// ä¿®æ”¹æ­»äº¡åˆ¤å®š
export const updateDeathJudgment = async (data) => {
  await delay();
  const index = mockDeathJudgmentData.findIndex(item => item.id == data.id);
  if (index !== -1) {
    mockDeathJudgmentData[index] = {
      ...mockDeathJudgmentData[index],
      ...data,
      updateTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
    };
    return {
      code: 200,
      message: "修改成功",
      data: mockDeathJudgmentData[index]
    };
  } else {
    return {
      code: 404,
      message: "死亡判定记录不存在"
    };
  }
};
// åˆ é™¤æ­»äº¡åˆ¤å®š
export const delDeathJudgment = async (ids) => {
  await delay();
  const idArray = Array.isArray(ids) ? ids : [ids];
  idArray.forEach(id => {
    const index = mockDeathJudgmentData.findIndex(item => item.id == id);
    if (index !== -1) {
      mockDeathJudgmentData.splice(index, 1);
      // åŒæ—¶åˆ é™¤å¯¹åº”的附件记录
      const attachmentIndex = mockAttachmentData.findIndex(item => item.judgmentId == id);
      while (attachmentIndex !== -1) {
        mockAttachmentData.splice(attachmentIndex, 1);
      }
    }
  });
  return {
    code: 200,
    message: "删除成功"
  };
};
// å¯¼å‡ºæ­»äº¡åˆ¤å®š
export const exportDeathJudgment = async (queryParams) => {
  await delay(1000);
  // æ¨¡æ‹Ÿå¯¼å‡ºåŠŸèƒ½
  const { data } = await listDeathJudgment(queryParams);
  return {
    code: 200,
    message: "导出成功",
    data: {
      fileName: `死亡判定数据_${new Date().getTime()}.xlsx`,
      downloadUrl: "/api/export/deathJudgment"
    }
  };
};
// é™„件相关API
export const uploadAttachment = async (file, judgmentId, type) => {
  await delay(1500);
  const newAttachment = {
    id: Math.max(...mockAttachmentData.map(item => item.id), 0) + 1,
    judgmentId: parseInt(judgmentId),
    type: type,
    typeName: getTypeName(type),
    fileName: file.name,
    fileSize: file.size,
    uploadTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
    uploader: "当前用户",
    fileUrl: URL.createObjectURL(file)
  };
  mockAttachmentData.push(newAttachment);
  return {
    code: 200,
    message: "上传成功",
    data: newAttachment
  };
};
export const deleteAttachment = async (attachmentId) => {
  await delay();
  const index = mockAttachmentData.findIndex(item => item.id == attachmentId);
  if (index !== -1) {
    mockAttachmentData.splice(index, 1);
    return {
      code: 200,
      message: "删除成功"
    };
  } else {
    return {
      code: 404,
      message: "附件不存在"
    };
  }
};
export const getAttachments = async (judgmentId) => {
  await delay();
  const attachments = mockAttachmentData.filter(item => item.judgmentId == judgmentId);
  return {
    code: 200,
    message: "success",
    data: attachments
  };
};
// è¾…助函数
function getTypeName(type) {
  const typeMap = {
    "1": "脑死亡判定表",
    "2": "脑电图评估表",
    "3": "短潜伏期体感诱发电位评估表",
    "4": "经颅多普勒超声评估记录",
    "5": "卫健委脑损伤质控中心 - ä¸´åºŠç»¼åˆè¯„估表",
    "6": "UW评分表",
    "7": "心死亡判定表"
  };
  return typeMap[type] || "未知类型";
}
src/views/business/ethicalReview/ethicalReviewInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1526 @@
<template>
  <div class="ethics-review-detail">
    <el-card class="detail-card">
      <!-- åŸºç¡€ä¿¡æ¯ -->
      <div slot="header" class="clearfix">
        <span class="detail-title">伦理审查基本信息</span>
        <div style="float: right;">
          <el-button type="primary" @click="handleSave" :loading="saveLoading">
            ä¿å­˜
          </el-button>
          <el-button
            type="warning"
            @click="handleEndReview"
            :disabled="form.ethicsConclusion === 'terminated'"
          >
            ç»“束审查
          </el-button>
        </div>
      </div>
      <el-form :model="form" ref="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="住院号" prop="hospitalNo">
              <el-input v-model="form.hospitalNo" readonly />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="捐献者姓名" prop="donorName">
              <el-input v-model="form.donorName" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="性别" prop="gender">
              <el-select v-model="form.gender" style="width: 100%">
                <el-option label="男" value="0" />
                <el-option label="女" value="1" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="年龄" prop="age">
              <el-input v-model="form.age" />
            </el-form-item>
          </el-col>
          <el-col :span="16">
            <el-form-item label="疾病诊断" prop="diagnosis">
              <el-input v-model="form.diagnosis" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="伦理结论" prop="ethicsConclusion">
              <el-select v-model="form.ethicsConclusion" style="width: 100%">
                <el-option label="审查中" value="reviewing" />
                <el-option label="同意" value="approved" />
                <el-option
                  label="修改后同意"
                  value="approved_with_modifications"
                />
                <el-option label="修改后重审" value="re-review" />
                <el-option label="不同意" value="disapproved" />
                <el-option label="终止审查" value="terminated" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="审查时间" prop="reviewTime">
              <el-date-picker
                v-model="form.reviewTime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="登记人" prop="registrant">
              <el-input v-model="form.registrant" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="伦理意见" prop="ethicsOpinion">
              <el-input
                type="textarea"
                :rows="3"
                v-model="form.ethicsOpinion"
                placeholder="请输入伦理审查意见"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="登记时间" prop="registrationTime">
          <el-date-picker
            v-model="form.registrationTime"
            type="datetime"
            value-format="yyyy-MM-dd HH:mm:ss"
            style="width: 100%"
          />
        </el-form-item>
      </el-form>
    </el-card>
    <!-- é™„件上传 -->
    <el-card class="attachment-card">
      <div slot="header" class="clearfix">
        <span class="detail-title">相关附件</span>
        <el-button type="primary" size="mini" @click="handleUploadAttachment">
          ä¸Šä¼ é™„ä»¶
        </el-button>
      </div>
      <el-table :data="attachments" style="width: 100%">
        <el-table-column label="文件名称" min-width="200">
          <template slot-scope="scope">
            <div class="file-info">
              <i
                class="el-icon-document"
                style="margin-right: 8px; color: #409EFF;"
              ></i>
              <span>{{ scope.row.fileName }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="文件类型" width="100" align="center">
          <template slot-scope="scope">
            <el-tag size="small">{{ getFileType(scope.row.fileName) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="文件大小" width="100" align="center">
          <template slot-scope="scope">
            <span>{{ formatFileSize(scope.row.fileSize) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="上传时间" width="160" align="center">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.uploadTime) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="上传人" width="100" align="center">
          <template slot-scope="scope">
            <span>{{ scope.row.uploader }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="120" align="center">
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handlePreviewAttachment(scope.row)"
              >预览</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-download"
              @click="handleDownloadAttachment(scope.row)"
              >下载</el-button
            >
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- ä¸“家审查情况 -->
    <el-card class="expert-card">
      <div slot="header" class="clearfix">
        <span class="detail-title"
          >专家审查情况 (18位普通专家 + 1位主任专家)</span
        >
        <div style="float: right;">
          <el-button
            size="mini"
            type="primary"
            @click="handleSendToNormalExperts"
            :disabled="!canSendToNormalExperts"
          >
            å‘送普通专家
          </el-button>
          <el-button
            size="mini"
            type="success"
            @click="handleSendToChiefExpert"
            :disabled="!canSendToChiefExpert"
          >
            å‘送主任专家
          </el-button>
          <el-button
            size="mini"
            type="warning"
            @click="handleBatchSend"
            :disabled="!canBatchSend"
          >
            æ‰¹é‡å‘送
          </el-button>
        </div>
      </div>
 <!-- ä¸“家统计信息 -->
      <div
        class="expert-stats"
        style="margin-top: 20px; padding: 15px; background: #f5f7fa; border-radius: 4px;"
      >
        <el-row :gutter="20">
          <el-col :span="6">
            <div class="stat-item">
              <span class="stat-label">普通专家已同意:</span>
              <span class="stat-value">{{ approvedNormalExperts }}/18</span>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="stat-item">
              <span class="stat-label">主任专家状态:</span>
              <span class="stat-value">{{ chiefExpertStatus }}</span>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="stat-item">
              <span class="stat-label">总完成进度:</span>
              <span class="stat-value">{{ completionRate }}%</span>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="stat-item">
              <span class="stat-label">审查结果:</span>
              <span class="stat-value">
                <el-tag :type="overallConclusionFilter">
                  {{ overallConclusionText }}
                </el-tag>
              </span>
            </div>
          </el-col>
        </el-row>
      </div>
      <!-- ä¸“家审查表格 -->
      <el-table
        :data="expertReviews"
        v-loading="expertLoading"
        style="width: 100%"
        heiht="300"
        :row-class-name="getExpertRowClassName"
      >
        <el-table-column label="序号" width="60" align="center" type="index" />
        <el-table-column
          label="专家姓名"
          width="120"
          align="center"
          fixed="left"
        >
          <template slot-scope="scope">
            <span>{{ scope.row.expertName }}</span>
            <el-tag
              v-if="scope.row.isChief"
              size="mini"
              type="danger"
              style="margin-left: 5px;"
              >主任</el-tag
            >
          </template>
        </el-table-column>
        <el-table-column label="专家类型" width="100" align="center">
          <template slot-scope="scope">
            <span :class="scope.row.isChief ? 'chief-expert' : 'normal-expert'">
              {{ scope.row.isChief ? "主任专家" : "普通专家" }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="审查状态" width="100" align="center">
          <template slot-scope="scope">
            <el-tag :type="statusFilter(scope.row.reviewStatus)" size="small">
              {{ statusTextFilter(scope.row.reviewStatus) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="专家结论" width="120" align="center">
          <template slot-scope="scope">
            <el-tag
              v-if="scope.row.expertConclusion"
              :type="conclusionFilter(scope.row.expertConclusion)"
              size="small"
            >
              {{ conclusionTextFilter(scope.row.expertConclusion) }}
            </el-tag>
            <span v-else class="no-data">-</span>
          </template>
        </el-table-column>
        <el-table-column label="审查意见" min-width="200" show-overflow-tooltip>
          <template slot-scope="scope">
            <span :class="{ 'expert-opinion': scope.row.expertOpinion }">
              {{ scope.row.expertOpinion || "暂无意见" }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="审查时间" width="160" align="center">
          <template slot-scope="scope">
            <span>{{
              scope.row.reviewTime ? parseTime(scope.row.reviewTime) : "未审查"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180" align="center" fixed="right">
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-s-promotion"
              @click="handleSendToExpert(scope.row)"
              :disabled="scope.row.reviewStatus === 'submitted'"
              :class="{ 'sent-button': scope.row.reviewStatus === 'submitted' }"
            >
              {{ scope.row.reviewStatus === "submitted" ? "已发送" : "发送" }}
            </el-button>
            <el-button
              size="mini"
              type="text"
              icon="el-icon-edit"
              @click="handleEditExpertReview(scope.row)"
              :disabled="scope.row.reviewStatus !== 'submitted'"
            >
              ç¼–辑
            </el-button>
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleViewExpertReview(scope.row)"
            >
              è¯¦æƒ…
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- å‘送专家对话框 -->
    <el-dialog
      title="发送专家审查"
      :visible.sync="sendDialogVisible"
      width="500px"
    >
      <el-form :model="sendForm" ref="sendForm" label-width="100px">
        <el-form-item label="专家类型" prop="expertType">
          <el-radio-group v-model="sendForm.expertType">
            <el-radio label="normal">普通专家</el-radio>
            <el-radio label="chief">主任专家</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item
          label="选择专家"
          prop="expertIds"
          v-if="sendForm.expertType === 'normal'"
        >
          <el-select
            v-model="sendForm.expertIds"
            multiple
            placeholder="请选择专家"
            style="width: 100%"
          >
            <el-option
              v-for="expert in availableExperts"
              :key="expert.id"
              :label="expert.name"
              :value="expert.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="发送内容" prop="content">
          <el-input
            type="textarea"
            :rows="4"
            v-model="sendForm.content"
            placeholder="请输入发送给专家的审查内容说明"
          />
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="sendDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSendConfirm"
          >确认发送</el-button
        >
      </div>
    </el-dialog>
  </div>
</template>
<script>
import {
  getEthicsReviewDetail,
  updateEthicsReview,
  sendExpertReview,
  endEthicsReview,
  uploadAttachment,
  deleteAttachment,
  getAttachments
} from "./ethicsReview";
export default {
  name: "EthicsReviewDetail",
  data() {
    return {
      // è¡¨å•数据
      form: {
        id: undefined,
        hospitalNo: "",
        donorName: "",
        gender: "",
        age: "",
        diagnosis: "",
        ethicsConclusion: "reviewing",
        ethicsOpinion: "",
        reviewTime: "",
        registrant: "",
        registrationTime: new Date()
          .toISOString()
          .replace("T", " ")
          .substring(0, 19)
      },
      // è¡¨å•验证规则
      rules: {
        donorName: [
          { required: true, message: "捐献者姓名不能为空", trigger: "blur" }
        ],
        ethicsConclusion: [
          { required: true, message: "伦理结论不能为空", trigger: "change" }
        ],
        reviewTime: [
          { required: true, message: "审查时间不能为空", trigger: "change" }
        ]
      },
      // ä¿å­˜åŠ è½½çŠ¶æ€
      saveLoading: false,
      // é™„件数据
      attachments: [],
      expertReviews: [
        // æ™®é€šä¸“家(18位)- åˆå§‹çŠ¶æ€ä¸ºç”³è¯·ä¸­
        {
          id: 1,
          expertName: "张教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 2,
          expertName: "李教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 3,
          expertName: "王教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 4,
          expertName: "刘教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 5,
          expertName: "陈教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 6,
          expertName: "杨教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 7,
          expertName: "黄教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 8,
          expertName: "赵教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 9,
          expertName: "周教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 10,
          expertName: "吴教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 11,
          expertName: "徐教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 12,
          expertName: "孙教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 13,
          expertName: "朱教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 14,
          expertName: "马教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 15,
          expertName: "胡教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 16,
          expertName: "林教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 17,
          expertName: "郭教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        {
          id: 18,
          expertName: "何教授",
          isChief: false,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        },
        // ä¸»ä»»ä¸“家(1位)
        {
          id: 19,
          expertName: "主任专家",
          isChief: true,
          reviewStatus: "applying",
          expertConclusion: "",
          expertOpinion: "",
          reviewTime: ""
        }
      ],
      expertLoading: false,
      attachmentLoading: false,
      // å‘送对话框
      sendDialogVisible: false,
      sendForm: {
        expertType: "normal",
        expertIds: [],
        content: ""
      },
      // ä¸Šä¼ ç›¸å…³
      uploadDialogVisible: false,
      uploadLoading: false,
      tempFileList: [],
      // å¯ç”¨ä¸“家列表
      availableExperts: [
        { id: 1, name: "张教授", type: "normal" },
        { id: 2, name: "李教授", type: "normal" },
        { id: 3, name: "王教授", type: "normal" },
        { id: 4, name: "赵主任", type: "chief" }
      ]
    };
  },
  computed: {
    // è®¡ç®—属性:普通专家同意数量
    approvedNormalExperts() {
      return this.expertReviews.filter(
        expert => !expert.isChief && expert.expertConclusion === "approved"
      ).length;
    },
    // è®¡ç®—属性:主任专家状态
    chiefExpertStatus() {
      const chiefExpert = this.expertReviews.find(expert => expert.isChief);
      return chiefExpert
        ? this.statusTextFilter(chiefExpert.reviewStatus)
        : "未分配";
    },
    // è®¡ç®—属性:完成进度
    completionRate() {
      const totalExperts = this.expertReviews.length;
      const completedExperts = this.expertReviews.filter(
        expert => expert.reviewStatus === "submitted"
      ).length;
      return totalExperts > 0
        ? Math.round((completedExperts / totalExperts) * 100)
        : 0;
    },
    // è®¡ç®—属性:总体结论
    overallConclusionText() {
      if (this.approvedNormalExperts >= 12) {
        return "通过";
      } else if (this.approvedNormalExperts >= 9) {
        return "修改后通过";
      } else {
        return "不通过";
      }
    },
    overallConclusionFilter() {
      if (this.approvedNormalExperts >= 12) {
        return "success";
      } else if (this.approvedNormalExperts >= 9) {
        return "warning";
      } else {
        return "danger";
      }
    },
    // æ˜¯å¦å¯ä»¥å‘送给普通专家
    canSendToNormalExperts() {
      return (
        this.expertReviews.filter(
          expert => !expert.isChief && expert.reviewStatus === "applying"
        ).length > 0
      );
    },
    // æ˜¯å¦å¯ä»¥å‘送给主任专家(需要至少12个普通专家同意)
    canSendToChiefExpert() {
      return (
        this.approvedNormalExperts >= 12 &&
        this.expertReviews.filter(
          expert => expert.isChief && expert.reviewStatus === "applying"
        ).length > 0
      );
    },
    // æ˜¯å¦å¯ä»¥æ‰¹é‡å‘送
    canBatchSend() {
      return (
        this.expertReviews.filter(expert => expert.reviewStatus === "applying")
          .length > 0
      );
    },
    // æ˜¯å¦å¯ä»¥å‘送专家审查
    canSendToExperts() {
      return this.form.id && this.form.ethicsConclusion === "reviewing";
    },
    // å½“前用户信息
    currentUser() {
      return JSON.parse(sessionStorage.getItem("user") || "{}");
    }
  },
  created() {
    const id = this.$route.query.id;
    if (id) {
      this.getDetail(id);
      this.getAttachments(id);
      // ä¸å†éœ€è¦ä»ŽæŽ¥å£èŽ·å–ä¸“å®¶åˆ—è¡¨ï¼Œä½¿ç”¨å›ºå®šçš„expertReviews数据
    } else if (this.$route.path.includes("/add")) {
      this.generateHospitalNo();
      this.form.registrant = this.currentUser.username || "当前用户";
    }
  },
  methods: {
    // ç”Ÿæˆä½é™¢å·
    generateHospitalNo() {
      const timestamp = Date.now().toString();
      this.form.hospitalNo = "D" + timestamp.slice(-6);
    },
    getExpertRowClassName({ row }) {
      return row.isChief ? "chief-expert-row" : "normal-expert-row";
    },
    // èŽ·å–è¯¦æƒ…
    getDetail(id) {
      getEthicsReviewDetail(id)
        .then(response => {
          if (response.code === 200) {
            this.form = response.data;
          }
        })
        .catch(error => {
          console.error("获取伦理审查详情失败:", error);
          this.$message.error("获取详情失败");
        });
    },
    // èŽ·å–ä¸“å®¶å®¡æŸ¥åˆ—è¡¨
    getExpertReviews(ethicsReviewId) {
      this.expertLoading = true;
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…项目中从接口获取
      setTimeout(() => {
        this.expertReviews = [
          // æ™®é€šä¸“家(18位)
          {
            id: 1,
            expertName: "张教授",
            isChief: false,
            reviewStatus: "submitted",
            expertConclusion: "approved",
            expertOpinion: "符合伦理要求",
            reviewTime: "2023-12-01 10:30:00"
          },
          {
            id: 2,
            expertName: "李教授",
            isChief: false,
            reviewStatus: "submitted",
            expertConclusion: "approved",
            expertOpinion: "方案设计合理",
            reviewTime: "2023-12-01 11:20:00"
          },
          {
            id: 3,
            expertName: "王教授",
            isChief: false,
            reviewStatus: "applying",
            expertConclusion: "",
            expertOpinion: "",
            reviewTime: ""
          },
          // ä¸»ä»»ä¸“家(1位)
          {
            id: 19,
            expertName: "赵主任",
            isChief: true,
            reviewStatus: "applying",
            expertConclusion: "",
            expertOpinion: "",
            reviewTime: ""
          }
        ];
        this.expertLoading = false;
      }, 500);
    },
    // èŽ·å–é™„ä»¶åˆ—è¡¨
    getAttachments(ethicsReviewId) {
      this.attachmentLoading = true;
      getAttachments(ethicsReviewId)
        .then(response => {
          if (response.code === 200) {
            this.attachments = response.data;
          }
          this.attachmentLoading = false;
        })
        .catch(error => {
          console.error("获取附件列表失败:", error);
          this.attachmentLoading = false;
        });
    },
    // çŠ¶æ€è¿‡æ»¤å™¨
    statusFilter(status) {
      const statusMap = {
        applying: "info",
        submitted: "success"
      };
      return statusMap[status] || "info";
    },
    statusTextFilter(status) {
      const statusMap = {
        applying: "申请中",
        submitted: "已提交"
      };
      return statusMap[status] || "未知";
    },
    // ç»“论过滤器
    conclusionFilter(conclusion) {
      const conclusionMap = {
        approved: "success",
        approved_with_modifications: "warning",
        disapproved: "danger"
      };
      return conclusionMap[conclusion] || "info";
    },
    conclusionTextFilter(conclusion) {
      const conclusionMap = {
        approved: "同意",
        approved_with_modifications: "修改后同意",
        disapproved: "不同意"
      };
      return conclusionMap[conclusion] || "未知";
    },
    // ä¿å­˜ä¿¡æ¯
    handleSave() {
      this.$refs.form.validate(valid => {
        if (valid) {
          this.saveLoading = true;
          const apiMethod = this.form.id ? updateEthicsReview : addEthicsReview;
          apiMethod(this.form)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("保存成功");
                if (!this.form.id) {
                  this.form.id = response.data.id;
                  this.$router.replace({
                    query: { ...this.$route.query, id: this.form.id }
                  });
                }
              }
            })
            .catch(error => {
              console.error("保存失败:", error);
              this.$message.error("保存失败");
            })
            .finally(() => {
              this.saveLoading = false;
            });
        }
      });
    },
    // å‘送专家审查
    handleSendToExperts() {
      this.sendDialogVisible = true;
    },
    // å‘送给普通专家
    handleSendToNormalExperts() {
      const normalExperts = this.expertReviews.filter(
        expert => !expert.isChief && expert.reviewStatus === "applying"
      );
      this.sendForm.expertIds = normalExperts.map(expert => expert.id);
      this.sendForm.expertType = "normal";
      this.sendDialogVisible = true;
    },
    // å‘送给主任专家
    handleSendToChiefExpert() {
      const chiefExpert = this.expertReviews.find(
        expert => expert.isChief && expert.reviewStatus === "applying"
      );
      if (chiefExpert) {
        this.sendForm.expertIds = [chiefExpert.id];
        this.sendForm.expertType = "chief";
        this.sendDialogVisible = true;
      }
    },
    // æ‰¹é‡å‘送
    handleBatchSend() {
      const applyingExperts = this.expertReviews.filter(
        expert => expert.reviewStatus === "applying"
      );
      this.sendForm.expertIds = applyingExperts.map(expert => expert.id);
      this.sendForm.expertType = "batch";
      this.sendDialogVisible = true;
    },
    // å‘送给单个专家
    handleSendToExpert(expert) {
      this.sendForm.expertIds = [expert.id];
      this.sendForm.expertType = expert.isChief ? "chief" : "normal";
      this.sendDialogVisible = true;
    },
    // ç¡®è®¤å‘送
    handleSendConfirm() {
      if (this.sendForm.expertIds.length === 0) {
        this.$message.warning("请选择要发送的专家");
        return;
      }
      sendExpertReview({
        ethicsReviewId: this.form.id,
        expertIds: this.sendForm.expertIds,
        content: this.sendForm.content
      })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("发送成功");
            this.sendDialogVisible = false;
            this.getExpertReviews(this.form.id);
            this.sendForm = {
              expertType: "normal",
              expertIds: [],
              content: ""
            };
          }
        })
        .catch(error => {
          console.error("发送失败:", error);
          this.$message.error("发送失败");
        });
    },
    // ç»“束审查
    handleEndReview() {
      this.$confirm(
        "确定要结束本次伦理审查吗?结束后将无法修改专家审查结果。",
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning"
        }
      )
        .then(() => {
          endEthicsReview(this.form.id)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("审查已结束");
                this.form.ethicsConclusion = "terminated";
              }
            })
            .catch(error => {
              console.error("结束审查失败:", error);
              this.$message.error("结束审查失败");
            });
        })
        .catch(() => {});
    },
    // ç¼–辑专家审查
    handleEditExpertReview(expert) {
      this.$prompt("请输入审查意见", "编辑专家审查", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        inputValue: expert.expertOpinion || "",
        inputValidator: value => {
          if (!value || value.trim() === "") {
            return "审查意见不能为空";
          }
          return true;
        }
      })
        .then(({ value }) => {
          // æ¨¡æ‹Ÿæ›´æ–°ä¸“家审查
          const index = this.expertReviews.findIndex(e => e.id === expert.id);
          if (index !== -1) {
            this.expertReviews[index].expertOpinion = value;
            this.$message.success("审查意见已更新");
          }
        })
        .catch(() => {});
    },
    // æŸ¥çœ‹ä¸“家审查详情
    handleViewExpertReview(expert) {
      this.$alert(
        `
        <div>
          <p><strong>专家姓名:</strong>${expert.expertName}</p>
          <p><strong>专家类型:</strong>${
            expert.isChief ? "主任专家" : "普通专家"
          }</p>
          <p><strong>审查状态:</strong>${this.statusTextFilter(
            expert.reviewStatus
          )}</p>
          <p><strong>专家结论:</strong>${
            expert.expertConclusion
              ? this.conclusionTextFilter(expert.expertConclusion)
              : "未提交"
          }</p>
          <p><strong>审查意见:</strong>${expert.expertOpinion || "无"}</p>
          <p><strong>审查时间:</strong>${expert.reviewTime || "未审查"}</p>
        </div>
      `,
        "专家审查详情",
        {
          dangerouslyUseHTMLString: true,
          customClass: "expert-review-detail-dialog"
        }
      );
    },
    // ä¸Šä¼ é™„ä»¶
    handleUploadAttachment() {
      this.uploadDialogVisible = true;
    },
    // ä¸Šä¼ å‰æ ¡éªŒ
    beforeUpload(file) {
      const allowedTypes = [
        "application/pdf",
        "image/jpeg",
        "image/png",
        "application/msword",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
      ];
      const maxSize = 10 * 1024 * 1024;
      const isTypeOk =
        allowedTypes.includes(file.type) ||
        file.name.endsWith(".pdf") ||
        file.name.endsWith(".jpg") ||
        file.name.endsWith(".jpeg") ||
        file.name.endsWith(".png") ||
        file.name.endsWith(".doc") ||
        file.name.endsWith(".docx") ||
        file.name.endsWith(".xls") ||
        file.name.endsWith(".xlsx");
      if (!isTypeOk) {
        this.$message.error("文件格式不支持");
        return false;
      }
      if (file.size > maxSize) {
        this.$message.error("文件大小不能超过10MB");
        return false;
      }
      return true;
    },
    // æ–‡ä»¶é€‰æ‹©å˜åŒ–
    handleFileChange(file, fileList) {
      this.tempFileList = fileList;
    },
    // æäº¤ä¸Šä¼ 
    submitUpload() {
      if (this.tempFileList.length === 0) {
        this.$message.warning("请先选择要上传的文件");
        return;
      }
      this.uploadLoading = true;
      const uploadPromises = this.tempFileList.map(file => {
        const formData = new FormData();
        formData.append("file", file.raw);
        formData.append("ethicsReviewId", this.form.id);
        return uploadAttachment(formData);
      });
      Promise.all(uploadPromises)
        .then(responses => {
          this.$message.success("文件上传成功");
          this.uploadDialogVisible = false;
          this.tempFileList = [];
          this.getAttachments(this.form.id);
        })
        .catch(error => {
          console.error("上传失败:", error);
          this.$message.error("文件上传失败");
        })
        .finally(() => {
          this.uploadLoading = false;
        });
    },
    // é¢„览附件
    handlePreviewAttachment(attachment) {
      if (attachment.fileName.endsWith(".pdf")) {
        window.open(attachment.fileUrl, "_blank");
      } else if (attachment.fileName.match(/\.(jpg|jpeg|png)$/i)) {
        this.$alert(
          `<img src="${attachment.fileUrl}" style="max-width: 100%;" alt="${attachment.fileName}">`,
          "图片预览",
          {
            dangerouslyUseHTMLString: true,
            customClass: "image-preview-dialog"
          }
        );
      } else {
        this.$message.info("该文件类型暂不支持在线预览,请下载后查看");
      }
    },
    // ä¸‹è½½é™„ä»¶
    handleDownloadAttachment(attachment) {
      const link = document.createElement("a");
      link.href = attachment.fileUrl;
      link.download = attachment.fileName;
      link.click();
      this.$message.success(`开始下载: ${attachment.fileName}`);
    },
    // åˆ é™¤é™„ä»¶
    handleRemoveAttachment(attachment) {
      this.$confirm("确定要删除这个附件吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          deleteAttachment(attachment.id)
            .then(response => {
              if (response.code === 200) {
                this.$message.success("附件删除成功");
                this.getAttachments(this.form.id);
              }
            })
            .catch(error => {
              console.error("删除附件失败:", error);
              this.$message.error("删除附件失败");
            });
        })
        .catch(() => {});
    },
    // èŽ·å–æ–‡ä»¶ç±»åž‹
    getFileType(fileName) {
      const ext = fileName
        .split(".")
        .pop()
        .toLowerCase();
      const typeMap = {
        pdf: "PDF",
        doc: "DOC",
        docx: "DOCX",
        xls: "XLS",
        xlsx: "XLSX",
        jpg: "JPG",
        jpeg: "JPEG",
        png: "PNG"
      };
      return typeMap[ext] || ext.toUpperCase();
    },
    // æ–‡ä»¶å¤§å°æ ¼å¼åŒ–
    formatFileSize(size) {
      if (size === 0) return "0 B";
      const k = 1024;
      const sizes = ["B", "KB", "MB", "GB"];
      const i = Math.floor(Math.log(size) / Math.log(k));
      return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time) {
      if (!time) return "";
      const date = new Date(time);
      return `${date.getFullYear()}-${(date.getMonth() + 1)
        .toString()
        .padStart(2, "0")}-${date
        .getDate()
        .toString()
        .padStart(2, "0")} ${date
        .getHours()
        .toString()
        .padStart(2, "0")}:${date
        .getMinutes()
        .toString()
        .padStart(2, "0")}`;
    }
  }
};
</script>
<style scoped>
.ethics-review-detail {
  padding: 20px;
  background-color: #f5f7fa;
}
.detail-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.expert-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.attachment-card {
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.detail-title {
  font-size: 18px;
  font-weight: 600;
  color: #303133;
}
.expert-stats {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: rgb(43, 181, 245);
  border-radius: 8px;
  margin-bottom: 20px;
}
.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px;
}
.stat-label {
  font-size: 12px;
  opacity: 0.9;
  margin-bottom: 5px;
}
.stat-value {
  font-size: 18px;
  font-weight: bold;
}
.upload-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  padding: 10px;
  background-color: #f8f9fa;
  border-radius: 4px;
}
.upload-title {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
}
.file-info {
  display: flex;
  align-items: center;
}
.empty-attachment {
  text-align: center;
  padding: 40px 0;
  color: #909399;
}
/* è¡¨å•样式优化 */
:deep(.el-form-item__label) {
  font-weight: 500;
}
:deep(.el-input__inner) {
  border-radius: 4px;
}
:deep(.el-textarea__inner) {
  border-radius: 4px;
  resize: vertical;
}
/* è¡¨æ ¼æ ·å¼ä¼˜åŒ– */
:deep(.el-table) {
  border-radius: 8px;
  overflow: hidden;
}
:deep(.el-table th) {
  background-color: #f5f7fa;
  color: #606266;
  font-weight: 500;
}
:deep(.el-table .cell) {
  padding: 8px 12px;
}
/* æŒ‰é’®æ ·å¼ä¼˜åŒ– */
:deep(.el-button--primary) {
  background: linear-gradient(135deg, #409eff 0%, #3375e0 100%);
  border: none;
  border-radius: 4px;
}
:deep(.el-button--success) {
  background: linear-gradient(135deg, #67c23a 0%, #529b2f 100%);
  border: none;
  border-radius: 4px;
}
:deep(.el-button--warning) {
  background: linear-gradient(135deg, #e6a23c 0%, #d18c2a 100%);
  border: none;
  border-radius: 4px;
}
:deep(.el-button--danger) {
  background: linear-gradient(135deg, #f56c6c 0%, #e05b5b 100%);
  border: none;
  border-radius: 4px;
}
/* æ ‡ç­¾æ ·å¼ */
:deep(.el-tag) {
  border-radius: 12px;
  border: none;
  font-weight: 500;
}
/* å¯¹è¯æ¡†æ ·å¼ä¼˜åŒ– */
:deep(.el-dialog) {
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
:deep(.el-dialog__header) {
  background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
  border-bottom: 1px solid #e4e7ed;
  padding: 15px 20px;
}
:deep(.el-dialog__title) {
  font-weight: 600;
  color: #303133;
}
/* ä¸Šä¼ ç»„件样式 */
:deep(.el-upload-dragger) {
  border: 2px dashed #dcdfe6;
  border-radius: 6px;
  background-color: #fafafa;
  transition: all 0.3s ease;
}
:deep(.el-upload-dragger:hover) {
  border-color: #409eff;
  background-color: #f0f7ff;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .ethics-review-detail {
    padding: 10px;
  }
  .expert-stats .el-col {
    margin-bottom: 10px;
  }
  .upload-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
}
/* åŠ¨ç”»æ•ˆæžœ */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
/* åŠ è½½çŠ¶æ€ */
.loading-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
}
/* ä¸“家类型样式 */
.normal-expert {
  color: #409eff;
  font-weight: 500;
}
.chief-expert {
  color: #f56c6c;
  font-weight: 600;
}
/* ä¸“家行样式 */
:deep(.normal-expert-row) {
  background-color: #fafafa;
}
:deep(.chief-expert-row) {
  background-color: #fff7e6;
}
:deep(.normal-expert-row:hover) {
  background-color: #f0f7ff;
}
:deep(.chief-expert-row:hover) {
  background-color: #ffecc2;
}
/* æ— æ•°æ®æ ·å¼ */
.no-data {
  color: #909399;
  font-style: italic;
}
/* ä¸“家意见样式 */
.expert-opinion {
  color: #303133;
  line-height: 1.5;
}
/* å·²å‘送按钮样式 */
.sent-button {
  color: #67c23a !important;
}
/* è¡¨æ ¼è¡Œæ‚¬åœæ•ˆæžœ */
:deep(.el-table__row:hover) {
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
}
/* è‡ªå®šä¹‰æ»šåŠ¨æ¡ */
:deep(::-webkit-scrollbar) {
  width: 6px;
  height: 6px;
}
:deep(::-webkit-scrollbar-track) {
  background: #f1f1f1;
  border-radius: 3px;
}
:deep(::-webkit-scrollbar-thumb) {
  background: #c1c1c1;
  border-radius: 3px;
}
:deep(::-webkit-scrollbar-thumb:hover) {
  background: #a8a8a8;
}
/* ä¸“家审查表格特殊样式 */
.expert-table-special :deep(.el-table__row) {
  transition: all 0.3s ease;
}
.expert-table-special :deep(.el-table__row:hover) {
  background-color: #f0f7ff;
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* ä¸»ä»»ä¸“家行高亮 */
:deep(.chief-expert-row) {
  background-color: #fff7e6 !important;
}
:deep(.chief-expert-row:hover) {
  background-color: #ffecc2 !important;
}
</style>
src/views/business/ethicalReview/ethicsReview.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,392 @@
// æ¨¡æ‹Ÿä¼¦ç†å®¡æŸ¥æ•°æ®
const mockEthicsReviewData = [
  {
    id: 1,
    hospitalNo: "D202312001",
    donorName: "张三",
    gender: "0",
    age: 45,
    diagnosis: "脑外伤",
    ethicsConclusion: "reviewing",
    ethicsOpinion: "该病例符合器官捐献伦理审查基本要求,建议提交专家委员会审议",
    reviewTime: "2023-12-01 14:30:00",
    judgmentDoctor: "王医生",
    judgmentDescription: "经过多次脑死亡判定,符合脑死亡临床诊断标准",
    registrant: "李协调员",
    registrationTime: "2023-12-01 15:00:00",
    createTime: "2023-12-01 10:00:00"
  },
  {
    id: 2,
    hospitalNo: "D202312002",
    donorName: "李四",
    gender: "1",
    age: 38,
    diagnosis: "心脏骤停",
    ethicsConclusion: "approved",
    ethicsOpinion: "家属意愿明确,医疗程序合规,同意进行器官捐献",
    reviewTime: "2023-12-02 09:15:00",
    judgmentDoctor: "刘医生",
    judgmentDescription: "心死亡判定,心电图呈直线,无自主呼吸",
    registrant: "张协调员",
    registrationTime: "2023-12-02 10:00:00",
    createTime: "2023-12-02 08:30:00"
  }
];
// æ¨¡æ‹Ÿä¸“家审查数据
const mockExpertReviewData = [
  {
    id: 1,
    ethicsReviewId: 1,
    expertName: "张教授",
    isChief: false,
    reviewStatus: "submitted",
    expertConclusion: "approved",
    expertOpinion: "病例资料完整,符合伦理审查要求",
    reviewTime: "2023-12-01 16:30:00"
  },
  {
    id: 2,
    ethicsReviewId: 1,
    expertName: "李教授",
    isChief: false,
    reviewStatus: "submitted",
    expertConclusion: "approved",
    expertOpinion: "捐献流程规范,同意审查",
    reviewTime: "2023-12-01 17:20:00"
  },
  {
    id: 19,
    ethicsReviewId: 1,
    expertName: "赵主任",
    isChief: true,
    reviewStatus: "applying",
    expertConclusion: "",
    expertOpinion: "",
    reviewTime: ""
  }
];
// æ¨¡æ‹Ÿé™„件数据
const mockAttachmentData = [
  {
    id: 1,
    ethicsReviewId: 1,
    fileName: "伦理审查申请表.pdf",
    fileSize: 2048576,
    uploadTime: "2023-12-01 09:30:00",
    uploader: "张医生",
    fileUrl: "/attachments/ethics_application_1.pdf"
  },
  {
    id: 2,
    ethicsReviewId: 1,
    fileName: "专家评审意见汇总.docx",
    fileSize: 512345,
    uploadTime: "2023-12-01 14:20:00",
    uploader: "李医生",
    fileUrl: "/attachments/expert_review_1.docx"
  }
];
// æ¨¡æ‹ŸAPI响应延迟
const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms));
// æŸ¥è¯¢ä¼¦ç†å®¡æŸ¥åˆ—表
export const listEthicsReview = async (queryParams = {}) => {
  await delay();
  const {
    pageNum = 1,
    pageSize = 10,
    hospitalNo,
    donorName,
    ethicsConclusion,
    reviewTimeRange = []
  } = queryParams;
  // è¿‡æ»¤æ•°æ®
  let filteredData = mockEthicsReviewData.filter(item => {
    let match = true;
    if (hospitalNo && !item.hospitalNo.includes(hospitalNo)) {
      match = false;
    }
    if (donorName && !item.donorName.includes(donorName)) {
      match = false;
    }
    if (ethicsConclusion && item.ethicsConclusion !== ethicsConclusion) {
      match = false;
    }
    if (reviewTimeRange.length === 2) {
      const reviewTime = new Date(item.reviewTime);
      const startTime = new Date(reviewTimeRange[0]);
      const endTime = new Date(reviewTimeRange[1]);
      endTime.setDate(endTime.getDate() + 1);
      if (reviewTime < startTime || reviewTime >= endTime) {
        match = false;
      }
    }
    return match;
  });
  // åˆ†é¡µ
  const startIndex = (pageNum - 1) * pageSize;
  const endIndex = startIndex + parseInt(pageSize);
  const paginatedData = filteredData.slice(startIndex, endIndex);
  return {
    code: 200,
    message: "success",
    data: {
      rows: paginatedData,
      total: filteredData.length,
      pageNum: parseInt(pageNum),
      pageSize: parseInt(pageSize)
    }
  };
};
// èŽ·å–ä¼¦ç†å®¡æŸ¥è¯¦ç»†ä¿¡æ¯
export const getEthicsReviewDetail = async (id) => {
  await delay();
  const detail = mockEthicsReviewData.find(item => item.id == id);
  if (detail) {
    return {
      code: 200,
      message: "success",
      data: detail
    };
  } else {
    return {
      code: 404,
      message: "伦理审查记录不存在"
    };
  }
};
// æ–°å¢žä¼¦ç†å®¡æŸ¥
export const addEthicsReview = async (data) => {
  await delay();
  const newId = Math.max(...mockEthicsReviewData.map(item => item.id), 0) + 1;
  const hospitalNo = `D${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(newId).padStart(3, '0')}`;
  const newRecord = {
    ...data,
    id: newId,
    hospitalNo,
    registrationTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
    createTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
  };
  mockEthicsReviewData.unshift(newRecord);
  return {
    code: 200,
    message: "新增成功",
    data: newRecord
  };
};
// ä¿®æ”¹ä¼¦ç†å®¡æŸ¥
export const updateEthicsReview = async (data) => {
  await delay();
  const index = mockEthicsReviewData.findIndex(item => item.id == data.id);
  if (index !== -1) {
    mockEthicsReviewData[index] = {
      ...mockEthicsReviewData[index],
      ...data,
      updateTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
    };
    return {
      code: 200,
      message: "修改成功",
      data: mockEthicsReviewData[index]
    };
  } else {
    return {
      code: 404,
      message: "伦理审查记录不存在"
    };
  }
};
// åˆ é™¤ä¼¦ç†å®¡æŸ¥
export const delEthicsReview = async (ids) => {
  await delay();
  const idArray = Array.isArray(ids) ? ids : [ids];
  idArray.forEach(id => {
    const index = mockEthicsReviewData.findIndex(item => item.id == id);
    if (index !== -1) {
      mockEthicsReviewData.splice(index, 1);
    }
  });
  return {
    code: 200,
    message: "删除成功"
  };
};
// ç»“束伦理审查
export const endEthicsReview = async (id) => {
  await delay();
  const index = mockEthicsReviewData.findIndex(item => item.id == id);
  if (index !== -1) {
    mockEthicsReviewData[index].ethicsConclusion = 'terminated';
    mockEthicsReviewData[index].reviewTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
    return {
      code: 200,
      message: "审查已结束",
      data: mockEthicsReviewData[index]
    };
  } else {
    return {
      code: 404,
      message: "伦理审查记录不存在"
    };
  }
};
// å¯¼å‡ºä¼¦ç†å®¡æŸ¥
export const exportEthicsReview = async (queryParams) => {
  await delay(1000);
  const { data } = await listEthicsReview(queryParams);
  return {
    code: 200,
    message: "导出成功",
    data: {
      fileName: `伦理审查数据_${new Date().getTime()}.xlsx`,
      downloadUrl: "/api/export/ethicsReview"
    }
  };
};
// èŽ·å–ä¸“å®¶å®¡æŸ¥åˆ—è¡¨
export const getExpertReviews = async (ethicsReviewId) => {
  await delay();
  const reviews = mockExpertReviewData.filter(item => item.ethicsReviewId == ethicsReviewId);
  return {
    code: 200,
    message: "success",
    data: reviews
  };
};
// å‘送专家审查
export const sendExpertReview = async (data) => {
  await delay(1500);
  const { ethicsReviewId, expertIds, content } = data;
  // æ¨¡æ‹Ÿå‘送短信给专家
  expertIds.forEach(expertId => {
    const expert = mockExpertReviewData.find(item => item.id === expertId);
    if (expert) {
      expert.reviewStatus = 'submitted';
      expert.reviewTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
    }
  });
  return {
    code: 200,
    message: "专家审查邀请发送成功",
    data: {
      sentCount: expertIds.length,
      content: content
    }
  };
};
// èŽ·å–é™„ä»¶åˆ—è¡¨
export const getAttachments = async (ethicsReviewId) => {
  await delay();
  const attachments = mockAttachmentData.filter(item => item.ethicsReviewId == ethicsReviewId);
  return {
    code: 200,
    message: "success",
    data: attachments
  };
};
// ä¸Šä¼ é™„ä»¶
export const uploadAttachment = async (formData) => {
  await delay(2000);
  const newAttachment = {
    id: Math.max(...mockAttachmentData.map(item => item.id), 0) + 1,
    ethicsReviewId: parseInt(formData.get('ethicsReviewId')),
    fileName: `附件_${new Date().getTime()}.pdf`,
    fileSize: 1024000,
    uploadTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
    uploader: "当前用户",
    fileUrl: `/attachments/ethics_${formData.get('ethicsReviewId')}_${new Date().getTime()}.pdf`
  };
  mockAttachmentData.push(newAttachment);
  return {
    code: 200,
    message: "上传成功",
    data: newAttachment
  };
};
// åˆ é™¤é™„ä»¶
export const deleteAttachment = async (attachmentId) => {
  await delay();
  const index = mockAttachmentData.findIndex(item => item.id == attachmentId);
  if (index !== -1) {
    mockAttachmentData.splice(index, 1);
    return {
      code: 200,
      message: "删除成功"
    };
  } else {
    return {
      code: 404,
      message: "附件不存在"
    };
  }
};
export default {
  listEthicsReview,
  getEthicsReviewDetail,
  addEthicsReview,
  updateEthicsReview,
  delEthicsReview,
  endEthicsReview,
  exportEthicsReview,
  getExpertReviews,
  sendExpertReview,
  getAttachments,
  uploadAttachment,
  deleteAttachment
};
src/views/business/ethicalReview/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,480 @@
<template>
  <div class="ethics-review-list">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-card class="search-card">
      <el-form
        :model="queryParams"
        ref="queryForm"
        :inline="true"
        label-width="100px"
      >
        <el-form-item label="住院号" prop="hospitalNo">
          <el-input
            v-model="queryParams.hospitalNo"
            placeholder="请输入住院号"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="捐献者姓名" prop="donorName">
          <el-input
            v-model="queryParams.donorName"
            placeholder="请输入捐献者姓名"
            clearable
            style="width: 200px"
            @keyup.enter.native="handleQuery"
          />
        </el-form-item>
        <el-form-item label="伦理结论" prop="ethicsConclusion">
          <el-select
            v-model="queryParams.ethicsConclusion"
            placeholder="请选择伦理结论"
            clearable
            style="width: 200px"
          >
            <el-option label="审查中" value="reviewing" />
            <el-option label="同意" value="approved" />
            <el-option label="修改后同意" value="approved_with_modifications" />
            <el-option label="修改后重审" value="re-review" />
            <el-option label="不同意" value="disapproved" />
            <el-option label="终止审查" value="terminated" />
          </el-select>
        </el-form-item>
        <el-form-item label="审查时间范围" prop="reviewTimeRange">
          <el-date-picker
            v-model="queryParams.reviewTimeRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
            style="width: 240px"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="handleQuery"
            >搜索</el-button
          >
          <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- æ“ä½œæŒ‰é’® -->
    <el-card class="tool-card">
      <el-row :gutter="10">
        <el-col :span="16">
          <el-button type="primary" icon="el-icon-plus" @click="handleCreate"
            >新建审查</el-button
          >
          <el-button
            type="success"
            icon="el-icon-edit"
            :disabled="single"
            @click="handleUpdate"
            >修改</el-button
          >
          <el-button
            type="danger"
            icon="el-icon-delete"
            :disabled="multiple"
            @click="handleDelete"
            >删除</el-button
          >
          <el-button
            type="warning"
            icon="el-icon-download"
            @click="handleExport"
            >导出</el-button
          >
          <el-button
            type="info"
            icon="el-icon-check"
            :disabled="multiple"
            @click="handleEndReview"
            >结束审查</el-button
          >
        </el-col>
        <el-col :span="8" style="text-align: right">
          <el-tooltip content="刷新" placement="top">
            <el-button icon="el-icon-refresh" circle @click="getList" />
          </el-tooltip>
        </el-col>
      </el-row>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <el-table
        v-loading="loading"
        :data="ethicsReviewList"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column
          label="住院号"
          align="center"
          prop="hospitalNo"
          width="120"
        />
        <el-table-column
          label="捐献者姓名"
          align="center"
          prop="donorName"
          width="120"
        />
        <el-table-column label="性别" align="center" prop="gender" width="80">
          <template slot-scope="scope">
            <dict-tag
              :options="dict.type.sys_user_sex"
              :value="parseInt(scope.row.gender)"
            />
          </template>
        </el-table-column>
        <el-table-column label="年龄" align="center" prop="age" width="80" />
        <el-table-column
          label="疾病诊断"
          align="center"
          prop="diagnosis"
          min-width="180"
          show-overflow-tooltip
        />
        <el-table-column
          label="伦理结论"
          align="center"
          prop="ethicsConclusion"
          width="120"
        >
          <template slot-scope="scope">
            <el-tag :type="conclusionFilter(scope.row.ethicsConclusion)">
              {{ conclusionTextFilter(scope.row.ethicsConclusion) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="伦理意见"
          align="center"
          prop="ethicsOpinion"
          min-width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="审查时间"
          align="center"
          prop="reviewTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.reviewTime
                ? parseTime(scope.row.reviewTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="登记时间"
          align="center"
          prop="registrationTime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{
              scope.row.registrationTime
                ? parseTime(scope.row.registrationTime, "{y}-{m}-{d} {h}:{i}")
                : "-"
            }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="登记人"
          align="center"
          prop="registrant"
          width="100"
        />
        <el-table-column
          label="操作"
          align="center"
          width="200"
          class-name="small-padding fixed-width"
        >
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleView(scope.row)"
              >详情</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-edit"
              @click="handleUpdate(scope.row)"
              >修改</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-check"
              @click="handleEndReview(scope.row)"
              :disabled="scope.row.ethicsConclusion === 'terminated'"
              >结束</el-button
            >
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              style="color: #F56C6C"
              @click="handleDelete(scope.row)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="queryParams.pageNum"
        :limit.sync="queryParams.pageSize"
        @pagination="getList"
      />
    </el-card>
  </div>
</template>
<script>
import { listEthicsReview, delEthicsReview, exportEthicsReview, endEthicsReview } from "./ethicsReview";
import Pagination from "@/components/Pagination";
export default {
  name: "EthicsReviewList",
  components: { Pagination },
  dicts: ["sys_user_sex"],
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ€»æ¡æ•°
      total: 0,
      // ä¼¦ç†å®¡æŸ¥è¡¨æ ¼æ•°æ®
      ethicsReviewList: [],
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        hospitalNo: undefined,
        donorName: undefined,
        ethicsConclusion: undefined,
        reviewTimeRange: []
      }
    };
  },
  created() {
    this.getList();
  },
  methods: {
    // ä¼¦ç†ç»“论过滤器
    conclusionFilter(conclusion) {
      const conclusionMap = {
        "reviewing": "warning", // å®¡æŸ¥ä¸­
        "approved": "success", // åŒæ„
        "approved_with_modifications": "primary", // ä¿®æ”¹åŽåŒæ„
        "re-review": "info", // ä¿®æ”¹åŽé‡å®¡
        "disapproved": "danger", // ä¸åŒæ„
        "terminated": "info" // ç»ˆæ­¢å®¡æŸ¥
      };
      return conclusionMap[conclusion] || "info";
    },
    conclusionTextFilter(conclusion) {
      const conclusionMap = {
        "reviewing": "审查中",
        "approved": "同意",
        "approved_with_modifications": "修改后同意",
        "re-review": "修改后重审",
        "disapproved": "不同意",
        "terminated": "终止审查"
      };
      return conclusionMap[conclusion] || "未知";
    },
    // æŸ¥è¯¢ä¼¦ç†å®¡æŸ¥åˆ—表
    getList() {
      this.loading = true;
      listEthicsReview(this.queryParams)
        .then(response => {
          if (response.code === 200) {
            this.ethicsReviewList = response.data.rows;
            this.total = response.data.total;
          } else {
            this.$message.error("获取数据失败");
          }
          this.loading = false;
        })
        .catch(error => {
          console.error("获取伦理审查列表失败:", error);
          this.loading = false;
          this.$message.error("获取数据失败");
        });
    },
    // æœç´¢æŒ‰é’®æ“ä½œ
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŒ‰é’®æ“ä½œ
    resetQuery() {
      this.$refs.queryForm.resetFields();
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleView(row) {
      this.$router.push({
        path: "/case/ethicalReviewInfo",
        query: { id: row.id }
      });
    },
    // æ–°å¢žæŒ‰é’®æ“ä½œ
    handleCreate() {
      this.$router.push("/case/ethicalReviewInfo");
    },
    // ä¿®æ”¹æŒ‰é’®æ“ä½œ
    handleUpdate(row) {
      const id = row.id || this.ids[0];
      this.$router.push({
        path: "/case/ethicalReviewInfo",
        query: { id: id }
      });
    },
    // åˆ é™¤æŒ‰é’®æ“ä½œ
    handleDelete(row) {
      const ids = row.id ? [row.id] : this.ids;
      this.$confirm("是否确认删除选中的数据项?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          return delEthicsReview(ids);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("删除成功");
            this.getList();
          }
        })
        .catch(() => {});
    },
    // ç»“束审查操作
    handleEndReview(row) {
      const ids = row.id ? [row.id] : this.ids;
      this.$confirm("是否确认结束选中的审查项目?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          return endEthicsReview(ids);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("审查已结束");
            this.getList();
          }
        })
        .catch(() => {});
    },
    // å¯¼å‡ºæŒ‰é’®æ“ä½œ
    handleExport() {
      const queryParams = this.queryParams;
      this.$confirm("是否确认导出所有伦理审查数据?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.loading = true;
          return exportEthicsReview(queryParams);
        })
        .then(response => {
          if (response.code === 200) {
            this.$message.success("导出成功");
          }
          this.loading = false;
        })
        .catch(() => {
          this.loading = false;
        });
    },
    // æ—¶é—´æ ¼å¼åŒ–
    parseTime(time, pattern) {
      if (!time) return "";
      const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
      let date;
      if (typeof time === "object") {
        date = time;
      } else {
        if (typeof time === "string" && /^[0-9]+$/.test(time)) {
          time = parseInt(time);
        }
        if (typeof time === "number" && time.toString().length === 10) {
          time = time * 1000;
        }
        date = new Date(time);
      }
      const formatObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
      };
      const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
        let value = formatObj[key];
        if (key === "a") {
          return ["日", "一", "二", "三", "四", "五", "六"][value];
        }
        if (result.length > 0 && value < 10) {
          value = "0" + value;
        }
        return value || 0;
      });
      return time_str;
    }
  }
};
</script>
<style scoped>
.ethics-review-list {
  padding: 20px;
}
.search-card {
  margin-bottom: 20px;
}
.tool-card {
  margin-bottom: 20px;
}
.fixed-width .el-button {
  margin: 0 5px;
}
</style>
src/views/business/maintain/components/BloodRoutinePanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,693 @@
<template>
  <div class="medical-panel">
    <div class="panel-header">
      <h3>血常规记录表</h3>
      <div class="panel-actions">
        <el-button
          v-if="isEditing"
          type="primary"
          size="small"
          @click="addColumn"
          icon="el-icon-plus"
        >
          æ–°å¢žè®°å½•列
        </el-button>
        <el-tooltip content="刷新表格布局" placement="top">
          <el-button
            size="small"
            @click="forceTableLayout"
            icon="el-icon-refresh"
          >
            åˆ·æ–°å¸ƒå±€
          </el-button>
        </el-tooltip>
      </div>
    </div>
    <el-table
      :data="tableData"
      border
      style="width: 100%"
      class="medical-table"
      v-fit-columns
      :key="tableKey"
      @header-dragend="handleHeaderDragEnd"
      v-loading="tableLoading"
    >
      <el-table-column
        prop="itemName"
        label="检测项目"
        width="200"
        fixed
        class-name="leave-alone"
      >
        <template #default="scope">
          <div class="item-name-cell">
            <span :class="{'required-item': scope.row.required}">
              {{ scope.row.itemName }}
            </span>
            <el-tooltip
              v-if="scope.row.reference"
              :content="`参考范围: ${scope.row.reference}`"
              placement="top"
            >
              <i class="el-icon-info reference-icon"></i>
            </el-tooltip>
          </div>
        </template>
      </el-table-column>
      <el-table-column
        v-for="(col, index) in dynamicColumns"
        :key="col.key"
        :label="col.label"
        :min-width="140"
        :resizable="isEditing"
        header-align="center"
        align="center"
      >
        <template #default="scope">
          <div class="cell-content-wrapper">
            <el-input
              v-if="isEditing"
              v-model="scope.row.values[index]"
              size="small"
              :placeholder="getPlaceholder(scope.row)"
              @blur="handleValueChange(scope.row, index)"
              class="value-input"
              :title="scope.row.values[index]"
            />
            <div v-else class="value-display-container">
              <span class="value-text" :title="scope.row.values[index]">
                {{ scope.row.values[index] || '-' }}
              </span>
              <span v-if="scope.row.values[index] && scope.row.unit" class="unit-text">
                {{ scope.row.unit }}
              </span>
            </div>
            <div v-if="scope.row.reference && scope.row.values[index]" class="validation-indicator">
              <i
                v-if="isValueValid(scope.row, scope.row.values[index])"
                class="el-icon-success valid-icon"
                title="数值在正常范围内"
              ></i>
              <i
                v-else
                class="el-icon-warning invalid-icon"
                title="数值超出正常范围"
              ></i>
            </div>
          </div>
        </template>
      </el-table-column>
      <!-- <el-table-column
        v-if="isEditing"
        label="操作"
        width="120"
        fixed="right"
        class-name="leave-alone"
      >
        <template #default>
          <el-button link type="primary" @click="addColumn" size="small">
            æ–°å¢žåˆ—
          </el-button>
        </template>
      </el-table-column> -->
    </el-table>
    <!-- ç»Ÿè®¡ä¿¡æ¯ -->
    <div v-if="showStatistics" class="statistics-section">
      <el-card shadow="never">
        <div class="stats-content">
          <span class="stats-title">数据统计:</span>
          <span class="stats-item">总记录数: {{ dynamicColumns.length }} ä¸ªæ—¶é—´ç‚¹</span>
          <span class="stats-item">已填写: {{ filledCount }} é¡¹</span>
          <span class="stats-item">完成度: {{ completionRate }}%</span>
        </div>
      </el-card>
    </div>
    <!-- é™„件上传区域 -->
    <div class="attachment-section">
      <div class="attachment-header">
        <i class="el-icon-paperclip"></i>
        <span class="attachment-title">附件上传</span>
        <span class="attachment-tip">支持上传检验报告单等文件 (最多10个)</span>
      </div>
      <upload-attachment
        :file-list="attachments"
        @change="handleAttachmentChange"
        :limit="10"
        :accept="'.pdf,.jpg,.jpeg,.png,.doc,.docx'"
      />
    </div>
    <!-- åˆ—编辑对话框 -->
    <el-dialog
      :title="`${editingColumnIndex !== null ? '编辑' : '新增'}时间点`"
      :visible.sync="columnDialogVisible"
      width="450px"
      @closed="handleDialogClosed"
    >
      <el-form
        :model="columnForm"
        label-width="80px"
        ref="columnForm"
        :rules="columnRules"
      >
        <el-form-item label="日期" prop="date">
          <el-date-picker
            v-model="columnForm.date"
            type="date"
            placeholder="选择日期"
            value-format="yyyy-MM-dd"
            style="width: 100%"
            :disabled-date="disableFutureDates"
          />
        </el-form-item>
        <el-form-item label="时间" prop="time">
          <el-time-picker
            v-model="columnForm.time"
            placeholder="选择时间"
            value-format="HH:mm"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input
            v-model="columnForm.remark"
            type="textarea"
            :rows="2"
            placeholder="可选填写时间点备注信息"
            maxlength="100"
            show-word-limit
          />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="columnDialogVisible = false">取消</el-button>
        <el-button
          v-if="editingColumnIndex !== null"
          type="danger"
          @click="handleDeleteColumn"
        >
          åˆ é™¤
        </el-button>
        <el-button type="primary" @click="confirmAddColumn" :loading="saveLoading">
          ç¡®å®š
        </el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
import UploadAttachment from "@/components/UploadAttachment";
export default {
  name: 'BloodRoutinePanel',
  components: {
    UploadAttachment,
  },
  props: {
    isEditing: {
      type: Boolean,
      default: false
    },
    initialData: {
      type: Array,
      default: () => []
    },
    showStatistics: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      tableData: [],
      dynamicColumns: [
        {
          label: '2024-12-27\n08:00',
          key: 'time1',
          date: '2024-12-27',
          time: '08:00',
          remark: '晨起检测'
        }
      ],
      attachments: [],
      columnDialogVisible: false,
      columnForm: {
        date: '',
        time: '',
        remark: ''
      },
      editingColumnIndex: null,
      tableKey: 0,
      tableLoading: false,
      saveLoading: false,
      columnRules: {
        date: [
          { required: true, message: '请选择日期', trigger: 'change' }
        ],
        time: [
          { required: true, message: '请选择时间', trigger: 'change' }
        ]
      }
    };
  },
  computed: {
    filledCount() {
      let count = 0;
      this.tableData.forEach(row => {
        row.values.forEach(value => {
          if (value && value.toString().trim() !== '') {
            count++;
          }
        });
      });
      return count;
    },
    completionRate() {
      const total = this.tableData.length * this.dynamicColumns.length;
      return total > 0 ? Math.round((this.filledCount / total) * 100) : 0;
    }
  },
  watch: {
    isEditing(newVal) {
      if (!newVal) {
        this.$emit('data-change', {
          type: 'blood_routine',
          data: this.tableData,
          columns: this.dynamicColumns,
          attachments: this.attachments
        });
      }
      this.$nextTick(() => {
        this.forceTableLayout();
      });
    },
    dynamicColumns: {
      handler() {
        this.$nextTick(() => {
          this.forceTableLayout();
        });
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    initTableData() {
      const medicalItems = [
        {
          itemName: 'WBC',
          unit: '×10⁹/L',
          required: true,
          reference: '3.5-9.5',
          min: 3.5,
          max: 9.5,
          type: 'number'
        },
        {
          itemName: 'NEUT%',
          unit: '%',
          required: true,
          reference: '40-75',
          min: 40,
          max: 75,
          type: 'number'
        },
        {
          itemName: 'Hb',
          unit: 'g/L',
          required: true,
          reference: '130-175',
          min: 130,
          max: 175,
          type: 'number'
        },
        {
          itemName: '血小板',
          unit: '×10⁹/L',
          required: true,
          reference: '125-350',
          min: 125,
          max: 350,
          type: 'number'
        }
      ];
      this.tableData = medicalItems.map(item => ({
        ...item,
        values: new Array(this.dynamicColumns.length).fill('')
      }));
    },
    getPlaceholder(row) {
      return row.reference ? `参考: ${row.reference}` : '请输入数值';
    },
    isValueValid(row, value) {
      if (!value || !row.min || !row.max) return true;
      const numValue = parseFloat(value);
      return !isNaN(numValue) && numValue >= row.min && numValue <= row.max;
    },
    addColumn() {
      this.editingColumnIndex = null;
      this.columnForm = {
        date: new Date().toISOString().split('T')[0],
        time: '08:00',
        remark: ''
      };
      this.columnDialogVisible = true;
      this.$nextTick(() => {
        this.$refs.columnForm && this.$refs.columnForm.clearValidate();
      });
    },
    editColumn(index) {
      this.editingColumnIndex = index;
      const column = this.dynamicColumns[index];
      this.columnForm = {
        date: column.date,
        time: column.time,
        remark: column.remark || ''
      };
      this.columnDialogVisible = true;
    },
    confirmAddColumn() {
      this.$refs.columnForm.validate((valid) => {
        if (!valid) {
          this.$message.warning('请完善时间点信息');
          return;
        }
        this.saveLoading = true;
        if (this.editingColumnIndex !== null) {
          // ç¼–辑现有列
          const column = this.dynamicColumns[this.editingColumnIndex];
          column.label = `${this.columnForm.date}\n${this.columnForm.time}`;
          column.date = this.columnForm.date;
          column.time = this.columnForm.time;
          column.remark = this.columnForm.remark;
          this.$message.success('时间点修改成功');
        } else {
          // æ–°å¢žåˆ—
          const newIndex = this.dynamicColumns.length + 1;
          const newColumn = {
            label: `${this.columnForm.date}\n${this.columnForm.time}`,
            key: `time${Date.now()}`,
            date: this.columnForm.date,
            time: this.columnForm.time,
            remark: this.columnForm.remark
          };
          this.dynamicColumns.push(newColumn);
          this.tableData.forEach(row => {
            row.values.push('');
          });
          this.$message.success('时间点添加成功');
        }
        this.columnDialogVisible = false;
        this.saveLoading = false;
        this.tableKey += 1;
      });
    },
    handleDeleteColumn() {
      if (this.editingColumnIndex !== null) {
        this.$confirm('确定要删除这个时间点吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.dynamicColumns.splice(this.editingColumnIndex, 1);
          this.tableData.forEach(row => {
            row.values.splice(this.editingColumnIndex, 1);
          });
          this.columnDialogVisible = false;
          this.tableKey += 1;
          this.$message.success('时间点删除成功');
        });
      }
    },
    handleDialogClosed() {
      this.editingColumnIndex = null;
      this.columnForm = {
        date: '',
        time: '',
        remark: ''
      };
      this.$refs.columnForm && this.$refs.columnForm.clearValidate();
    },
    disableFutureDates(time) {
      return time.getTime() > Date.now();
    },
    handleValueChange(row, columnIndex) {
      this.$emit('data-change', {
        type: 'blood_routine',
        data: this.tableData,
        columns: this.dynamicColumns
      });
    },
    handleAttachmentChange(fileList) {
      this.attachments = fileList;
      this.$emit('attachment-change', {
        type: 'blood_routine',
        attachments: fileList
      });
    },
    forceTableLayout() {
      this.$nextTick(() => {
        const table = this.$el.querySelector('.el-table');
        if (table) {
          window.dispatchEvent(new Event('resize'));
        }
      });
    },
    handleHeaderDragEnd() {
      this.forceTableLayout();
    },
    exportData() {
      return {
        tableData: this.tableData,
        columns: this.dynamicColumns,
        statistics: {
          filledCount: this.filledCount,
          completionRate: this.completionRate,
          totalColumns: this.dynamicColumns.length
        },
        exportTime: new Date().toISOString(),
        attachments: this.attachments
      };
    }
  },
  mounted() {
    this.initTableData();
  }
};
</script>
<style scoped>
.medical-panel {
  padding: 20px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid #ebeef5;
}
.panel-header h3 {
  margin: 0;
  color: #303133;
  font-size: 20px;
  font-weight: 600;
}
.panel-actions {
  display: flex;
  gap: 12px;
  align-items: center;
}
.medical-table {
  margin-bottom: 24px;
  border-radius: 4px;
  overflow: hidden;
}
.medical-table ::v-deep .el-table__body-wrapper {
  overflow-x: auto;
}
.item-name-cell {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.reference-icon {
  color: #909399;
  font-size: 14px;
  cursor: help;
  margin-left: 8px;
}
.required-item::before {
  content: "*";
  color: #f56c6c;
  margin-right: 4px;
}
.cell-content-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 32px;
}
.value-input {
  flex: 1;
  min-width: 80px;
}
.value-display-container {
  display: flex;
  align-items: center;
  gap: 4px;
}
.value-text {
  font-weight: 500;
  color: #303133;
  max-width: 80px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.unit-text {
  color: #909399;
  font-size: 12px;
  flex-shrink: 0;
}
.validation-indicator {
  flex-shrink: 0;
}
.valid-icon {
  color: #67c23a;
  font-size: 14px;
}
.invalid-icon {
  color: #e6a23c;
  font-size: 14px;
}
.statistics-section {
  margin-bottom: 20px;
}
.stats-content {
  display: flex;
  gap: 20px;
  align-items: center;
  flex-wrap: wrap;
}
.stats-title {
  font-weight: 600;
  color: #303133;
}
.stats-item {
  color: #606266;
  font-size: 14px;
}
.attachment-section {
  margin-top: 24px;
  padding: 20px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background: #fafafa;
}
.attachment-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
}
.attachment-title {
  font-weight: 600;
  color: #409eff;
}
.attachment-tip {
  font-size: 12px;
  color: #909399;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .medical-panel {
    padding: 12px;
  }
  .panel-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  .panel-actions {
    width: 100%;
    justify-content: flex-end;
  }
  .stats-content {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
  .cell-content-wrapper {
    flex-direction: column;
    gap: 4px;
  }
}
/* åŠ¨ç”»æ•ˆæžœ */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>
src/views/business/maintain/components/LiverKidneyPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,492 @@
<template>
  <div class="medical-panel">
    <div class="panel-header">
      <h3>肝功能肾功能记录表</h3>
      <el-button
        v-if="isEditing"
        type="primary"
        size="small"
        @click="addColumn"
        icon="el-icon-plus"
      >
        æ–°å¢žè®°å½•列
      </el-button>
    </div>
    <el-table
      :data="tableData"
      border
      style="width: 100%"
      class="medical-table"
      :key="tableKey"
      v-fit-columns
      @header-dragend="handleHeaderDragEnd"
    >
      <el-table-column
        prop="itemName"
        label="检测项目"
        width="200"
        fixed
        class-name="leave-alone"
      >
        <template #default="scope">
          <span :class="{'required-item': scope.row.required}">
            {{ scope.row.itemName }}
          </span>
        </template>
      </el-table-column>
      <el-table-column
        v-for="(col, index) in dynamicColumns"
        :key="col.key"
        :label="col.label"
        :min-width="120"
        :resizable="isEditing"
      >
        <template #default="scope">
          <div class="cell-content">
            <el-input
              v-if="isEditing"
              v-model="scope.row.values[index]"
              size="small"
              :placeholder="`请输入${scope.row.unit ? scope.row.unit : '数值'}`"
              @blur="handleValueChange(scope.row, index)"
              class="value-input"
            />
            <span v-else class="value-display">
              {{ scope.row.values[index] || '-' }}
              <span v-if="scope.row.values[index] && scope.row.unit" class="unit">
                {{ scope.row.unit }}
              </span>
            </span>
            <span v-if="scope.row.reference" class="reference-range">
              ({{ scope.row.reference }})
            </span>
          </div>
        </template>
      </el-table-column>
    </el-table>
    <!-- é™„件上传区域 -->
    <div class="attachment-section">
      <div class="attachment-title">
        <i class="el-icon-paperclip"></i>
        é™„件上传
        <span class="attachment-tip">支持上传检验报告单等文件</span>
      </div>
      <upload-attachment
        :file-list="attachments"
        @change="handleAttachmentChange"
        :limit="10"
        :accept="'.pdf,.jpg,.jpeg,.png,.doc,.docx'"
      />
    </div>
    <!-- åˆ—编辑对话框 -->
    <el-dialog
      title="编辑时间点"
      :visible.sync="columnDialogVisible"
      width="400px"
      @closed="handleDialogClosed"
    >
      <el-form :model="columnForm" label-width="80px" ref="columnForm">
        <el-form-item
          label="日期"
          prop="date"
          :rules="[{ required: true, message: '请选择日期', trigger: 'change' }]"
        >
          <el-date-picker
            v-model="columnForm.date"
            type="date"
            placeholder="选择日期"
            value-format="yyyy-MM-dd"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item
          label="时间"
          prop="time"
          :rules="[{ required: true, message: '请选择时间', trigger: 'change' }]"
        >
          <el-time-picker
            v-model="columnForm.time"
            placeholder="选择时间"
            value-format="HH:mm"
            style="width: 100%"
          />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="columnDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="confirmAddColumn">确定</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
import UploadAttachment from "@/components/UploadAttachment";
export default {
  name: 'LiverKidneyPanel',
  components: {
    UploadAttachment,
  },
  props: {
    isEditing: {
      type: Boolean,
      default: false
    },
    initialData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      tableData: [],
      dynamicColumns: [
        {
          label: '2024-12-27\n08:00',
          key: 'time1',
          date: '2024-12-27',
          time: '08:00'
        },
        {
          label: '2024-12-27\n14:00',
          key: 'time2',
          date: '2024-12-27',
          time: '14:00'
        }
      ],
      attachments: [],
      columnDialogVisible: false,
      columnForm: {
        date: '',
        time: ''
      },
      tableKey: 0 // ç”¨äºŽå¼ºåˆ¶é‡æ–°æ¸²æŸ“表格
    };
  },
  watch: {
    isEditing(newVal) {
      if (!newVal) {
        // ä¿å­˜æ•°æ®
        this.$emit('data-change', {
          type: 'liver_kidney',
          data: this.tableData,
          columns: this.dynamicColumns,
          attachments: this.attachments
        });
      }
      // ç¼–辑模式切换时重新计算列宽
      this.$nextTick(() => {
        this.forceTableLayout();
      });
    },
    dynamicColumns: {
      handler() {
        // åˆ—变化时重新计算布局
        this.$nextTick(() => {
          this.forceTableLayout();
        });
      },
      deep: true
    }
  },
  methods: {
    initTableData() {
      const medicalItems = [
        {
          itemName: '血钠',
          unit: 'mmol/L',
          required: true,
          reference: '135-145'
        },
        {
          itemName: '血钾',
          unit: 'mmol/L',
          required: true,
          reference: '3.5-5.5'
        },
        {
          itemName: 'BUN',
          unit: 'mg/dL',
          required: true,
          reference: '<20'
        },
        {
          itemName: '肌酐',
          unit: 'μmol/L',
          required: true,
          reference: '<100'
        },
        {
          itemName: '总胆红素',
          unit: 'μmol/L',
          required: true,
          reference: '<21'
        },
        {
          itemName: 'ALT',
          unit: 'U/L',
          required: true,
          reference: '<50'
        },
        {
          itemName: 'AST',
          unit: 'U/L',
          required: true,
          reference: '<40'
        },
        {
          itemName: 'GGT',
          unit: 'U/L',
          required: true,
          reference: '<57'
        },
        {
          itemName: 'ALP',
          unit: 'U/L',
          required: true,
          reference: '<120'
        },
        {
          itemName: 'PT',
          unit: '秒',
          required: true,
          reference: '9.4-12.5'
        },
        {
          itemName: 'INR',
          unit: '',
          required: true,
          reference: '0.85-1.15'
        }
      ];
      this.tableData = medicalItems.map(item => ({
        ...item,
        values: new Array(this.dynamicColumns.length).fill('')
      }));
    },
    addColumn() {
      this.columnForm = {
        date: new Date().toISOString().split('T')[0],
        time: '08:00'
      };
      this.columnDialogVisible = true;
    },
    confirmAddColumn() {
      this.$refs.columnForm.validate((valid) => {
        if (!valid) {
          this.$message.warning('请完善时间点信息');
          return;
        }
        const newIndex = this.dynamicColumns.length + 1;
        const newColumn = {
          label: `${this.columnForm.date}\n${this.columnForm.time}`,
          key: `time${newIndex}`,
          date: this.columnForm.date,
          time: this.columnForm.time
        };
        this.dynamicColumns.push(newColumn);
        // ä¸ºæ‰€æœ‰è¡Œæ–°å¢žä¸€ä¸ªç©ºå€¼
        this.tableData.forEach(row => {
          row.values.push('');
        });
        this.columnDialogVisible = false;
        this.$message.success('时间点添加成功');
        // å¼ºåˆ¶è¡¨æ ¼é‡æ–°æ¸²æŸ“
        this.tableKey += 1;
      });
    },
    handleDialogClosed() {
      this.columnForm = {
        date: '',
        time: ''
      };
      this.$refs.columnForm && this.$refs.columnForm.clearValidate();
    },
    handleValueChange(row, columnIndex) {
      this.$emit('data-change', {
        type: 'liver_kidney',
        data: this.tableData,
        columns: this.dynamicColumns
      });
    },
    handleAttachmentChange(fileList) {
      this.attachments = fileList;
      this.$emit('attachment-change', {
        type: 'liver_kidney',
        attachments: fileList
      });
    },
    // å¼ºåˆ¶è¡¨æ ¼é‡æ–°å¸ƒå±€[1,3](@ref)
    forceTableLayout() {
      this.$nextTick(() => {
        const table = this.$el.querySelector('.el-table');
        if (table && table.querySelector('colgroup')) {
          // è§¦å‘表格重新计算布局
          this.$nextTick(() => {
            window.dispatchEvent(new Event('resize'));
          });
        }
      });
    },
    handleHeaderDragEnd() {
      // åˆ—宽拖拽结束后重新计算布局
      this.forceTableLayout();
    },
    exportData() {
      return {
        tableData: this.tableData,
        columns: this.dynamicColumns,
        exportTime: new Date().toISOString(),
        attachments: this.attachments
      };
    }
  },
  mounted() {
    this.initTableData();
    // åˆå§‹æ¸²æŸ“后计算列宽
    this.$nextTick(() => {
      this.forceTableLayout();
    });
  }
};
</script>
<style scoped>
.medical-panel {
  padding: 20px;
  background: #fff;
  border-radius: 4px;
  overflow: hidden;
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 15px;
  border-bottom: 1px solid #ebeef5;
}
.panel-header h3 {
  margin: 0;
  color: #303133;
  font-size: 18px;
  font-weight: 600;
}
.medical-table {
  margin-bottom: 30px;
  min-width: 100%;
}
.medical-table ::v-deep .el-table__body-wrapper {
  overflow-x: auto;
}
.cell-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 32px;
}
.value-input {
  flex: 1;
  margin-right: 8px;
}
.value-display {
  font-weight: 500;
  color: #303133;
}
.unit {
  color: #909399;
  font-size: 12px;
  margin-left: 4px;
}
.reference-range {
  color: #67c23a;
  font-size: 12px;
  font-style: italic;
  margin-left: 8px;
}
.required-item::before {
  content: "*";
  color: #f56c6c;
  margin-right: 4px;
}
.attachment-section {
  margin-top: 30px;
  padding: 20px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background: #fafafa;
}
.attachment-title {
  font-weight: bold;
  margin-bottom: 15px;
  color: #409eff;
  display: flex;
  align-items: center;
}
.attachment-tip {
  font-size: 12px;
  color: #909399;
  margin-left: 10px;
  font-weight: normal;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .medical-panel {
    padding: 10px;
  }
  .panel-header {
    flex-direction: column;
    align-items: flex-start;
  }
  .panel-header h3 {
    margin-bottom: 10px;
  }
  .cell-content {
    flex-direction: column;
    align-items: flex-start;
  }
  .reference-range {
    margin-left: 0;
    margin-top: 4px;
  }
}
</style>
在上述文件截断后对比
src/views/business/maintain/components/UrineRoutinePanel.vue src/views/business/maintain/maintainInfo.vue src/views/project/donatebaseinfo/index.vue