WXL (wul)
11 小时以前 e9f35f9782f3d99d742c294fe503fc1294f203c4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
<template>
  <div class="charts-panel" ref="pdfTarget">
    <!-- 顶部筛选:时间区间 + 区间类型 + PDF导出 -->
    <div class="export-bar">
      <div class="filter-bar">
        <span class="filter-label">时间范围:</span>
        <el-date-picker
          v-model="queryParams.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="yyyy-MM-dd"
          size="small"
          class="filter-date"
        />
        <span class="filter-label">区间类型:</span>
        <el-radio-group v-model="queryParams.interval" size="small" @change="refreshAll">
          <el-radio-button label="day">按天</el-radio-button>
          <el-radio-button label="month">按月</el-radio-button>
          <el-radio-button label="year">按年</el-radio-button>
        </el-radio-group>
        <el-button type="primary" size="small" icon="el-icon-search" @click="refreshAll">查询</el-button>
        <el-button size="small" icon="el-icon-refresh" @click="handleReset">重置</el-button>
      </div>
      <el-button type="danger" size="small" icon="el-icon-download" @click="exportPDF">导出PDF</el-button>
    </div>
 
    <el-row :gutter="20">
      <!-- 1. 时间维度 - 折线图 -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>时间维度 · 门诊患者满意度趋势</span>
            <el-tag type="success" size="small">环比 {{ trendMoM }}</el-tag>
          </div>
          <div ref="chart1" class="chart-box"></div>
        </el-card>
      </el-col>
 
      <!-- 2. 分类维度 - 分组柱状图 -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>分类维度 · 各指标{{ intervalLabel }}对比</span>
          </div>
          <div ref="chart2" class="chart-box"></div>
        </el-card>
      </el-col>
    </el-row>
 
    <el-row :gutter="20" style="margin-top: 20px">
      <!-- 3. 单题选项 - 数据表格(可选择题目、科室/病区) -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>单题 · 选项统计</span>
            <div class="header-right">
              <el-select v-model="singleQuestionId" size="small" class="sel sel-lg" @change="refreshAll">
                <el-option v-for="q in questionOptions" :key="q.id" :label="q.title" :value="q.id" />
              </el-select>
              <el-select v-model="singleDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable placeholder="全部科室/病区" @change="refreshAll">
                <el-option-group label="科室">
                  <el-option v-for="d in deptOptions" :key="'dept:' + d" :label="d" :value="'dept:' + d" />
                </el-option-group>
                <el-option-group label="病区">
                  <el-option v-for="w in wardOptions" :key="'ward:' + w" :label="w" :value="'ward:' + w" />
                </el-option-group>
              </el-select>
            </div>
          </div>
          <el-table :data="singleQuestionData" border size="small">
            <el-table-column prop="option" label="选项" />
            <el-table-column prop="count" label="合计数量" width="120" align="center" />
            <el-table-column prop="rate" label="占比比例" width="120" align="center" />
          </el-table>
          <div class="table-footer">本题有效填写人次:<b>{{ singleQuestionMeta.total }}</b> 综合满意度:<b>{{ singleQuestionMeta.score }}</b></div>
        </el-card>
      </el-col>
 
      <!-- 4. 院区维度 - 分组柱状图 -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>院区维度 · 各院区{{ intervalLabel }}对比</span>
          </div>
          <div ref="chart4" class="chart-box"></div>
        </el-card>
      </el-col>
    </el-row>
 
    <el-row :gutter="20" style="margin-top: 20px">
      <!-- 5. 科室/病区维度 - 排名表格 -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>{{ rankDimLabel }}维度 · 门诊患者满意度得分排名</span>
            <div class="header-right">
              <el-radio-group v-model="rankDimension" size="mini" @change="onRankDimensionChange">
                <el-radio-button label="dept">按科室</el-radio-button>
                <el-radio-button label="ward">按病区</el-radio-button>
              </el-radio-group>
              <el-select v-model="rankDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable :placeholder="'全部' + rankDimLabel" @change="refreshAll">
                <el-option v-for="name in rankOptions" :key="name" :label="name" :value="name" />
              </el-select>
            </div>
          </div>
          <el-table :data="rankRows" border size="small" max-height="400">
            <el-table-column type="index" label="排名" width="60" align="center" />
            <el-table-column prop="name" :label="rankDimLabel" />
            <el-table-column prop="score" label="综合得分" width="120" align="center" />
          </el-table>
        </el-card>
      </el-col>
 
      <!-- 6. 单题×科室/病区 - 交叉排名表格 -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>单题×{{ crossDimLabel }} · 得分排名</span>
            <div class="header-right">
              <el-radio-group v-model="crossDimension" size="mini" @change="onCrossDimensionChange">
                <el-radio-button label="dept">按科室</el-radio-button>
                <el-radio-button label="ward">按病区</el-radio-button>
              </el-radio-group>
              <el-select v-model="crossDeptWards" multiple collapse-tags size="small" class="sel" clearable filterable :placeholder="'全部' + crossDimLabel" @change="refreshAll">
                <el-option v-for="name in crossOptions" :key="name" :label="name" :value="name" />
              </el-select>
            </div>
          </div>
          <el-table :data="crossRows" border size="small" max-height="400">
            <el-table-column type="index" label="排名" width="60" align="center" />
            <el-table-column prop="name" :label="crossDimLabel" />
            <el-table-column prop="score" label="综合得分" width="120" align="center" />
          </el-table>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
 
