| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | 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"; |
| | |
| | | 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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // å¨ç¶ç»ä»¶ 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' |
| | | } |
| | | ] |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 模æå¨å®è·åæ°æ® |
| | | 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 |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 模æå¨å®å©ç¨æ°æ® |
| | | 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 |
| | | }; |
| | |
| | | }, 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) => { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 模æå¨å®åé
æ°æ® |
| | | 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 |
| | | }; |
| | |
| | | <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> |
| | | |
| | |
| | | </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> |
| | |
| | | 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: "女" } |
| | |
| | | }, |
| | | 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"]; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 模ææç®è¿ç¨æ°æ® |
| | | 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 |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ¨¡ææ»äº¡å¤å®æ°æ® |
| | | 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] || "æªç¥ç±»å"; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 模æä¼¦çå®¡æ¥æ°æ® |
| | | 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 |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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 |