| | |
| | | params: query |
| | | }); |
| | | } |
| | | // 伦çå®¡æ¥æ¡£æ¡ä¸è½½ |
| | | export function ethicalReviewDownload(query) { |
| | | return request({ |
| | | url: "/project/ethicalreviewopinions/download", |
| | | method: "get", |
| | | params: query |
| | | }); |
| | | } |
| | | // 伦ç审æ¥ä¿¡æ¯è¯¦æ
|
| | | export function ethicalreviewInfo(id) { |
| | | return request({ |
| | |
| | | export * from "./maintain"; |
| | | export * from "./OrganUtilization"; |
| | | export * from "./transfer"; |
| | | export * from "./statistics"; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from '@/utils/request' |
| | | import { isStatisticsMock } from '@/utils/statisticsMode' |
| | | import { mockTotals } from './statisticsMock' |
| | | |
| | | // mock 模å¼ä¸è¿åæ¨¡ææ°æ®ï¼å¦å请æ±ç宿¥å£ |
| | | function post(url, data) { |
| | | if (isStatisticsMock()) { |
| | | return Promise.resolve({ code: 200, msg: 'æä½æå', data: mockTotals(url, data) }) |
| | | } |
| | | return request({ |
| | | url, |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // æ¡ä¾è¶å¿åºå |
| | | export function caseTrendArea(data) { |
| | | return post('/opo/totals/caseTrendArea', data) |
| | | } |
| | | // æ¡ä¾è¶å¿ |
| | | export function caseTrend(data) { |
| | | return post('/opo/totals/caseTrend', data) |
| | | } |
| | | // æ¡ä¾è¶å¿æä»½ |
| | | export function caseTrendMoon(data) { |
| | | return post('/opo/totals/caseTrendMoon', data) |
| | | } |
| | | // å¹³åå¹´é¾ |
| | | export function caseAvgAge(data) { |
| | | return post('/opo/totals/caseAvgAge', data) |
| | | } |
| | | // æ§å«ä¸è¡ååå¸ |
| | | export function caseBloodType(data) { |
| | | return post('/opo/totals/caseBloodType', data) |
| | | } |
| | | // æç®è
年龿§å«åå¸ |
| | | export function caseSex(data) { |
| | | return post('/opo/totals/caseSex', data) |
| | | } |
| | | // æç®å¨å®ç»è®¡æç» |
| | | export function organ(data) { |
| | | return post('/opo/totals/organ', data) |
| | | } |
| | | // å¨å®è·å/ç§»æ¤è¶å¿ |
| | | export function organMoon(data) { |
| | | return post('/opo/totals/organMoon', data) |
| | | } |
| | | // å¨å®ç±»ååå¸ |
| | | export function organType(data) { |
| | | return post('/opo/totals/organType', data) |
| | | } |
| | | // æ»ç»è®¡ï¼æ»æ½å¨æç®æ¡ä¾ / æ»å®ææç®æ°ï¼ |
| | | export function totals(data) { |
| | | return post('/opo/totals/totals', data) |
| | | } |
| | | // æ¡ä¾æç»å表 |
| | | export function trendList(data) { |
| | | return post('/opo/totals/trendList', data) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // ç»è®¡æ¨¡åæ¨¡ææ°æ® |
| | | // ååºåæ®µä¸ /opo/totals/* è¿åç Totals(opoç»è®¡) å¯¹è±¡ä¿æä¸è´ |
| | | // mock 模å¼ä¸ç± statistics.js è¿åï¼é¡µé¢é»è¾æ éåºåæ°æ®æ¥æº |
| | | |
| | | const REGIONS = ['éå²å°åº', 'æ¥ç
§å°åº', 'æµåå°åº', 'çå°å°åº', '卿µ·å°åº', 'æ½åå°åº', '临æ²å°åº']; |
| | | const MONTHS = ['2024-01', '2024-02', '2024-03', '2024-04', '2024-05', '2024-06']; |
| | | const YEARS = ['2022', '2023', '2024']; |
| | | const ORGANS = ['å
¨è', 'è¾è', 'å¿è', 'èº', 'è°è
º', 'ç¼è§è']; |
| | | const BLOOD_TYPES = ['A', 'B', 'O', 'AB']; |
| | | const AGE_GROUPS = ['1', '2', '3', '4']; |
| | | |
| | | // ç¡®å®æ§ä¼ªéæºï¼ä¿è¯æ¯æ¬¡æ¸²ææ°æ®ä¸è´ |
| | | function createRand(seed) { |
| | | let s = seed % 2147483647; |
| | | if (s <= 0) s += 2147483646; |
| | | return function () { |
| | | s = (s * 16807) % 2147483647; |
| | | return (s - 1) / 2147483646; |
| | | }; |
| | | } |
| | | |
| | | // æ¡ä¾è¶å¿ï¼æä»½ç»´åº¦ï¼ |
| | | function mockCaseTrend() { |
| | | const r = createRand(11); |
| | | return MONTHS.map((m) => { |
| | | const potential = 40 + Math.round(r() * 40); |
| | | return { donateTime: m, potentialCount: potential, completeCount: Math.round(potential * (0.6 + r() * 0.2)), totalCount: potential }; |
| | | }); |
| | | } |
| | | |
| | | // æ¡ä¾è¶å¿ï¼å°åºç»´åº¦ï¼ |
| | | function mockCaseTrendArea() { |
| | | const r = createRand(22); |
| | | return REGIONS.map((area) => { |
| | | const potential = 30 + Math.round(r() * 70); |
| | | return { area, potentialCount: potential, completeCount: Math.round(potential * (0.6 + r() * 0.2)) }; |
| | | }); |
| | | } |
| | | |
| | | // æ¡ä¾æç»ï¼å°åº x æä»½ï¼ |
| | | function mockCaseTrendMoon() { |
| | | const r = createRand(33); |
| | | const rows = []; |
| | | REGIONS.forEach((area) => { |
| | | MONTHS.forEach((m) => { |
| | | const potential = 5 + Math.round(r() * 15); |
| | | rows.push({ area, donateTime: m, potentialCount: potential, completeCount: Math.round(potential * (0.5 + r() * 0.4)), totalCount: potential }); |
| | | }); |
| | | }); |
| | | return rows; |
| | | } |
| | | |
| | | // å¹³åå¹´é¾ |
| | | function mockCaseAvgAge() { |
| | | return [{ avgAge: 45.6 }]; |
| | | } |
| | | |
| | | // æ§å«ä¸è¡ååå¸ |
| | | function mockCaseBloodType() { |
| | | const r = createRand(44); |
| | | return BLOOD_TYPES.map((boodType) => { |
| | | const man = 20 + Math.round(r() * 30); |
| | | const woman = 20 + Math.round(r() * 30); |
| | | return { boodType, manCount: man, womanCount: woman, boodTypeCount: man + woman }; |
| | | }); |
| | | } |
| | | |
| | | // æç®è
年龿§å«åå¸ |
| | | function mockCaseSex() { |
| | | const r = createRand(55); |
| | | return AGE_GROUPS.map((ageGroup) => ({ |
| | | ageGroup, |
| | | manCount: 15 + Math.round(r() * 40), |
| | | womanCount: 15 + Math.round(r() * 40) |
| | | })); |
| | | } |
| | | |
| | | // æç®å¨å®ç»è®¡æç»ï¼å¹´ä»½ x å¨å® x å°åºï¼ |
| | | function mockOrgan() { |
| | | const r = createRand(66); |
| | | const rows = []; |
| | | YEARS.forEach((year) => { |
| | | ORGANS.forEach((organName) => { |
| | | REGIONS.forEach((area) => { |
| | | const get = 10 + Math.round(r() * 40); |
| | | rows.push({ donateTime: year, organName, area, getCount: get, useCount: Math.round(get * (0.7 + r() * 0.25)) }); |
| | | }); |
| | | }); |
| | | }); |
| | | return rows; |
| | | } |
| | | |
| | | // å¨å®è·å/ç§»æ¤è¶å¿ï¼æä»½ç»´åº¦ï¼ |
| | | function mockOrganMoon() { |
| | | const r = createRand(77); |
| | | return MONTHS.map((m) => { |
| | | const get = 50 + Math.round(r() * 60); |
| | | return { donateTime: m, getCount: get, useCount: Math.round(get * (0.75 + r() * 0.2)) }; |
| | | }); |
| | | } |
| | | |
| | | // å¨å®ç±»ååå¸ |
| | | function mockOrganType() { |
| | | const r = createRand(88); |
| | | return ORGANS.map((organName) => { |
| | | const get = 40 + Math.round(r() * 80); |
| | | return { organName, getCount: get, useCount: Math.round(get * (0.75 + r() * 0.2)) }; |
| | | }); |
| | | } |
| | | |
| | | // æ»ç»è®¡ï¼æ»æ½å¨æç®æ¡ä¾ / æ»å®ææç®æ°ï¼ |
| | | function mockTotalsAll() { |
| | | return [{ totalPotential: 486, totalCompletion: 327 }]; |
| | | } |
| | | |
| | | // æ¡ä¾æç»å表ï¼trendListï¼ |
| | | function mockTrendList() { |
| | | const r = createRand(99); |
| | | const NAMES = ['å¼ ä¸', 'æå', 'çäº', 'èµµå
', 'éä¸', 'åå
«', 'å¨ä¹']; |
| | | const HOSPITALS = ['éå²å¤§å¦éå±å»é¢', 'éå²å¸ç«å»é¢', 'éå²å¸ä¸å¿å»é¢', 'å±±ä¸å¤§å¦é½é²å»é¢(éå²)']; |
| | | const DEATHS = ['èå¤ä¼¤', 'å¿æéª¤å', 'èè¡ç®¡æå¤', 'é¢
èè¿ç¤', '缺氧æ§èç
']; |
| | | const WORKFLOWS = [1, 2, 3, 4, 5, 6, 7, 99]; |
| | | return NAMES.map((name, i) => { |
| | | const month = MONTHS[i % MONTHS.length]; |
| | | const day = String(1 + Math.floor(r() * 27)).padStart(2, '0'); |
| | | return { |
| | | id: 1000 + i, |
| | | caseNo: `C${month.replace('-', '')}00${i + 1}`, |
| | | name, |
| | | inpatientNo: `ZY${month.replace('-', '')}00${i + 1}`, |
| | | workflow: WORKFLOWS[i % WORKFLOWS.length], |
| | | organName: ORGANS[i % ORGANS.length], |
| | | donatetime: `${month}-${day} 10:30`, |
| | | deathReason: DEATHS[i % DEATHS.length], |
| | | treatmentHospitalName: HOSPITALS[i % HOSPITALS.length] |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | const MOCK_MAP = { |
| | | '/opo/totals/caseTrend': mockCaseTrend, |
| | | '/opo/totals/caseTrendArea': mockCaseTrendArea, |
| | | '/opo/totals/caseTrendMoon': mockCaseTrendMoon, |
| | | '/opo/totals/caseAvgAge': mockCaseAvgAge, |
| | | '/opo/totals/caseBloodType': mockCaseBloodType, |
| | | '/opo/totals/caseSex': mockCaseSex, |
| | | '/opo/totals/organ': mockOrgan, |
| | | '/opo/totals/organMoon': mockOrganMoon, |
| | | '/opo/totals/organType': mockOrganType, |
| | | '/opo/totals/totals': mockTotalsAll, |
| | | '/opo/totals/trendList': mockTrendList |
| | | }; |
| | | |
| | | export function mockTotals(url) { |
| | | const fn = MOCK_MAP[url]; |
| | | return fn ? fn() : []; |
| | | } |
| | |
| | | // background-color: rgba(94, 255, 0, 0.198); |
| | | } |
| | | |
| | | // business 模åï¼å¤§å®½å±å±
ä¸é宽çç½ï¼å°å±èªå¨éºæ»¡ |
| | | .app-main.business-mode > * { |
| | | max-width: 1200px; |
| | | margin: 0 auto; |
| | | } |
| | | |
| | | .components-container { |
| | | margin: 30px 50px; |
| | | position: relative; |
| | |
| | | <!-- PDFé¢è§ --> |
| | | <div v-else-if="fileType === 'pdf'" class="preview-container pdf-preview"> |
| | | <div class="pdf-toolbar"> |
| | | <el-button-group> |
| | | <el-button-group v-if="!useNativePdf"> |
| | | <el-button |
| | | size="mini" |
| | | @click="changePage(currentPage - 1)" |
| | |
| | | </el-button> |
| | | </el-button-group> |
| | | |
| | | <el-button-group class="zoom-controls"> |
| | | <el-button-group v-if="!useNativePdf" class="zoom-controls"> |
| | | <el-button size="mini" @click="zoomOut" :disabled="scale <= 50"> |
| | | <i class="el-icon-zoom-out"></i> ç¼©å° |
| | | </el-button> |
| | |
| | | </el-button> |
| | | </div> |
| | | |
| | | <div class="pdf-content" v-loading="pdfLoading"> |
| | | <!-- pdf.js å è·¨å(CORS)å 载失败æ¶ï¼é级为æµè§å¨åç PDF é¢è§ï¼iframe 导èªä¸ååæºçç¥éå¶ï¼ --> |
| | | <iframe |
| | | v-if="useNativePdf" |
| | | :src="fileUrl" |
| | | class="pdf-native-iframe" |
| | | frameborder="0" |
| | | ></iframe> |
| | | |
| | | <div class="pdf-content" v-else v-loading="pdfLoading"> |
| | | <pdf |
| | | ref="pdf" |
| | | :src="pdfUrl" |
| | |
| | | pageCount: 0, |
| | | scale: 100, |
| | | pageRotate: 0, |
| | | // pdf.js å 载失败ï¼å¦ CORSï¼æ¶é级为æµè§å¨åçé¢è§ |
| | | useNativePdf: false, |
| | | // å¾çç¸å
³ |
| | | imageScale: 100 |
| | | }; |
| | |
| | | }, |
| | | /** è®¡ç® baseUrlHt */ |
| | | calculateBaseUrl() { |
| | | // è·åå½åæµè§å¨å°å |
| | | const currentUrl = window.location.href; |
| | | console.log("å½åæµè§å¨å°å:", currentUrl); |
| | | |
| | | try { |
| | | // ä½¿ç¨ URL 对象解æ |
| | | const urlObj = new URL(currentUrl); |
| | | // è·å主æºåï¼IP æååï¼ |
| | | const hostname = urlObj.hostname; |
| | | // æ¼æ¥ç«¯å£ :9095 |
| | | this.baseUrlHt = `http://${hostname}:9095`; |
| | | console.log("计ç®å¾å°ç baseUrlHt:", this.baseUrlHt); |
| | | } catch (error) { |
| | | console.error("è§£æURL失败:", error); |
| | | // å¤ç¨æ¹æ¡ï¼ä½¿ç¨æ£åæå |
| | | const match = currentUrl.match(/https?:\/\/([^:\/]+)/); |
| | | if (match) { |
| | | this.baseUrlHt = `http://${match[1]}:9095`; |
| | | } else { |
| | | // æåçå¤ç¨æ¹æ¡ |
| | | this.baseUrlHt = |
| | | process.env.VUE_APP_BASE_API || "http://localhost:9095"; |
| | | } |
| | | } |
| | | // èµ°å½åååï¼ååç¸å¯¹è·¯å¾ï¼ï¼ç± nginx ç /profile/ 代ç转åå°å端 9095ã |
| | | // ç´è¿ 9095 端å£ä¼å 端å£ä¸å产çè·¨å(CORS)ï¼èå端åå¨åé CORS ååºå¤´é®é¢ï¼ |
| | | // åå请æ±ä¸è§¦å CORS æ£æ¥ï¼å¯ä»æ ¹æºä¸ç»å¼è¯¥é®é¢ï¼å¾ç/PDF/ä¸è½½å
¨é¨çæï¼ã |
| | | this.baseUrlHt = ""; |
| | | console.log("æä»¶è®¿é®èµ°åå代ç: /profile/"); |
| | | }, |
| | | |
| | | /** åå§åé¢è§ */ |
| | |
| | | |
| | | // å¤çURLï¼æ¿æ¢ååé¨å |
| | | this.fileUrl = this.processFileUrl(originalUrl); |
| | | console.log(this.fileUrl,'this.fileUrl'); |
| | | |
| | | this.fileType = this.getFileType(this.fileName); |
| | | |
| | |
| | | this.scale = 100; |
| | | this.imageScale = 100; |
| | | this.pageCount = 0; |
| | | this.useNativePdf = false; |
| | | |
| | | // 模æå è½½å»¶è¿ |
| | | setTimeout(() => { |
| | |
| | | pdfErrorHandler(error) { |
| | | console.error("PDFå 载失败:", error); |
| | | this.pdfLoading = false; |
| | | this.$message.error("PDFæä»¶å 载失败ï¼è¯·å°è¯ä¸è½½åæ¥ç"); |
| | | this.previewVisible = false; |
| | | // pdf.js éè¿ fetch å 载跨å PDF å CORS éå¶ï¼å¤±è´¥æ¶é级为æµè§å¨åçé¢è§ |
| | | // ï¼iframe å±äºå¯¼èªè¯·æ±ï¼ä¸ååæºçç¥éå¶ï¼å¯ä»¥æ£å¸¸å 载跨å PDFï¼ |
| | | this.useNativePdf = true; |
| | | this.$message({ |
| | | type: "info", |
| | | message: "å·²åæ¢ä¸ºæµè§å¨åçé¢è§æ¨¡å¼" |
| | | }); |
| | | }, |
| | | |
| | | /** å¾çå è½½å®æ */ |
| | |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | /* åç PDF é¢è§ï¼CORS éçº§æ¹æ¡ï¼ */ |
| | | .pdf-native-iframe { |
| | | flex: 1; |
| | | border: none; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | /* Officeé¢è§æ ·å¼ */ |
| | | .office-content { |
| | | flex: 1; |
| | |
| | | <template> |
| | | <section class="app-main"> |
| | | <section class="app-main" :class="{ 'business-mode': isBusiness }"> |
| | | <transition name="fade-transform" mode="out-in"> |
| | | <keep-alive :include="cachedViews"> |
| | | <router-view :key="key" /> |
| | |
| | | }, |
| | | key() { |
| | | return this.$route.path |
| | | }, |
| | | isBusiness() { |
| | | return (this.$route.matched || []).some((r) => { |
| | | const cp = r.meta && r.meta.componentPath; |
| | | if (typeof cp !== 'string' || cp.indexOf('business/') !== 0) return false; |
| | | // ä¸å¡æ¨¡åç index å表页ä¸éè¦é宽ï¼course/index æ¯è¯¦æ
页ï¼ä¿çé宽 |
| | | if (/^business\/[^/]+\/index$/.test(cp) && cp !== 'business/course/index') return false; |
| | | return true; |
| | | }); |
| | | } |
| | | } |
| | | } |
| | |
| | | route.children = filterChildren(route.children) |
| | | } |
| | | if (route.component) { |
| | | // è®°å½åå§ç»ä»¶è·¯å¾ï¼ä¾å¸å±å±å¤ææå±æ¨¡åï¼business/project çï¼ |
| | | if (typeof route.component === 'string') { |
| | | route.meta = route.meta || {}; |
| | | route.meta.componentPath = route.component; // ä¾å¦ "business/assess/index" |
| | | } |
| | | // Layout ParentView ç»ä»¶ç¹æ®å¤ç |
| | | if (route.component === 'Layout') { |
| | | route.component = Layout |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // ç»è®¡å¾è¡¨ç»ä¸æ¸
ç½ç®æ´é£æ ¼ï¼echarts 4 å
¼å®¹ï¼ |
| | | // ä¾ statistics/*.vue å¤ç¨ï¼ä¿è¯è§è§ä¸è´ |
| | | |
| | | const toRgba = (hex, alpha) => { |
| | | const n = parseInt(hex.slice(1), 16); |
| | | const r = (n >> 16) & 255; |
| | | const g = (n >> 8) & 255; |
| | | const b = n & 255; |
| | | return `rgba(${r}, ${g}, ${b}, ${alpha})`; |
| | | }; |
| | | |
| | | export const COLORS = { |
| | | blue: '#5B8FF9', |
| | | green: '#5AD8A6', |
| | | amber: '#F6BD16', |
| | | coral: '#E8684A', |
| | | sky: '#6DC8EC', |
| | | purple: '#9270CA', |
| | | orange: '#FF9D4D', |
| | | teal: '#269A99' |
| | | }; |
| | | |
| | | export const PALETTE = Object.values(COLORS); |
| | | |
| | | const tipBase = { |
| | | backgroundColor: '#ffffff', |
| | | borderColor: '#e8eaf0', |
| | | borderWidth: 1, |
| | | padding: [10, 14], |
| | | textStyle: { color: '#333', fontSize: 13 }, |
| | | extraCssText: 'box-shadow:0 6px 18px rgba(31,45,61,.1);border-radius:10px;' |
| | | }; |
| | | |
| | | // åæ è½´ç±» tooltipï¼æ±ç¶/æçº¿ï¼ |
| | | export function axisTooltip() { |
| | | return { trigger: 'axis', ...tipBase, axisPointer: { lineStyle: { color: '#c9d2e0' } } }; |
| | | } |
| | | |
| | | // å¾å½¢ç±» tooltipï¼é¥¼å¾ï¼ |
| | | export function itemTooltip() { |
| | | return { trigger: 'item', ...tipBase, formatter: '{b}ï¼{c} ({d}%)' }; |
| | | } |
| | | |
| | | export function grid() { |
| | | return { left: 8, right: 16, top: 44, bottom: 8, containLabel: true }; |
| | | } |
| | | |
| | | // ç±»ç®è½´ |
| | | export function xAxis(data) { |
| | | return { |
| | | type: 'category', |
| | | data, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: '#8a919f', fontSize: 12 } |
| | | }; |
| | | } |
| | | |
| | | // æ°å¼è½´ |
| | | export function yAxis(name) { |
| | | return { |
| | | type: 'value', |
| | | name, |
| | | nameTextStyle: { color: '#b0b6c3', fontSize: 12 }, |
| | | axisLabel: { color: '#8a919f', fontSize: 12 }, |
| | | splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } } |
| | | }; |
| | | } |
| | | |
| | | export function legend(data) { |
| | | return { |
| | | data, |
| | | icon: 'roundRect', |
| | | itemWidth: 10, |
| | | itemHeight: 10, |
| | | itemGap: 18, |
| | | textStyle: { color: '#8a919f', fontSize: 12 } |
| | | }; |
| | | } |
| | | |
| | | // æ±ç¶ç³»å |
| | | export function barSeries(name, data, color, opts = {}) { |
| | | return { |
| | | name, |
| | | type: 'bar', |
| | | data, |
| | | barMaxWidth: opts.maxWidth || 20, |
| | | itemStyle: { color, borderRadius: opts.radius || [4, 4, 0, 0] }, |
| | | stack: opts.stack, |
| | | yAxisIndex: opts.yAxisIndex |
| | | }; |
| | | } |
| | | |
| | | // æçº¿ç³»å |
| | | export function lineSeries(name, data, color, opts = {}) { |
| | | const s = { |
| | | name, |
| | | type: 'line', |
| | | data, |
| | | smooth: true, |
| | | symbol: 'circle', |
| | | symbolSize: opts.symbolSize || 6, |
| | | lineStyle: { width: 3, color }, |
| | | itemStyle: { color }, |
| | | yAxisIndex: opts.yAxisIndex |
| | | }; |
| | | if (opts.area) { |
| | | s.areaStyle = { |
| | | color: { |
| | | type: 'linear', |
| | | x: 0, |
| | | y: 0, |
| | | x2: 0, |
| | | y2: 1, |
| | | colorStops: [ |
| | | { offset: 0, color: toRgba(color, 0.24) }, |
| | | { offset: 1, color: toRgba(color, 0) } |
| | | ] |
| | | } |
| | | }; |
| | | } |
| | | return s; |
| | | } |
| | | |
| | | // ç¯å½¢å¾ç³»å |
| | | export function donutSeries(name, data) { |
| | | return { |
| | | name, |
| | | type: 'pie', |
| | | radius: ['52%', '72%'], |
| | | center: ['50%', '50%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { borderColor: '#fff', borderWidth: 2 }, |
| | | label: { show: false }, |
| | | labelLine: { show: false }, |
| | | data, |
| | | color: PALETTE |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // ç»è®¡æ¨¡åæ°æ®æºå¼å
³ï¼mock(æ¨¡ææ°æ®) / real(ç宿¥å£) |
| | | // å
¨å±å
±äº«ï¼åæ¢åç»è®¡å页é¢è¯»ååä¸ç¶æ |
| | | import Vue from 'vue'; |
| | | |
| | | const KEY = 'opo-statistics-mock'; |
| | | const initial = localStorage.getItem(KEY) === '1'; |
| | | |
| | | const state = Vue.observable({ |
| | | useMock: initial |
| | | }); |
| | | |
| | | export function setStatisticsMock(useMock) { |
| | | state.useMock = !!useMock; |
| | | localStorage.setItem(KEY, state.useMock ? '1' : '0'); |
| | | } |
| | | |
| | | export function isStatisticsMock() { |
| | | return state.useMock; |
| | | } |
| | | |
| | | export default state; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // /opo/totals/* ç»è®¡æ¥å£å
Œ
±å·¥å
· |
| | | // æ¥å£è¯·æ±ä½ä¸º Totals(opoç»è®¡) 对象ï¼ååºä¸º RuoYi Result { code, msg, data, total } |
| | | // data å¯è½æ¯ Totals æ°ç»ãå个 Totals å¯¹è±¡ï¼æå
äºä¸å± {list/rows} ç对象 |
| | | |
| | | // å½ä¸åè¿åç»æä¸º Totals æ°ç» |
| | | export function toTotalsList(result) { |
| | | const data = result && result.data; |
| | | if (!data) return []; |
| | | if (Array.isArray(data)) return data; |
| | | if (Array.isArray(data.list)) return data.list; |
| | | if (Array.isArray(data.rows)) return data.rows; |
| | | // å个ç»è®¡å¯¹è±¡ |
| | | return [data]; |
| | | } |
| | | |
| | | // ç»è£
æ¥è¯¢æ¡ä»¶ï¼åªä¼ æå¼çåæ®µ |
| | | // area: å°åºåç§°(䏿)ï¼startTime/endTime: yyyy-MMï¼ageGroup/sex/boodType/organName 为çé维度 |
| | | export function buildTotalsQuery({ area, startTime, endTime, ageGroup, sex, boodType, organName } = {}) { |
| | | const q = {}; |
| | | if (area) q.area = area; |
| | | if (startTime) q.startTime = startTime; |
| | | if (endTime) q.endTime = endTime; |
| | | if (ageGroup) q.ageGroup = ageGroup; |
| | | if (sex) q.sex = sex; |
| | | if (boodType) q.boodType = boodType; |
| | | if (organName) q.organName = organName; |
| | | return q; |
| | | } |
| | | |
| | | // æä»½/æ¶é´å符串ç»ä¸æ yyyy-MM |
| | | export function toMonth(value) { |
| | | if (!value) return ''; |
| | | const s = String(value); |
| | | const m = s.match(/(\d{4})-(\d{2})/); |
| | | return m ? `${m[1]}-${m[2]}` : s; |
| | | } |
| | | |
| | | // æ°åå®å
¨åæ´ |
| | | export function toNum(v) { |
| | | const n = Number(v); |
| | | return Number.isFinite(n) ? n : 0; |
| | | } |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | <template> |
| | | <div class="confirmation-detail"> |
| | | <case-basic-info :case-id="caseId" :show-attachment="true":column="3" /> |
| | | <case-basic-info :case-id="caseId" :show-attachment="true" :column="3" /> |
| | | |
| | | <el-card class="detail-card paper-card"> |
| | | <!-- åºç¡ä¿¡æ¯ --> |
| | | <div slot="header" class="clearfix"> |
| | | <span class="detail-title">æç®ç¡®è®¤ä¿¡æ¯</span> |
| | | |
| | | </div> |
| | | |
| | | <el-form :model="form" ref="form" label-width="120px" class="paper-form"> |
| | | <div class="form-row"> |
| | | <el-form-item label="åè°å1" prop="coordinatedusernameo" class="col-1-3"> |
| | | <el-form-item |
| | | label="åè°å1" |
| | | prop="coordinatedusernameo" |
| | | class="col-1-3" |
| | | > |
| | | <el-input v-model="form.coordinatedusernameo" /> |
| | | </el-form-item> |
| | | <el-form-item label="åè°å2" prop="coordinatedusernamet" class="col-1-3"> |
| | | <el-form-item |
| | | label="åè°å2" |
| | | prop="coordinatedusernamet" |
| | | class="col-1-3" |
| | | > |
| | | <el-input v-model="form.coordinatedusernamet" /> |
| | | </el-form-item> |
| | | <!-- <el-col :span="8"> |
| | |
| | | </el-alert> |
| | | <div style="margin-top: 15px;"> |
| | | <el-button |
| | | style="float: right; margin-bottom: 5px;" |
| | | style="float: right; margin-bottom: 5px;" |
| | | type="primary" |
| | | size="mini" |
| | | icon="el-icon-plus" |
| | |
| | | </div> |
| | | |
| | | <div class="form-row"> |
| | | <el-form-item label="家屿è§å¤æ³¨" prop="relativeRemark" class="col-full"> |
| | | <el-form-item |
| | | label="家屿è§å¤æ³¨" |
| | | prop="relativeRemark" |
| | | class="col-full" |
| | | > |
| | | <el-input |
| | | type="textarea" |
| | | :rows="3" |
| | |
| | | }, |
| | | |
| | | getAttachmentsByType(type) { |
| | | console.log(type); |
| | | console.log(this.attachmentData[type]); |
| | | |
| | | return this.attachmentData[type] || []; |
| | | }, |
| | | |
| | |
| | | uploader: "å½åç¨æ·" |
| | | }; |
| | | this.attachmentData[this.currentUploadType].push(newAttachment); |
| | | console.log(this.attachmentData,'this.attachmentData'); |
| | | |
| | | this.updateAssessannexField(); |
| | | this.$message.success("ä¸ä¼ æå"); |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | display: none; |
| | | } |
| | | |
| | | .paper-form >>> .col-1-3 { flex: 0 0 33.3333%; } |
| | | .paper-form >>> .col-half { flex: 0 0 50%; } |
| | | .paper-form >>> .col-2-3 { flex: 0 0 66.6666%; } |
| | | .paper-form >>> .col-full { flex: 0 0 100%; } |
| | | .paper-form >>> .col-1-3 { |
| | | flex: 0 0 33.3333%; |
| | | } |
| | | .paper-form >>> .col-half { |
| | | flex: 0 0 50%; |
| | | } |
| | | .paper-form >>> .col-2-3 { |
| | | flex: 0 0 66.6666%; |
| | | } |
| | | .paper-form >>> .col-full { |
| | | flex: 0 0 100%; |
| | | } |
| | | |
| | | .paper-form >>> .el-form-item__label { |
| | | flex: 0 0 150px; |
| | |
| | | line-height: 1.3; |
| | | white-space: nowrap; |
| | | } |
| | | .paper-form >>> .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label:before { |
| | | .paper-form |
| | | >>> .el-form-item.is-required:not(.is-no-asterisk) |
| | | > .el-form-item__label:before { |
| | | color: #f56c6c; |
| | | margin-right: 2px; |
| | | } |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | .register-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | /* æ¯ä¸è¡ä¸º flex è¡ï¼èªå¸¦ä¸è¾¹æ¡ä¿è¯ç½æ ¼è¿ç»æ§ */ |
| | |
| | | |
| | | // 伦ç审æ¥ç»è®ºææ¬ï¼åªè¯»å±ç¤ºï¼ |
| | | expertConclusionLabel() { |
| | | const map = { 0: "ä¸åæ", 1: "åæ", 2: "审æ¥ä¸" }; |
| | | const map = { 0: "ä¸åæ", 1: "åæ", 2: "审æ¥ä¸", 3: "弿", 4: "ä¿®æ¹ååæ" }; |
| | | return map[this.form.expertConclusion] || "-"; |
| | | }, |
| | | |
| | |
| | | console.log(file, "file"); |
| | | |
| | | this.currentPreviewFile = { |
| | | fileName: file, |
| | | // ç¾åéä»¶ URL éå¸¸æ æ©å±åï¼å¦ .../file-1787645145583ï¼ï¼ |
| | | // é¢è§ç»ä»¶ä¾èµæä»¶åæ©å±åè¯å«æä»¶ç±»åï¼è¿éè¡¥ .png 让å
¶æå¾çé¢è§ |
| | | fileName: "æåç¾å.png", |
| | | fileUrl: file, |
| | | fileType: "png" |
| | | }; |
| | |
| | | getReviewStatusFilter(status) { |
| | | const statusMap = { |
| | | "0": "info", // å¾
æ¥æ¶ |
| | | "1": "warning", // æªæ¥æ¶ |
| | | "1": "warning", // å·²åé |
| | | "2": "success", // å·²æ¥æ¶ |
| | | "3": "danger", // è¶
æ¶ |
| | | "4": "danger", // 䏿¢ |
| | |
| | | getReviewStatusText(status) { |
| | | const statusMap = { |
| | | "0": "å¾
æ¥æ¶", |
| | | "1": "æªæ¥æ¶", |
| | | "1": "å·²åé", |
| | | "2": "å·²æ¥æ¶", |
| | | "3": "è¶
æ¶", |
| | | "4": "䏿¢", |
| | |
| | | // ç»è®ºè¿æ»¤å¨ |
| | | getConclusionFilter(conclusion) { |
| | | const conclusionMap = { |
| | | "0": "danger", // ä¸åæ |
| | | "1": "success", // åæ |
| | | "2": "warning", // 审æ¥ä¸ |
| | | "0": "danger" // ä¸åæ |
| | | "3": "info", // 弿 |
| | | "4": "success" // ä¿®æ¹ååæ |
| | | }; |
| | | return conclusionMap[conclusion] || "info"; |
| | | }, |
| | | |
| | | getConclusionText(conclusion) { |
| | | const conclusionMap = { |
| | | "0": "ä¸åæ", |
| | | "1": "åæ", |
| | | "2": "审æ¥ä¸", |
| | | "0": "ä¸åæ" |
| | | "3": "弿", |
| | | "4": "ä¿®æ¹ååæ" |
| | | }; |
| | | return conclusionMap[conclusion] || "æªç¥"; |
| | | }, |
| | |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | min-height: 0; |
| | | min-width: 0; |
| | | /* éè¦ï¼å
许flexå项å缩 */ |
| | | max-width: 1600px; |
| | | /* è¶
宽å±ä¸éï¼é¿å
æè¿°å表æ¯åè¿å®½ãå
容ç¨çï¼å³ä¾§çåºééç©ºç½ */ |
| | | } |
| | | |
| | | .basic-info-section { |
| | |
| | | flex-direction: column; |
| | | min-height: 400px; |
| | | /* æå°é«åº¦ */ |
| | | min-width: 0; |
| | | /* éè¦ï¼å
许 flex å项æ¶ç¼© */ |
| | | /* max-height: 1000px; */ |
| | | /* æå¤§é«åº¦ï¼å¯æ ¹æ®éè¦è°æ´ */ |
| | | overflow: hidden; |
| | |
| | | .stage-content-wrapper { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | overflow-x: auto; |
| | | /* é¶æ®µè¯¦æ
å
é¨å¯æ»å¨ */ |
| | | margin-top: 20px; |
| | | padding-right: 8px; |
| | | min-height: 0; |
| | | /* éè¦ */ |
| | | min-width: 0; |
| | | /* éè¦ï¼å
许 flex å项æ¶ç¼©ï¼å¦ååºå®å宽çè¡¨æ ¼ä¼æå
容æå®½å¹¶æº¢åº */ |
| | | } |
| | | |
| | | /* 约æå Stage åç»ä»¶æ ¹èç¹å®½åº¦ï¼é
å䏿¹ min-width:0ï¼ |
| | | 让 el-table å¨èªèº«å
é¨åºç°æ¨ªåæ»å¨æ¡ï¼è䏿¯è¢«å¤å± overflow:hidden è£æ */ |
| | | .stage-content-wrapper :deep(> div) { |
| | | width: 100%; |
| | | min-width: 0; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .stage-content-wrapper::-webkit-scrollbar { |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | <el-option label="åæ" value="1" /> |
| | | <el-option label="审æ¥ä¸" value="2" /> |
| | | <el-option label="ä¸åæ" value="0" /> |
| | | <el-option label="弿" value="3" /> |
| | | <el-option label="ä¿®æ¹ååæ" value="4" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="伦ç审æ¥ç»è®ºæ¶é´" prop="expertTime" class="col-1-3"> |
| | |
| | | <el-row :gutter="20" style="margin-top: 20px;"> |
| | | <el-col :span="12"> |
| | | <div class="history-stat-item"> |
| | | <div class="history-stat-label">æªæ¥æ¶æ°é</div> |
| | | <div class="history-stat-label">å·²åéæªæ¥æ¶æ°é</div> |
| | | <div class="history-stat-value"> |
| | | {{ expertHistoryData.notAcceptCount || 0 }} |
| | | </div> |
| | |
| | | console.log(file, "file"); |
| | | |
| | | this.currentPreviewFile = { |
| | | fileName: file, |
| | | // ç¾åéä»¶ URL éå¸¸æ æ©å±åï¼å¦ .../file-1787645145583ï¼ï¼ |
| | | // é¢è§ç»ä»¶ä¾èµæä»¶åæ©å±åè¯å«æä»¶ç±»åï¼è¿éè¡¥ .png 让å
¶æå¾çé¢è§ |
| | | fileName: "æåç¾å.png", |
| | | fileUrl: file, |
| | | fileType: "png" |
| | | }; |
| | |
| | | // 审æ¥ç¶æè¿æ»¤å¨ |
| | | getReviewStatusFilter(status) { |
| | | const statusMap = { |
| | | "0": "info", // å¾
æ¥æ¶ |
| | | "1": "warning", // æªæ¥æ¶ |
| | | "0": "info", // å¾
åé |
| | | "1": "warning", // å·²åé |
| | | "2": "success", // å·²æ¥æ¶ |
| | | "3": "danger", // è¶
æ¶ |
| | | "4": "danger", // 䏿¢ |
| | |
| | | |
| | | getReviewStatusText(status) { |
| | | const statusMap = { |
| | | "0": "å¾
æ¥æ¶", |
| | | "1": "æªæ¥æ¶", |
| | | "0": "å¾
åé", |
| | | "1": "å·²åé", |
| | | "2": "å·²æ¥æ¶", |
| | | "3": "è¶
æ¶", |
| | | "4": "䏿¢", |
| | |
| | | // ç»è®ºè¿æ»¤å¨ |
| | | getConclusionFilter(conclusion) { |
| | | const conclusionMap = { |
| | | "0": "danger", // ä¸åæ |
| | | "1": "success", // åæ |
| | | "2": "warning", // 审æ¥ä¸ |
| | | "0": "danger" // ä¸åæ |
| | | "3": "info", // 弿 |
| | | "4": "success" // ä¿®æ¹ååæ |
| | | }; |
| | | return conclusionMap[conclusion] || "info"; |
| | | }, |
| | | |
| | | getConclusionText(conclusion) { |
| | | const conclusionMap = { |
| | | "0": "ä¸åæ", |
| | | "1": "åæ", |
| | | "2": "审æ¥ä¸", |
| | | "0": "ä¸åæ" |
| | | "3": "弿", |
| | | "4": "ä¿®æ¹ååæ" |
| | | }; |
| | | return conclusionMap[conclusion] || "æªç¥"; |
| | | }, |
| | |
| | | deptName: expert.unitname || "", |
| | | title: expert.title || "", |
| | | donorno: expert.telephone || "", |
| | | receiveStatus: "0", // å¾
æ¥æ¶ |
| | | receiveStatus: "0", // å¾
åé |
| | | expertconclusion: "", |
| | | expertopinion: "", |
| | | conclusionannex: "", |
| | |
| | | |
| | | if (index != -1) { |
| | | this.form.ethicalreviewopinionsList[index].receiveStatus = "1"; // å·²æ¥æ¶ |
| | | this.form.ethicalreviewopinionsList[index].expertconclusion = "2"; // 审æ¥ä¸ |
| | | this.form.ethicalreviewopinionsList[ |
| | | index |
| | | ].startTime = this.sendForm.startTime; |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | >ä¿®æ¹</el-button |
| | | > --> |
| | | <el-button |
| | | type="warning" |
| | | icon="el-icon-download" |
| | | @click="handleExport" |
| | | >导åº</el-button |
| | | > |
| | | <el-button |
| | | type="info" |
| | | icon="el-icon-check" |
| | | :disabled="single" |
| | |
| | | prop="inpatientno" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | label="å§å" |
| | | align="center" |
| | | prop="name" |
| | | width="100" |
| | | /> |
| | | <el-table-column label="å§å" align="center" prop="name" width="100" /> |
| | | |
| | | <el-table-column label="æ§å«" align="center" prop="sex" width="80"> |
| | | <template slot-scope="scope"> |
| | |
| | | <el-table-column |
| | | label="æä½" |
| | | align="center" |
| | | width="180" |
| | | width="240" |
| | | fixed="right" |
| | | class-name="small-padding fixed-width" |
| | | > |
| | |
| | | :disabled="scope.row.status === '2'" |
| | | >ç»æ</el-button |
| | | > |
| | | <el-button |
| | | size="mini" |
| | | type="text" |
| | | icon="el-icon-download" |
| | | @click="handleDownload(scope.row)" |
| | | >ä¸è½½</el-button |
| | | > |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { reviewinitiateBaseInfoList, ethicalreviewedit, } from "@/api/businessApi"; |
| | | import { |
| | | reviewinitiateBaseInfoList, |
| | | ethicalreviewedit, |
| | | ethicalReviewDownload |
| | | } from "@/api/businessApi"; |
| | | import Pagination from "@/components/Pagination"; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | export default { |
| | | name: "EthicsReviewList", |
| | | components: { Pagination }, |
| | | dicts: ["sys_user_sex", "ReviewForm_status",'sys_ethical','expert_Conclusion'], |
| | | dicts: [ |
| | | "sys_user_sex", |
| | | "ReviewForm_status", |
| | | "sys_ethical", |
| | | "expert_Conclusion" |
| | | ], |
| | | data() { |
| | | return { |
| | | // é®ç½©å± |
| | |
| | | delete params.reviewTimeRange; |
| | | |
| | | // å¦æææ¶é´èå´åæ° |
| | | if (this.queryParams.reviewTimeRange && this.queryParams.reviewTimeRange.length === 2) { |
| | | params.expertTimeStart = this.queryParams.reviewTimeRange[0] + ' 00:00:00'; |
| | | params.expertTimeEnd = this.queryParams.reviewTimeRange[1] + ' 23:59:59'; |
| | | if ( |
| | | this.queryParams.reviewTimeRange && |
| | | this.queryParams.reviewTimeRange.length === 2 |
| | | ) { |
| | | params.expertTimeStart = |
| | | this.queryParams.reviewTimeRange[0] + " 00:00:00"; |
| | | params.expertTimeEnd = |
| | | this.queryParams.reviewTimeRange[1] + " 23:59:59"; |
| | | } |
| | | |
| | | reviewinitiateBaseInfoList(params) |
| | |
| | | handleView(row) { |
| | | this.$router.push({ |
| | | path: "/case/ethicalReviewInfo", |
| | | query: { infoid: row.infoid,id:row.id } |
| | | query: { infoid: row.infoid, id: row.id } |
| | | }); |
| | | }, |
| | | // æ°å¢æé®æä½ |
| | |
| | | const infoid = row.infoid; |
| | | this.$router.push({ |
| | | path: "/case/ethicalReviewInfo", |
| | | query: { infoid: infoid,id:row.id } |
| | | query: { infoid: infoid, id: row.id } |
| | | }); |
| | | }, |
| | | // ç»æå®¡æ¥æä½ |
| | | handleEndReview(row) { |
| | | const selectedRow = row || (this.selectedRows.length === 1 ? this.selectedRows[0] : null); |
| | | const selectedRow = |
| | | row || (this.selectedRows.length === 1 ? this.selectedRows[0] : null); |
| | | if (!selectedRow) { |
| | | this.$message.warning("è¯·éæ©ä¸æ¡è®°å½"); |
| | | return; |
| | |
| | | // è°ç¨ä¿®æ¹æ¥å£ï¼å°ç¶ææ¹ä¸ºç»æï¼status: '2'ï¼ |
| | | const updateData = { |
| | | infoid: selectedRow.infoid, |
| | | status: '2', |
| | | status: "2", |
| | | // å¯ä»¥ä¿çåææ°æ® |
| | | expertName: selectedRow.expertName, |
| | | expertConclusion: selectedRow.expertConclusion, |
| | |
| | | }) |
| | | .catch(() => {}); |
| | | }, |
| | | // å¯¼åºæé®æä½ |
| | | handleExport() { |
| | | const queryParams = { ...this.queryParams }; |
| | | delete queryParams.pageNum; |
| | | delete queryParams.pageSize; |
| | | |
| | | // å¤çæ¶é´èå´åæ° |
| | | if (queryParams.reviewTimeRange && queryParams.reviewTimeRange.length === 2) { |
| | | queryParams.expertTimeStart = queryParams.reviewTimeRange[0] + ' 00:00:00'; |
| | | queryParams.expertTimeEnd = queryParams.reviewTimeRange[1] + ' 23:59:59'; |
| | | } |
| | | delete queryParams.reviewTimeRange; |
| | | |
| | | this.$confirm("æ¯å¦ç¡®è®¤å¯¼åºææä¼¦çå®¡æ¥æ°æ®ï¼", "è¦å", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | }) |
| | | .then(() => { |
| | | this.loading = true; |
| | | // return ethicalreviewexport(queryParams); |
| | | }) |
| | | .then(response => { |
| | | if (response.code === 200) { |
| | | this.$message.success("å¯¼åºæå"); |
| | | // å¤çå¯¼åºæä»¶ä¸è½½ |
| | | if (response.data) { |
| | | const blob = new Blob([response.data], { type: 'application/vnd.ms-excel' }); |
| | | const downloadElement = document.createElement('a'); |
| | | const href = window.URL.createObjectURL(blob); |
| | | downloadElement.href = href; |
| | | downloadElement.download = '伦ç审æ¥å表.xlsx'; |
| | | document.body.appendChild(downloadElement); |
| | | downloadElement.click(); |
| | | document.body.removeChild(downloadElement); |
| | | window.URL.revokeObjectURL(href); |
| | | } |
| | | } else { |
| | | this.$message.error(response.msg || "导åºå¤±è´¥"); |
| | | // ä¸è½½ä¼¦çå®¡æ¥æ¡£æ¡ |
| | | handleDownload(row) { |
| | | ethicalReviewDownload({ nitiateId: row.id }) |
| | | .then(res => { |
| | | if (res.code !== 200 || !res.data || !res.data.downloadUrl) { |
| | | this.$message.error(res.msg || "ä¸è½½å¤±è´¥"); |
| | | return; |
| | | } |
| | | this.loading = false; |
| | | const fileUrl = res.data; |
| | | // æ¼æ¥ä¸è½½å°åï¼origin + /dev-api(å¼å代çåç¼) + /profile + downloadUrlï¼ |
| | | // åç«¯éææä»¶å®é
æå¨ /profile ä¸ï¼downloadUrl å°äºè¿å±åç¼ |
| | | const urlBase = process.env.VUE_APP_BASE_API; |
| | | const curWWWPath = window.document.location.href; |
| | | const pos = curWWWPath.indexOf(window.document.location.pathname); |
| | | const fullUrl = |
| | | curWWWPath.substring(0, pos) + urlBase + "/profile" + fileUrl.downloadUrl; |
| | | |
| | | fetch(fullUrl, { |
| | | headers: { Authorization: "Bearer " + getToken() } |
| | | }) |
| | | .then(response => response.blob()) |
| | | .then(blob => { |
| | | const url = window.URL.createObjectURL(new Blob([blob])); |
| | | const link = document.createElement("a"); |
| | | link.href = url; |
| | | link.setAttribute("download", fileUrl.downloadName); |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | link.parentNode.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | this.loading = false; |
| | | .catch(error => { |
| | | console.error("ä¸è½½æ¡£æ¡å¤±è´¥:", error); |
| | | this.$message.error("ä¸è½½å¤±è´¥"); |
| | | }); |
| | | }, |
| | | // æ¶é´æ ¼å¼å |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | .paper-form { |
| | | border-left: 1px solid #d0d3db; |
| | | background: #fff; |
| | | /* è¶
宽å±éå¶è¡¨åæå¤§å®½åº¦ï¼å³ä¾§çç½ï¼é¿å
åæ®µåå¸è¿å¼ */ |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .paper-form >>> .form-row { |
| | |
| | | </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-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-col :span="8" 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 }"> |
| | |
| | | type="text" |
| | | @click="toggleChartType('trend')" |
| | | > |
| | | {{ chartTypes.trend === 'line' ? 'æ±ç¶å¾' : 'æçº¿å¾' }} |
| | | {{ chartTypes.trend === "line" ? "æ±ç¶å¾" : "æçº¿å¾" }} |
| | | </el-button> |
| | | </div> |
| | | <EChartsWrapper |
| | |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æä»½" prop="month" align="center" > |
| | | <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" > |
| | | <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" |
| | |
| | | <span>{{ row.completionRate }}%</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¡ä¾æç»" align="center" > |
| | | <el-table-column label="æ¡ä¾æç»" align="center"> |
| | | <template slot-scope="{ row }"> |
| | | <el-button |
| | | type="text" |
| | | size="mini" |
| | | @click="viewCaseDetail(row)" |
| | | > |
| | | <el-button type="text" size="mini" @click="viewCaseDetail(row)"> |
| | | æ¥ç({{ row.caseCount || 0 }}) |
| | | </el-button> |
| | | </template> |
| | |
| | | </el-card> |
| | | |
| | | <!-- æ¡ä¾æç»å¼¹çª --> |
| | | <el-dialog |
| | | title="æ¡ä¾æç»" |
| | | :visible.sync="showDetail" |
| | | |
| | | append-to-body |
| | | > |
| | | <case-detail |
| | | :query="detailQuery" |
| | | @close="showDetail = false" |
| | | /> |
| | | <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'; |
| | | |
| | | import EChartsWrapper from "@/components/charts/EChartsWrapper.vue"; |
| | | import CaseDetail from "./components/CaseDetail.vue"; |
| | | import { caseTrendArea, caseTrend, caseTrendMoon, totals,trendList } from "@/api/businessApi"; |
| | | import { toTotalsList, toMonth, toNum } from "@/utils/totals"; |
| | | import statisticsMode from "@/utils/statisticsMode"; |
| | | import { COLORS, axisTooltip, grid, xAxis, yAxis, legend, barSeries, lineSeries } from "@/utils/chartStyle"; |
| | | export default { |
| | | name: 'CaseStatistics', |
| | | name: "CaseStatistics", |
| | | components: { |
| | | EChartsWrapper, |
| | | CaseDetail |
| | |
| | | limit: 10 |
| | | }, |
| | | |
| | | // å°åºé项 |
| | | // å°åºé项ï¼value ç´æ¥ä¼ å°åºåç§°ç»æ¥å£ area åæ®µï¼ |
| | | regionOptions: [ |
| | | { label: 'éå²å°åº', value: 'qingdao' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'rizhao' }, |
| | | { label: 'æµåå°åº', value: 'jinan' }, |
| | | { label: 'çå°å°åº', value: 'yantai' }, |
| | | { label: '卿µ·å°åº', value: 'weihai' }, |
| | | { label: 'æ½åå°åº', value: 'weifang' }, |
| | | { label: '临æ²å°åº', value: 'linyi' } |
| | | { label: "éå²å°åº", value: "éå²å°åº" }, |
| | | { label: "æ¥ç
§å°åº", value: "æ¥ç
§å°åº" }, |
| | | { label: "æµåå°åº", value: "æµåå°åº" }, |
| | | { label: "çå°å°åº", value: "çå°å°åº" }, |
| | | { label: "卿µ·å°åº", value: "卿µ·å°åº" }, |
| | | { label: "æ½åå°åº", value: "æ½åå°åº" }, |
| | | { label: "临æ²å°åº", value: "临æ²å°åº" } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | |
| | | |
| | | // æ¦è§æ°æ® |
| | | 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' } |
| | | { |
| | | title: "æ»æ½å¨æç®æ¡ä¾", |
| | | value: 0, |
| | | desc: "ç»è®¡å¨æå
", |
| | | icon: "el-icon-s-flag", |
| | | color: "#5B8FF9" |
| | | }, |
| | | { |
| | | title: "æ»å®ææç®æ°é", |
| | | value: 0, |
| | | desc: "ç»è®¡å¨æå
", |
| | | icon: "el-icon-circle-check", |
| | | color: "#5AD8A6" |
| | | }, |
| | | { |
| | | title: "å¹³å宿ç", |
| | | value: "0%", |
| | | desc: "ç»è®¡å¨æå
", |
| | | icon: "el-icon-s-data", |
| | | color: "#F6BD16" |
| | | } |
| | | ], |
| | | |
| | | // å¾è¡¨ç±»å |
| | | chartTypes: { |
| | | trend: 'line' |
| | | trend: "line" |
| | | }, |
| | | |
| | | // å¾è¡¨æ°æ® |
| | |
| | | |
| | | // å¼¹çªæ§å¶ |
| | | 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 } |
| | | ] |
| | | } |
| | | detailQuery: {} |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | // æ°æ®æº(mock/ç宿¥å£)忢æ¶éæ°å è½½ |
| | | this.$watch(() => statisticsMode.useMock, () => this.loadData()); |
| | | }, |
| | | methods: { |
| | | // ç»è£
æ¥å£æ¥è¯¢æ¡ä»¶ |
| | | buildQuery() { |
| | | const q = {}; |
| | | if (this.query.region && this.query.region.length) { |
| | | q.area = this.query.region.join(","); |
| | | } |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | q.startTime = this.query.monthRange[0]; |
| | | q.endTime = this.query.monthRange[1]; |
| | | } |
| | | return q; |
| | | }, |
| | | |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | // æ¨¡ææ¥å£è°ç¨ |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | const q = this.buildQuery(); |
| | | const [areaRes, trendRes, moonRes, totalsRes] = await Promise.all([ |
| | | caseTrendArea(q), |
| | | caseTrend(q), |
| | | caseTrendMoon(q), |
| | | totals(q) |
| | | ]); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | // å°åºåå¸å¯¹æ¯ |
| | | this.chartData.region = toTotalsList(areaRes).map(item => ({ |
| | | region: item.area || item.region || "", |
| | | potential: toNum(item.potentialCount), |
| | | completed: toNum(item.completeCount) |
| | | })); |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | // æ¡ä¾è¶å¿ |
| | | this.chartData.trend = toTotalsList(trendRes).map(item => ({ |
| | | month: toMonth(item.donateTime) || toMonth(item.month), |
| | | potential: toNum(item.potentialCount), |
| | | completed: toNum(item.completeCount) |
| | | })); |
| | | |
| | | 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 |
| | | ); |
| | | } |
| | | // æç»è¡¨ï¼å°åº x æä»½ï¼ |
| | | const all = toTotalsList(moonRes).map(item => { |
| | | const potential = toNum(item.potentialCount); |
| | | const completed = toNum(item.completeCount); |
| | | const rate = |
| | | potential > 0 |
| | | ? Number(((completed / potential) * 100).toFixed(1)) |
| | | : 0; |
| | | return { |
| | | region: item.area || item.region || "", |
| | | month: toMonth(item.donateTime) || toMonth(item.month), |
| | | potentialCount: potential, |
| | | completedCount: completed, |
| | | completionRate: rate, |
| | | caseCount: toNum(item.totalCount) || potential |
| | | }; |
| | | }); |
| | | this.total = all.length; |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = all.slice(start, start + this.query.limit); |
| | | |
| | | this.tableData = filteredData; |
| | | this.total = filteredData.length; |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | this.updateOverviewData(filteredData); |
| | | |
| | | // æ´æ°å¾è¡¨æ°æ® |
| | | this.chartData.trend = [...this.mockData.trend]; |
| | | this.chartData.region = [...this.mockData.region]; |
| | | |
| | | this.updateOverviewData(toTotalsList(totalsRes)); |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.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); |
| | | // æ´æ°æ¦è§æ°æ®ï¼æ»æ½å¨/æ»å®ææç®æ°æ¥èª /opo/totals/totals æ¥å£ï¼ |
| | | updateOverviewData(totalsList) { |
| | | const totalPotential = totalsList.reduce( |
| | | (sum, item) => sum + toNum(item.totalPotential), |
| | | 0 |
| | | ); |
| | | const totalCompleted = totalsList.reduce( |
| | | (sum, item) => sum + toNum(item.totalCompletion), |
| | | 0 |
| | | ); |
| | | const avgRate = |
| | | totalPotential > 0 |
| | | ? ((totalCompleted / totalPotential) * 100).toFixed(1) |
| | | : 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'; |
| | | const isLine = this.chartTypes.trend === "line"; |
| | | const xData = this.chartData.trend.map(item => item.month); |
| | | const potential = this.chartData.trend.map(item => item.potential); |
| | | const completed = this.chartData.trend.map(item => item.completed); |
| | | |
| | | const series = isLine |
| | | ? [ |
| | | lineSeries("æ½å¨æç®æ°", potential, COLORS.blue, { area: true }), |
| | | lineSeries("宿æç®æ°", completed, COLORS.green, { area: true }) |
| | | ] |
| | | : [ |
| | | barSeries("æ½å¨æç®æ°", potential, COLORS.blue), |
| | | barSeries("宿æç®æ°", completed, COLORS.green) |
| | | ]; |
| | | |
| | | 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 } : {} |
| | | } |
| | | ] |
| | | tooltip: axisTooltip(), |
| | | legend: legend(["æ½å¨æç®æ°", "宿æç®æ°"]), |
| | | grid: grid(), |
| | | xAxis: xAxis(xData), |
| | | yAxis: yAxis(), |
| | | series |
| | | }; |
| | | }, |
| | | |
| | | // è·åå°åºå¾è¡¨é
ç½® |
| | | 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: 'æ°é(个)' |
| | | }, |
| | | tooltip: axisTooltip(), |
| | | legend: legend(["æ½å¨æç®æ°", "宿æç®æ°"]), |
| | | grid: grid(), |
| | | xAxis: xAxis(this.chartData.region.map(item => item.region)), |
| | | yAxis: yAxis(), |
| | | 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' } |
| | | } |
| | | barSeries("æ½å¨æç®æ°", this.chartData.region.map(item => item.potential), COLORS.blue), |
| | | barSeries("宿æç®æ°", this.chartData.region.map(item => item.completed), COLORS.green) |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åè¿åº¦æ¡é¢è² |
| | | getProgressColor(percentage) { |
| | | if (percentage >= 60) return '#67C23A'; |
| | | if (percentage >= 50) return '#E6A23C'; |
| | | return '#F56C6C'; |
| | | if (percentage >= 60) return COLORS.green; |
| | | if (percentage >= 50) return COLORS.amber; |
| | | return COLORS.coral; |
| | | }, |
| | | |
| | | // æ ¼å¼åæä»½æ¾ç¤º |
| | | formatMonth(month) { |
| | | if (!month) return ''; |
| | | return month.replace('-', 'å¹´') + 'æ'; |
| | | if (!month) return ""; |
| | | return month.replace("-", "å¹´") + "æ"; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | |
| | | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | // è¿éå¯ä»¥è°ç¨å¯¼åºæ¥å£ |
| | | this.$message.info("导åºåè½å¼åä¸..."); |
| | | }, |
| | | |
| | | // 忢å¾è¡¨ç±»å |
| | | toggleChartType(type) { |
| | | this.chartTypes[type] = this.chartTypes[type] === 'line' ? 'bar' : 'line'; |
| | | this.chartTypes[type] = this.chartTypes[type] === "line" ? "bar" : "line"; |
| | | }, |
| | | |
| | | // æ¥çæ¡ä¾è¯¦æ
|
| | |
| | | |
| | | .overview-desc { |
| | | font-size: 12px; |
| | | color: #C0C4CC; |
| | | color: #c0c4cc; |
| | | } |
| | | |
| | | .chart-row { |
| | |
| | | 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" > |
| | | <el-table-column label="æ¡ä¾ç¼å·" prop="caseNo" align="center" /> |
| | | <el-table-column label="æç®è
å§å" prop="name" align="center" /> |
| | | <el-table-column label="ä½é¢å·" prop="inpatientNo" align="center" /> |
| | | <el-table-column label="æ¡ä¾ç¶æ" prop="workflow" align="center"> |
| | | <template slot-scope="{ row }"> |
| | | <el-tag :type="getStatusTag(row.status)" size="small"> |
| | | {{ getStatusText(row.status) }} |
| | | <el-tag :type="getStatusTag(row.workflow)" size="small"> |
| | | {{ getStatusText(row.workflow) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æç®å¨å®" prop="organs" align="center" > |
| | | <el-table-column label="æç®å¨å®" prop="organName" align="center"> |
| | | <template slot-scope="{ row }"> |
| | | <span v-if="row.organs && row.organs.length > 0"> |
| | | {{ row.organs.join('ã') }} |
| | | </span> |
| | | <span v-if="row.organName">{{ row.organName }}</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-column label="æ¡ä¾æ¶é´" prop="donatetime" align="center" /> |
| | | <el-table-column label="æ»äº¡åå " prop="deathReason" align="center" /> |
| | | <el-table-column label="å»é¢" prop="treatmentHospitalName" align="center" width="200" /> |
| | | </el-table> |
| | | |
| | | <div style="text-align: center; margin-top: 20px;"> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { trendList } from '@/api/businessApi'; |
| | | import { toTotalsList } from '@/utils/totals'; |
| | | |
| | | export default { |
| | | name: 'CaseDetail', |
| | | props: { |
| | |
| | | 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: 'éå²å¸ä¸å¿å»é¢' } |
| | | ] |
| | | caseList: [] |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadList(); |
| | | }, |
| | | watch: { |
| | | query: { |
| | | handler() { |
| | | this.loadList(); |
| | | }, |
| | | deep: true |
| | | } |
| | | }, |
| | | methods: { |
| | | getStatusTag(status) { |
| | | const map = { completed: 'success', potential: 'warning', abandoned: 'danger' }; |
| | | return map[status] || 'info'; |
| | | // å è½½æ¡ä¾æç»ï¼/opo/totals/trendList |
| | | async loadList() { |
| | | this.loading = true; |
| | | try { |
| | | const params = {}; |
| | | if (this.query.region) params.area = this.query.region; |
| | | if (this.query.month) { |
| | | params.startTime = `${this.query.month}-01`; |
| | | params.endTime = this.endOfMonth(this.query.month); |
| | | } |
| | | const res = await trendList(params); |
| | | this.caseList = toTotalsList(res); |
| | | } catch (error) { |
| | | console.error('å è½½æ¡ä¾æç»å¤±è´¥:', error); |
| | | this.$message.error('æ¡ä¾æç»å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | getStatusText(status) { |
| | | const map = { completed: '已宿', potential: 'æ½å¨æç®', abandoned: 'æ¾å¼æç®' }; |
| | | return map[status] || 'æªç¥'; |
| | | // æä»½(yyyy-MM) -> 彿æåä¸å¤©(yyyy-MM-dd) |
| | | endOfMonth(month) { |
| | | const [y, m] = String(month).split('-').map(Number); |
| | | const lastDay = new Date(y, m, 0).getDate(); |
| | | return `${month}-${String(lastDay).padStart(2, '0')}`; |
| | | }, |
| | | // 工使µç¶æææ¡ |
| | | getStatusText(workflow) { |
| | | const map = { |
| | | 1: 'ä¾è
ç»´æ¤', |
| | | 2: 'æç®ææ¿', |
| | | 3: 'å»å¦è¯ä¼°', |
| | | 4: 'æ»äº¡å¤å®', |
| | | 5: '伦ç审æ¥', |
| | | 6: 'å¨å®åé
', |
| | | 7: 'å¨å®å©ç¨', |
| | | 99: 'æç®å®æ' |
| | | }; |
| | | return map[workflow] || 'æªç¥'; |
| | | }, |
| | | getStatusTag(workflow) { |
| | | if (workflow === 99) return 'success'; |
| | | if (workflow) return 'warning'; |
| | | return 'info'; |
| | | } |
| | | } |
| | | }; |
| | |
| | | <span>ä¸å½åç±»åå¸</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'category-chart'" |
| | | :options="categoryChartOptions" |
| | | v-if="useMock" |
| | | :id="'china-category-chart'" |
| | | :options="chinaCategoryOptions" |
| | | height="400px" |
| | | /> |
| | | <el-empty v-else description="åç«¯ææªæä¾ä¸å½åç±»ç»è®¡æ¥å£"></el-empty> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>æ»äº¡åå åæ</span> |
| | | <span>æ»äº¡åå åå¸</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'death-chart'" |
| | | :options="deathChartOptions" |
| | | v-if="useMock" |
| | | :id="'death-cause-chart'" |
| | | :options="deathCauseOptions" |
| | | height="400px" |
| | | /> |
| | | <el-empty v-else description="åç«¯ææªæä¾æ»äº¡åå ç»è®¡æ¥å£"></el-empty> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | <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> |
| | | <template v-if="useMock"> |
| | | <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" /> |
| | | <el-table-column label="å¹´é¾" prop="age" align="center" /> |
| | | <el-table-column label="è¡å" prop="bloodType" align="center" /> |
| | | <el-table-column label="å°åº" prop="region" align="center" /> |
| | | <el-table-column label="æç®æ¶é´" prop="donationTime" align="center" /> |
| | | </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-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]" |
| | | /> |
| | | </template> |
| | | <el-empty v-else description="åç«¯ææªæä¾æç®è
详ç»ä¿¡æ¯æ¥å£"></el-empty> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | // ä¿®æ£å¯¼å
¥è·¯å¾ |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | import { caseSex, caseBloodType, caseAvgAge } from '@/api/businessApi'; |
| | | import { toTotalsList, toNum } from '@/utils/totals'; |
| | | import statisticsMode, { isStatisticsMock } from '@/utils/statisticsMode'; |
| | | import { COLORS, PALETTE, axisTooltip, grid, legend } from '@/utils/chartStyle'; |
| | | |
| | | const AGE_GROUP_MAP = { |
| | | '1': '<17å²', |
| | | '2': '18-49å²', |
| | | '3': '50-69å²', |
| | | '4': '>69å²' |
| | | }; |
| | | |
| | | export default { |
| | | name: 'DonorStatistics', |
| | |
| | | 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' } |
| | | { label: 'éå²å°åº', value: 'éå²å°åº' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'æ¥ç
§å°åº' }, |
| | | { label: 'æµåå°åº', value: 'æµåå°åº' }, |
| | | { label: 'çå°å°åº', value: 'çå°å°åº' }, |
| | | { label: '卿µ·å°åº', value: '卿µ·å°åº' }, |
| | | { label: 'æ½åå°åº', value: 'æ½åå°åº' }, |
| | | { label: '临æ²å°åº', value: '临æ²å°åº' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | |
| | | 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: 'æ§å«', |
| | |
| | | // å¾è¡¨é
ç½® |
| | | ageChartOptions: {}, |
| | | genderBloodChartOptions: {}, |
| | | categoryChartOptions: {}, |
| | | deathChartOptions: {}, |
| | | |
| | | // æ¥å£è¿åæ°æ® |
| | | sexData: [], |
| | | bloodData: [], |
| | | |
| | | // æ æ¥å£å¾è¡¨çæ¨¡ææ°æ®ï¼ä»
mock 模å¼å±ç¤ºï¼ |
| | | chinaCategoryData: [ |
| | | { name: 'ä¸å½ä¸ç±»(DBD)', value: 45 }, |
| | | { name: 'ä¸å½äºç±»(DCD)', value: 35 }, |
| | | { name: 'ä¸å½ä¸ç±»(DBCD)', value: 20 } |
| | | ], |
| | | deathCauseData: [ |
| | | { name: 'èè¡ç®¡ç¾ç
', value: 40 }, |
| | | { name: 'é¢
èæä¼¤', value: 30 }, |
| | | { name: 'å¿èç¾ç
', value: 15 }, |
| | | { name: 'å
¶ä»åå ', value: 15 } |
| | | ], |
| | | |
| | | // æç®è
详ç»ä¿¡æ¯æ¨¡ææ°æ®ï¼æ å端æ¥å£ï¼ä»
mock 模å¼å±ç¤ºï¼ |
| | | donorList: [ |
| | | { donorNo: 'D2024001', name: 'å¼ ä¼', gender: 'ç·', age: 45, bloodType: 'A', region: 'éå²å°åº', donationTime: '2024-03-15' }, |
| | | { donorNo: 'D2024002', name: 'çè³', gender: '女', age: 38, bloodType: 'B', region: 'éå²å°åº', donationTime: '2024-03-22' }, |
| | | { donorNo: 'D2024003', name: 'æå¨', gender: '女', age: 52, bloodType: 'O', region: 'æ¥ç
§å°åº', donationTime: '2024-04-02' }, |
| | | { donorNo: 'D2024004', name: 'åæ´', gender: 'ç·', age: 29, bloodType: 'AB', region: 'éå²å°åº', donationTime: '2024-04-18' }, |
| | | { donorNo: 'D2024005', name: 'éé', gender: '女', age: 47, bloodType: 'A', region: 'æµåå°åº', donationTime: '2024-05-06' }, |
| | | { donorNo: 'D2024006', name: 'æ¨å¼º', gender: 'ç·', age: 33, bloodType: 'B', region: 'çå°å°åº', donationTime: '2024-05-21' }, |
| | | { donorNo: 'D2024007', name: 'èµµæ', gender: '女', age: 41, bloodType: 'O', region: 'éå²å°åº', donationTime: '2024-06-03' }, |
| | | { donorNo: 'D2024008', name: 'é»ç£', gender: 'ç·', age: 55, bloodType: 'A', region: 'æ½åå°åº', donationTime: '2024-06-17' }, |
| | | { donorNo: 'D2024009', name: '卿°', gender: 'ç·', age: 62, bloodType: 'O', region: '临æ²å°åº', donationTime: '2024-07-01' }, |
| | | { donorNo: 'D2024010', name: 'å´é', gender: '女', age: 26, bloodType: 'AB', region: 'æ¥ç
§å°åº', donationTime: '2024-07-15' }, |
| | | { donorNo: 'D2024011', name: 'å¾ç£', gender: 'ç·', age: 48, bloodType: 'B', region: 'éå²å°åº', donationTime: '2024-08-09' }, |
| | | { donorNo: 'D2024012', name: 'å丽', gender: '女', age: 35, bloodType: 'O', region: '卿µ·å°åº', donationTime: '2024-08-24' }, |
| | | { donorNo: 'D2024013', name: '马è¶
', gender: 'ç·', age: 44, bloodType: 'A', region: 'æµåå°åº', donationTime: '2024-09-05' }, |
| | | { donorNo: 'D2024014', name: 'æ±å©·', gender: '女', age: 58, bloodType: 'B', region: 'éå²å°åº', donationTime: '2024-09-19' }, |
| | | { donorNo: 'D2024015', name: 'è¡å', gender: 'ç·', age: 31, bloodType: 'O', region: 'çå°å°åº', donationTime: '2024-10-08' }, |
| | | { donorNo: 'D2024016', name: 'éæ¶', gender: 'ç·', age: 68, bloodType: 'A', region: 'éå²å°åº', donationTime: '2024-10-22' }, |
| | | { donorNo: 'D2024017', name: 'ä½å¹³', gender: '女', age: 39, bloodType: 'AB', region: 'æ½åå°åº', donationTime: '2024-11-06' }, |
| | | { donorNo: 'D2024018', name: 'é«ç¿', gender: 'ç·', age: 50, bloodType: 'B', region: '临æ²å°åº', donationTime: '2024-11-20' }, |
| | | { donorNo: 'D2024019', name: 'æè³', gender: '女', age: 27, bloodType: 'O', region: 'éå²å°åº', donationTime: '2024-12-04' }, |
| | | { donorNo: 'D2024020', name: 'ç½å', gender: 'ç·', age: 42, bloodType: 'A', region: 'æ¥ç
§å°åº', donationTime: '2024-12-18' }, |
| | | { donorNo: 'D2023021', name: 'éç½', gender: '女', age: 34, bloodType: 'B', region: 'éå²å°åº', donationTime: '2023-06-10' }, |
| | | { donorNo: 'D2023022', name: 'æ¢å®', gender: 'ç·', age: 57, bloodType: 'O', region: 'æµåå°åº', donationTime: '2023-07-25' }, |
| | | { donorNo: 'D2023023', name: 'è°¢å¨', gender: '女', age: 49, bloodType: 'A', region: '卿µ·å°åº', donationTime: '2023-08-14' }, |
| | | { donorNo: 'D2023024', name: 'é©å°', gender: 'ç·', age: 23, bloodType: 'AB', region: 'éå²å°åº', donationTime: '2023-09-30' } |
| | | ], |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | 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 } |
| | | ] |
| | | } |
| | | total: 0 |
| | | }; |
| | | }, |
| | | computed: { |
| | | useMock() { |
| | | return isStatisticsMock(); |
| | | }, |
| | | chinaCategoryOptions() { |
| | | return this.buildPieOptions('ä¸å½åç±»', this.chinaCategoryData); |
| | | }, |
| | | deathCauseOptions() { |
| | | return this.buildPieOptions('æ»äº¡åå ', this.deathCauseData); |
| | | } |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | // æ°æ®æº(mock/ç宿¥å£)忢æ¶éæ°å è½½ |
| | | this.$watch(() => statisticsMode.useMock, () => this.loadData()); |
| | | }, |
| | | methods: { |
| | | // ç»è£
æ¥è¯¢æ¡ä»¶ |
| | | buildQuery() { |
| | | const q = {}; |
| | | if (this.query.region && this.query.region.length) { |
| | | q.area = this.query.region.join(','); |
| | | } |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | q.startTime = this.query.monthRange[0]; |
| | | q.endTime = this.query.monthRange[1]; |
| | | } |
| | | if (this.query.sex) q.sex = this.query.sex; |
| | | if (this.query.bloodType) q.boodType = this.query.bloodType; |
| | | return q; |
| | | }, |
| | | |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | const q = this.buildQuery(); |
| | | const [sexRes, bloodRes, avgRes] = await Promise.all([ |
| | | caseSex(q), |
| | | caseBloodType(q), |
| | | caseAvgAge(q) |
| | | ]); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | this.sexData = toTotalsList(sexRes); |
| | | this.bloodData = toTotalsList(bloodRes); |
| | | const avgList = toTotalsList(avgRes); |
| | | |
| | | 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.updateOverviewData(avgList); |
| | | this.updateCharts(); |
| | | |
| | | // æç®è
详ç»ä¿¡æ¯æ /opo/totals è忥å£ï¼ä½¿ç¨æ¨¡ææ°æ®è¡¥å
¨ |
| | | this.updateDonorTable(); |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | |
| | | }, |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | 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; |
| | | updateOverviewData(avgList) { |
| | | const males = this.sexData.reduce((sum, item) => sum + toNum(item.manCount), 0); |
| | | const females = this.sexData.reduce((sum, item) => sum + toNum(item.womanCount), 0); |
| | | const total = males + females; |
| | | const avgAge = avgList.length |
| | | ? toNum(avgList.reduce((sum, item) => sum + toNum(item.avgAge), 0) / avgList.length) |
| | | : 0; |
| | | |
| | | 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[1].value = total > 0 ? `${((males / total) * 100).toFixed(1)}%` : '0%'; |
| | | this.overviewData[2].value = total > 0 ? `${((females / total) * 100).toFixed(1)}%` : '0%'; |
| | | this.overviewData[3].value = avgAge.toFixed(1); |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // å¹´é¾åå¸å¾ |
| | | // å¹´é¾åå¸å¾ï¼å·¦ç·å³å¥³ï¼æ¸
ç½åæ£éåå¡ï¼ |
| | | this.ageChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | tooltip: axisTooltip(), |
| | | legend: legend(['ç·æ§', '女æ§']), |
| | | grid: grid(), |
| | | xAxis: { |
| | | type: 'value', |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: '#8a919f', fontSize: 12, formatter: value => Math.abs(value) }, |
| | | splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } } |
| | | }, |
| | | legend: { |
| | | data: ['ç·æ§', '女æ§'] |
| | | yAxis: { |
| | | type: 'category', |
| | | data: this.sexData.map(item => AGE_GROUP_MAP[item.ageGroup] || item.ageGroup || 'æªç¥'), |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: '#606266', fontSize: 13 } |
| | | }, |
| | | 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' } |
| | | barMaxWidth: 18, |
| | | data: this.sexData.map(item => toNum(item.manCount)), |
| | | itemStyle: { color: COLORS.blue, borderRadius: [4, 0, 0, 4] } |
| | | }, |
| | | { |
| | | name: '女æ§', |
| | | type: 'bar', |
| | | stack: 'æ§å«', |
| | | data: this.mockData.age.map(item => -item.female), |
| | | itemStyle: { color: '#F56C6C' } |
| | | barMaxWidth: 18, |
| | | data: this.sexData.map(item => -toNum(item.womanCount)), |
| | | itemStyle: { color: COLORS.coral, borderRadius: [0, 4, 4, 0] } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // æ§å«ä¸è¡åç»åå¾ |
| | | // æ§å«ä¸è¡ååå¸å¾ï¼åå¿ç¯ï¼é
è²æ¸
ç½ï¼ |
| | | const males = this.bloodData.reduce((sum, item) => sum + toNum(item.manCount), 0); |
| | | const females = this.bloodData.reduce((sum, item) => sum + toNum(item.womanCount), 0); |
| | | const bloodPie = this.bloodData.map(item => ({ |
| | | name: item.boodType || 'æªç¥', |
| | | value: toNum(item.boodTypeCount) || (toNum(item.manCount) + toNum(item.womanCount)) |
| | | })); |
| | | |
| | | 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%' |
| | | }, |
| | | legend: { data: ['ç·æ§', '女æ§', ...this.bloodData.map(item => item.boodType || 'æªç¥')], top: 20 }, |
| | | series: [ |
| | | { |
| | | name: 'æ§å«åå¸', |
| | | type: 'pie', |
| | | radius: [0, '30%'], |
| | | radius: [0, '40%'], |
| | | center: ['25%', '50%'], |
| | | label: { |
| | | position: 'inner', |
| | | fontSize: 12 |
| | | }, |
| | | data: this.mockData.gender |
| | | label: { position: 'inner', fontSize: 12 }, |
| | | data: [ |
| | | { name: 'ç·æ§', value: males }, |
| | | { name: '女æ§', value: females } |
| | | ], |
| | | color: [COLORS.blue, COLORS.coral] |
| | | }, |
| | | { |
| | | name: 'è¡ååå¸', |
| | | type: 'pie', |
| | | radius: ['40%', '55%'], |
| | | radius: ['55%', '75%'], |
| | | 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 } |
| | | ] |
| | | label: { fontSize: 14, color: '#606266' }, |
| | | data: bloodPie, |
| | | color: [COLORS.amber, COLORS.teal, COLORS.purple, COLORS.orange] |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // ä¸å½åç±»åå¸å¾ |
| | | this.categoryChartOptions = { |
| | | // æç®è
详ç»ä¿¡æ¯ï¼æ æ¥å£ï¼æ¨¡ææ°æ®è¡¥å
¨ + è¿æ»¤åé¡µï¼ |
| | | updateDonorTable() { |
| | | let list = this.donorList; |
| | | if (this.query.region && this.query.region.length) { |
| | | list = list.filter(item => this.query.region.includes(item.region)); |
| | | } |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | const start = this.query.monthRange[0]; |
| | | const end = this.query.monthRange[1]; |
| | | list = list.filter(item => { |
| | | const m = item.donationTime.slice(0, 7); |
| | | return m >= start && m <= end; |
| | | }); |
| | | } |
| | | if (this.query.gender) { |
| | | list = list.filter(item => item.gender === this.query.gender); |
| | | } |
| | | if (this.query.bloodType) { |
| | | list = list.filter(item => item.bloodType === this.query.bloodType); |
| | | } |
| | | this.total = list.length; |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = list.slice(start, start + this.query.limit); |
| | | }, |
| | | |
| | | // çæé¥¼å¾é
ç½®ï¼æ æ¥å£å¾è¡¨ç mock å±ç¤ºï¼ |
| | | buildPieOptions(name, data) { |
| | | return { |
| | | color: PALETTE, |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | right: 10, |
| | | top: 'center', |
| | | data: this.mockData.category.map(item => item.name) |
| | | left: 'left', |
| | | top: 'middle', |
| | | itemWidth: 10, |
| | | itemHeight: 10, |
| | | itemGap: 16, |
| | | textStyle: { fontSize: 14, color: '#606266' }, |
| | | data: data.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'ä¸å½åç±»', |
| | | name, |
| | | type: 'pie', |
| | | radius: ['30%', '70%'], |
| | | center: ['40%', '50%'], |
| | | roseType: 'radius', |
| | | label: { |
| | | show: true |
| | | }, |
| | | radius: '55%', |
| | | center: ['55%', '50%'], |
| | | itemStyle: { borderColor: '#fff', borderWidth: 2 }, |
| | | label: { fontSize: 15, color: '#606266', formatter: '{b} {d}%' }, |
| | | labelLine: { lineStyle: { color: '#c0c4cc' } }, |
| | | data, |
| | | emphasis: { |
| | | label: { |
| | | show: true |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: 'rgba(0, 0, 0, 0.5)' |
| | | } |
| | | }, |
| | | 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'; |
| | | }, |
| | | |
| | | // å¤çæ¥è¯¢ |
| | |
| | | // å¤çå¯¼åº |
| | | handleExport() { |
| | | this.$message.info('导åºåè½å¼åä¸...'); |
| | | }, |
| | | |
| | | // æ¥çæç®è
详æ
|
| | | viewDonorDetail(row) { |
| | | this.$router.push({ |
| | | path: '/donor/detail', |
| | | query: { id: row.id } |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | |
| | | <!-- src/views/statistics/index.vue --> |
| | | <template> |
| | | <div class="statistics-home"> |
| | | <el-card class="mode-card" shadow="never"> |
| | | <div class="mode-bar"> |
| | | <span class="mode-label">ç»è®¡æ°æ®æºï¼</span> |
| | | <el-switch |
| | | v-model="useMock" |
| | | active-text="æ¨¡ææ°æ®" |
| | | inactive-text="ç宿¥å£" |
| | | active-color="#E6A23C" |
| | | inactive-color="#409EFF" |
| | | @change="handleModeChange" |
| | | /> |
| | | <span class="mode-tip">忢åè¿å
¥åç»è®¡é¡µé¢çæï¼æ¨¡ææ°æ®ç¨äºæ¼ç¤ºï¼ç宿¥å£è¯»åå端ï¼</span> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6" v-for="item in menuList" :key="item.path"> |
| | | <el-card |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { isStatisticsMock, setStatisticsMock } from '@/utils/statisticsMode'; |
| | | |
| | | export default { |
| | | name: 'StatisticsHome', |
| | | data() { |
| | | return { |
| | | useMock: isStatisticsMock(), |
| | | menuList: [ |
| | | { |
| | | title: 'æ¡ä¾ç»è®¡', |
| | | desc: 'å°åºãæä»½ç»´åº¦çæ¡ä¾ç»è®¡åæ', |
| | | icon: 'el-icon-s-data', |
| | | color: '#409EFF', |
| | | path: '/statistics/case' |
| | | path: '/statics/caseStatistics' |
| | | }, |
| | | { |
| | | title: 'æç®å¨å®ç»è®¡', |
| | | desc: 'å¨å®è·åãç§»æ¤æ°éç»è®¡', |
| | | icon: 'el-icon-s-claim', |
| | | color: '#67C23A', |
| | | path: '/statistics/organ' |
| | | path: '/statics/organstatistics' |
| | | }, |
| | | { |
| | | title: 'è·åçç»è®¡', |
| | | desc: 'æç®è·åçãå¨å®å¼ç¨çåæ', |
| | | icon: 'el-icon-s-flag', |
| | | color: '#E6A23C', |
| | | path: '/statistics/rate' |
| | | path: '/statics/statisticsStatistics' |
| | | }, |
| | | { |
| | | title: 'æç®ææ¿ç»è®¡', |
| | | desc: 'æç®ææ¿è¶å¿ä¸åå¸åæ', |
| | | icon: 'el-icon-star-on', |
| | | color: '#F56C6C', |
| | | path: '/statistics/willingness' |
| | | path: '/statics/donorstatistics' |
| | | }, |
| | | { |
| | | title: 'æç®è
åæ', |
| | |
| | | }; |
| | | }, |
| | | methods: { |
| | | handleModeChange(val) { |
| | | setStatisticsMock(val); |
| | | }, |
| | | goToPage(path) { |
| | | this.$router.push(path); |
| | | } |
| | |
| | | padding: 20px; |
| | | } |
| | | |
| | | .mode-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .mode-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .mode-label { |
| | | font-size: 14px; |
| | | color: #303133; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .mode-tip { |
| | | margin-left: 16px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .stat-card { |
| | | cursor: pointer; |
| | | margin-bottom: 20px; |
| | |
| | | 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" > |
| | | <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"> |
| | | <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" |
| | |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | import OrganDetail from './components/OrganDetail.vue'; |
| | | import { organ, organMoon, organType } from '@/api/businessApi'; |
| | | import { toTotalsList, toMonth, toNum } from '@/utils/totals'; |
| | | import statisticsMode from '@/utils/statisticsMode'; |
| | | import { COLORS, PALETTE, axisTooltip, grid, xAxis, yAxis, legend, barSeries } from '@/utils/chartStyle'; |
| | | |
| | | export default { |
| | | name: 'OrganStatistics', |
| | |
| | | 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' } |
| | | { label: 'éå²å°åº', value: 'éå²å°åº' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'æ¥ç
§å°åº' }, |
| | | { label: 'æµåå°åº', value: 'æµåå°åº' }, |
| | | { label: 'çå°å°åº', value: 'çå°å°åº' }, |
| | | { label: '卿µ·å°åº', value: '卿µ·å°åº' }, |
| | | { label: 'æ½åå°åº', value: 'æ½åå°åº' }, |
| | | { label: '临æ²å°åº', value: '临æ²å°åº' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | |
| | | prop: 'organType', |
| | | type: 'select', |
| | | options: [ |
| | | { label: 'å
¨è', value: 'liver' }, |
| | | { label: 'è¾è', value: 'kidney' }, |
| | | { label: 'å¿è', value: 'heart' }, |
| | | { label: 'èº', value: 'lung' }, |
| | | { label: 'è°è
º', value: 'pancreas' }, |
| | | { label: 'ç¼è§è', value: 'cornea' } |
| | | { label: 'å
¨è', value: 'å
¨è' }, |
| | | { label: 'è¾è', value: 'è¾è' }, |
| | | { label: 'å¿è', value: 'å¿è' }, |
| | | { label: 'èº', value: 'èº' }, |
| | | { label: 'è°è
º', value: 'è°è
º' }, |
| | | { label: 'ç¼è§è', value: 'ç¼è§è' } |
| | | ], |
| | | width: '200px' |
| | | } |
| | |
| | | |
| | | // æ¦è§æ°æ® |
| | | 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' } |
| | | { title: 'æ»è·åæ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-claim', color: '#5B8FF9' }, |
| | | { title: 'æ»ç§»æ¤æ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-success', color: '#5AD8A6' }, |
| | | { title: 'æ»ç§»æ¤ç', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-data', color: '#F6BD16' }, |
| | | { title: 'æ»å¼ç¨æ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-warning', color: '#E8684A' } |
| | | ], |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | trendChartOptions: {}, |
| | | distributionChartOptions: {}, |
| | | |
| | | // å¾è¡¨æ°æ® |
| | | chartData: { |
| | | trend: [], |
| | | distribution: [] |
| | | }, |
| | | |
| | | // å¼¹çªæ§å¶ |
| | | 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 } |
| | | ] |
| | | } |
| | | detailQuery: {} |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | // æ°æ®æº(mock/ç宿¥å£)忢æ¶éæ°å è½½ |
| | | this.$watch(() => statisticsMode.useMock, () => this.loadData()); |
| | | }, |
| | | methods: { |
| | | // ç»è£
æ¥è¯¢æ¡ä»¶ |
| | | buildQuery() { |
| | | const q = {}; |
| | | if (this.query.region && this.query.region.length) { |
| | | q.area = this.query.region.join(','); |
| | | } |
| | | if (this.query.yearRange && this.query.yearRange.length === 2) { |
| | | q.startTime = this.query.yearRange[0]; |
| | | q.endTime = this.query.yearRange[1]; |
| | | } |
| | | if (this.query.organType) { |
| | | q.organName = this.query.organType; |
| | | } |
| | | return q; |
| | | }, |
| | | |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | const q = this.buildQuery(); |
| | | const [moonRes, typeRes, organRes] = await Promise.all([ |
| | | organMoon(q), |
| | | organType(q), |
| | | organ(q) |
| | | ]); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | // è·å/ç§»æ¤è¶å¿ |
| | | this.chartData.trend = toTotalsList(moonRes).map(item => ({ |
| | | time: toMonth(item.donateTime) || toMonth(item.month), |
| | | acquisition: toNum(item.getCount), |
| | | transplant: toNum(item.useCount) |
| | | })); |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | // å¨å®ç±»ååå¸ |
| | | this.chartData.distribution = toTotalsList(typeRes).map(item => ({ |
| | | name: item.organName || '', |
| | | value: toNum(item.getCount) |
| | | })); |
| | | |
| | | 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 all = toTotalsList(organRes).map(item => { |
| | | const get = toNum(item.getCount); |
| | | const use = toNum(item.useCount); |
| | | const rate = get > 0 ? Number(((use / get) * 100).toFixed(1)) : 0; |
| | | return { |
| | | year: String(item.donateTime || item.year || '').slice(0, 4), |
| | | organName: item.organName || '', |
| | | acquisitionCount: get, |
| | | transplantCount: use, |
| | | transplantRate: rate, |
| | | abandonCount: get - use, |
| | | region: item.area || item.region || '' |
| | | }; |
| | | }); |
| | | this.total = all.length; |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = filteredData.slice(start, start + this.query.limit); |
| | | this.total = filteredData.length; |
| | | this.tableData = all.slice(start, start + this.query.limit); |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | this.updateOverviewData(filteredData); |
| | | // æ´æ°æ¦è§ |
| | | this.updateOverviewData(all); |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | 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; |
| | | const totalAcquisition = data.reduce((sum, item) => sum + toNum(item.acquisitionCount), 0); |
| | | const totalTransplant = data.reduce((sum, item) => sum + toNum(item.transplantCount), 0); |
| | | const totalAbandon = data.reduce((sum, item) => sum + toNum(item.abandonCount), 0); |
| | | const transplantRate = totalAcquisition > 0 ? ((totalTransplant / totalAcquisition) * 100).toFixed(1) : 0; |
| | | |
| | | this.overviewData[0].value = totalAcquisition; |
| | | this.overviewData[1].value = totalTransplant; |
| | |
| | | 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: 'æ°é(个)' |
| | | }, |
| | | tooltip: axisTooltip(), |
| | | legend: legend(['è·åæ°é', 'ç§»æ¤æ°é']), |
| | | grid: grid(), |
| | | xAxis: xAxis(this.chartData.trend.map(item => item.time)), |
| | | yAxis: yAxis(), |
| | | 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' } |
| | | } |
| | | barSeries('è·åæ°é', this.chartData.trend.map(item => item.acquisition), COLORS.blue), |
| | | barSeries('ç§»æ¤æ°é', this.chartData.trend.map(item => item.transplant), COLORS.green) |
| | | ] |
| | | }; |
| | | |
| | | // åå¸å¾ |
| | | this.distributionChartOptions = { |
| | | color: PALETTE, |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | |
| | | legend: { |
| | | orient: 'vertical', |
| | | left: 'left', |
| | | data: this.mockData.distribution.map(item => item.name) |
| | | top: 'middle', |
| | | itemWidth: 10, |
| | | itemHeight: 10, |
| | | itemGap: 16, |
| | | textStyle: { fontSize: 14, color: '#606266' }, |
| | | data: this.chartData.distribution.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'å¨å®åå¸', |
| | | type: 'pie', |
| | | radius: '55%', |
| | | center: ['50%', '60%'], |
| | | data: this.mockData.distribution, |
| | | center: ['55%', '50%'], |
| | | itemStyle: { borderColor: '#fff', borderWidth: 2 }, |
| | | label: { fontSize: 14, color: '#606266', formatter: '{b} {d}%' }, |
| | | labelLine: { lineStyle: { color: '#c0c4cc' } }, |
| | | data: this.chartData.distribution, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | |
| | | <el-col :span="12"> |
| | | <el-card> |
| | | <div slot="header" class="clearfix"> |
| | | <span>å¼ç¨çææåæ</span> |
| | | <span>å°åºè·å/å¼ç¨ç对æ¯</span> |
| | | </div> |
| | | <EChartsWrapper |
| | | :id="'abandon-chart'" |
| | |
| | | <el-table-column label="å¨å®æ»æ°" prop="totalOrgans" align="center" /> |
| | | <el-table-column label="ç§»æ¤æ»æ°" prop="totalTransplants" align="center" /> |
| | | </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'; |
| | | import { organ, organMoon, caseTrendMoon } from '@/api/businessApi'; |
| | | import { toTotalsList, toMonth, toNum } from '@/utils/totals'; |
| | | import statisticsMode from '@/utils/statisticsMode'; |
| | | import { COLORS, axisTooltip, grid, xAxis, legend, barSeries, lineSeries } from '@/utils/chartStyle'; |
| | | |
| | | export default { |
| | | name: 'RateStatistics', |
| | |
| | | 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' } |
| | | { label: 'éå²å°åº', value: 'éå²å°åº' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'æ¥ç
§å°åº' }, |
| | | { label: 'æµåå°åº', value: 'æµåå°åº' }, |
| | | { label: 'çå°å°åº', value: 'çå°å°åº' }, |
| | | { label: '卿µ·å°åº', value: '卿µ·å°åº' }, |
| | | { label: 'æ½åå°åº', value: 'æ½åå°åº' }, |
| | | { label: '临æ²å°åº', value: '临æ²å°åº' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | |
| | | { |
| | | key: 'acquisition', |
| | | title: 'æç®è·åç', |
| | | value: '85.5%', |
| | | value: '0%', |
| | | trendIcon: 'el-icon-top', |
| | | trendText: 'â2.7%', |
| | | trendText: 'â', |
| | | trendClass: 'up' |
| | | }, |
| | | { |
| | | key: 'avg', |
| | | title: 'å¹³åå¨å®æ°', |
| | | value: '3.2', |
| | | value: '0', |
| | | trendIcon: 'el-icon-top', |
| | | trendText: 'â0.2', |
| | | trendText: 'â', |
| | | trendClass: 'up' |
| | | }, |
| | | { |
| | | key: 'abandon', |
| | | title: 'å¨å®å¼ç¨ç', |
| | | value: '12.3%', |
| | | value: '0%', |
| | | trendIcon: 'el-icon-bottom', |
| | | trendText: 'â2.7%', |
| | | trendText: 'â', |
| | | trendClass: 'down' |
| | | } |
| | | ], |
| | |
| | | trendChartOptions: {}, |
| | | abandonChartOptions: {}, |
| | | |
| | | // å¾è¡¨æ°æ® |
| | | trendData: [], |
| | | regionCompareData: [], |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | 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 } |
| | | ] |
| | | } |
| | | total: 0 |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | // æ°æ®æº(mock/ç宿¥å£)忢æ¶éæ°å è½½ |
| | | this.$watch(() => statisticsMode.useMock, () => this.loadData()); |
| | | }, |
| | | methods: { |
| | | // ç»è£
æ¥è¯¢æ¡ä»¶ |
| | | buildQuery() { |
| | | const q = {}; |
| | | if (this.query.region && this.query.region.length) { |
| | | q.area = this.query.region.join(','); |
| | | } |
| | | if (this.query.yearRange && this.query.yearRange.length === 2) { |
| | | q.startTime = this.query.yearRange[0]; |
| | | q.endTime = this.query.yearRange[1]; |
| | | } |
| | | return q; |
| | | }, |
| | | |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | const q = this.buildQuery(); |
| | | const [organRes, moonRes, caseMoonRes] = await Promise.all([ |
| | | organ(q), |
| | | organMoon(q), |
| | | caseTrendMoon(q) |
| | | ]); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | // å¨å®ææè¶å¿ï¼è·åç(ç§»æ¤/è·å)ãå¼ç¨ç |
| | | this.trendData = toTotalsList(moonRes) |
| | | .map(item => { |
| | | const get = toNum(item.getCount); |
| | | const use = toNum(item.useCount); |
| | | return { |
| | | month: toMonth(item.donateTime) || toMonth(item.month), |
| | | acquisition: get > 0 ? Number(((use / get) * 100).toFixed(1)) : 0, |
| | | abandon: get > 0 ? Number((((get - use) / get) * 100).toFixed(1)) : 0 |
| | | }; |
| | | }) |
| | | .sort((a, b) => String(a.month).localeCompare(String(b.month))); |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | // æ 年份|å°åº æ±æ»å¨å®æ°æ® |
| | | const organByKey = {}; |
| | | toTotalsList(organRes).forEach(item => { |
| | | const year = String(item.donateTime || item.year || '').slice(0, 4); |
| | | const region = item.area || item.region || ''; |
| | | const key = `${year}|${region}`; |
| | | if (!organByKey[key]) organByKey[key] = { year, region, organs: 0, transplants: 0 }; |
| | | organByKey[key].organs += toNum(item.getCount); |
| | | organByKey[key].transplants += toNum(item.useCount); |
| | | }); |
| | | |
| | | 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) |
| | | ); |
| | | } |
| | | // æ 年份|å°åº æ±æ»æ¡ä¾æ°æ®ï¼æç®æ»æ°ãæ½å¨æ¡ä¾ï¼ |
| | | const caseByKey = {}; |
| | | toTotalsList(caseMoonRes).forEach(item => { |
| | | const month = toMonth(item.donateTime) || toMonth(item.month); |
| | | const year = String(month).slice(0, 4); |
| | | const region = item.area || item.region || ''; |
| | | const key = `${year}|${region}`; |
| | | if (!caseByKey[key]) caseByKey[key] = { year, region, potential: 0, complete: 0 }; |
| | | caseByKey[key].potential += toNum(item.potentialCount); |
| | | caseByKey[key].complete += toNum(item.completeCount); |
| | | }); |
| | | |
| | | this.tableData = filteredData; |
| | | this.total = filteredData.length; |
| | | // åå¹¶ 年份|å°åº æç» |
| | | const keys = new Set([...Object.keys(organByKey), ...Object.keys(caseByKey)]); |
| | | const rows = []; |
| | | keys.forEach(key => { |
| | | const o = organByKey[key] || {}; |
| | | const c = caseByKey[key] || {}; |
| | | const year = o.year || c.year || ''; |
| | | const region = o.region || c.region || ''; |
| | | const organs = o.organs || 0; |
| | | const transplants = o.transplants || 0; |
| | | const potential = c.potential || 0; |
| | | const complete = c.complete || 0; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | rows.push({ |
| | | year, |
| | | region, |
| | | acquisitionRate: potential > 0 ? Number(((complete / potential) * 100).toFixed(1)) : 0, |
| | | avgOrgans: complete > 0 ? Number((organs / complete).toFixed(1)) : 0, |
| | | abandonRate: organs > 0 ? Number((((organs - transplants) / organs) * 100).toFixed(1)) : 0, |
| | | totalDonation: complete, |
| | | totalOrgans: organs, |
| | | totalTransplants: transplants |
| | | }); |
| | | }); |
| | | rows.sort((a, b) => String(a.year + a.region).localeCompare(String(b.year + b.region))); |
| | | |
| | | this.total = rows.length; |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = rows.slice(start, start + this.query.limit); |
| | | |
| | | // å°åºç»´åº¦çè·åç/å¼ç¨çï¼ç¨æ½å¨/宿ãå¨å®/ç§»æ¤åå§æ°æ®æ±æ» |
| | | const regionCompare = {}; |
| | | toTotalsList(caseMoonRes).forEach(item => { |
| | | const region = item.area || item.region || ''; |
| | | if (!regionCompare[region]) regionCompare[region] = { region, potential: 0, complete: 0 }; |
| | | regionCompare[region].potential += toNum(item.potentialCount); |
| | | regionCompare[region].complete += toNum(item.completeCount); |
| | | }); |
| | | const organRegion = {}; |
| | | toTotalsList(organRes).forEach(item => { |
| | | const region = item.area || item.region || ''; |
| | | if (!organRegion[region]) organRegion[region] = { organs: 0, transplants: 0 }; |
| | | organRegion[region].organs += toNum(item.getCount); |
| | | organRegion[region].transplants += toNum(item.useCount); |
| | | }); |
| | | const regionNames = new Set([...Object.keys(regionCompare), ...Object.keys(organRegion)]); |
| | | this.regionCompareData = [...regionNames].map(region => { |
| | | const c = regionCompare[region] || {}; |
| | | const o = organRegion[region] || {}; |
| | | const potential = c.potential || 0; |
| | | const complete = c.complete || 0; |
| | | const organs = o.organs || 0; |
| | | const transplants = o.transplants || 0; |
| | | return { |
| | | region, |
| | | acquisition: potential > 0 ? Number(((complete / potential) * 100).toFixed(1)) : 0, |
| | | abandon: organs > 0 ? Number((((organs - transplants) / organs) * 100).toFixed(1)) : 0 |
| | | }; |
| | | }); |
| | | |
| | | const totals = { |
| | | totalPotential: Object.values(caseByKey).reduce((s, c) => s + c.potential, 0), |
| | | totalDonation: Object.values(caseByKey).reduce((s, c) => s + c.complete, 0), |
| | | totalOrgans: Object.values(organByKey).reduce((s, o) => s + o.organs, 0), |
| | | totalTransplants: Object.values(organByKey).reduce((s, o) => s + o.transplants, 0) |
| | | }; |
| | | |
| | | this.updateOverviewData(totals); |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°ä»ªè¡¨ç |
| | | updateOverviewData(totals) { |
| | | const { totalPotential, totalDonation, totalOrgans, totalTransplants } = totals; |
| | | |
| | | const acquisitionRate = totalPotential > 0 ? Number(((totalDonation / totalPotential) * 100).toFixed(1)) : 0; |
| | | const avgOrgans = totalDonation > 0 ? Number((totalOrgans / totalDonation).toFixed(1)) : 0; |
| | | const abandonRate = totalOrgans > 0 ? Number((((totalOrgans - totalTransplants) / totalOrgans) * 100).toFixed(1)) : 0; |
| | | |
| | | this.dashboardData[0].value = `${acquisitionRate}%`; |
| | | this.dashboardData[1].value = `${avgOrgans}`; |
| | | this.dashboardData[2].value = `${abandonRate}%`; |
| | | this.dashboardData[0].trendText = acquisitionRate >= 80 ? 'âè¾¾æ ' : 'åä½'; |
| | | this.dashboardData[2].trendText = abandonRate <= 15 ? 'âè¾¾æ ' : 'åé«'; |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // è¶å¿å¾ |
| | | // è·åçè¶å¿å¾ |
| | | this.trendChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross', |
| | | label: { |
| | | backgroundColor: '#6a7985' |
| | | } |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æç®è·åç', 'å¹³åå¨å®æ°', 'å¨å®å¼ç¨ç'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | tooltip: axisTooltip(), |
| | | legend: legend(['å¨å®å©ç¨ç', 'å¨å®å¼ç¨ç']), |
| | | grid: grid(), |
| | | xAxis: { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: this.mockData.trend.map(item => item.year + 'å¹´') |
| | | data: this.trendData.map(item => item.month), |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: '#8a919f', fontSize: 12 } |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | axisLabel: { |
| | | formatter: '{value}' |
| | | } |
| | | axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' }, |
| | | splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } } |
| | | }, |
| | | 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 |
| | | } |
| | | lineSeries('å¨å®å©ç¨ç', this.trendData.map(item => item.acquisition), COLORS.blue, { area: true }), |
| | | lineSeries('å¨å®å¼ç¨ç', this.trendData.map(item => item.abandon), COLORS.coral) |
| | | ] |
| | | }; |
| | | |
| | | // å¼ç¨çææå¾ |
| | | // å°åºè·å/å¼ç¨ç对æ¯å¾ |
| | | this.abandonChartOptions = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | | }, |
| | | legend: { |
| | | orient: 'vertical', |
| | | left: 'left', |
| | | data: this.mockData.abandon.map(item => item.name) |
| | | tooltip: axisTooltip(), |
| | | legend: legend(['æç®è·åç', 'å¨å®å¼ç¨ç']), |
| | | grid: grid(), |
| | | xAxis: xAxis(this.regionCompareData.map(item => item.region)), |
| | | yAxis: { |
| | | type: 'value', |
| | | axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' }, |
| | | splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } } |
| | | }, |
| | | 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 |
| | | } |
| | | barSeries('æç®è·åç', this.regionCompareData.map(item => item.acquisition), COLORS.blue), |
| | | barSeries('å¨å®å¼ç¨ç', this.regionCompareData.map(item => item.abandon), COLORS.coral) |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·å仪表çå¾è¡¨é
ç½® |
| | | // è·å仪表çå¾è¡¨é
ç½®ï¼è¡¨ç + æéï¼æ°å¼ç±ä¸æ¹å¤§æ°åå±ç¤ºï¼å»åº¦å¤ç½®ï¼ |
| | | getDashboardOptions(item) { |
| | | const value = parseFloat(item.value); |
| | | const max = item.key === 'avg' ? 5 : 100; |
| | | const data = [{ value, name: item.title }]; |
| | | const color = item.key === 'abandon' ? COLORS.coral : item.key === 'acquisition' ? COLORS.blue : COLORS.green; |
| | | // å¹³åå¨å®æ°èå´å°ç¨æ´æ°å»åº¦ï¼ç¾åæ¯ç¨ 0/25/50/75/100 äºä¸ªå»åº¦ï¼é¿å
æ°åæ¥æ¤ |
| | | const splitNumber = item.key === 'avg' ? 5 : 4; |
| | | |
| | | return { |
| | | tooltip: { |
| | | formatter: '{b}: {c}' + (item.key === 'abandon' ? '%' : item.key === 'acquisition' ? '%' : '') |
| | | }, |
| | | series: [ |
| | | { |
| | | name: item.title, |
| | | type: 'gauge', |
| | | progress: { |
| | | show: true, |
| | | width: 18 |
| | | }, |
| | | startAngle: 210, |
| | | endAngle: -30, |
| | | min: 0, |
| | | max, |
| | | splitNumber, |
| | | radius: '60%', |
| | | center: ['50%', '52%'], |
| | | axisLine: { |
| | | lineStyle: { |
| | | width: 18 |
| | | width: 10, |
| | | color: [[value / max, color], [1, '#eef0f5']] |
| | | } |
| | | }, |
| | | axisTick: { |
| | | show: false |
| | | pointer: { |
| | | length: '60%', |
| | | width: 4, |
| | | itemStyle: { color } |
| | | }, |
| | | splitLine: { |
| | | length: 15, |
| | | lineStyle: { |
| | | width: 2, |
| | | color: '#999' |
| | | } |
| | | }, |
| | | anchor: { show: false }, |
| | | axisTick: { show: false }, |
| | | // length å½é¶ + è´è·ç¦»ï¼ä½¿å»åº¦æ°åè½å¨è¡¨çåå¼§å¤ä¾§ï¼å¤å´ï¼ |
| | | splitLine: { show: false, length: 0 }, |
| | | axisLabel: { |
| | | distance: 25, |
| | | color: '#999', |
| | | distance: -18, |
| | | color: '#8a919f', |
| | | 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 |
| | | title: { show: false }, |
| | | detail: { show: false }, |
| | | data: [{ value, name: item.title }] |
| | | } |
| | | ] |
| | | }; |
| | |
| | | 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('导åºåè½å¼åä¸...'); |
| | |
| | | <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 |
| | | v-if="useMock" |
| | | :id="'abandon-analysis-chart'" |
| | | :options="abandonChartOptions" |
| | | height="400px" |
| | | /> |
| | | <el-empty v-else description="åç«¯ææªæä¾å¨å®å¼ç¨åå ç»è®¡æ¥å£"></el-empty> |
| | | </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]" |
| | | /> |
| | | <template v-if="useMock"> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="perDonorTable" |
| | | 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> |
| | | </template> |
| | | <el-empty v-else description="åç«¯ææªæä¾æ¯ä¾ä½å¨å®è·åæç»æ¥å£"></el-empty> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | |
| | | <script> |
| | | import FilterPanel from '@/components/charts/FilterPanel.vue'; |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | import { organType } from '@/api/businessApi'; |
| | | import { toTotalsList, toNum } from '@/utils/totals'; |
| | | import statisticsMode, { isStatisticsMock } from '@/utils/statisticsMode'; |
| | | import { COLORS, axisTooltip, grid, xAxis, yAxis, legend, barSeries } from '@/utils/chartStyle'; |
| | | |
| | | export default { |
| | | name: 'OrganUtilization', |
| | |
| | | query: { |
| | | region: [], |
| | | monthRange: [], |
| | | ageRange: [], |
| | | chinaCategory: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | |
| | | 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' } |
| | | { label: 'éå²å°åº', value: 'éå²å°åº' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'æ¥ç
§å°åº' }, |
| | | { label: 'æµåå°åº', value: 'æµåå°åº' }, |
| | | { label: 'çå°å°åº', value: 'çå°å°åº' }, |
| | | { label: '卿µ·å°åº', value: '卿µ·å°åº' }, |
| | | { label: 'æ½åå°åº', value: 'æ½åå°åº' }, |
| | | { label: '临æ²å°åº', value: '临æ²å°åº' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | |
| | | 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' } |
| | | { label: 'å
¨è', value: 'å
¨è' }, |
| | | { label: 'è¾è', value: 'è¾è' }, |
| | | { label: 'å¿è', value: 'å¿è' }, |
| | | { label: 'èº', value: 'èº' }, |
| | | { label: 'è°è
º', value: 'è°è
º' }, |
| | | { label: 'ç¼è§è', value: 'ç¼è§è' } |
| | | ], |
| | | 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' } |
| | | ], |
| | | |
| | | // å è½½ç¶æ |
| | |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | acquisitionChartOptions: {}, |
| | | abandonChartOptions: {}, |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | tableData: [], |
| | | total: 0, |
| | | // å¨å®ç±»åå叿°æ® |
| | | organData: [], |
| | | |
| | | // å¼ç¨åææ¥è¯¢åæ° |
| | | 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: 'çå°å°åº' |
| | | } |
| | | ] |
| | | } |
| | | // æ æ¥å£é¨åçæ¨¡ææ°æ®ï¼ä»
mock 模å¼å±ç¤ºï¼ |
| | | abandonReasons: [ |
| | | { name: 'å¨å®è´¨éä¸åæ ¼', value: 45 }, |
| | | { name: 'ä¾ä½ç¾ç
', value: 25 }, |
| | | { name: 'è¿è¾é®é¢', value: 15 }, |
| | | { name: 'åä½å¹é
é®é¢', value: 10 }, |
| | | { name: 'å
¶ä»åå ', value: 5 } |
| | | ], |
| | | perDonorTable: [ |
| | | { 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; |
| | | } |
| | | computed: { |
| | | useMock() { |
| | | return isStatisticsMock(); |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | 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 = { |
| | | abandonChartOptions() { |
| | | return { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b} : {c} ({d}%)' |
| | |
| | | legend: { |
| | | orient: 'vertical', |
| | | left: 'left', |
| | | data: this.mockData.abandon.map(item => item.name) |
| | | data: this.abandonReasons.map(item => item.name) |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | type: 'pie', |
| | | radius: '55%', |
| | | center: ['50%', '60%'], |
| | | data: this.mockData.abandon, |
| | | data: this.abandonReasons, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | |
| | | } |
| | | ] |
| | | }; |
| | | } |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | // æ°æ®æº(mock/ç宿¥å£)忢æ¶éæ°å è½½ |
| | | this.$watch(() => statisticsMode.useMock, () => this.loadData()); |
| | | }, |
| | | methods: { |
| | | // ç»è£
æ¥è¯¢æ¡ä»¶ |
| | | buildQuery() { |
| | | const q = {}; |
| | | if (this.query.region && this.query.region.length) { |
| | | q.area = this.query.region.join(','); |
| | | } |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | q.startTime = this.query.monthRange[0]; |
| | | q.endTime = this.query.monthRange[1]; |
| | | } |
| | | if (this.query.organType) { |
| | | q.organName = this.query.organType; |
| | | } |
| | | return q; |
| | | }, |
| | | |
| | | // è·åç³»åé¢è² |
| | | getSeriesColor(index) { |
| | | const colors = [ |
| | | '#409EFF', '#8cc5ff', // èè |
| | | '#67C23A', '#95d475', // è¾è |
| | | '#E6A23C', '#f3c96b', // å¿è |
| | | '#F56C6C', '#f89898', // èº |
| | | '#909399', '#b1b3b8' // ç¼è§è |
| | | ]; |
| | | return { color: colors[index % colors.length] }; |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | const q = this.buildQuery(); |
| | | const typeRes = await organType(q); |
| | | |
| | | this.organData = toTotalsList(typeRes).map(item => ({ |
| | | name: item.organName || 'æªç¥', |
| | | acquisition: toNum(item.getCount), |
| | | transplant: toNum(item.useCount) |
| | | })); |
| | | |
| | | this.updateCharts(); |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | this.acquisitionChartOptions = { |
| | | tooltip: axisTooltip(), |
| | | legend: legend(['è·åæ°é', 'ç§»æ¤æ°é']), |
| | | grid: grid(), |
| | | xAxis: xAxis(this.organData.map(item => item.name)), |
| | | yAxis: yAxis('æ°é(个)'), |
| | | series: [ |
| | | barSeries('è·åæ°é', this.organData.map(item => item.acquisition), COLORS.blue), |
| | | barSeries('ç§»æ¤æ°é', this.organData.map(item => item.transplant), COLORS.green) |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // è·åæ°éæ ç¾ |
| | |
| | | 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> |
| | | |
| | | <style scoped> |
| | | .statistics-page { |
| | | padding: 20px; |
| | | } |
| | | </style> |
| | |
| | | {{ 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> |
| | | </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'; |
| | | import { caseTrend, caseTrendArea, caseTrendMoon } from '@/api/businessApi'; |
| | | import { toTotalsList, toMonth, toNum } from '@/utils/totals'; |
| | | import statisticsMode from '@/utils/statisticsMode'; |
| | | import { COLORS, axisTooltip, grid, xAxis, legend, barSeries, lineSeries } from '@/utils/chartStyle'; |
| | | |
| | | // æç®ææ¿ = 宿æç® / æ½å¨æç®ï¼ç»ä¸å¤ç¨ case ç¸å
³æ¥å£æ¨å¯¼ |
| | | export default { |
| | | name: 'WillingnessStatistics', |
| | | components: { |
| | |
| | | query: { |
| | | region: [], |
| | | monthRange: [], |
| | | organType: null, |
| | | page: 1, |
| | | limit: 10 |
| | | }, |
| | |
| | | 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' } |
| | | { label: 'éå²å°åº', value: 'éå²å°åº' }, |
| | | { label: 'æ¥ç
§å°åº', value: 'æ¥ç
§å°åº' }, |
| | | { label: 'æµåå°åº', value: 'æµåå°åº' }, |
| | | { label: 'çå°å°åº', value: 'çå°å°åº' }, |
| | | { label: '卿µ·å°åº', value: '卿µ·å°åº' }, |
| | | { label: 'æ½åå°åº', value: 'æ½åå°åº' }, |
| | | { label: '临æ²å°åº', value: '临æ²å°åº' } |
| | | ], |
| | | width: '300px' |
| | | }, |
| | |
| | | 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' |
| | | } |
| | | ], |
| | | |
| | |
| | | |
| | | // æ¦è§æ°æ® |
| | | 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' } |
| | | { title: 'æ»æ¡ä¾æ°', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-flag', color: '#5B8FF9' }, |
| | | { title: 'æ»æç®æ°é', value: 0, desc: 'ç»è®¡å¨æå
', icon: 'el-icon-check', color: '#5AD8A6' }, |
| | | { title: 'å¹³åæç®ææ¿', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-s-data', color: '#F6BD16' }, |
| | | { title: 'è¶å¿åå', value: '0%', desc: 'ç»è®¡å¨æå
', icon: 'el-icon-trend', color: '#E8684A' } |
| | | ], |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | trendChartOptions: {}, |
| | | regionChartOptions: {}, |
| | | |
| | | // å¾è¡¨æ°æ® |
| | | trendData: [], |
| | | regionData: [], |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | 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 } |
| | | ] |
| | | } |
| | | total: 0 |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | // æ°æ®æº(mock/ç宿¥å£)忢æ¶éæ°å è½½ |
| | | this.$watch(() => statisticsMode.useMock, () => this.loadData()); |
| | | }, |
| | | methods: { |
| | | // ç»è£
æ¥è¯¢æ¡ä»¶ |
| | | buildQuery() { |
| | | const q = {}; |
| | | if (this.query.region && this.query.region.length) { |
| | | q.area = this.query.region.join(','); |
| | | } |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | q.startTime = this.query.monthRange[0]; |
| | | q.endTime = this.query.monthRange[1]; |
| | | } |
| | | return q; |
| | | }, |
| | | |
| | | // è®¡ç®ææ¿ç |
| | | calcWillingness(potential, complete) { |
| | | const p = toNum(potential); |
| | | return p > 0 ? Number(((toNum(complete) / p) * 100).toFixed(1)) : 0; |
| | | }, |
| | | |
| | | // å è½½æ°æ® |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | const q = this.buildQuery(); |
| | | const [trendRes, areaRes, moonRes] = await Promise.all([ |
| | | caseTrend(q), |
| | | caseTrendArea(q), |
| | | caseTrendMoon(q) |
| | | ]); |
| | | |
| | | // å¤çè¡¨æ ¼æ°æ® |
| | | let filteredData = [...this.mockData.table]; |
| | | // è¶å¿ï¼æä»½ç»´åº¦ï¼ |
| | | this.trendData = toTotalsList(trendRes) |
| | | .map(item => { |
| | | const potential = toNum(item.potentialCount); |
| | | const complete = toNum(item.completeCount); |
| | | return { |
| | | month: toMonth(item.donateTime) || toMonth(item.month), |
| | | potential, |
| | | complete, |
| | | willingness: this.calcWillingness(potential, complete) |
| | | }; |
| | | }) |
| | | .sort((a, b) => String(a.month).localeCompare(String(b.month))); |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('å°åº', '').toLowerCase()) |
| | | ); |
| | | } |
| | | // å°åºå¯¹æ¯ |
| | | this.regionData = toTotalsList(areaRes).map(item => { |
| | | const potential = toNum(item.potentialCount); |
| | | const complete = toNum(item.completeCount); |
| | | return { |
| | | region: item.area || item.region || '', |
| | | potential, |
| | | complete, |
| | | willingness: this.calcWillingness(potential, complete) |
| | | }; |
| | | }); |
| | | |
| | | 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 |
| | | ); |
| | | } |
| | | // æç»è¡¨ï¼å°åº x æä»½ï¼ |
| | | const rows = toTotalsList(moonRes) |
| | | .map(item => { |
| | | const potential = toNum(item.potentialCount); |
| | | const complete = toNum(item.completeCount); |
| | | return { |
| | | region: item.area || item.region || '', |
| | | month: toMonth(item.donateTime) || toMonth(item.month), |
| | | potential, |
| | | complete |
| | | }; |
| | | }) |
| | | .sort((a, b) => String(a.month).localeCompare(String(b.month))); |
| | | |
| | | 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)); |
| | | // 计ç®ç¯æ¯ååï¼åä¸å°åºï¼ç¸å¯¹ä¸ä¸æä»½ææ¿çååï¼ç¾åç¹ï¼ |
| | | rows.forEach(row => { |
| | | const willingness = this.calcWillingness(row.potential, row.complete); |
| | | // 该å°åºææä»½æåºåçä¸ä¸ä¸ªæ |
| | | const regionRows = rows.filter(r => r.region === row.region); |
| | | const idx = regionRows.indexOf(row); |
| | | let monthCompare = 0; |
| | | if (idx > 0) { |
| | | const prev = regionRows[idx - 1]; |
| | | const prevWillingness = this.calcWillingness(prev.potential, prev.complete); |
| | | monthCompare = Number((willingness - prevWillingness).toFixed(1)); |
| | | } |
| | | } |
| | | row.willingnessRate = willingness; |
| | | row.caseCount = row.potential; |
| | | row.donationCount = row.complete; |
| | | row.monthCompare = monthCompare; |
| | | }); |
| | | |
| | | this.tableData = filteredData; |
| | | this.total = filteredData.length; |
| | | this.total = rows.length; |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = rows.slice(start, start + this.query.limit); |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | this.updateOverviewData(filteredData); |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | this.updateOverviewData(rows); |
| | | this.updateCharts(); |
| | | |
| | | } catch (error) { |
| | | console.error('å è½½æ°æ®å¤±è´¥:', error); |
| | | this.$message.error('æ°æ®å 载失败'); |
| | |
| | | }, |
| | | |
| | | // æ´æ°æ¦è§æ°æ® |
| | | updateOverviewData(data) { |
| | | if (data.length === 0) return; |
| | | updateOverviewData(rows) { |
| | | const totalCases = rows.reduce((sum, r) => sum + toNum(r.potential), 0); |
| | | const totalDonations = rows.reduce((sum, r) => sum + toNum(r.complete), 0); |
| | | const avgWillingness = this.calcWillingness(totalCases, totalDonations); |
| | | |
| | | 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; |
| | | // è¶å¿ååï¼æè¿ä¸¤ä¸ªæææ¿çå·®å¼ |
| | | let trend = 0; |
| | | if (this.trendData.length >= 2) { |
| | | const last = this.trendData[this.trendData.length - 1].willingness; |
| | | const prev = this.trendData[this.trendData.length - 2].willingness; |
| | | trend = Number((last - prev).toFixed(1)); |
| | | } |
| | | |
| | | this.overviewData[0].value = totalCases; |
| | | this.overviewData[1].value = totalDonations; |
| | |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | updateCharts() { |
| | | // è¶å¿å¾ |
| | | const dualAxis = [ |
| | | { |
| | | type: 'value', |
| | | name: 'ææ¿ç(%)', |
| | | nameTextStyle: { color: '#b0b6c3', fontSize: 12 }, |
| | | axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' }, |
| | | splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } } |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'æ°é(个)', |
| | | nameTextStyle: { color: '#b0b6c3', fontSize: 12 }, |
| | | axisLabel: { color: '#8a919f', fontSize: 12 }, |
| | | splitLine: { show: false } |
| | | } |
| | | ]; |
| | | |
| | | // è¶å¿å¾ï¼ææ¿çæçº¿ + æ°éæ±ç¶ï¼åè½´ï¼ |
| | | this.trendChartOptions = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross', |
| | | label: { |
| | | backgroundColor: '#6a7985' |
| | | } |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æç®ææ¿', 'æ¡ä¾æ°é', 'æç®æ°é'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | tooltip: axisTooltip(), |
| | | legend: legend(['æç®ææ¿', 'æ¡ä¾æ°é', 'æç®æ°é']), |
| | | grid: grid(), |
| | | xAxis: { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: this.mockData.trend.map(item => item.month.replace('-', 'å¹´') + 'æ') |
| | | boundaryGap: true, |
| | | data: this.trendData.map(item => item.month), |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: '#8a919f', fontSize: 12 } |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'ææ¿ç(%)', |
| | | min: 60, |
| | | max: 90, |
| | | axisLabel: { |
| | | formatter: '{value}%' |
| | | } |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'æ°é(个)', |
| | | position: 'right' |
| | | } |
| | | ], |
| | | yAxis: dualAxis, |
| | | 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' } |
| | | } |
| | | lineSeries('æç®ææ¿', this.trendData.map(item => item.willingness), COLORS.blue, { area: true }), |
| | | barSeries('æ¡ä¾æ°é', this.trendData.map(item => item.potential), COLORS.green, { yAxisIndex: 1 }), |
| | | barSeries('æç®æ°é', this.trendData.map(item => item.complete), COLORS.amber, { yAxisIndex: 1 }) |
| | | ] |
| | | }; |
| | | |
| | | // å°åºå¯¹æ¯å¾ |
| | | 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' |
| | | } |
| | | ], |
| | | tooltip: axisTooltip(), |
| | | legend: legend(['æç®ææ¿', 'æ¡ä¾æ°é', 'æç®æ°é']), |
| | | grid: grid(), |
| | | xAxis: xAxis(this.regionData.map(item => item.region)), |
| | | yAxis: dualAxis, |
| | | 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' } |
| | | } |
| | | barSeries('æç®ææ¿', this.regionData.map(item => item.willingness), COLORS.blue), |
| | | barSeries('æ¡ä¾æ°é', this.regionData.map(item => item.potential), COLORS.green, { yAxisIndex: 1 }), |
| | | barSeries('æç®æ°é', this.regionData.map(item => item.complete), COLORS.amber, { yAxisIndex: 1 }) |
| | | ] |
| | | }; |
| | | }, |
| | |
| | | |
| | | // è·åè¿åº¦æ¡é¢è² |
| | | getProgressColor(percentage) { |
| | | if (percentage >= 80) return '#67C23A'; |
| | | if (percentage >= 70) return '#E6A23C'; |
| | | return '#F56C6C'; |
| | | if (percentage >= 80) return COLORS.green; |
| | | if (percentage >= 70) return COLORS.amber; |
| | | return COLORS.coral; |
| | | }, |
| | | |
| | | // è·åæ¯è¾æ ç¾ç±»å |
| | |
| | | this.query = { |
| | | region: [], |
| | | monthRange: [], |
| | | 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(); |
| | | }, |
| | | |
| | |
| | | } |
| | | }; |
| | | </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> |