<script>
import * as echarts from 'echarts';
 
// 单题选项
const QUESTION_OPTIONS = ['非常满意', '满意', '一般', '不满意'];
// 可选题目
const QUESTIONS = [
  { id: 22, title: '第22题:抱怨和不满能否及时得到回应?' },
  { id: 1, title: '第1题:您对我院工作人员的服务质量满意不满意?' },
  { id: 5, title: '第5题:您对医院服务设施的总体印象如何?' },
  { id: 8, title: '第8题:医生对您是否尊重?' },
  { id: 12, title: '第12题:护士对您是否尊重?' },
  { id: 15, title: '第15题:您对预约挂号是否满意?' },
];
const DEPTS = ['保健科', '介入治疗科', '专科护士门诊', '康复医学科', '皮肤科', '内分泌科', '消化内科', '肾内科', '呼吸内科', '心血管内科', '神经内科', '血液科', '风湿免疫科', '肿瘤内科', '感染科', '老年科', '普外科', '骨科', '泌尿外科', '妇产科'];
const WARDS = ['一病区', '二病区', '三病区', '四病区', '五病区', '六病区', '七病区', '八病区'];
 
// 确定性伪随机
function hashString(str) {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}
function mulberry32(a) {
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
 
export default {
  name: 'ChartsPanel',
  data() {
    return {
      queryParams: {
        dateRange: [],
        interval: 'month',
      },
      deptOptions: DEPTS,
      wardOptions: WARDS,
      questionOptions: QUESTIONS,
 
      // 单题筛选
      singleQuestionId: 22,
      singleDeptWards: [],
 
      // 排名维度筛选
      rankDimension: 'dept',
      rankDeptWards: [],
      crossDimension: 'dept',
      crossDeptWards: [],
 
      // 图表数据
      trend: { buckets: [], data: [], seriesName: '门诊患者满意度' },
      categoryCompare: { categories: [], seriesNames: [], series1: [], series2: [] },
      hospitalCompare: { hospitals: [], seriesNames: [], series1: [], series2: [] },
 
      // 表格数据
      singleQuestionData: [],
      singleQuestionMeta: { total: '0', score: '0.00%' },
      deptRankData: [],
      wardRankData: [],
      crossDeptData: [],
      crossWardData: [],
    };
  },
  computed: {
    intervalLabel() {
      return { day: '按天', month: '按月', year: '按年' }[this.queryParams.interval] || '按月';
    },
    trendMoM() {
      const d = this.trend.data;
      if (d.length < 2) return '0.00';
      const diff = d[d.length - 1] - d[d.length - 2];
      return (diff >= 0 ? '+' : '') + diff.toFixed(2);
    },
    currentSingleQuestion() {
      return this.questionOptions.find((q) => q.id === this.singleQuestionId) || {};
    },
    rankDimLabel() {
      return this.rankDimension === 'dept' ? '科室' : '病区';
    },
    crossDimLabel() {
      return this.crossDimension === 'dept' ? '科室' : '病区';
    },
    rankOptions() {
      return this.rankDimension === 'dept' ? this.deptOptions : this.wardOptions;
    },
    crossOptions() {
      return this.crossDimension === 'dept' ? this.deptOptions : this.wardOptions;
    },
    rankRows() {
      const list = this.rankDimension === 'dept' ? this.deptRankData : this.wardRankData;
      if (!this.rankDeptWards.length) return list;
      return list.filter((r) => this.rankDeptWards.includes(r.name));
    },
    crossRows() {
      const list = this.crossDimension === 'dept' ? this.crossDeptData : this.crossWardData;
      if (!this.crossDeptWards.length) return list;
      return list.filter((r) => this.crossDeptWards.includes(r.name));
    },
  },
  mounted() {
    this.$nextTick(() => {
      if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
        this.queryParams.dateRange = this.defaultRange();
      }
      this.initCharts();
      this.refreshAll();
    });
  },
  methods: {
    initCharts() {
      echarts.init(this.$refs.chart1);
      echarts.init(this.$refs.chart2);
      echarts.init(this.$refs.chart4);
      window.addEventListener('resize', this.handleResize);
    },
    handleResize() {
      ['chart1', 'chart2', 'chart4'].forEach((ref) => {
        const c = echarts.getInstanceByDom(this.$refs[ref]);
        if (c) c.resize();
      });
    },
    handleReset() {
      this.queryParams.dateRange = this.defaultRange();
      this.queryParams.interval = 'month';
      this.refreshAll();
    },
    onRankDimensionChange() {
      this.rankDeptWards = [];
      this.refreshAll();
    },
    onCrossDimensionChange() {
      this.crossDeptWards = [];
      this.refreshAll();
    },
    defaultRange() {
      const end = new Date();
      const start = new Date();
      start.setDate(start.getDate() - 29);
      const f = (d) => {
        const y = d.getFullYear();
        const m = String(d.getMonth() + 1).padStart(2, '0');
        const day = String(d.getDate()).padStart(2, '0');
        return `${y}-${m}-${day}`;
      };
      return [f(start), f(end)];
    },
    // 区间类型 => 时间桶 + 柱状图对比序列名
    intervalConfig() {
      const buckets = this.genBuckets(this.queryParams.interval);
      if (this.queryParams.interval === 'day') return { buckets, seriesNames: ['昨日', '今日'] };
      if (this.queryParams.interval === 'year') return { buckets, seriesNames: ['去年', '今年'] };
      return { buckets, seriesNames: ['上月', '本月'] };
    },
    genBuckets(type) {
      const end = this.queryParams.dateRange && this.queryParams.dateRange[1]
        ? new Date(this.queryParams.dateRange[1])
        : new Date();
      const buckets = [];
      if (type === 'day') {
        for (let i = 14; i >= 0; i--) {
          const d = new Date(end);
          d.setDate(end.getDate() - i);
          buckets.push(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
        }
      } else if (type === 'year') {
        for (let i = 3; i >= 0; i--) buckets.push(String(end.getFullYear() - i));
      } else {
        for (let i = 5; i >= 0; i--) {
          const d = new Date(end.getFullYear(), end.getMonth() - i, 1);
          buckets.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`);
        }
      }
      return buckets;
    },
    // 刷新全部数据(模拟,接口就绪后替换为真实请求)
    refreshAll() {
      const baseChart = JSON.stringify(this.queryParams);
      const baseTime = JSON.stringify({ dateRange: this.queryParams.dateRange });
 
      // 图表:区间类型 + 时间范围
      this.genChartData(mulberry32(hashString(baseChart + 'charts')));
      // 单题表:时间范围 + 题目 + 科室/病区
      this.genSingleData(mulberry32(hashString(baseTime + JSON.stringify({ sq: this.singleQuestionId, sdw: this.singleDeptWards }))));
      // 科室/病区排名:时间范围 + 维度 + 选择
      this.genRankData(mulberry32(hashString(baseTime + JSON.stringify({ rd: this.rankDimension, rdw: this.rankDeptWards }))));
      // 单题×科室/病区排名:时间范围 + 维度 + 选择 + 题目
      this.genCrossData(mulberry32(hashString(baseTime + JSON.stringify({ cd: this.crossDimension, cdw: this.crossDeptWards, sq: this.singleQuestionId }))));
 
      this.renderCharts();
    },
    genChartData(rnd) {
      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
      const cfg = this.intervalConfig();
      const buckets = cfg.buckets;
 
      this.trend = {
        buckets,
        seriesName: '门诊患者满意度',
        data: buckets.map(() => Math.round(jitter(86, 90) * 100) / 100),
      };
 
      const categories = ['隐私保护', '环境与标识', '医务人员回应', '医生沟通', '挂号体验', '护士沟通'];
      this.categoryCompare = {
        categories,
        seriesNames: cfg.seriesNames,
        series1: categories.map(() => Math.round(jitter(82, 88) * 100) / 100),
        series2: categories.map(() => Math.round(jitter(83, 89) * 100) / 100),
      };
 
      const hospitals = ['院区一', '院区二', '院区三'];
      this.hospitalCompare = {
        hospitals,
        seriesNames: cfg.seriesNames,
        series1: hospitals.map(() => Math.round(jitter(85, 89) * 100) / 100),
        series2: hospitals.map(() => Math.round(jitter(86, 90) * 100) / 100),
      };
    },
    genSingleData(rnd) {
      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
      const scopeFactor = this.singleDeptWards.length ? 0.18 : 1;
      const total = Math.round(jitter(20000, 30000) * scopeFactor);
      const weights = [0.62, 0.27, 0.07, 0.04];
      const counts = weights.map((w) => Math.max(0, Math.round(total * w * jitter(0.92, 1.08))));
      const good = counts[0] + counts[1];
      this.singleQuestionData = QUESTION_OPTIONS.map((opt, i) => ({
        option: opt,
        count: counts[i],
        rate: ((counts[i] / total) * 100).toFixed(2) + '%',
      }));
      this.singleQuestionMeta = {
        total: total.toLocaleString(),
        score: ((good / total) * 100).toFixed(2) + '%',
      };
    },
    genRankData(rnd) {
      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
      this.deptRankData = DEPTS
        .map((d) => ({ name: d, score: jitter(70, 85).toFixed(2) }))
        .sort((a, b) => Number(b.score) - Number(a.score));
      this.wardRankData = WARDS
        .map((w) => ({ name: w, score: jitter(70, 85).toFixed(2) }))
        .sort((a, b) => Number(b.score) - Number(a.score));
    },
    genCrossData(rnd) {
      const jitter = (lo, hi) => lo + rnd() * (hi - lo);
      this.crossDeptData = DEPTS
        .map((d) => ({ name: d, score: jitter(70, 85).toFixed(2) }))
        .sort((a, b) => Number(b.score) - Number(a.score));
      this.crossWardData = WARDS
        .map((w) => ({ name: w, score: jitter(70, 85).toFixed(2) }))
        .sort((a, b) => Number(b.score) - Number(a.score));
    },
    renderCharts() {
      const c1 = echarts.getInstanceByDom(this.$refs.chart1);
      const c2 = echarts.getInstanceByDom(this.$refs.chart2);
      const c4 = echarts.getInstanceByDom(this.$refs.chart4);
      if (!c1 || !c2 || !c4) return;
 
      const lineGrid = { top: 32, right: 30, bottom: 32, left: 85 };
      const barGrid = { top: 48, right: 30, bottom: 60, left: 85 };
      const baseTooltip = { backgroundColor: 'rgba(255,255,255,0.95)', borderColor: '#e8e8e8', textStyle: { color: '#333', fontSize: 13 }, extraCssText: 'box-shadow: 0 2px 12px rgba(0,0,0,0.1); border-radius: 6px; padding: 10px 14px;' };
      const baseXAxis = { axisLine: { lineStyle: { color: '#e0e0e0' } }, axisTick: { show: false }, axisLabel: { color: '#666', fontSize: 12 } };
      const baseYAxis = { splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } }, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: '#999', fontSize: 11 } };
      const areaGrad = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(91,143,249,0.25)' },
        { offset: 1, color: 'rgba(91,143,249,0.02)' },
      ]);
 
      // 1. 折线图 - 时间维度
      c1.setOption({
        color: ['#5B8FF9'],
        grid: lineGrid,
        tooltip: { ...baseTooltip, trigger: 'axis' },
        xAxis: [{ ...baseXAxis, data: this.trend.buckets, boundaryGap: false }],
        yAxis: [{ ...baseYAxis, name: '满意度得分', nameTextStyle: { color: '#999', fontSize: 11 } }],
        series: [{
          name: this.trend.seriesName, type: 'line', smooth: true, symbol: 'circle', symbolSize: 8,
          data: this.trend.data,
          lineStyle: { width: 3, shadowBlur: 8, shadowColor: 'rgba(91,143,249,0.3)' },
          itemStyle: { color: '#5B8FF9' },
          areaStyle: { color: areaGrad },
        }],
      });
 
      // 2. 分组柱状图 - 分类维度
      c2.setOption({
        color: ['#6DC8EC', '#5B8FF9'],
        grid: barGrid,
        tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(0,0,0,0.03)' } } },
        legend: { data: this.categoryCompare.seriesNames, bottom: 8, textStyle: { color: '#666', fontSize: 12 } },
        xAxis: [{ ...baseXAxis, data: this.categoryCompare.categories, axisLabel: { ...baseXAxis.axisLabel, interval: 0 } }],
        yAxis: [{ ...baseYAxis, name: '得分', nameTextStyle: { color: '#999', fontSize: 11 } }],
        series: [
          { name: this.categoryCompare.seriesNames[0], type: 'bar', barWidth: 22, barGap: '30%',
            itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#6DC8EC' }, { offset: 1, color: '#48B0D5' }]) },
            data: this.categoryCompare.series1 },
          { name: this.categoryCompare.seriesNames[1], type: 'bar', barWidth: 22,
            itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#5B8FF9' }, { offset: 1, color: '#3D6FDB' }]) },
            data: this.categoryCompare.series2 },
        ],
      });
 
      // 4. 院区维度 - 分组柱状图
      c4.setOption({
        color: ['#6DC8EC', '#5B8FF9'],
        grid: barGrid,
        tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow' } },
        legend: { data: this.hospitalCompare.seriesNames, bottom: 8, textStyle: { color: '#666', fontSize: 12 } },
        xAxis: [{ ...baseXAxis, data: this.hospitalCompare.hospitals }],
        yAxis: [{ ...baseYAxis, name: '满意度得分', nameTextStyle: { color: '#999', fontSize: 11 } }],
        series: [
          { name: this.hospitalCompare.seriesNames[0], type: 'bar', barWidth: 24, barGap: '30%',
            itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#6DC8EC' }, { offset: 1, color: '#48B0D5' }]) },
            data: this.hospitalCompare.series1,
            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: (p) => p.value.toFixed(2) } },
          { name: this.hospitalCompare.seriesNames[1], type: 'bar', barWidth: 24,
            itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#5B8FF9' }, { offset: 1, color: '#3D6FDB' }]) },
            data: this.hospitalCompare.series2,
            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: (p) => p.value.toFixed(2) } },
        ],
      });
    },
 
    async exportPDF() {
      const chartRefs = [
        { ref: this.$refs.chart1, id: 'chart1' },
        { ref: this.$refs.chart2, id: 'chart2' },
        { ref: this.$refs.chart4, id: 'chart4' },
      ];
      const chartImages = {};
      for (const { ref, id } of chartRefs) {
        if (ref) {
          const instance = echarts.getInstanceByDom(ref);
          if (instance) {
            chartImages[id] = instance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' });
          }
        }
      }
 
      const printContent = this.$refs.pdfTarget.cloneNode(true);
      const chartBoxes = printContent.querySelectorAll('.chart-box');
      const imgKeys = Object.keys(chartImages);
      chartBoxes.forEach((box, i) => {
        if (imgKeys[i] && chartImages[imgKeys[i]]) {
          box.innerHTML = `<img src="${chartImages[imgKeys[i]]}" style="width:100%;height:auto;" />`;
        }
      });
 
      const win = window.open('', '_blank', 'width=900,height=800');
      if (!win) { this.$message.warning('请允许弹出窗口以导出PDF'); return; }
      win.document.write(`
        <html><head><meta charset="utf-8"><title>各项统计表</title>
        <style>
          body { font-family: 'Microsoft YaHei',sans-serif; padding: 20px; }
          .chart-card { break-inside: avoid; margin-bottom: 20px; border: 1px solid #ebeef5; border-radius: 6px; }
          .card-header { padding: 12px 16px; border-bottom: 1px solid #ebeef5; font-size: 15px; font-weight: 600; color: #303133; background: #f5f7fa; }
          .chart-box img { max-width: 100%; display: block; }
          table { width: 100%; border-collapse: collapse; margin: 12px 0; }
          th, td { border: 1px solid #d9d9d9; padding: 8px 12px; text-align: center; font-size: 12px; }
          th { background: #f0f2f5; font-weight: 600; }
          .table-footer { text-align: right; padding: 8px 0; font-size: 13px; color: #606266; }
          .export-bar, .filter-bar, .el-tag, .el-select, .el-radio-group { display: none; }
          @media print { @page { size: A4; margin: 10mm; } }
        </style></head><body>
        ${printContent.innerHTML}
        <script>window.onload=function(){window.print();}<\/script>
        </body></html>
      `);
      win.document.close();
    },
  },
};
</script>
 
<style lang="scss" scoped>
.export-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.filter-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; .filter-label { font-size: 13px; color: #606266; } .filter-date { width: 260px; } }
.chart-card { margin-bottom: 0; }
.chart-box { width: 100%; height: 360px; }
.card-header { display: flex; justify-content: space-between; align-items: center; span { font-size: 15px; font-weight: 600; color: #303133; } }
.header-right { display: flex; align-items: center; gap: 10px; .sel { width: 140px; } .sel-lg { width: 240px; } }
.table-footer { text-align: right; padding: 10px 0 0; font-size: 13px; color: #606266; }
</style>