| | |
| | | "core-js": "3.8.1", |
| | | "dayjs": "^1.11.1", |
| | | "dingtalk-jsapi": "^3.1.1", |
| | | "echarts": "4.9.0", |
| | | "echarts": "^4.9.0", |
| | | "element-china-area-data": "^5.0.2", |
| | | "element-ui": "2.15.6", |
| | | "file-saver": "2.0.5", |
| | | "file-saver": "^2.0.5", |
| | | "fuse.js": "6.4.3", |
| | | "highlight.js": "9.18.5", |
| | | "js-beautify": "1.13.0", |
| | |
| | | "vue-router": "3.4.9", |
| | | "vue-year-picker": "^1.1.0", |
| | | "vuedraggable": "2.24.3", |
| | | "vuex": "3.6.0" |
| | | "vuex": "3.6.0", |
| | | "xlsx": "^0.18.5" |
| | | }, |
| | | "devDependencies": { |
| | | "@vue/cli-plugin-babel": "4.4.6", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // api/statistics/index.js |
| | | import request from '@/utils/request'; |
| | | |
| | | // æ¡ä¾ç»è®¡ |
| | | export function getCaseStatistics(params) { |
| | | return request({ |
| | | url: '/statistics/case/list', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | |
| | | // æç®å¨å®ç»è®¡ |
| | | export function getOrganStatistics(params) { |
| | | return request({ |
| | | url: '/statistics/organ/list', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | |
| | | // è·åçç»è®¡ |
| | | export function getAcquisitionRate(params) { |
| | | return request({ |
| | | url: '/statistics/acquisition/rate', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | |
| | | // æç®ææ¿ç»è®¡ |
| | | export function getWillingness(params) { |
| | | return request({ |
| | | url: '/statistics/willingness', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | |
| | | // æç®è
åæ |
| | | export function getDonorAnalysis(params) { |
| | | return request({ |
| | | url: '/statistics/donor/analysis', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | |
| | | // å¨å®è·åå©ç¨ |
| | | export function getOrganUtilization(params) { |
| | | return request({ |
| | | url: '/statistics/organ/utilization', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/components/charts/EChartsWrapper.vue --> |
| | | <template> |
| | | <div :id="id" :style="chartStyle"></div> |
| | | </template> |
| | | |
| | | <script> |
| | | import echarts from 'echarts'; |
| | | |
| | | export default { |
| | | name: 'EChartsWrapper', |
| | | props: { |
| | | id: { |
| | | type: String, |
| | | default: `chart_${Date.now()}` |
| | | }, |
| | | width: { |
| | | type: [String, Number], |
| | | default: '100%' |
| | | }, |
| | | height: { |
| | | type: [String, Number], |
| | | default: '400px' |
| | | }, |
| | | options: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | theme: { |
| | | type: String, |
| | | default: null |
| | | } |
| | | }, |
| | | computed: { |
| | | chartStyle() { |
| | | return { |
| | | width: typeof this.width === 'number' ? `${this.width}px` : this.width, |
| | | height: typeof this.height === 'number' ? `${this.height}px` : this.height |
| | | }; |
| | | } |
| | | }, |
| | | data() { |
| | | return { |
| | | chart: null |
| | | }; |
| | | }, |
| | | watch: { |
| | | options: { |
| | | deep: true, |
| | | handler(newOptions) { |
| | | if (this.chart && newOptions) { |
| | | this.chart.setOption(newOptions, true); |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.initChart(); |
| | | window.addEventListener('resize', this.handleResize); |
| | | }, |
| | | beforeDestroy() { |
| | | if (this.chart) { |
| | | this.chart.dispose(); |
| | | this.chart = null; |
| | | } |
| | | window.removeEventListener('resize', this.handleResize); |
| | | }, |
| | | methods: { |
| | | initChart() { |
| | | this.chart = echarts.init(document.getElementById(this.id), this.theme); |
| | | this.chart.setOption(this.options); |
| | | }, |
| | | handleResize() { |
| | | if (this.chart) { |
| | | this.chart.resize(); |
| | | } |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/components/FilterPanel.vue --> |
| | | <template> |
| | | <el-card class="filter-panel"> |
| | | <el-form :inline="true" :model="formData" class="filter-form" label-width="80px"> |
| | | <!-- å¨ææ¸²æè¡¨ååæ®µ --> |
| | | <template v-for="field in fields"> |
| | | <!-- 䏿鿩ï¼åéï¼ --> |
| | | <el-form-item |
| | | v-if="field.type === 'select' && !field.multiple" |
| | | :key="field.prop" |
| | | :label="field.label" |
| | | > |
| | | <el-select |
| | | v-model="formData[field.prop]" |
| | | :placeholder="field.placeholder || `è¯·éæ©${field.label}`" |
| | | clearable |
| | | :style="{ width: field.width || '200px' }" |
| | | > |
| | | <el-option |
| | | v-for="item in field.options" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <!-- 䏿鿩ï¼å¤éï¼ --> |
| | | <el-form-item |
| | | v-if="field.type === 'select' && field.multiple" |
| | | :key="field.prop" |
| | | :label="field.label" |
| | | > |
| | | <el-select |
| | | v-model="formData[field.prop]" |
| | | :placeholder="field.placeholder || `è¯·éæ©${field.label}`" |
| | | clearable |
| | | multiple |
| | | collapse-tags |
| | | :style="{ width: field.width || '300px' }" |
| | | > |
| | | <el-option |
| | | v-for="item in field.options" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <!-- æ¥æèå´éæ© --> |
| | | <el-form-item |
| | | v-if="field.type === 'daterange'" |
| | | :key="field.prop" |
| | | :label="field.label" |
| | | > |
| | | <el-date-picker |
| | | v-model="formData[field.prop]" |
| | | :type="field.dateType || 'daterange'" |
| | | :range-separator="field.rangeSeparator || 'è³'" |
| | | :start-placeholder="field.startPlaceholder || 'å¼å§æ¥æ'" |
| | | :end-placeholder="field.endPlaceholder || 'ç»ææ¥æ'" |
| | | :value-format="field.valueFormat || 'yyyy-MM-dd'" |
| | | :style="{ width: field.width || '300px' }" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <!-- è¾å
¥æ¡ --> |
| | | <el-form-item |
| | | v-if="field.type === 'input'" |
| | | :key="field.prop" |
| | | :label="field.label" |
| | | > |
| | | <el-input |
| | | v-model="formData[field.prop]" |
| | | :placeholder="field.placeholder || `请è¾å
¥${field.label}`" |
| | | clearable |
| | | :style="{ width: field.width || '200px' }" |
| | | @keyup.enter.native="handleSearch" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <!-- èªå®ä¹ææ§½ --> |
| | | <el-form-item |
| | | v-if="field.type === 'slot'" |
| | | :key="field.prop" |
| | | :label="field.label" |
| | | > |
| | | <slot :name="field.slotName"></slot> |
| | | </el-form-item> |
| | | </template> |
| | | |
| | | <!-- æä½æé® --> |
| | | <el-form-item class="filter-actions"> |
| | | <el-button type="primary" icon="el-icon-search" @click="handleSearch">æ¥è¯¢</el-button> |
| | | <el-button icon="el-icon-refresh" @click="handleReset">éç½®</el-button> |
| | | <slot name="extra-actions"></slot> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | name: 'FilterPanel', |
| | | props: { |
| | | // 表ååæ®µé
ç½® |
| | | fields: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | // 表åé»è®¤å¼ |
| | | defaultValue: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | } |
| | | }, |
| | | data() { |
| | | return { |
| | | formData: { ...this.defaultValue } |
| | | }; |
| | | }, |
| | | watch: { |
| | | defaultValue: { |
| | | deep: true, |
| | | handler(newVal) { |
| | | this.formData = { ...newVal }; |
| | | } |
| | | } |
| | | }, |
| | | methods: { |
| | | handleSearch() { |
| | | this.$emit('search', this.formData); |
| | | }, |
| | | handleReset() { |
| | | this.formData = { ...this.defaultValue }; |
| | | this.$emit('reset', this.formData); |
| | | }, |
| | | // æ´é²ç»ç¶ç»ä»¶çæ¹æ³ |
| | | getFormData() { |
| | | return { ...this.formData }; |
| | | }, |
| | | setFormData(data) { |
| | | this.formData = { ...data }; |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .filter-panel { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .filter-form { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .filter-actions { |
| | | float: right; |
| | | margin-right: 0; |
| | | margin-bottom: 0; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /* src/styles/statistics.css */ |
| | | /* ç»è®¡é¡µé¢å
Œ
±æ ·å¼ */ |
| | | .statistics-container { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | .filter-panel { |
| | | margin-bottom: 20px; |
| | | background: white; |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .chart-container { |
| | | background: white; |
| | | border-radius: 4px; |
| | | margin-bottom: 20px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .chart-container .chart-header { |
| | | padding: 20px 20px 0; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .chart-container .chart-body { |
| | | padding: 20px; |
| | | height: 400px; |
| | | } |
| | | |
| | | .data-table { |
| | | background: white; |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | /* ååºå¼è®¾è®¡ */ |
| | | @media (max-width: 768px) { |
| | | .chart-container { |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .chart-container .chart-body { |
| | | height: 300px; |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // src/utils/excel.js |
| | | import * as XLSX from 'xlsx'; |
| | | import FileSaver from 'file-saver'; |
| | | |
| | | export function exportExcel(data, fileName = 'å¯¼åºæ°æ®') { |
| | | // å建工ä½ç°¿ |
| | | const wb = XLSX.utils.book_new(); |
| | | |
| | | // å建工ä½è¡¨ |
| | | const ws = XLSX.utils.aoa_to_sheet(data); |
| | | |
| | | // æ·»å å°å·¥ä½ç°¿ |
| | | XLSX.utils.book_append_sheet(wb, ws, 'Sheet1'); |
| | | |
| | | // çæExcelæä»¶ |
| | | const wbout = XLSX.write(wb, { |
| | | bookType: 'xlsx', |
| | | type: 'array' |
| | | }); |
| | | |
| | | // å建Blob对象 |
| | | const blob = new Blob([wbout], { |
| | | type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' |
| | | }); |
| | | |
| | | // ä¿åæä»¶ |
| | | FileSaver.saveAs(blob, `${fileName}_${new Date().getTime()}.xlsx`); |
| | | } |
| | | |
| | | export function exportJsonToExcel(jsonData, headers, fileName = 'å¯¼åºæ°æ®') { |
| | | const data = [headers]; |
| | | |
| | | jsonData.forEach(item => { |
| | | const row = headers.map(header => { |
| | | if (typeof header === 'object') { |
| | | return item[header.key] || ''; |
| | | } |
| | | return item[header] || ''; |
| | | }); |
| | | data.push(row); |
| | | }); |
| | | |
| | | exportExcel(data, fileName); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // src/utils/statistics.js |
| | | export const regionOptions = [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | ]; |
| | | |
| | | export const organOptions = [ |
| | | { label: 'å
¨è', value: 'liver' }, |
| | | { label: 'å·¦è¾', value: 'leftKidney' }, |
| | | { label: 'å³è¾', value: 'rightKidney' }, |
| | | { label: 'å¿è', value: 'heart' }, |
| | | { label: 'å
¨èº', value: 'lung' }, |
| | | { label: 'è°è
º', value: 'pancreas' }, |
| | | { label: 'ç¼è§è', value: 'cornea' } |
| | | ]; |
| | | |
| | | export const ageRangeOptions = [ |
| | | { label: '<17å²', value: '0-17' }, |
| | | { label: '18-49å²', value: '18-49' }, |
| | | { label: '50-69å²', value: '50-69' }, |
| | | { label: '>69å²', value: '70-100' } |
| | | ]; |
| | | |
| | | // 导åºExcelåè½ |
| | | export function exportToExcel(data, filename = 'å¯¼åºæ°æ®') { |
| | | import('xlsx').then(XLSX => { |
| | | const ws = XLSX.utils.json_to_sheet(data); |
| | | const wb = XLSX.utils.book_new(); |
| | | XLSX.utils.book_append_sheet(wb, ws, 'Sheet1'); |
| | | XLSX.writeFile(wb, `${filename}.xlsx`); |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/case.vue --> |
| | | <template> |
| | | <div class="statistics-page"> |
| | | <!-- çé颿¿ --> |
| | | <el-card class="filter-card"> |
| | | <el-form :inline="true" :model="query" class="filter-form"> |
| | | <el-form-item label="å°åº"> |
| | | <el-select |
| | | v-model="query.region" |
| | | placeholder="è¯·éæ©å°åº" |
| | | clearable |
| | | multiple |
| | | collapse-tags |
| | | style="width: 200px" |
| | | > |
| | | <el-option |
| | | v-for="item in regionOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æä»½èå´"> |
| | | <el-date-picker |
| | | v-model="query.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | value-format="yyyy-MM" |
| | | style="width: 300px" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-search" @click="handleSearch">æ¥è¯¢</el-button> |
| | | <el-button icon="el-icon-refresh" @click="handleReset">éç½®</el-button> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">导åº</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- æ°æ®æ¦è§ --> |
| | | <el-row :gutter="20" class="overview-row"> |
| | | <el-col :span="6" v-for="item in overviewData" :key="item.title"> |
| | | <el-card class="overview-card"> |
| | | <div class="overview-content"> |
| | | <div class="overview-icon" :style="{ backgroundColor: item.color }"> |
| | | <i :class="item.icon"></i> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-title">{{ item.title }}</div> |
| | | <div class="overview-value">{{ item.value }}</div> |
| | | <div class="overview-desc">{{ item.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <el-row :gutter="20" class="chart-row"> |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æ¡ä¾è¶å¿åæ</span> |
| | | <el-button |
| | | style="float: right; padding: 3px 0" |
| | | type="text" |
| | | @click="toggleChartType('trend')" |
| | | > |
| | | {{ chartTypes.trend === 'line' ? 'æ±ç¶å¾' : 'æçº¿å¾' }} |
| | | </el-button> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'trend-chart'" |
| | | :options="getTrendChartOptions()" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>å°åºåå¸å¯¹æ¯</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'region-chart'" |
| | | :options="getRegionChartOptions()" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æ¡ä¾ç»è®¡æç»</span> |
| | | <el-button |
| | | style="float: right;" |
| | | type="primary" |
| | | icon="el-icon-zoom-in" |
| | | @click="showDetail = true" |
| | | > |
| | | æ¥çæ¡ä¾æç» |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æä»½" prop="month" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | {{ formatMonth(row.month) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ½å¨æç®æ¡ä¾æ°" prop="potentialCount" align="center" /> |
| | | <el-table-column label="宿æç®æ°é" prop="completedCount" align="center" /> |
| | | <el-table-column label="宿ç" prop="completionRate" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-progress |
| | | :percentage="row.completionRate" |
| | | :color="getProgressColor(row.completionRate)" |
| | | :show-text="false" |
| | | /> |
| | | <span>{{ row.completionRate }}%</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¡ä¾æç»" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-button |
| | | type="text" |
| | | size="mini" |
| | | @click="viewCaseDetail(row)" |
| | | > |
| | | æ¥ç({{ row.caseCount || 0 }}) |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <el-pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page.sync="query.page" |
| | | :limit.sync="query.limit" |
| | | @pagination="handlePagination" |
| | | style="margin-top: 20px;" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | /> |
| | | </el-card> |
| | | |
| | | <!-- æ¡ä¾æç»å¼¹çª --> |
| | | <el-dialog |
| | | title="æ¡ä¾æç»" |
| | | :visible.sync="showDetail" |
| | | |
| | | append-to-body |
| | | > |
| | | <case-detail |
| | | :query="detailQuery" |
| | | @close="showDetail = false" |
| | | /> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | import CaseDetail from './components/CaseDetail.vue'; |
| | | |
| | | export default { |
| | | name: 'CaseStatistics', |
| | | components: { |
| | | EChartsWrapper, |
| | | CaseDetail |
| | | }, |
| | | data() { |
| | | return { |
| | | // æ¥è¯¢åæ° |
| | | query: { |
| | | region: [], |
| | | monthRange: [], |
| | | page: 1, |
| | | limit: 10 |
| | | }, |
| | | |
| | | // å°åºé项 |
| | | regionOptions: [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | | loading: false, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | total: 0, |
| | | |
| | | // æ¦è§æ°æ® |
| | | overviewData: [ |
| | | { title: 'æ»æ½å¨æç®æ¡ä¾', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-flag', color: '#409EFF' }, |
| | | { title: 'æ»å®ææç®æ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-circle-check', color: '#67C23A' }, |
| | | { title: 'å¹³å宿ç', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-data', color: '#E6A23C' }, |
| | | { title: 'æ¶åæ¡ä¾æ°', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-document', color: '#F56C6C' } |
| | | ], |
| | | |
| | | // å¾è¡¨ç±»å |
| | | chartTypes: { |
| | | trend: 'line' |
| | | }, |
| | | |
| | | // å¾è¡¨æ°æ® |
| | | chartData: { |
| | | trend: [], |
| | | region: [] |
| | | }, |
| | | |
| | | // å¼¹çªæ§å¶ |
| | | showDetail: false, |
| | | detailQuery: {}, |
| | | |
| | | // æ¨¡ææ°æ® |
| | | mockData: { |
| | | table: [ |
| | | { id: 1, region: 'éå²å°åº', month: '2024-01', potentialCount: 15, completedCount: 8, completionRate: 53.3, caseCount: 5 }, |
| | | { id: 2, region: 'éå²å°åº', month: '2024-02', potentialCount: 18, completedCount: 10, completionRate: 55.6, caseCount: 6 }, |
| | | { id: 3, region: 'æ¥ç
§å°åº', month: '2024-01', potentialCount: 8, completedCount: 4, completionRate: 50.0, caseCount: 3 }, |
| | | { id: 4, region: 'æ¥ç
§å°åº', month: '2024-02', potentialCount: 10, completedCount: 6, completionRate: 60.0, caseCount: 4 }, |
| | | { id: 5, region: 'æµåå°åº', month: '2024-01', potentialCount: 20, completedCount: 12, completionRate: 60.0, caseCount: 7 }, |
| | | { id: 6, region: 'æµåå°åº', month: '2024-02', potentialCount: 22, completedCount: 14, completionRate: 63.6, caseCount: 8 } |
| | | ], |
| | | trend: [ |
| | | { month: '2024-01', potential: 55, completed: 31 }, |
| | | { month: '2024-02', potential: 64, completed: 39 }, |
| | | { month: '2024-03', potential: 58, completed: 35 }, |
| | | { month: '2024-04', potential: 62, completed: 40 }, |
| | | { month: '2024-05', potential: 68, completed: 45 }, |
| | | { month: '2024-06', potential: 72, completed: 50 } |
| | | ], |
| | | region: [ |
| | | { region: 'éå²å°åº', potential: 150, completed: 85 }, |
| | | { region: 'æ¥ç
§å°åº', potential: 80, completed: 45 }, |
| | | { region: 'æµåå°åº', potential: 200, completed: 120 }, |
| | | { region: 'çå°å°åº', potential: 100, completed: 60 } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | }, |
| | | methods: { |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | // æ¨¡ææ¥å£è°ç¨ |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | const [start, end] = this.query.monthRange; |
| | | filteredData = filteredData.filter(item => |
| | | item.month >= start && item.month <= end |
| | | ); |
| | | } |
| | | |
| | | this.tableData = filteredData; |
| | | this.total = filteredData.length; |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | this.updateOverviewData(filteredData); |
| | | |
| | | // æ´æ°å¾è¡¨æ°æ® |
| | | this.chartData.trend = [...this.mockData.trend]; |
| | | this.chartData.region = [...this.mockData.region]; |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | updateOverviewData(data) { |
| | | if (data.length === 0) return; |
| | | |
| | | const totalPotential = data.reduce((sum, item) => sum + item.potentialCount, 0); |
| | | const totalCompleted = data.reduce((sum, item) => sum + item.completedCount, 0); |
| | | const avgRate = data.length > 0 |
| | | ? (data.reduce((sum, item) => sum + item.completionRate, 0) / data.length).toFixed(1) |
| | | : 0; |
| | | const totalCases = data.reduce((sum, item) => sum + (item.caseCount || 0), 0); |
| | | |
| | | this.overviewData[0].value = totalPotential; |
| | | this.overviewData[1].value = totalCompleted; |
| | | this.overviewData[2].value = `${avgRate}%`; |
| | | this.overviewData[3].value = totalCases; |
| | | }, |
| | | |
| | | // è·åè¶å¿å¾è¡¨é
ç½® |
| | | getTrendChartOptions() { |
| | | const isLine = this.chartTypes.trend === 'line'; |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æ½å¨æç®æ°', '宿æç®æ°'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: this.chartData.trend.map(item => this.formatMonth(item.month)) |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: 'æ°é(个)' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'æ½å¨æç®æ°', |
| | | type: isLine ? 'line' : 'bar', |
| | | data: this.chartData.trend.map(item => item.potential), |
| | | itemStyle: { color: '#409EFF' }, |
| | | smooth: isLine, |
| | | lineStyle: isLine ? { width: 3 } : {} |
| | | }, |
| | | { |
| | | name: '宿æç®æ°', |
| | | type: isLine ? 'line' : 'bar', |
| | | data: this.chartData.trend.map(item => item.completed), |
| | | itemStyle: { color: '#67C23A' }, |
| | | smooth: isLine, |
| | | lineStyle: isLine ? { width: 3 } : {} |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åå°åºå¾è¡¨é
ç½® |
| | | getRegionChartOptions() { |
| | | return { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æ½å¨æç®æ°', '宿æç®æ°'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: this.chartData.region.map(item => item.region) |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: 'æ°é(个)' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'æ½å¨æç®æ°', |
| | | type: 'bar', |
| | | data: this.chartData.region.map(item => item.potential), |
| | | itemStyle: { color: '#409EFF' } |
| | | }, |
| | | { |
| | | name: '宿æç®æ°', |
| | | type: 'bar', |
| | | data: this.chartData.region.map(item => item.completed), |
| | | itemStyle: { color: '#67C23A' } |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åè¿åº¦æ¡é¢è² |
| | | getProgressColor(percentage) { |
| | | if (percentage >= 60) return '#67C23A'; |
| | | if (percentage >= 50) return '#E6A23C'; |
| | | return '#F56C6C'; |
| | | }, |
| | | |
| | | // æ ¼å¼åæä»½æ¾ç¤º |
| | | formatMonth(month) { |
| | | if (!month) return ''; |
| | | return month.replace('-', 'å¹´') + 'æ'; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | | handleSearch() { |
| | | this.query.page = 1; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çéç½® |
| | | handleReset() { |
| | | this.query = { |
| | | region: [], |
| | | monthRange: [], |
| | | page: 1, |
| | | limit: 10 |
| | | }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå页 |
| | | handlePagination({ page, limit }) { |
| | | this.query.page = page; |
| | | this.query.limit = limit; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | // è¿éå¯ä»¥è°ç¨å¯¼åºæ¥å£ |
| | | }, |
| | | |
| | | // 忢å¾è¡¨ç±»å |
| | | toggleChartType(type) { |
| | | this.chartTypes[type] = this.chartTypes[type] === 'line' ? 'bar' : 'line'; |
| | | }, |
| | | |
| | | // æ¥çæ¡ä¾è¯¦æ
|
| | | viewCaseDetail(row) { |
| | | this.detailQuery = { |
| | | region: row.region, |
| | | month: row.month |
| | | }; |
| | | this.showDetail = true; |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .statistics-page { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .filter-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .filter-form { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .overview-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .overview-card { |
| | | height: 100%; |
| | | } |
| | | |
| | | .overview-content { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .overview-icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | } |
| | | |
| | | .overview-icon i { |
| | | font-size: 24px; |
| | | color: white; |
| | | } |
| | | |
| | | .overview-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .overview-title { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .overview-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .overview-desc { |
| | | font-size: 12px; |
| | | color: #C0C4CC; |
| | | } |
| | | |
| | | .chart-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/components/CaseDetail.vue --> |
| | | <template> |
| | | <div class="case-detail"> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="caseList" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="æ¡ä¾ç¼å·" prop="caseNo" align="center" /> |
| | | <el-table-column label="æç®è
å§å" prop="donorName" align="center" /> |
| | | <el-table-column label="ä½é¢å·" prop="inpatientNo" align="center" /> |
| | | <el-table-column label="æ¡ä¾ç¶æ" prop="status" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getStatusTag(row.status)" size="small"> |
| | | {{ getStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æç®å¨å®" prop="organs" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <span v-if="row.organs && row.organs.length > 0"> |
| | | {{ row.organs.join('ã') }} |
| | | </span> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æç®æ¶é´" prop="donationTime" align="center" /> |
| | | <el-table-column label="æ»äº¡åå " prop="deathCause" align="center" /> |
| | | <el-table-column label="å»é¢" prop="hospital" align="center" width="200" /> |
| | | </el-table> |
| | | |
| | | <div style="text-align: center; margin-top: 20px;"> |
| | | <el-button @click="$emit('close')">å
³é</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | name: 'CaseDetail', |
| | | props: { |
| | | query: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | } |
| | | }, |
| | | data() { |
| | | return { |
| | | loading: false, |
| | | caseList: [ |
| | | { caseNo: 'C202401001', donorName: 'å¼ ä¸', inpatientNo: 'ZY202401001', status: 'completed', organs: ['èè', 'è¾è'], donationTime: '2024-01-15 10:30', deathCause: 'èå¤ä¼¤', hospital: 'éå²å¤§å¦éå±å»é¢' }, |
| | | { caseNo: 'C202401002', donorName: 'æå', inpatientNo: 'ZY202401002', status: 'completed', organs: ['å¿è', 'ç¼è§è'], donationTime: '2024-01-20 14:20', deathCause: 'å¿æéª¤å', hospital: 'éå²å¸ç«å»é¢' }, |
| | | { caseNo: 'C202401003', donorName: 'çäº', inpatientNo: 'ZY202401003', status: 'potential', organs: [], donationTime: null, deathCause: 'èè¡ç®¡æå¤', hospital: 'éå²å¸ä¸å¿å»é¢' } |
| | | ] |
| | | }; |
| | | }, |
| | | methods: { |
| | | getStatusTag(status) { |
| | | const map = { completed: 'success', potential: 'warning', abandoned: 'danger' }; |
| | | return map[status] || 'info'; |
| | | }, |
| | | getStatusText(status) { |
| | | const map = { completed: '已宿', potential: 'æ½å¨æç®', abandoned: 'æ¾å¼æç®' }; |
| | | return map[status] || 'æªç¥'; |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .case-detail { |
| | | padding: 0; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/components/OrganDetail.vue --> |
| | | <template> |
| | | <div class="organ-detail"> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="organList" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="å¨å®ç¼å·" prop="organNo" align="center" /> |
| | | <el-table-column label="å¨å®åç§°" prop="organName" align="center" /> |
| | | <el-table-column label="æç®è
" prop="donorName" align="center" /> |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="年份" prop="year" align="center" /> |
| | | <el-table-column label="å¨å®ç¶æ" prop="status" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getStatusTag(row.status)" size="small"> |
| | | {{ getStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è·åæ¶é´" prop="acquisitionTime" align="center" /> |
| | | <el-table-column label="ç§»æ¤æ¶é´" prop="transplantTime" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | {{ row.transplantTime || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¼ç¨åå " prop="abandonReason" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | {{ row.abandonReason || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åä½å§å" prop="recipientName" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | {{ row.recipientName || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç§»æ¤å»é¢" prop="hospital" align="center" /> |
| | | <el-table-column label="æä½å»ç" prop="doctor" align="center" /> |
| | | </el-table> |
| | | |
| | | <div style="text-align: center; margin-top: 20px;"> |
| | | <el-button @click="$emit('close')">å
³é</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | name: 'OrganDetail', |
| | | props: { |
| | | query: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | } |
| | | }, |
| | | data() { |
| | | return { |
| | | loading: false, |
| | | organList: [ |
| | | { organNo: 'O202401001', organName: 'å
¨è', donorName: 'å¼ ä¸', region: 'éå²å°åº', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-15 10:30', transplantTime: '2024-01-15 12:45', abandonReason: '', recipientName: 'çæ', hospital: 'éå²å¤§å¦éå±å»é¢', doctor: 'æå»ç' }, |
| | | { organNo: 'O202401002', organName: 'å·¦è¾', donorName: 'å¼ ä¸', region: 'éå²å°åº', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-15 10:30', transplantTime: '2024-01-15 13:20', abandonReason: '', recipientName: 'é强', hospital: 'éå²å¸ç«å»é¢', doctor: 'å¼ å»ç' }, |
| | | { organNo: 'O202401003', organName: 'å³è¾', donorName: 'å¼ ä¸', region: 'éå²å°åº', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-15 10:30', transplantTime: '2024-01-15 13:50', abandonReason: '', recipientName: 'åä¼', hospital: 'éå²å¸ä¸å¿å»é¢', doctor: 'çå»ç' }, |
| | | { organNo: 'O202401004', organName: 'å
¨è', donorName: 'æå', region: 'éå²å°åº', year: '2024', status: 'abandoned', acquisitionTime: '2024-01-20 14:20', transplantTime: null, abandonReason: 'å¨å®è´¨éä¸åæ ¼', recipientName: '', hospital: 'éå²å¸ç«å»é¢', doctor: 'å¼ å»ç' }, |
| | | { organNo: 'O202401005', organName: 'å¿è', donorName: 'æå', region: 'éå²å°åº', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-20 14:20', transplantTime: '2024-01-20 15:30', abandonReason: '', recipientName: 'èµµå', hospital: 'éå²å¸ç«å»é¢', doctor: 'å¼ å»ç' } |
| | | ] |
| | | }; |
| | | }, |
| | | methods: { |
| | | getStatusTag(status) { |
| | | const map = { transplanted: 'success', abandoned: 'danger', pending: 'warning' }; |
| | | return map[status] || 'info'; |
| | | }, |
| | | getStatusText(status) { |
| | | const map = { transplanted: '已移æ¤', abandoned: 'å·²å¼ç¨', pending: 'å¾
ç§»æ¤' }; |
| | | return map[status] || 'æªç¥'; |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .organ-detail { |
| | | padding: 0; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/donor.vue --> |
| | | <template> |
| | | <div class="statistics-page"> |
| | | <!-- çé颿¿ --> |
| | | <FilterPanel |
| | | :fields="filterFields" |
| | | :default-value="query" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | > |
| | | <template #extra-actions> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">导åº</el-button> |
| | | </template> |
| | | </FilterPanel> |
| | | |
| | | <!-- æ°æ®æ¦è§ --> |
| | | <el-row :gutter="20" class="overview-row"> |
| | | <el-col :span="6" v-for="item in overviewData" :key="item.title"> |
| | | <el-card class="overview-card"> |
| | | <div class="overview-content"> |
| | | <div class="overview-icon" :style="{ backgroundColor: item.color }"> |
| | | <i :class="item.icon"></i> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-title">{{ item.title }}</div> |
| | | <div class="overview-value">{{ item.value }}</div> |
| | | <div class="overview-desc">{{ item.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <el-row :gutter="20" class="chart-row"> |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æç®è
å¹´é¾åå¸</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'age-chart'" |
| | | :options="ageChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æ§å«ä¸è¡ååå¸</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'gender-blood-chart'" |
| | | :options="genderBloodChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20" class="chart-row"> |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>ä¸å½åç±»åå¸</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'category-chart'" |
| | | :options="categoryChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æ»äº¡åå åæ</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'death-chart'" |
| | | :options="deathChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æç®è
详ç»ä¿¡æ¯</span> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="æç®è
ç¼å·" prop="donorNo" align="center" /> |
| | | <el-table-column label="å§å" prop="name" align="center" /> |
| | | <el-table-column label="æ§å«" prop="gender" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="row.gender === 'ç·' ? 'primary' : 'danger'" size="small"> |
| | | {{ row.gender }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¹´é¾" prop="age" align="center" /> |
| | | <el-table-column label="è¡å" prop="bloodType" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getBloodTypeTag(row.bloodType)" size="small"> |
| | | {{ row.bloodType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æç®æ¶é´" prop="donationTime" align="center" /> |
| | | <el-table-column label="ä¸å½åç±»" prop="chinaCategory" align="center" /> |
| | | <el-table-column label="æ»äº¡åå " prop="deathCause" align="center" /> |
| | | <el-table-column label="æç®å¨å®" prop="organs" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <span>{{ row.organs.join('ã') }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" align="center" " fixed="right"> |
| | | <template slot-scope="{ row }"> |
| | | <el-button |
| | | type="text" |
| | | size="mini" |
| | | @click="viewDonorDetail(row)" |
| | | > |
| | | 详æ
|
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <el-pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page.sync="query.page" |
| | | :limit.sync="query.limit" |
| | | @current-change="handlePagination" |
| | | @size-change="handlePageSizeChange" |
| | | style="margin-top: 20px;" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | /> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | // ä¿®æ£å¯¼å
¥è·¯å¾ |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | |
| | | export default { |
| | | name: 'DonorStatistics', |
| | | components: { |
| | | FilterPanel, |
| | | EChartsWrapper |
| | | }, |
| | | data() { |
| | | return { |
| | | // æ¥è¯¢åæ° |
| | | query: { |
| | | region: [], |
| | | monthRange: [], |
| | | ageRange: [], |
| | | gender: null, |
| | | bloodType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }, |
| | | |
| | | // çéåæ®µé
ç½® |
| | | filterFields: [ |
| | | { |
| | | label: 'å°åº', |
| | | prop: 'region', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: 'æä»½èå´', |
| | | prop: 'monthRange', |
| | | type: 'daterange', |
| | | dateType: 'monthrange', |
| | | startPlaceholder: 'å¼å§æä»½', |
| | | endPlaceholder: 'ç»ææä»½', |
| | | valueFormat: 'yyyy-MM', |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: '年龿®µ', |
| | | prop: 'ageRange', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: '<17å²', value: '0-17' }, |
| | | { label: '18-49å²', value: '18-49' }, |
| | | { label: '50-69å²', value: '50-69' }, |
| | | { label: '>69å²', value: '70-100' } |
| | | ], |
| | | width: '200px' |
| | | }, |
| | | { |
| | | label: 'æ§å«', |
| | | prop: 'gender', |
| | | type: 'select', |
| | | options: [ |
| | | { label: 'ç·', value: 'ç·' }, |
| | | { label: '女', value: '女' } |
| | | ], |
| | | width: '100px' |
| | | }, |
| | | { |
| | | label: 'è¡å', |
| | | prop: 'bloodType', |
| | | type: 'select', |
| | | options: [ |
| | | { label: 'Aå', value: 'A' }, |
| | | { label: 'Bå', value: 'B' }, |
| | | { label: 'Oå', value: 'O' }, |
| | | { label: 'ABå', value: 'AB' } |
| | | ], |
| | | width: '100px' |
| | | } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | | loading: false, |
| | | |
| | | // æ¦è§æ°æ® |
| | | overviewData: [ |
| | | { title: 'æ»æç®è
æ°', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-user', color: '#409EFF' }, |
| | | { title: 'ç·æ§å æ¯', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-male', color: '#67C23A' }, |
| | | { title: '女æ§å æ¯', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-female', color: '#F56C6C' }, |
| | | { title: 'å¹³åå¹´é¾', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-data', color: '#E6A23C' } |
| | | ], |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | ageChartOptions: {}, |
| | | genderBloodChartOptions: {}, |
| | | categoryChartOptions: {}, |
| | | deathChartOptions: {}, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | total: 0, |
| | | |
| | | // æ¨¡ææ°æ® |
| | | mockData: { |
| | | table: [ |
| | | { id: 1, donorNo: 'D2024001', name: 'å¼ ä¸', gender: 'ç·', age: 45, bloodType: 'A', region: 'éå²å°åº', donationTime: '2024-01-15', chinaCategory: 'ä¸å½ä¸ç±»(DBD)', deathCause: 'èå¤ä¼¤', organs: ['å
¨è', 'å·¦è¾', 'å³è¾'] }, |
| | | { id: 2, donorNo: 'D2024002', name: 'æå', gender: '女', age: 32, bloodType: 'B', region: 'éå²å°åº', donationTime: '2024-01-20', chinaCategory: 'ä¸å½äºç±»(DCD)', deathCause: 'å¿æéª¤å', organs: ['å¿è', 'ç¼è§è'] }, |
| | | { id: 3, donorNo: 'D2024003', name: 'çäº', gender: 'ç·', age: 58, bloodType: 'O', region: 'æ¥ç
§å°åº', donationTime: '2024-02-10', chinaCategory: 'ä¸å½ä¸ç±»(DBCD)', deathCause: 'èè¡ç®¡æå¤', organs: ['å
¨è', 'è°è
º'] }, |
| | | { id: 4, donorNo: 'D2024004', name: 'èµµå
', gender: 'ç·', age: 28, bloodType: 'AB', region: 'æµåå°åº', donationTime: '2024-02-15', chinaCategory: 'ä¸å½ä¸ç±»(DBD)', deathCause: '交éäºæ
', organs: ['å
¨èº', 'å·¦è¾', 'å³è¾'] }, |
| | | { id: 5, donorNo: 'D2024005', name: 'é±ä¸', gender: '女', age: 65, bloodType: 'A', region: 'çå°å°åº', donationTime: '2024-02-20', chinaCategory: 'ä¸å½äºç±»(DCD)', deathCause: 'å¿èæ¢æ»', organs: ['å¿è'] }, |
| | | { id: 6, donorNo: 'D2024006', name: 'åå
«', gender: 'ç·', age: 42, bloodType: 'B', region: '卿µ·å°åº', donationTime: '2024-03-05', chinaCategory: 'ä¸å½ä¸ç±»(DBD)', deathCause: 'èè¿ç¤', organs: ['å·¦è¾', 'å³è¾', 'ç¼è§è'] }, |
| | | { id: 7, donorNo: 'D2024007', name: 'å¨ä¹', gender: 'ç·', age: 37, bloodType: 'O', region: 'æ½åå°åº', donationTime: '2024-03-10', chinaCategory: 'ä¸å½ä¸ç±»(DBCD)', deathCause: 'èå¤ä¼¤', organs: ['èè', 'è°è
º', 'ç¼è§è'] }, |
| | | { id: 8, donorNo: 'D2024008', name: 'å´å', gender: '女', age: 25, bloodType: 'AB', region: '临æ²å°åº', donationTime: '2024-03-15', chinaCategory: 'ä¸å½äºç±»(DCD)', deathCause: '交éäºæ
', organs: ['å¿è', 'èº'] } |
| | | ], |
| | | age: [ |
| | | { ageRange: '<17å²', male: 5, female: 3 }, |
| | | { ageRange: '18-49å²', male: 45, female: 30 }, |
| | | { ageRange: '50-69å²', male: 35, female: 25 }, |
| | | { ageRange: '>69å²', male: 10, female: 8 } |
| | | ], |
| | | gender: [ |
| | | { name: 'ç·æ§', value: 95 }, |
| | | { name: '女æ§', value: 66 } |
| | | ], |
| | | blood: [ |
| | | { name: 'Aå', value: 45 }, |
| | | { name: 'Bå', value: 38 }, |
| | | { name: 'Oå', value: 52 }, |
| | | { name: 'ABå', value: 26 } |
| | | ], |
| | | category: [ |
| | | { name: 'ä¸å½ä¸ç±»(DBD)', value: 85 }, |
| | | { name: 'ä¸å½äºç±»(DCD)', value: 55 }, |
| | | { name: 'ä¸å½ä¸ç±»(DBCD)', value: 21 } |
| | | ], |
| | | death: [ |
| | | { name: 'èå¤ä¼¤', value: 45 }, |
| | | { name: 'å¿æéª¤å', value: 35 }, |
| | | { name: 'èè¡ç®¡æå¤', value: 35 }, |
| | | { name: '交éäºæ
', value: 20 }, |
| | | { name: 'å¿èæ¢æ»', value: 25 }, |
| | | { name: 'å
¶ä»', value: 15 } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | }, |
| | | methods: { |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | const [start, end] = this.query.monthRange; |
| | | filteredData = filteredData.filter(item => |
| | | item.donationTime >= start && item.donationTime <= end |
| | | ); |
| | | } |
| | | |
| | | if (this.query.ageRange.length > 0) { |
| | | filteredData = filteredData.filter(item => { |
| | | const age = item.age; |
| | | return this.query.ageRange.some(range => { |
| | | if (range === '0-17') return age < 18; |
| | | if (range === '18-49') return age >= 18 && age <= 49; |
| | | if (range === '50-69') return age >= 50 && age <= 69; |
| | | if (range === '70-100') return age >= 70; |
| | | return false; |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | if (this.query.gender) { |
| | | filteredData = filteredData.filter(item => item.gender === this.query.gender); |
| | | } |
| | | |
| | | if (this.query.bloodType) { |
| | | filteredData = filteredData.filter(item => item.bloodType === this.query.bloodType); |
| | | } |
| | | |
| | | // å页 |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = filteredData.slice(start, start + this.query.limit); |
| | | this.total = filteredData.length; |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | this.updateOverviewData(filteredData); |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | updateOverviewData(data) { |
| | | if (data.length === 0) return; |
| | | |
| | | const total = data.length; |
| | | const males = data.filter(item => item.gender === 'ç·').length; |
| | | const females = data.filter(item => item.gender === '女').length; |
| | | const avgAge = data.reduce((sum, item) => sum + item.age, 0) / total; |
| | | |
| | | this.overviewData[0].value = total; |
| | | this.overviewData[1].value = `${((males / total) * 100).toFixed(1)}%`; |
| | | this.overviewData[2].value = `${((females / total) * 100).toFixed(1)}%`; |
| | | this.overviewData[3].value = avgAge.toFixed(1); |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // å¹´é¾åå¸å¾ |
| | | this.ageChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['ç·æ§', '女æ§'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: [ |
| | | { |
| | | type: 'value', |
| | | position: 'bottom', |
| | | axisLabel: { |
| | | formatter: function(value) { |
| | | return Math.abs(value); |
| | | } |
| | | } |
| | | } |
| | | ], |
| | | yAxis: [ |
| | | { |
| | | type: 'category', |
| | | axisTick: { |
| | | show: false |
| | | }, |
| | | data: this.mockData.age.map(item => item.ageRange) |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'ç·æ§', |
| | | type: 'bar', |
| | | stack: 'æ§å«', |
| | | data: this.mockData.age.map(item => item.male), |
| | | itemStyle: { color: '#409EFF' } |
| | | }, |
| | | { |
| | | name: '女æ§', |
| | | type: 'bar', |
| | | stack: 'æ§å«', |
| | | data: this.mockData.age.map(item => -item.female), |
| | | itemStyle: { color: '#F56C6C' } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // æ§å«ä¸è¡åç»åå¾ |
| | | this.genderBloodChartOptions = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | data: [ |
| | | 'ç·æ§', '女æ§', |
| | | 'Aå', 'Bå', 'Oå', 'ABå' |
| | | ], |
| | | top: 20 |
| | | }, |
| | | grid: { |
| | | top: '20%', |
| | | bottom: '20%', |
| | | left: '10%', |
| | | right: '10%' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'æ§å«åå¸', |
| | | type: 'pie', |
| | | radius: [0, '30%'], |
| | | center: ['25%', '50%'], |
| | | label: { |
| | | position: 'inner', |
| | | fontSize: 12 |
| | | }, |
| | | data: this.mockData.gender |
| | | }, |
| | | { |
| | | name: 'è¡ååå¸', |
| | | type: 'pie', |
| | | radius: ['40%', '55%'], |
| | | center: ['25%', '50%'], |
| | | data: this.mockData.blood |
| | | }, |
| | | { |
| | | name: 'æ§å«è¡å对æ¯', |
| | | type: 'pie', |
| | | radius: [0, '30%'], |
| | | center: ['75%', '50%'], |
| | | label: { |
| | | position: 'inner', |
| | | fontSize: 12 |
| | | }, |
| | | data: [ |
| | | { name: 'ç·-A', value: 20 }, |
| | | { name: 'ç·-B', value: 15 }, |
| | | { name: 'ç·-O', value: 25 }, |
| | | { name: 'ç·-AB', value: 10 }, |
| | | { name: '女-A', value: 12 }, |
| | | { name: '女-B', value: 8 }, |
| | | { name: '女-O', value: 10 }, |
| | | { name: '女-AB', value: 5 } |
| | | ] |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // ä¸å½åç±»åå¸å¾ |
| | | this.categoryChartOptions = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | right: 10, |
| | | top: 'center', |
| | | data: this.mockData.category.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'ä¸å½åç±»', |
| | | type: 'pie', |
| | | radius: ['30%', '70%'], |
| | | center: ['40%', '50%'], |
| | | roseType: 'radius', |
| | | label: { |
| | | show: true |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true |
| | | } |
| | | }, |
| | | data: this.mockData.category |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // æ»äº¡åå åæå¾ |
| | | this.deathChartOptions = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{b}: {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | left: 10, |
| | | top: 'center', |
| | | data: this.mockData.death.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'æ»äº¡åå ', |
| | | type: 'pie', |
| | | radius: ['30%', '60%'], |
| | | center: ['60%', '50%'], |
| | | avoidLabelOverlap: false, |
| | | label: { |
| | | show: true, |
| | | formatter: '{b|{b}}\n{c|{c} ({d}%)}', |
| | | rich: { |
| | | b: { |
| | | fontSize: 12, |
| | | lineHeight: 20 |
| | | }, |
| | | c: { |
| | | fontSize: 10, |
| | | color: '#999' |
| | | } |
| | | } |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true, |
| | | fontSize: '16', |
| | | fontWeight: 'bold' |
| | | } |
| | | }, |
| | | data: this.mockData.death |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åè¡åæ ç¾ç±»å |
| | | getBloodTypeTag(type) { |
| | | const map = { 'A': 'danger', 'B': 'primary', 'O': 'success', 'AB': 'warning' }; |
| | | return map[type] || 'info'; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | | handleSearch(formData) { |
| | | this.query = { ...this.query, ...formData, page: 1 }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çéç½® |
| | | handleReset() { |
| | | this.query = { |
| | | region: [], |
| | | monthRange: [], |
| | | ageRange: [], |
| | | gender: null, |
| | | bloodType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå页 |
| | | handlePagination(page) { |
| | | this.query.page = page; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå页大å°åå |
| | | handlePageSizeChange(size) { |
| | | this.query.limit = size; |
| | | this.query.page = 1; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | }, |
| | | |
| | | // æ¥çæç®è
详æ
|
| | | viewDonorDetail(row) { |
| | | this.$router.push({ |
| | | path: '/donor/detail', |
| | | query: { id: row.id } |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .statistics-page { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .overview-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .overview-card { |
| | | height: 100%; |
| | | } |
| | | |
| | | .overview-content { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .overview-icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | } |
| | | |
| | | .overview-icon i { |
| | | font-size: 24px; |
| | | color: white; |
| | | } |
| | | |
| | | .overview-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .overview-title { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .overview-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .overview-desc { |
| | | font-size: 12px; |
| | | color: #C0C4CC; |
| | | } |
| | | |
| | | .chart-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/index.vue --> |
| | | <template> |
| | | <div class="statistics-home"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6" v-for="item in menuList" :key="item.path"> |
| | | <el-card |
| | | class="stat-card" |
| | | shadow="hover" |
| | | @click.native="goToPage(item.path)" |
| | | > |
| | | <div class="card-content"> |
| | | <div class="card-icon" :style="{ backgroundColor: item.color }"> |
| | | <i :class="item.icon"></i> |
| | | </div> |
| | | <div class="card-info"> |
| | | <h3>{{ item.title }}</h3> |
| | | <p>{{ item.desc }}</p> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | name: 'StatisticsHome', |
| | | data() { |
| | | return { |
| | | menuList: [ |
| | | { |
| | | title: 'æ¡ä¾ç»è®¡', |
| | | desc: 'å°åºãæä»½ç»´åº¦çæ¡ä¾ç»è®¡åæ', |
| | | icon: 'el-icon-s-data', |
| | | color: '#409EFF', |
| | | path: '/statistics/case' |
| | | }, |
| | | { |
| | | title: 'æç®å¨å®ç»è®¡', |
| | | desc: 'å¨å®è·åãç§»æ¤æ°éç»è®¡', |
| | | icon: 'el-icon-s-claim', |
| | | color: '#67C23A', |
| | | path: '/statistics/organ' |
| | | }, |
| | | { |
| | | title: 'è·åçç»è®¡', |
| | | desc: 'æç®è·åçãå¨å®å¼ç¨çåæ', |
| | | icon: 'el-icon-s-flag', |
| | | color: '#E6A23C', |
| | | path: '/statistics/rate' |
| | | }, |
| | | { |
| | | title: 'æç®ææ¿ç»è®¡', |
| | | desc: 'æç®ææ¿è¶å¿ä¸åå¸åæ', |
| | | icon: 'el-icon-star-on', |
| | | color: '#F56C6C', |
| | | path: '/statistics/willingness' |
| | | }, |
| | | { |
| | | title: 'æç®è
åæ', |
| | | desc: 'å¤ç»´åº¦æç®è
ç¹å¾åæ', |
| | | icon: 'el-icon-user', |
| | | color: '#909399', |
| | | path: '/statistics/donor' |
| | | }, |
| | | { |
| | | title: 'å¨å®è·åå©ç¨', |
| | | desc: 'å¨å®è·åå©ç¨æçåæ', |
| | | icon: 'el-icon-setting', |
| | | color: '#9966CC', |
| | | path: '/statistics/utilization' |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | methods: { |
| | | goToPage(path) { |
| | | this.$router.push(path); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .statistics-home { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .stat-card { |
| | | cursor: pointer; |
| | | margin-bottom: 20px; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .stat-card:hover { |
| | | transform: translateY(-5px); |
| | | box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .card-content { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .card-icon { |
| | | width: 60px; |
| | | height: 60px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 20px; |
| | | } |
| | | |
| | | .card-icon i { |
| | | font-size: 30px; |
| | | color: white; |
| | | } |
| | | |
| | | .card-info h3 { |
| | | margin: 0 0 8px 0; |
| | | color: #303133; |
| | | font-size: 18px; |
| | | } |
| | | |
| | | .card-info p { |
| | | margin: 0; |
| | | color: #909399; |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/organ.vue --> |
| | | <template> |
| | | <div class="statistics-page"> |
| | | <!-- çé颿¿ --> |
| | | <FilterPanel |
| | | :fields="filterFields" |
| | | :default-value="query" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | > |
| | | <template #extra-actions> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">导åº</el-button> |
| | | </template> |
| | | </FilterPanel> |
| | | |
| | | <!-- æ°æ®æ¦è§ --> |
| | | <el-row :gutter="20" class="overview-row"> |
| | | <el-col :span="6" v-for="item in overviewData" :key="item.title"> |
| | | <el-card class="overview-card"> |
| | | <div class="overview-content"> |
| | | <div class="overview-icon" :style="{ backgroundColor: item.color }"> |
| | | <i :class="item.icon"></i> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-title">{{ item.title }}</div> |
| | | <div class="overview-value">{{ item.value }}</div> |
| | | <div class="overview-desc">{{ item.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <el-row :gutter="20" class="chart-row"> |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>å¨å®è·å/ç§»æ¤è¶å¿</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'organ-trend-chart'" |
| | | :options="trendChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>å¨å®ç±»ååå¸</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'organ-distribution-chart'" |
| | | :options="distributionChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æç®å¨å®ç»è®¡æç»</span> |
| | | <el-button |
| | | style="float: right; margin-left: 10px;" |
| | | type="primary" |
| | | icon="el-icon-zoom-in" |
| | | @click="showOrganDetail = true" |
| | | > |
| | | æ¥çå¨å®æç» |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="年份" prop="year" align="center" /> |
| | | <el-table-column label="å¨å®åç§°" prop="organName" align="center" /> |
| | | <el-table-column label="è·åæ°é" prop="acquisitionCount" align="center" /> |
| | | <el-table-column label="ç§»æ¤æ°é" prop="transplantCount" align="center" /> |
| | | <el-table-column label="ç§»æ¤ç" prop="transplantRate" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getRateTag(row.transplantRate)" size="small"> |
| | | {{ row.transplantRate }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¼ç¨æ°é" prop="abandonCount" align="center" /> |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æä½" align="center" fixed="right"> |
| | | <template slot-scope="{ row }"> |
| | | <el-button |
| | | type="text" |
| | | size="mini" |
| | | @click="viewOrganDetail(row)" |
| | | > |
| | | 详æ
|
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <el-pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page.sync="query.page" |
| | | :limit.sync="query.limit" |
| | | @current-change="handlePagination" |
| | | @size-change="handlePageSizeChange" |
| | | style="margin-top: 20px;" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | /> |
| | | </el-card> |
| | | |
| | | <!-- å¨å®æç»å¼¹çª --> |
| | | <el-dialog |
| | | title="å¨å®æç»" |
| | | :visible.sync="showOrganDetail" |
| | | width="80%" |
| | | append-to-body |
| | | > |
| | | <OrganDetail |
| | | :query="detailQuery" |
| | | @close="showOrganDetail = false" |
| | | /> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | import OrganDetail from './components/OrganDetail.vue'; |
| | | |
| | | export default { |
| | | name: 'OrganStatistics', |
| | | components: { |
| | | FilterPanel, |
| | | EChartsWrapper, |
| | | OrganDetail |
| | | }, |
| | | data() { |
| | | return { |
| | | // æ¥è¯¢åæ° |
| | | query: { |
| | | region: [], |
| | | yearRange: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }, |
| | | |
| | | // çéåæ®µé
ç½® |
| | | filterFields: [ |
| | | { |
| | | label: 'å°åº', |
| | | prop: 'region', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: '年份èå´', |
| | | prop: 'yearRange', |
| | | type: 'daterange', |
| | | dateType: 'yearrange', |
| | | startPlaceholder: 'å¼å§å¹´ä»½', |
| | | endPlaceholder: 'ç»æå¹´ä»½', |
| | | valueFormat: 'yyyy', |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: 'å¨å®ç±»å', |
| | | prop: 'organType', |
| | | type: 'select', |
| | | options: [ |
| | | { label: 'å
¨è', value: 'liver' }, |
| | | { label: 'è¾è', value: 'kidney' }, |
| | | { label: 'å¿è', value: 'heart' }, |
| | | { label: 'èº', value: 'lung' }, |
| | | { label: 'è°è
º', value: 'pancreas' }, |
| | | { label: 'ç¼è§è', value: 'cornea' } |
| | | ], |
| | | width: '200px' |
| | | } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | | loading: false, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | total: 0, |
| | | |
| | | // æ¦è§æ°æ® |
| | | overviewData: [ |
| | | { title: 'æ»è·åæ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-claim', color: '#409EFF' }, |
| | | { title: 'æ»ç§»æ¤æ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-success', color: '#67C23A' }, |
| | | { title: 'æ»ç§»æ¤ç', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-data', color: '#E6A23C' }, |
| | | { title: 'æ»å¼ç¨æ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-warning', color: '#F56C6C' } |
| | | ], |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | trendChartOptions: {}, |
| | | distributionChartOptions: {}, |
| | | |
| | | // å¼¹çªæ§å¶ |
| | | showOrganDetail: false, |
| | | detailQuery: {}, |
| | | |
| | | // æ¨¡ææ°æ® |
| | | mockData: { |
| | | table: [ |
| | | { id: 1, year: '2024', organName: 'å
¨è', acquisitionCount: 15, transplantCount: 12, transplantRate: 80.0, abandonCount: 3, region: 'éå²å°åº' }, |
| | | { id: 2, year: '2024', organName: 'è¾è', acquisitionCount: 20, transplantCount: 18, transplantRate: 90.0, abandonCount: 2, region: 'éå²å°åº' }, |
| | | { id: 3, year: '2024', organName: 'å
¨è', acquisitionCount: 8, transplantCount: 6, transplantRate: 75.0, abandonCount: 2, region: 'æ¥ç
§å°åº' }, |
| | | { id: 4, year: '2024', organName: 'å¿è', acquisitionCount: 5, transplantCount: 4, transplantRate: 80.0, abandonCount: 1, region: 'éå²å°åº' }, |
| | | { id: 5, year: '2023', organName: 'å
¨è', acquisitionCount: 12, transplantCount: 10, transplantRate: 83.3, abandonCount: 2, region: 'éå²å°åº' }, |
| | | { id: 6, year: '2023', organName: 'ç¼è§è', acquisitionCount: 25, transplantCount: 22, transplantRate: 88.0, abandonCount: 3, region: 'æµåå°åº' } |
| | | ], |
| | | trend: [ |
| | | { year: '2020', acquisition: 120, transplant: 100 }, |
| | | { year: '2021', acquisition: 150, transplant: 130 }, |
| | | { year: '2022', acquisition: 180, transplant: 160 }, |
| | | { year: '2023', acquisition: 220, transplant: 200 }, |
| | | { year: '2024', acquisition: 250, transplant: 230 } |
| | | ], |
| | | distribution: [ |
| | | { name: 'å
¨è', value: 85 }, |
| | | { name: 'è¾è', value: 120 }, |
| | | { name: 'å¿è', value: 45 }, |
| | | { name: 'èº', value: 60 }, |
| | | { name: 'è°è
º', value: 30 }, |
| | | { name: 'ç¼è§è', value: 200 } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | }, |
| | | methods: { |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | |
| | | if (this.query.yearRange && this.query.yearRange.length === 2) { |
| | | const [start, end] = this.query.yearRange; |
| | | filteredData = filteredData.filter(item => |
| | | parseInt(item.year) >= parseInt(start) && parseInt(item.year) <= parseInt(end) |
| | | ); |
| | | } |
| | | |
| | | if (this.query.organType) { |
| | | const organMap = { liver: 'è', kidney: 'è¾', heart: 'å¿è', lung: 'èº', pancreas: 'è°è
º', cornea: 'ç¼è§è' }; |
| | | const organName = organMap[this.query.organType]; |
| | | if (organName) { |
| | | filteredData = filteredData.filter(item => item.organName.includes(organName)); |
| | | } |
| | | } |
| | | |
| | | // å页 |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = filteredData.slice(start, start + this.query.limit); |
| | | this.total = filteredData.length; |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | this.updateOverviewData(filteredData); |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | updateOverviewData(data) { |
| | | if (data.length === 0) return; |
| | | |
| | | const totalAcquisition = data.reduce((sum, item) => sum + item.acquisitionCount, 0); |
| | | const totalTransplant = data.reduce((sum, item) => sum + item.transplantCount, 0); |
| | | const totalAbandon = data.reduce((sum, item) => sum + item.abandonCount, 0); |
| | | const transplantRate = totalAcquisition > 0 ? (totalTransplant / totalAcquisition * 100).toFixed(1) : 0; |
| | | |
| | | this.overviewData[0].value = totalAcquisition; |
| | | this.overviewData[1].value = totalTransplant; |
| | | this.overviewData[2].value = `${transplantRate}%`; |
| | | this.overviewData[3].value = totalAbandon; |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // è¶å¿å¾ |
| | | this.trendChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['è·åæ°é', 'ç§»æ¤æ°é'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: this.mockData.trend.map(item => item.year + 'å¹´') |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: 'æ°é(个)' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'è·åæ°é', |
| | | type: 'bar', |
| | | data: this.mockData.trend.map(item => item.acquisition), |
| | | itemStyle: { color: '#409EFF' } |
| | | }, |
| | | { |
| | | name: 'ç§»æ¤æ°é', |
| | | type: 'bar', |
| | | data: this.mockData.trend.map(item => item.transplant), |
| | | itemStyle: { color: '#67C23A' } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // åå¸å¾ |
| | | this.distributionChartOptions = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | left: 'left', |
| | | data: this.mockData.distribution.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'å¨å®åå¸', |
| | | type: 'pie', |
| | | radius: '55%', |
| | | center: ['50%', '60%'], |
| | | data: this.mockData.distribution, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: 'rgba(0, 0, 0, 0.5)' |
| | | } |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åç§»æ¤çæ ç¾ç±»å |
| | | getRateTag(rate) { |
| | | if (rate >= 85) return 'success'; |
| | | if (rate >= 75) return 'warning'; |
| | | return 'danger'; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | | handleSearch(formData) { |
| | | this.query = { ...this.query, ...formData, page: 1 }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çéç½® |
| | | handleReset() { |
| | | this.query = { |
| | | region: [], |
| | | yearRange: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå页 |
| | | handlePagination(page) { |
| | | this.query.page = page; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå页大å°åå |
| | | handlePageSizeChange(size) { |
| | | this.query.limit = size; |
| | | this.query.page = 1; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | }, |
| | | |
| | | // æ¥çå¨å®è¯¦æ
|
| | | viewOrganDetail(row) { |
| | | this.detailQuery = { |
| | | year: row.year, |
| | | organType: row.organName, |
| | | region: row.region |
| | | }; |
| | | this.showOrganDetail = true; |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .statistics-page { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .filter-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .filter-form { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .overview-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .overview-card { |
| | | height: 100%; |
| | | } |
| | | |
| | | .overview-content { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .overview-icon { |
| | | width: 50px; |
| | | height: 50px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | } |
| | | |
| | | .overview-icon i { |
| | | font-size: 24px; |
| | | color: white; |
| | | } |
| | | |
| | | .overview-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .overview-title { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .overview-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .overview-desc { |
| | | font-size: 12px; |
| | | color: #C0C4CC; |
| | | } |
| | | |
| | | .chart-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/rate.vue --> |
| | | <template> |
| | | <div class="statistics-page"> |
| | | <!-- çé颿¿ --> |
| | | <FilterPanel |
| | | :fields="filterFields" |
| | | :default-value="query" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | > |
| | | <template #extra-actions> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">导åº</el-button> |
| | | </template> |
| | | </FilterPanel> |
| | | |
| | | <!-- æ ¸å¿ææ 仪表ç --> |
| | | <el-row :gutter="20" class="dashboard-row"> |
| | | <el-col :span="8" v-for="item in dashboardData" :key="item.title"> |
| | | <el-card class="dashboard-card"> |
| | | <div class="dashboard-title">{{ item.title }}</div> |
| | | <div class="dashboard-value">{{ item.value }}</div> |
| | | <div class="dashboard-chart"> |
| | | <EChartsWrapper |
| | | :id="`dashboard-${item.key}`" |
| | | :options="getDashboardOptions(item)" |
| | | height="150px" |
| | | /> |
| | | </div> |
| | | <div class="dashboard-trend"> |
| | | <span :class="item.trendClass"> |
| | | <i :class="item.trendIcon"></i> {{ item.trendText }} |
| | | </span> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <el-row :gutter="20" class="chart-row"> |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>è·åçè¶å¿åæ</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'rate-trend-chart'" |
| | | :options="trendChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>å¼ç¨çææåæ</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'abandon-chart'" |
| | | :options="abandonChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>è·åçç»è®¡æç»</span> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="年份" prop="year" align="center" /> |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æç®è·åç" prop="acquisitionRate" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getRateTag(row.acquisitionRate, 'acquisition')" size="small"> |
| | | {{ row.acquisitionRate }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¹³åå¨å®æ°" prop="avgOrgans" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getRateTag(row.avgOrgans, 'avg')" size="small"> |
| | | {{ row.avgOrgans }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¨å®å¼ç¨ç" prop="abandonRate" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getRateTag(row.abandonRate, 'abandon')" size="small"> |
| | | {{ row.abandonRate }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æç®æ»æ°" prop="totalDonation" align="center" /> |
| | | <el-table-column label="å¨å®æ»æ°" prop="totalOrgans" align="center" /> |
| | | <el-table-column label="ç§»æ¤æ»æ°" prop="totalTransplants" align="center" /> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | |
| | | export default { |
| | | name: 'RateStatistics', |
| | | components: { |
| | | FilterPanel, |
| | | EChartsWrapper |
| | | }, |
| | | data() { |
| | | return { |
| | | // æ¥è¯¢åæ° |
| | | query: { |
| | | region: [], |
| | | yearRange: [], |
| | | page: 1, |
| | | limit: 10 |
| | | }, |
| | | |
| | | // çéåæ®µé
ç½® |
| | | filterFields: [ |
| | | { |
| | | label: 'å°åº', |
| | | prop: 'region', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: '年份èå´', |
| | | prop: 'yearRange', |
| | | type: 'daterange', |
| | | dateType: 'yearrange', |
| | | startPlaceholder: 'å¼å§å¹´ä»½', |
| | | endPlaceholder: 'ç»æå¹´ä»½', |
| | | valueFormat: 'yyyy', |
| | | width: '300px' |
| | | } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | | loading: false, |
| | | |
| | | // ä»ªè¡¨çæ°æ® |
| | | dashboardData: [ |
| | | { |
| | | key: 'acquisition', |
| | | title: 'æç®è·åç', |
| | | value: '85.5%', |
| | | trendIcon: 'el-icon-top', |
| | | trendText: 'â2.7%', |
| | | trendClass: 'up' |
| | | }, |
| | | { |
| | | key: 'avg', |
| | | title: 'å¹³åå¨å®æ°', |
| | | value: '3.2', |
| | | trendIcon: 'el-icon-top', |
| | | trendText: 'â0.2', |
| | | trendClass: 'up' |
| | | }, |
| | | { |
| | | key: 'abandon', |
| | | title: 'å¨å®å¼ç¨ç', |
| | | value: '12.3%', |
| | | trendIcon: 'el-icon-bottom', |
| | | trendText: 'â2.7%', |
| | | trendClass: 'down' |
| | | } |
| | | ], |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | trendChartOptions: {}, |
| | | abandonChartOptions: {}, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | total: 0, |
| | | |
| | | // æ¨¡ææ°æ® |
| | | mockData: { |
| | | table: [ |
| | | { id: 1, year: '2024', region: 'éå²å°åº', acquisitionRate: 85.5, avgOrgans: 3.2, abandonRate: 12.3, totalDonation: 280, totalOrgans: 896, totalTransplants: 765 }, |
| | | { id: 2, year: '2024', region: 'æ¥ç
§å°åº', acquisitionRate: 80.3, avgOrgans: 2.9, abandonRate: 15.8, totalDonation: 140, totalOrgans: 406, totalTransplants: 325 }, |
| | | { id: 3, year: '2024', region: 'æµåå°åº', acquisitionRate: 89.1, avgOrgans: 3.4, abandonRate: 9.5, totalDonation: 320, totalOrgans: 1088, totalTransplants: 970 }, |
| | | { id: 4, year: '2023', region: 'éå²å°åº', acquisitionRate: 82.8, avgOrgans: 3.0, abandonRate: 15.0, totalDonation: 250, totalOrgans: 750, totalTransplants: 620 }, |
| | | { id: 5, year: '2023', region: 'æ¥ç
§å°åº', acquisitionRate: 78.5, avgOrgans: 2.7, abandonRate: 18.2, totalDonation: 120, totalOrgans: 324, totalTransplants: 252 }, |
| | | { id: 6, year: '2023', region: 'æµåå°åº', acquisitionRate: 85.2, avgOrgans: 3.1, abandonRate: 12.5, totalDonation: 280, totalOrgans: 868, totalTransplants: 740 } |
| | | ], |
| | | trend: [ |
| | | { year: '2020', acquisition: 78.5, avgOrgans: 2.8, abandon: 18.5 }, |
| | | { year: '2021', acquisition: 81.2, avgOrgans: 3.0, abandon: 16.3 }, |
| | | { year: '2022', acquisition: 84.6, avgOrgans: 3.2, abandon: 13.8 }, |
| | | { year: '2023', acquisition: 87.3, avgOrgans: 3.4, abandon: 11.2 }, |
| | | { year: '2024', acquisition: 89.1, avgOrgans: 3.5, abandon: 9.5 } |
| | | ], |
| | | abandon: [ |
| | | { name: 'å¨å®è´¨éä¸åæ ¼', value: 45 }, |
| | | { name: 'ä¾ä½ç¾ç
', value: 25 }, |
| | | { name: 'è¿è¾é®é¢', value: 15 }, |
| | | { name: 'åä½å¹é
é®é¢', value: 10 }, |
| | | { name: 'å
¶ä»åå ', value: 5 } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | }, |
| | | methods: { |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | |
| | | if (this.query.yearRange && this.query.yearRange.length === 2) { |
| | | const [start, end] = this.query.yearRange; |
| | | filteredData = filteredData.filter(item => |
| | | parseInt(item.year) >= parseInt(start) && parseInt(item.year) <= parseInt(end) |
| | | ); |
| | | } |
| | | |
| | | this.tableData = filteredData; |
| | | this.total = filteredData.length; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // è¶å¿å¾ |
| | | this.trendChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross', |
| | | label: { |
| | | backgroundColor: '#6a7985' |
| | | } |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æç®è·åç', 'å¹³åå¨å®æ°', 'å¨å®å¼ç¨ç'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: this.mockData.trend.map(item => item.year + 'å¹´') |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | axisLabel: { |
| | | formatter: '{value}' |
| | | } |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'æç®è·åç', |
| | | type: 'line', |
| | | data: this.mockData.trend.map(item => item.acquisition), |
| | | itemStyle: { color: '#409EFF' }, |
| | | smooth: true |
| | | }, |
| | | { |
| | | name: 'å¹³åå¨å®æ°', |
| | | type: 'line', |
| | | data: this.mockData.trend.map(item => item.avgOrgans * 20), // ç¼©æ¾æ¾ç¤º |
| | | itemStyle: { color: '#67C23A' }, |
| | | smooth: true |
| | | }, |
| | | { |
| | | name: 'å¨å®å¼ç¨ç', |
| | | type: 'line', |
| | | data: this.mockData.trend.map(item => item.abandon), |
| | | itemStyle: { color: '#F56C6C' }, |
| | | smooth: true |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // å¼ç¨çææå¾ |
| | | this.abandonChartOptions = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | left: 'left', |
| | | data: this.mockData.abandon.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'å¼ç¨åå ', |
| | | type: 'pie', |
| | | radius: ['40%', '70%'], |
| | | avoidLabelOverlap: false, |
| | | label: { |
| | | show: false, |
| | | position: 'center' |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true, |
| | | fontSize: '20', |
| | | fontWeight: 'bold' |
| | | } |
| | | }, |
| | | labelLine: { |
| | | show: false |
| | | }, |
| | | data: this.mockData.abandon |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·å仪表çå¾è¡¨é
ç½® |
| | | getDashboardOptions(item) { |
| | | const value = parseFloat(item.value); |
| | | const max = item.key === 'avg' ? 5 : 100; |
| | | const data = [{ value, name: item.title }]; |
| | | |
| | | return { |
| | | tooltip: { |
| | | formatter: '{b}: {c}' + (item.key === 'abandon' ? '%' : item.key === 'acquisition' ? '%' : '') |
| | | }, |
| | | series: [ |
| | | { |
| | | name: item.title, |
| | | type: 'gauge', |
| | | progress: { |
| | | show: true, |
| | | width: 18 |
| | | }, |
| | | axisLine: { |
| | | lineStyle: { |
| | | width: 18 |
| | | } |
| | | }, |
| | | axisTick: { |
| | | show: false |
| | | }, |
| | | splitLine: { |
| | | length: 15, |
| | | lineStyle: { |
| | | width: 2, |
| | | color: '#999' |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | distance: 25, |
| | | color: '#999', |
| | | fontSize: 12 |
| | | }, |
| | | anchor: { |
| | | show: true, |
| | | showAbove: true, |
| | | size: 25, |
| | | itemStyle: { |
| | | borderWidth: 10 |
| | | } |
| | | }, |
| | | title: { |
| | | show: false |
| | | }, |
| | | detail: { |
| | | valueAnimation: true, |
| | | fontSize: 20, |
| | | offsetCenter: [0, '70%'] |
| | | }, |
| | | data: data, |
| | | max: max |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åæ ç¾ç±»å |
| | | getRateTag(value, type) { |
| | | if (type === 'acquisition') { |
| | | if (value >= 85) return 'success'; |
| | | if (value >= 80) return 'warning'; |
| | | return 'danger'; |
| | | } else if (type === 'avg') { |
| | | if (value >= 3.2) return 'success'; |
| | | if (value >= 2.8) return 'warning'; |
| | | return 'danger'; |
| | | } else if (type === 'abandon') { |
| | | if (value <= 10) return 'success'; |
| | | if (value <= 15) return 'warning'; |
| | | return 'danger'; |
| | | } |
| | | return ''; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | | handleSearch(formData) { |
| | | this.query = { ...this.query, ...formData, page: 1 }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çéç½® |
| | | handleReset() { |
| | | this.query = { |
| | | region: [], |
| | | yearRange: [], |
| | | page: 1, |
| | | limit: 10 |
| | | }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .dashboard-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .dashboard-card { |
| | | text-align: center; |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .dashboard-title { |
| | | font-size: 16px; |
| | | color: #606266; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .dashboard-value { |
| | | font-size: 28px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin: 10px 0; |
| | | } |
| | | |
| | | .dashboard-chart { |
| | | height: 150px; |
| | | margin: 20px 0; |
| | | } |
| | | |
| | | .dashboard-trend { |
| | | font-size: 14px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .dashboard-trend .up { |
| | | color: #67C23A; |
| | | } |
| | | |
| | | .dashboard-trend .down { |
| | | color: #F56C6C; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/utilization.vue --> |
| | | <template> |
| | | <div class="statistics-page"> |
| | | <!-- çé颿¿ --> |
| | | <FilterPanel |
| | | :fields="filterFields" |
| | | :default-value="query" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | > |
| | | <template #extra-actions> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">导åº</el-button> |
| | | </template> |
| | | </FilterPanel> |
| | | |
| | | <!-- 第ä¸é¨åï¼å¨å®è·å/ç§»æ¤ç»è®¡ --> |
| | | <el-card style="margin-bottom: 20px;"> |
| | | <div slot="header" class="clearfix"> |
| | | <h3>1. å¨å®è·å/ç§»æ¤æ°éç»è®¡</h3> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'acquisition-transplant-chart'" |
| | | :options="acquisitionChartOptions" |
| | | height="500px" |
| | | /> |
| | | </el-card> |
| | | |
| | | <!-- 第äºé¨åï¼å¨å®å¼ç¨åå åæ --> |
| | | <el-card style="margin-bottom: 20px;"> |
| | | <div slot="header" class="clearfix"> |
| | | <h3>2. å¨å®å¼ç¨åå åæ</h3> |
| | | <el-form :inline="true" style="float: right;"> |
| | | <el-form-item label="å¨å®ç±»å" style="margin-bottom: 0;"> |
| | | <el-select |
| | | v-model="abandonQuery.organType" |
| | | placeholder="è¯·éæ©å¨å®ç±»å" |
| | | clearable |
| | | size="mini" |
| | | style="width: 150px" |
| | | @change="handleAbandonQuery" |
| | | > |
| | | <el-option |
| | | v-for="item in organOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'abandon-analysis-chart'" |
| | | :options="abandonChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | |
| | | <!-- 第ä¸é¨åï¼æ¯ä¾ä½å¨å®è·åæ°é --> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <h3>3. æ¯ä¾ä½å¨å®è·åæ°é</h3> |
| | | <el-form :inline="true" style="float: right;"> |
| | | <el-form-item label="年份" style="margin-bottom: 0;"> |
| | | <el-date-picker |
| | | v-model="perDonorQuery.year" |
| | | type="year" |
| | | placeholder="鿩年份" |
| | | value-format="yyyy" |
| | | size="mini" |
| | | style="width: 120px" |
| | | @change="handlePerDonorQuery" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å¨å®ç±»å" style="margin-bottom: 0;"> |
| | | <el-select |
| | | v-model="perDonorQuery.organType" |
| | | placeholder="è¯·éæ©å¨å®ç±»å" |
| | | clearable |
| | | size="mini" |
| | | style="width: 150px" |
| | | @change="handlePerDonorQuery" |
| | | > |
| | | <el-option |
| | | v-for="item in organOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="年份" prop="year" align="center" /> |
| | | <el-table-column label="ä¾ä½å§å" prop="donorName" align="center" /> |
| | | <el-table-column label="è·åæ°é" prop="acquisitionCount" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getCountTag(row.acquisitionCount)" size="small"> |
| | | {{ row.acquisitionCount }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¨è" prop="liver" align="center" /> |
| | | <el-table-column label="å·¦åè" prop="leftLiver" align="center" /> |
| | | <el-table-column label="å³åè" prop="rightLiver" align="center" /> |
| | | <el-table-column label="å¿è" prop="heart" align="center" /> |
| | | <el-table-column label="å°è " prop="intestine" align="center" /> |
| | | <el-table-column label="è°è
º" prop="pancreas" align="center" /> |
| | | <el-table-column label="å·¦è¾" prop="leftKidney" align="center" /> |
| | | <el-table-column label="å³è¾" prop="rightKidney" align="center" /> |
| | | <el-table-column label="å
¨èº" prop="fullLung" align="center" /> |
| | | <el-table-column label="å·¦èº" prop="leftLung" align="center" /> |
| | | <el-table-column label="å³èº" prop="rightLung" align="center" /> |
| | | <el-table-column label="å·¦ç¼è§è" prop="leftCornea" align="center" /> |
| | | <el-table-column label="å³ç¼è§è" prop="rightCornea" align="center" /> |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æä½" align="center" fixed="right"> |
| | | <template slot-scope="{ row }"> |
| | | <el-button |
| | | type="text" |
| | | size="mini" |
| | | @click="viewDonorDetail(row)" |
| | | > |
| | | 详æ
|
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <el-pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page.sync="query.page" |
| | | :limit.sync="query.limit" |
| | | @current-change="handlePagination" |
| | | @size-change="handlePageSizeChange" |
| | | style="margin-top: 20px;" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | /> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | |
| | | export default { |
| | | name: 'OrganUtilization', |
| | | components: { |
| | | FilterPanel, |
| | | EChartsWrapper |
| | | }, |
| | | data() { |
| | | return { |
| | | // æ¥è¯¢åæ° |
| | | query: { |
| | | region: [], |
| | | monthRange: [], |
| | | ageRange: [], |
| | | chinaCategory: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }, |
| | | |
| | | // çéåæ®µé
ç½® |
| | | filterFields: [ |
| | | { |
| | | label: 'å°åº', |
| | | prop: 'region', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: 'æä»½èå´', |
| | | prop: 'monthRange', |
| | | type: 'daterange', |
| | | dateType: 'monthrange', |
| | | startPlaceholder: 'å¼å§æä»½', |
| | | endPlaceholder: 'ç»ææä»½', |
| | | valueFormat: 'yyyy-MM', |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: '年龿®µ', |
| | | prop: 'ageRange', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: '<17å²', value: '0-17' }, |
| | | { label: '18-49å²', value: '18-49' }, |
| | | { label: '50-69å²', value: '50-69' }, |
| | | { label: '>69å²', value: '70-100' } |
| | | ], |
| | | width: '200px' |
| | | }, |
| | | { |
| | | label: 'ä¸å½åç±»', |
| | | prop: 'chinaCategory', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: 'ä¸å½ä¸ç±»(DBD)', value: '1' }, |
| | | { label: 'ä¸å½äºç±»(DCD)', value: '2' }, |
| | | { label: 'ä¸å½ä¸ç±»(DBCD)', value: '3' } |
| | | ], |
| | | width: '200px' |
| | | }, |
| | | { |
| | | label: 'å¨å®ç±»å', |
| | | prop: 'organType', |
| | | type: 'select', |
| | | options: [ |
| | | { label: 'å
¨è', value: 'liver' }, |
| | | { label: 'è¾è', value: 'kidney' }, |
| | | { label: 'å¿è', value: 'heart' }, |
| | | { label: 'èº', value: 'lung' }, |
| | | { label: 'ç¼è§è', value: 'cornea' } |
| | | ], |
| | | width: '200px' |
| | | } |
| | | ], |
| | | |
| | | // å¨å®é项 |
| | | organOptions: [ |
| | | { label: 'å
¨è', value: 'liver' }, |
| | | { label: 'å·¦åè', value: 'leftLiver' }, |
| | | { label: 'å³åè', value: 'rightLiver' }, |
| | | { label: 'å¿è', value: 'heart' }, |
| | | { label: 'å°è ', value: 'intestine' }, |
| | | { label: 'è°è
º', value: 'pancreas' }, |
| | | { label: 'å·¦è¾', value: 'leftKidney' }, |
| | | { label: 'å³è¾', value: 'rightKidney' }, |
| | | { label: 'å
¨èº', value: 'fullLung' }, |
| | | { label: 'å·¦èº', value: 'leftLung' }, |
| | | { label: 'å³èº', value: 'rightLung' }, |
| | | { label: 'å·¦ç¼è§è', value: 'leftCornea' }, |
| | | { label: 'å³ç¼è§è', value: 'rightCornea' } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | | loading: false, |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | acquisitionChartOptions: {}, |
| | | abandonChartOptions: {}, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | total: 0, |
| | | |
| | | // å¼ç¨åææ¥è¯¢åæ° |
| | | abandonQuery: { |
| | | organType: null |
| | | }, |
| | | |
| | | // æ¯ä¾ä½æ¥è¯¢åæ° |
| | | perDonorQuery: { |
| | | year: '2024', |
| | | organType: null |
| | | }, |
| | | |
| | | // æ¨¡ææ°æ® |
| | | mockData: { |
| | | acquisition: [ |
| | | { |
| | | month: '2024-01', |
| | | region: 'éå²å°åº', |
| | | liver: { acquisition: 8, transplant: 7 }, |
| | | kidney: { acquisition: 12, transplant: 10 }, |
| | | heart: { acquisition: 3, transplant: 3 }, |
| | | lung: { acquisition: 5, transplant: 4 }, |
| | | cornea: { acquisition: 20, transplant: 18 } |
| | | }, |
| | | { |
| | | month: '2024-02', |
| | | region: 'éå²å°åº', |
| | | liver: { acquisition: 10, transplant: 9 }, |
| | | kidney: { acquisition: 15, transplant: 13 }, |
| | | heart: { acquisition: 4, transplant: 3 }, |
| | | lung: { acquisition: 6, transplant: 5 }, |
| | | cornea: { acquisition: 25, transplant: 22 } |
| | | }, |
| | | { |
| | | month: '2024-01', |
| | | region: 'æ¥ç
§å°åº', |
| | | liver: { acquisition: 5, transplant: 4 }, |
| | | kidney: { acquisition: 8, transplant: 7 }, |
| | | heart: { acquisition: 2, transplant: 2 }, |
| | | lung: { acquisition: 3, transplant: 2 }, |
| | | cornea: { acquisition: 15, transplant: 13 } |
| | | } |
| | | ], |
| | | abandon: [ |
| | | { name: 'å¨å®è´¨éä¸åæ ¼', value: 45 }, |
| | | { name: 'ä¾ä½ç¾ç
', value: 25 }, |
| | | { name: 'è¿è¾é®é¢', value: 15 }, |
| | | { name: 'åä½å¹é
é®é¢', value: 10 }, |
| | | { name: 'å
¶ä»åå ', value: 5 } |
| | | ], |
| | | table: [ |
| | | { |
| | | id: 1, |
| | | year: '2024', |
| | | donorName: 'å¼ ä¸', |
| | | acquisitionCount: 5, |
| | | liver: 1, |
| | | leftLiver: 0, |
| | | rightLiver: 0, |
| | | heart: 0, |
| | | intestine: 0, |
| | | pancreas: 0, |
| | | leftKidney: 1, |
| | | rightKidney: 1, |
| | | fullLung: 0, |
| | | leftLung: 0, |
| | | rightLung: 0, |
| | | leftCornea: 1, |
| | | rightCornea: 1, |
| | | region: 'éå²å°åº' |
| | | }, |
| | | { |
| | | id: 2, |
| | | year: '2024', |
| | | donorName: 'æå', |
| | | acquisitionCount: 3, |
| | | liver: 0, |
| | | leftLiver: 0, |
| | | rightLiver: 0, |
| | | heart: 1, |
| | | intestine: 0, |
| | | pancreas: 0, |
| | | leftKidney: 0, |
| | | rightKidney: 0, |
| | | fullLung: 0, |
| | | leftLung: 1, |
| | | rightLung: 1, |
| | | leftCornea: 0, |
| | | rightCornea: 0, |
| | | region: 'éå²å°åº' |
| | | }, |
| | | { |
| | | id: 3, |
| | | year: '2024', |
| | | donorName: 'çäº', |
| | | acquisitionCount: 6, |
| | | liver: 1, |
| | | leftLiver: 0, |
| | | rightLiver: 0, |
| | | heart: 0, |
| | | intestine: 0, |
| | | pancreas: 1, |
| | | leftKidney: 1, |
| | | rightKidney: 1, |
| | | fullLung: 0, |
| | | leftLung: 0, |
| | | rightLung: 0, |
| | | leftCornea: 1, |
| | | rightCornea: 1, |
| | | region: 'æ¥ç
§å°åº' |
| | | }, |
| | | { |
| | | id: 4, |
| | | year: '2024', |
| | | donorName: 'èµµå
', |
| | | acquisitionCount: 4, |
| | | liver: 0, |
| | | leftLiver: 0, |
| | | rightLiver: 0, |
| | | heart: 0, |
| | | intestine: 0, |
| | | pancreas: 0, |
| | | leftKidney: 1, |
| | | rightKidney: 1, |
| | | fullLung: 0, |
| | | leftLung: 0, |
| | | rightLung: 0, |
| | | leftCornea: 1, |
| | | rightCornea: 1, |
| | | region: 'æµåå°åº' |
| | | }, |
| | | { |
| | | id: 5, |
| | | year: '2023', |
| | | donorName: 'é±ä¸', |
| | | acquisitionCount: 3, |
| | | liver: 0, |
| | | leftLiver: 0, |
| | | rightLiver: 0, |
| | | heart: 1, |
| | | intestine: 0, |
| | | pancreas: 0, |
| | | leftKidney: 0, |
| | | rightKidney: 0, |
| | | fullLung: 1, |
| | | leftLung: 0, |
| | | rightLung: 1, |
| | | leftCornea: 0, |
| | | rightCornea: 0, |
| | | region: 'çå°å°åº' |
| | | } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | }, |
| | | methods: { |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | // è¿éå¯ä»¥æ·»å æä»½çéé»è¾ |
| | | } |
| | | |
| | | if (this.query.ageRange.length > 0) { |
| | | // è¿éå¯ä»¥æ·»å å¹´é¾çéé»è¾ |
| | | } |
| | | |
| | | if (this.query.chinaCategory.length > 0) { |
| | | // è¿éå¯ä»¥æ·»å ä¸å½åç±»çéé»è¾ |
| | | } |
| | | |
| | | if (this.query.organType) { |
| | | const organMap = { liver: 'liver', kidney: ['leftKidney', 'rightKidney'], heart: 'heart', lung: ['fullLung', 'leftLung', 'rightLung'], cornea: ['leftCornea', 'rightCornea'] }; |
| | | const organProp = organMap[this.query.organType]; |
| | | if (organProp) { |
| | | if (Array.isArray(organProp)) { |
| | | filteredData = filteredData.filter(item => |
| | | organProp.some(prop => item[prop] > 0) |
| | | ); |
| | | } else { |
| | | filteredData = filteredData.filter(item => item[organProp] > 0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // å页 |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = filteredData.slice(start, start + this.query.limit); |
| | | this.total = filteredData.length; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // è·åç§»æ¤ç»è®¡å¾ |
| | | const regions = [...new Set(this.mockData.acquisition.map(item => item.region))]; |
| | | const months = [...new Set(this.mockData.acquisition.map(item => item.month))]; |
| | | const organTypes = ['liver', 'kidney', 'heart', 'lung', 'cornea']; |
| | | const organNames = ['èè', 'è¾è', 'å¿è', 'èº', 'ç¼è§è']; |
| | | |
| | | const seriesData = []; |
| | | const xAxisData = []; |
| | | |
| | | // çæxè½´æ°æ® |
| | | months.forEach(month => { |
| | | regions.forEach(region => { |
| | | xAxisData.push(`${month}\n${region}`); |
| | | }); |
| | | }); |
| | | |
| | | // çæç³»åæ°æ® |
| | | organTypes.forEach((organType, index) => { |
| | | const data = []; |
| | | months.forEach(month => { |
| | | regions.forEach(region => { |
| | | const item = this.mockData.acquisition.find(d => d.month === month && d.region === region); |
| | | if (item) { |
| | | data.push(item[organType]?.acquisition || 0); |
| | | } else { |
| | | data.push(0); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | seriesData.push({ |
| | | name: `${organNames[index]}-è·å`, |
| | | type: 'bar', |
| | | stack: 'æ»é', |
| | | data: data, |
| | | itemStyle: this.getSeriesColor(index * 2) |
| | | }); |
| | | |
| | | const transplantData = []; |
| | | months.forEach(month => { |
| | | regions.forEach(region => { |
| | | const item = this.mockData.acquisition.find(d => d.month === month && d.region === region); |
| | | if (item) { |
| | | transplantData.push(item[organType]?.transplant || 0); |
| | | } else { |
| | | transplantData.push(0); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | seriesData.push({ |
| | | name: `${organNames[index]}-ç§»æ¤`, |
| | | type: 'bar', |
| | | stack: 'æ»é', |
| | | data: transplantData, |
| | | itemStyle: this.getSeriesColor(index * 2 + 1) |
| | | }); |
| | | }); |
| | | |
| | | this.acquisitionChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: seriesData.map(item => item.name), |
| | | top: 20 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: xAxisData, |
| | | axisLabel: { |
| | | interval: 0, |
| | | rotate: 45 |
| | | } |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: 'æ°é(个)' |
| | | }, |
| | | series: seriesData |
| | | }; |
| | | |
| | | // å¼ç¨åæå¾ |
| | | this.abandonChartOptions = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | left: 'left', |
| | | data: this.mockData.abandon.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'å¼ç¨åå ', |
| | | type: 'pie', |
| | | radius: '55%', |
| | | center: ['50%', '60%'], |
| | | data: this.mockData.abandon, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: 'rgba(0, 0, 0, 0.5)' |
| | | } |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åç³»åé¢è² |
| | | getSeriesColor(index) { |
| | | const colors = [ |
| | | '#409EFF', '#8cc5ff', // èè |
| | | '#67C23A', '#95d475', // è¾è |
| | | '#E6A23C', '#f3c96b', // å¿è |
| | | '#F56C6C', '#f89898', // èº |
| | | '#909399', '#b1b3b8' // ç¼è§è |
| | | ]; |
| | | return { color: colors[index % colors.length] }; |
| | | }, |
| | | |
| | | // è·åæ°éæ ç¾ |
| | | getCountTag(count) { |
| | | if (count >= 5) return 'success'; |
| | | if (count >= 3) return 'warning'; |
| | | return 'danger'; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | | handleSearch(formData) { |
| | | this.query = { ...this.query, ...formData, page: 1 }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çéç½® |
| | | handleReset() { |
| | | this.query = { |
| | | region: [], |
| | | monthRange: [], |
| | | ageRange: [], |
| | | chinaCategory: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå页 |
| | | handlePagination(page) { |
| | | this.query.page = page; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå页大å°åå |
| | | handlePageSizeChange(size) { |
| | | this.query.limit = size; |
| | | this.query.page = 1; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå¼ç¨æ¥è¯¢ |
| | | handleAbandonQuery() { |
| | | // è¿éå¯ä»¥æ ¹æ®abandonQueryçéæ°æ® |
| | | console.log('å¼ç¨æ¥è¯¢:', this.abandonQuery); |
| | | }, |
| | | |
| | | // å¤çæ¯ä¾ä½æ¥è¯¢ |
| | | handlePerDonorQuery() { |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | }, |
| | | |
| | | // æ¥çæç®è
详æ
|
| | | viewDonorDetail(row) { |
| | | this.$router.push({ |
| | | path: '/donor/detail', |
| | | query: { id: row.id } |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- src/views/statistics/willingness.vue --> |
| | | <template> |
| | | <div class="statistics-page"> |
| | | <!-- çé颿¿ --> |
| | | <FilterPanel |
| | | :fields="filterFields" |
| | | :default-value="query" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | > |
| | | <template #extra-actions> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">导åº</el-button> |
| | | </template> |
| | | </FilterPanel> |
| | | |
| | | <!-- æ°æ®æ¦è§ --> |
| | | <el-row :gutter="20" class="overview-row"> |
| | | <el-col :span="6" v-for="item in overviewData" :key="item.title"> |
| | | <el-card class="overview-card"> |
| | | <div class="overview-content"> |
| | | <div class="overview-icon" :style="{ backgroundColor: item.color }"> |
| | | <i :class="item.icon"></i> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-title">{{ item.title }}</div> |
| | | <div class="overview-value">{{ item.value }}</div> |
| | | <div class="overview-desc">{{ item.desc }}</div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <el-row :gutter="20" class="chart-row"> |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æç®ææ¿è¶å¿</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'willingness-trend-chart'" |
| | | :options="trendChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>å°åºæç®ææ¿å¯¹æ¯</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'region-compare-chart'" |
| | | :options="regionChartOptions" |
| | | height="400px" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æç®ææ¿ç»è®¡æç»</span> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | style="width: 100%" |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æä»½" prop="month" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | {{ formatMonth(row.month) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¨å®åç§°" prop="organName" align="center" /> |
| | | <el-table-column label="æ¡ä¾æ°é" prop="caseCount" align="center" /> |
| | | <el-table-column label="æç®æ°é" prop="donationCount" align="center" /> |
| | | <el-table-column label="æç®ææ¿" prop="willingnessRate" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-progress |
| | | :percentage="parseFloat(row.willingnessRate)" |
| | | :color="getProgressColor(parseFloat(row.willingnessRate))" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¯æ¯åå" prop="monthCompare" align="center" > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getCompareTag(row.monthCompare)" size="small"> |
| | | {{ row.monthCompare > 0 ? '+' : '' }}{{ row.monthCompare }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | |
| | | export default { |
| | | name: 'WillingnessStatistics', |
| | | components: { |
| | | FilterPanel, |
| | | EChartsWrapper |
| | | }, |
| | | data() { |
| | | return { |
| | | // æ¥è¯¢åæ° |
| | | query: { |
| | | region: [], |
| | | monthRange: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }, |
| | | |
| | | // çéåæ®µé
ç½® |
| | | filterFields: [ |
| | | { |
| | | label: 'å°åº', |
| | | prop: 'region', |
| | | type: 'select', |
| | | multiple: true, |
| | | options: [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: 'æä»½èå´', |
| | | prop: 'monthRange', |
| | | type: 'daterange', |
| | | dateType: 'monthrange', |
| | | startPlaceholder: 'å¼å§æä»½', |
| | | endPlaceholder: 'ç»ææä»½', |
| | | valueFormat: 'yyyy-MM', |
| | | width: '300px' |
| | | }, |
| | | { |
| | | label: 'å¨å®ç±»å', |
| | | prop: 'organType', |
| | | type: 'select', |
| | | options: [ |
| | | { label: 'å
¨è', value: 'liver' }, |
| | | { label: 'è¾è', value: 'kidney' }, |
| | | { label: 'å¿è', value: 'heart' }, |
| | | { label: 'èº', value: 'lung' }, |
| | | { label: 'ç¼è§è', value: 'cornea' }, |
| | | { label: 'å¤å¨å®', value: 'multi' } |
| | | ], |
| | | width: '200px' |
| | | } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | | loading: false, |
| | | |
| | | // æ¦è§æ°æ® |
| | | overviewData: [ |
| | | { title: 'æ»æ¡ä¾æ°', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-flag', color: '#409EFF' }, |
| | | { title: 'æ»æç®æ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-check', color: '#67C23A' }, |
| | | { title: 'å¹³åæç®ææ¿', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-data', color: '#E6A23C' }, |
| | | { title: 'è¶å¿åå', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-trend', color: '#F56C6C' } |
| | | ], |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | trendChartOptions: {}, |
| | | regionChartOptions: {}, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | total: 0, |
| | | |
| | | // æ¨¡ææ°æ® |
| | | mockData: { |
| | | table: [ |
| | | { id: 1, region: 'éå²å°åº', month: '2024-01', organName: 'å
¨è', caseCount: 15, donationCount: 10, willingnessRate: 66.7, monthCompare: 2.5 }, |
| | | { id: 2, region: 'éå²å°åº', month: '2024-01', organName: 'è¾è', caseCount: 20, donationCount: 16, willingnessRate: 80.0, monthCompare: 1.2 }, |
| | | { id: 3, region: 'æ¥ç
§å°åº', month: '2024-01', organName: 'å
¨è', caseCount: 8, donationCount: 5, willingnessRate: 62.5, monthCompare: 0.8 }, |
| | | { id: 4, region: 'æ¥ç
§å°åº', month: '2024-01', organName: 'ç¼è§è', caseCount: 12, donationCount: 9, willingnessRate: 75.0, monthCompare: 3.1 }, |
| | | { id: 5, region: 'éå²å°åº', month: '2024-02', organName: 'å
¨è', caseCount: 18, donationCount: 12, willingnessRate: 66.7, monthCompare: 0.0 }, |
| | | { id: 6, region: 'éå²å°åº', month: '2024-02', organName: 'å¿è', caseCount: 5, donationCount: 4, willingnessRate: 80.0, monthCompare: 5.2 }, |
| | | { id: 7, region: 'æµåå°åº', month: '2024-02', organName: 'å
¨è', caseCount: 25, donationCount: 20, willingnessRate: 80.0, monthCompare: 1.8 }, |
| | | { id: 8, region: 'æµåå°åº', month: '2024-02', organName: 'å¤å¨å®', caseCount: 8, donationCount: 6, willingnessRate: 75.0, monthCompare: 2.3 } |
| | | ], |
| | | trend: [ |
| | | { month: '2023-10', willingness: 72.5, cases: 80, donations: 58 }, |
| | | { month: '2023-11', willingness: 73.8, cases: 85, donations: 63 }, |
| | | { month: '2023-12', willingness: 75.2, cases: 90, donations: 68 }, |
| | | { month: '2024-01', willingness: 76.8, cases: 95, donations: 73 }, |
| | | { month: '2024-02', willingness: 78.3, cases: 100, donations: 78 }, |
| | | { month: '2024-03', willingness: 79.1, cases: 105, donations: 83 } |
| | | ], |
| | | region: [ |
| | | { region: 'éå²å°åº', willingness: 78.3, cases: 150, donations: 118 }, |
| | | { region: 'æ¥ç
§å°åº', willingness: 72.5, cases: 80, donations: 58 }, |
| | | { region: 'æµåå°åº', willingness: 82.6, cases: 200, donations: 165 }, |
| | | { region: 'çå°å°åº', willingness: 75.8, cases: 100, donations: 76 } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | }, |
| | | methods: { |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | const [start, end] = this.query.monthRange; |
| | | filteredData = filteredData.filter(item => |
| | | item.month >= start && item.month <= end |
| | | ); |
| | | } |
| | | |
| | | if (this.query.organType) { |
| | | const organMap = { liver: 'è', kidney: 'è¾', heart: 'å¿è', lung: 'èº', cornea: 'ç¼è§è', multi: 'å¤å¨å®' }; |
| | | const organName = organMap[this.query.organType]; |
| | | if (organName) { |
| | | filteredData = filteredData.filter(item => item.organName.includes(organName)); |
| | | } |
| | | } |
| | | |
| | | this.tableData = filteredData; |
| | | this.total = filteredData.length; |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | this.updateOverviewData(filteredData); |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | updateOverviewData(data) { |
| | | if (data.length === 0) return; |
| | | |
| | | const totalCases = data.reduce((sum, item) => sum + item.caseCount, 0); |
| | | const totalDonations = data.reduce((sum, item) => sum + item.donationCount, 0); |
| | | const avgWillingness = data.length > 0 |
| | | ? (data.reduce((sum, item) => sum + parseFloat(item.willingnessRate), 0) / data.length).toFixed(1) |
| | | : 0; |
| | | |
| | | // 计ç®è¶å¿ååï¼åæè¿ä¸¤ä¸ªæçå¹³åååï¼ |
| | | const recentData = data.slice(-2); |
| | | const trend = recentData.length >= 2 |
| | | ? ((recentData[1].willingnessRate - recentData[0].willingnessRate) / recentData[0].willingnessRate * 100).toFixed(1) |
| | | : 0; |
| | | |
| | | this.overviewData[0].value = totalCases; |
| | | this.overviewData[1].value = totalDonations; |
| | | this.overviewData[2].value = `${avgWillingness}%`; |
| | | this.overviewData[3].value = `${trend > 0 ? '+' : ''}${trend}%`; |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // è¶å¿å¾ |
| | | this.trendChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross', |
| | | label: { |
| | | backgroundColor: '#6a7985' |
| | | } |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æç®ææ¿', 'æ¡ä¾æ°é', 'æç®æ°é'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: this.mockData.trend.map(item => item.month.replace('-', 'å¹´') + 'æ') |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'ææ¿ç(%)', |
| | | min: 60, |
| | | max: 90, |
| | | axisLabel: { |
| | | formatter: '{value}%' |
| | | } |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'æ°é(个)', |
| | | position: 'right' |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'æç®ææ¿', |
| | | type: 'line', |
| | | smooth: true, |
| | | data: this.mockData.trend.map(item => item.willingness), |
| | | itemStyle: { color: '#409EFF' }, |
| | | markLine: { |
| | | data: [ |
| | | { yAxis: 75, name: 'ç®æ 线' } |
| | | ], |
| | | lineStyle: { |
| | | type: 'dashed', |
| | | color: '#F56C6C' |
| | | } |
| | | } |
| | | }, |
| | | { |
| | | name: 'æ¡ä¾æ°é', |
| | | type: 'bar', |
| | | yAxisIndex: 1, |
| | | data: this.mockData.trend.map(item => item.cases), |
| | | itemStyle: { color: '#67C23A' } |
| | | }, |
| | | { |
| | | name: 'æç®æ°é', |
| | | type: 'bar', |
| | | yAxisIndex: 1, |
| | | data: this.mockData.trend.map(item => item.donations), |
| | | itemStyle: { color: '#E6A23C' } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // å°åºå¯¹æ¯å¾ |
| | | this.regionChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æç®ææ¿', 'æ¡ä¾æ°é', 'æç®æ°é'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: this.mockData.region.map(item => item.region) |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'ææ¿ç(%)', |
| | | min: 60, |
| | | max: 90, |
| | | axisLabel: { |
| | | formatter: '{value}%' |
| | | } |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'æ°é(个)', |
| | | position: 'right' |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'æç®ææ¿', |
| | | type: 'bar', |
| | | data: this.mockData.region.map(item => item.willingness), |
| | | itemStyle: { color: '#409EFF' } |
| | | }, |
| | | { |
| | | name: 'æ¡ä¾æ°é', |
| | | type: 'bar', |
| | | yAxisIndex: 1, |
| | | data: this.mockData.region.map(item => item.cases), |
| | | itemStyle: { color: '#67C23A' } |
| | | }, |
| | | { |
| | | name: 'æç®æ°é', |
| | | type: 'bar', |
| | | yAxisIndex: 1, |
| | | data: this.mockData.region.map(item => item.donations), |
| | | itemStyle: { color: '#E6A23C' } |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // æ ¼å¼åæä»½ |
| | | formatMonth(month) { |
| | | if (!month) return ''; |
| | | return month.replace('-', 'å¹´') + 'æ'; |
| | | }, |
| | | |
| | | // è·åè¿åº¦æ¡é¢è² |
| | | getProgressColor(percentage) { |
| | | if (percentage >= 80) return '#67C23A'; |
| | | if (percentage >= 70) return '#E6A23C'; |
| | | return '#F56C6C'; |
| | | }, |
| | | |
| | | // è·åæ¯è¾æ ç¾ç±»å |
| | | getCompareTag(value) { |
| | | if (value > 0) return 'success'; |
| | | if (value < 0) return 'danger'; |
| | | return 'info'; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | | handleSearch(formData) { |
| | | this.query = { ...this.query, ...formData, page: 1 }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çéç½® |
| | | handleReset() { |
| | | this.query = { |
| | | region: [], |
| | | monthRange: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | } |
| | | } |
| | | }; |
| | | </script> |