WXL (wul)
11 小时以前 e9f35f9782f3d99d742c294fe503fc1294f203c4
测试完成
已修改6个文件
已添加4个文件
1910 ■■■■■ 文件已修改
src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue 443 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/DataOverview.vue 467 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/index.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/outsideChainnew.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/outsideChainwtnew.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/dedicated/heart/index.vue 388 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/dedicated/tubeFeed/index.vue 391 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/repositoryai/templateku/configurat/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
随访外链通用9.12.zip 补丁 | 查看 | 原始文档 | blame | 历史
随访通用 9.14.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
@@ -1,7 +1,28 @@
<template>
  <div class="charts-panel" ref="pdfTarget">
    <!-- PDF导出按钮 -->
    <!-- é¡¶éƒ¨ç­›é€‰ï¼šæ—¶é—´åŒºé—´ + åŒºé—´ç±»åž‹ + 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>
@@ -11,7 +32,7 @@
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>时间维度 Â· é—¨è¯Šæ‚£è€…满意度趋势</span>
            <el-tag type="success" size="small">环比 +0.97</el-tag>
            <el-tag type="success" size="small">环比 {{ trendMoM }}</el-tag>
          </div>
          <div ref="chart1" class="chart-box"></div>
        </el-card>
@@ -21,7 +42,7 @@
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>分类维度 Â· å„指标年度对比</span>
            <span>分类维度 Â· å„指标{{ intervalLabel }}对比</span>
          </div>
          <div ref="chart2" class="chart-box"></div>
        </el-card>
@@ -29,18 +50,31 @@
    </el-row>
    <el-row :gutter="20" style="margin-top: 20px">
      <!-- 3. å•题选项 - æ•°æ®è¡¨æ ¼ -->
      <!-- 3. å•题选项 - æ•°æ®è¡¨æ ¼ï¼ˆå¯é€‰æ‹©é¢˜ç›®ã€ç§‘室/病区) -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>单题 Â· ç¬¬22题:抱怨和不满能否及时得到回应?</span>
            <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>26,233</b> 综合满意度:<b>83.20%</b></div>
          <div class="table-footer">本题有效填写人次:<b>{{ singleQuestionMeta.total }}</b> 综合满意度:<b>{{ singleQuestionMeta.score }}</b></div>
        </el-card>
      </el-col>
@@ -48,7 +82,7 @@
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>院区维度 Â· å„院区年度对比</span>
            <span>院区维度 Â· å„院区{{ intervalLabel }}对比</span>
          </div>
          <div ref="chart4" class="chart-box"></div>
        </el-card>
@@ -56,29 +90,47 @@
    </el-row>
    <el-row :gutter="20" style="margin-top: 20px">
      <!-- 5. ç§‘室维度 - æŽ’名表格 -->
      <!-- 5. ç§‘室/病区维度 - æŽ’名表格 -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>科室维度 Â· é—¨è¯Šæ‚£è€…满意度得分排名</span>
            <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="deptRankData" border size="small">
          <el-table :data="rankRows" border size="small" max-height="400">
            <el-table-column type="index" label="排名" width="60" align="center" />
            <el-table-column prop="dept" label="科室" />
            <el-table-column prop="name" :label="rankDimLabel" />
            <el-table-column prop="score" label="综合得分" width="120" align="center" />
          </el-table>
        </el-card>
      </el-col>
      <!-- 6. å•题科室 - äº¤å‰åˆ†æžè¡¨æ ¼ -->
      <!-- 6. å•题×科室/病区 - äº¤å‰æŽ’名表格 -->
      <el-col :span="12">
        <el-card shadow="never" class="chart-card">
          <div slot="header" class="card-header">
            <span>单题×科室 Â· ç¬¬22题各科室得分</span>
            <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="crossDeptData" border size="small" max-height="400">
          <el-table :data="crossRows" border size="small" max-height="400">
            <el-table-column type="index" label="排名" width="60" align="center" />
            <el-table-column prop="dept" label="科室" />
            <el-table-column prop="name" :label="crossDimLabel" />
            <el-table-column prop="score" label="综合得分" width="120" align="center" />
          </el-table>
        </el-card>
@@ -90,113 +142,336 @@
<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",
  name: 'ChartsPanel',
  data() {
    return {
      singleQuestionData: [
        { option: "非常满意", count: 16513, rate: "62.95%" },
        { option: "满意", count: 7088, rate: "27.02%" },
        { option: "一般", count: 1760, rate: "6.71%" },
        { option: "不满意", count: 872, rate: "3.32%" },
      ],
      deptRankData: [
        { dept: "保健科", score: "81.54" }, { dept: "介入治疗科", score: "80.72" },
        { dept: "专科护士门诊", score: "79.27" }, { dept: "康复医学科", score: "78.65" },
        { dept: "皮肤科", score: "78.12" }, { dept: "内分泌科", score: "77.89" },
        { dept: "消化内科", score: "77.34" }, { dept: "肾内科", score: "76.50" },
        { dept: "呼吸内科", score: "75.88" }, { dept: "心血管内科", score: "75.21" },
      ],
      crossDeptData: [
        { dept: "保健科", score: "81.24" }, { dept: "介入治疗科", score: "81.22" },
        { dept: "专科护士门诊", score: "80.15" }, { dept: "康复医学科", score: "79.88" },
        { dept: "皮肤科", score: "79.01" }, { dept: "内分泌科", score: "78.45" },
        { dept: "消化内科", score: "77.92" }, { dept: "肾内科", score: "77.13" },
        { dept: "呼吸内科", score: "76.80" }, { dept: "心血管内科", score: "76.05" },
        { dept: "神经内科", score: "75.62" }, { dept: "血液科", score: "75.01" },
        { dept: "风湿免疫科", score: "74.38" }, { dept: "肿瘤内科", score: "73.85" },
        { dept: "感染科", score: "73.12" }, { dept: "老年科", score: "72.56" },
        { dept: "普外科", score: "71.90" }, { dept: "骨科", score: "71.23" },
        { dept: "泌尿外科", score: "70.55" }, { dept: "妇产科", score: "69.88" },
      ],
      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: [],
    };
  },
  mounted() { this.$nextTick(() => { this.initCharts(); }); },
  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() {
      const quarters = ['第一季度', '第二季度', '第三季度', '第四季度'];
      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 = ['院区一', '院区二', '院区三'];
      const baseGrid = { top: 20, right: 30, bottom: 30, left: 50 };
      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. æŠ˜çº¿å›¾ - æ—¶é—´ç»´åº¦
      const c1 = echarts.init(this.$refs.chart1);
      c1.setOption({
        color: ['#5B8FF9'],
        grid: { ...baseGrid, top: 25 },
        grid: lineGrid,
        tooltip: { ...baseTooltip, trigger: 'axis' },
        xAxis: [{ ...baseXAxis, data: quarters, boundaryGap: false }],
        yAxis: [{ ...baseYAxis, min: 86, max: 91, name: '满意度得分', nameTextStyle: { color: '#999', fontSize: 11 } }],
        xAxis: [{ ...baseXAxis, data: this.trend.buckets, boundaryGap: false }],
        yAxis: [{ ...baseYAxis, name: '满意度得分', nameTextStyle: { color: '#999', fontSize: 11 } }],
        series: [{
          name: '门诊患者满意度', type: 'line', smooth: true, symbol: 'circle', symbolSize: 8,
          data: [88.20, 87.32, 88.50, 89.17],
          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: 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)' }
          ])},
          areaStyle: { color: areaGrad },
        }],
      });
      // 2. åˆ†ç»„柱状图 - åˆ†ç±»ç»´åº¦
      const c2 = echarts.init(this.$refs.chart2);
      c2.setOption({
        color: ['#6DC8EC', '#5B8FF9'],
        grid: baseGrid,
        grid: barGrid,
        tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(0,0,0,0.03)' } } },
        legend: { data: ['2023å¹´', '2024å¹´'], bottom: 0, textStyle: { color: '#666', fontSize: 12 } },
        xAxis: [{ ...baseXAxis, data: categories, axisLabel: { ...baseXAxis.axisLabel, interval: 0 } }],
        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: '2023å¹´', type: 'bar', barWidth: 22, barGap: '30%',
          { 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: [86.34, 85.12, 83.45, 86.22, 88.55, 87.33] },
          { name: '2024å¹´', type: 'bar', barWidth: 22,
            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: [88.06, 84.57, 82.88, 87.77, 88.12, 87.00] },
            data: this.categoryCompare.series2 },
        ],
      });
      // 4. é™¢åŒºç»´åº¦ - åˆ†ç»„柱状图
      const c4 = echarts.init(this.$refs.chart4);
      c4.setOption({
        color: ['#6DC8EC', '#5B8FF9'],
        grid: baseGrid,
        grid: barGrid,
        tooltip: { ...baseTooltip, trigger: 'axis', axisPointer: { type: 'shadow' } },
        legend: { data: ['2023å¹´', '2024å¹´'], bottom: 0, textStyle: { color: '#666', fontSize: 12 } },
        xAxis: [{ ...baseXAxis, data: hospitals }],
        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: '2023å¹´', type: 'bar', barWidth: 24, barGap: '30%',
          { 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: [86.20, 87.69, 85.55],
            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: p => p.value.toFixed(2) } },
          { name: '2024å¹´', type: 'bar', barWidth: 24,
            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: [88.15, 88.14, 85.88],
            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: p => p.value.toFixed(2) } },
            data: this.hospitalCompare.series2,
            label: { show: true, position: 'top', color: '#666', fontSize: 12, fontWeight: 500, formatter: (p) => p.value.toFixed(2) } },
        ],
      });
      // å“åº”式
      window.addEventListener('resize', () => { c1.resize(); c2.resize(); c4.resize(); });
    },
    async exportPDF() {
      // å…ˆå°† ECharts å®žä¾‹è½¬ä¸ºå›¾ç‰‡ dataURL
      const chartRefs = [
        { ref: this.$refs.chart1, id: 'chart1' },
        { ref: this.$refs.chart2, id: 'chart2' },
@@ -212,19 +487,7 @@
        }
      }
      // å…‹éš†å†…容并替换图表为图片
      const printContent = this.$refs.pdfTarget.cloneNode(true);
      for (const { ref, id } of chartRefs) {
        if (chartImages[id] && ref) {
          const boxes = printContent.querySelectorAll('.chart-box');
          for (const box of boxes) {
            if (box.innerHTML.includes(id) || boxes.length === chartRefs.length) {
              box.innerHTML = `<img src="${chartImages[id]}" style="width:100%;height:auto;" />`;
            }
          }
        }
      }
      // æ›´å¯é çš„æ–¹å¼ï¼šæŒ‰ä½ç½®åŒ¹é…
      const chartBoxes = printContent.querySelectorAll('.chart-box');
      const imgKeys = Object.keys(chartImages);
      chartBoxes.forEach((box, i) => {
@@ -246,7 +509,7 @@
          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, .el-tag { display: none; }
          .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}
@@ -260,9 +523,11 @@
</script>
<style lang="scss" scoped>
.export-bar { text-align: right; margin-bottom: 16px; }
.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>
src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -1,6 +1,54 @@
<template>
  <div class="data-overview">
    <!-- è¡¨æ ¼ä¸€ï¼šå„服务项目满意度统计 -->
    <!-- é¡¶éƒ¨ç­›é€‰ï¼šæ—¶é—´èŒƒå›´ + ç§‘室/病区,控制本页所有数据 -->
    <el-card shadow="never" class="overview-card filter-card">
      <el-form inline class="filter-form">
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="queryParams.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
            size="small"
          />
        </el-form-item>
        <el-form-item label="科室/病区">
          <el-select
            v-model="queryParams.deptWard"
            clearable
            filterable
            placeholder="全部"
            size="small"
            class="filter-select"
          >
            <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>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" size="small" icon="el-icon-search" @click="fetchData">查询</el-button>
          <el-button size="small" icon="el-icon-refresh" @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- è¡¨æ ¼ä¸€ï¼šå„服务项目满意度统计(固定五项选项的题目) -->
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">各服务项目满意度统计</span>
@@ -32,31 +80,48 @@
      </el-table>
    </el-card>
    <!-- è¡¨æ ¼äºŒï¼šå•选题/多选题统计 -->
    <!-- è¡¨æ ¼äºŒï¼šå•选题/多选题统计(其余题目) -->
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">单选题/多选题统计</span>
        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table2')">导出Excel</el-button>
      </div>
      <div class="question-block" v-for="(q, qi) in questionStats" :key="qi">
        <h4 class="q-title">{{ q.title }}</h4>
        <el-table :data="q.options" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
          <el-table-column prop="option" label="选项" min-width="180" />
          <el-table-column prop="count" label="数量" width="100" align="center" />
          <el-table-column prop="rate" label="比例" width="100" align="center" />
        </el-table>
        <div v-if="q.total" class="q-footer">本题有效填写人次:<b>{{ q.total }}</b></div>
      <div id="table2">
        <div class="question-block" v-for="(q, qi) in questionStats" :key="qi">
          <h4 class="q-title">{{ q.title }}</h4>
          <el-table :data="q.options" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
            <el-table-column prop="option" label="选项" min-width="180" />
            <el-table-column prop="count" label="数量" width="100" align="center" />
            <el-table-column prop="rate" label="比例" width="100" align="center" />
          </el-table>
          <div v-if="q.total" class="q-footer">本题有效填写人次:<b>{{ q.total }}</b></div>
        </div>
      </div>
    </el-card>
    <!-- è¡¨æ ¼ä¸‰ï¼šå„科室满意度评分对比 -->
    <!-- è¡¨æ ¼ä¸‰ï¼šå„科室/病区满意度评分对比 -->
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">各科室满意度评分对比</span>
        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table3')">导出Excel</el-button>
        <span class="card-title">各科室/病区满意度评分对比</span>
        <div class="header-right">
          <el-radio-group v-model="statDimension" size="small" @change="onStatDimensionChange">
            <el-radio-button label="dept">按科室</el-radio-button>
            <el-radio-button label="ward">按病区</el-radio-button>
          </el-radio-group>
          <el-select v-model="statDeptWard" size="small" class="stat-select" placeholder="沿用顶部条件">
            <el-option label="沿用顶部条件" value="inherit" />
            <template v-if="statDimension === 'dept'">
              <el-option v-for="d in deptOptions" :key="'dept:' + d" :label="d" :value="'dept:' + d" />
            </template>
            <template v-else>
              <el-option v-for="w in wardOptions" :key="'ward:' + w" :label="w" :value="'ward:' + w" />
            </template>
          </el-select>
          <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table3')">导出Excel</el-button>
        </div>
      </div>
      <el-table id="table3" :data="deptComparison" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
        <el-table-column prop="dept" label="科室"fixed="left" align="center" />
      <el-table id="table3" :data="table3Rows" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
        <el-table-column prop="name" :label="statDimension === 'dept' ? '科室' : '病区'" fixed="left" align="center" />
        <el-table-column label="第1题" align="center">
          <el-table-column prop="q1VeryGood" label="非常满意" width="100" />
          <el-table-column prop="q1Good" label="满意" width="90" />
@@ -71,7 +136,7 @@
          <el-table-column prop="q2Bad" label="不满意" width="90" />
          <el-table-column prop="q2Score" label="得分" width="100" />
        </el-table-column>
        <el-table-column prop="totalScore" label="综合得分"  align="center" />
        <el-table-column prop="totalScore" label="综合得分" align="center" />
      </el-table>
    </el-card>
  </div>
@@ -80,68 +145,312 @@
<script>
import XLSX from 'xlsx';
// æ»¡æ„åº¦é¢˜çš„固定选项(顺序即表格列顺序)
const FIXED_OPTIONS = ['非常满意', '满意', '一般', '不满意', '不适用'];
const DEPTS = ['内分泌科', '皮肤科', '肿瘤内科', '消化内科', '肾内科'];
const WARDS = ['一病区', '二病区', '三病区', '四病区', '五病区'];
// æ»¡æ„åº¦é¢˜ï¼ˆå›ºå®šäº”项选项,counts é¡ºåºå¯¹åº” FIXED_OPTIONS)
const SATISFACTION_BASE = [
  ['诊疗过程中,医务人员是否注意保护您的隐私?', [3219, 1351, 142, 33, 0]],
  ['您对医院服务设施的总体印象如何?', [3110, 1369, 217, 49, 0]],
  ['医院的厕所是否清洁无异味?', [2973, 1504, 235, 33, 0]],
  ['医院内的路标和指示是否明确?', [3234, 1289, 196, 26, 0]],
  ['医院的空间布局是否便利?', [3037, 1460, 209, 39, 0]],
  ['看病期间,您的抱怨和不满能否及时得到回应?', [3019, 1317, 294, 115, 0]],
  ['医生是否用您听得懂的方式解释问题?', [3402, 967, 254, 122, 0]],
  ['医生对您是否尊重?', [3495, 897, 249, 104, 0]],
  ['医生是否仔细倾听您讲话?', [3422, 937, 251, 135, 0]],
  ['对挂号服务人员服务态度是否满意', [3458, 1205, 52, 30, 0]],
  ['您对预约挂号是否满意?', [3527, 1088, 72, 58, 0]],
  ['护士对您是否尊重?', [3420, 1184, 113, 28, 0]],
  ['护士是否用您听得懂的方式解释问题?', [3346, 1250, 122, 27, 0]],
  ['护士是否仔细倾听您讲话?', [3378, 1205, 134, 28, 0]],
];
// å…¶ä½™é¢˜ç›®ï¼ˆå•选/多选,非固定满意度选项)
const OTHER_BASE = [
  {
    title: '您对我院工作人员的服务质量满意不满意?【分值单选题】',
    type: 'single',
    options: [['非常满意', 1084], ['满意', 1959], ['一般', 167], ['不满意', 153]],
  },
  {
    title: '您对哪部分服务不满意?【多选题】',
    type: 'multiple',
    total: 408,
    options: [['医生诊断和诊疗', 131], ['护理服务', 47], ['其他医务人员技术水平', 30], ['工人', 72]],
  },
  {
    title: '医生诊断和治疗中哪些服务质量不满意?【多选题】',
    type: 'multiple',
    total: 131,
    options: [['医生诊断错误', 23], ['对治疗效果不满意', 72], ['其他', 54]],
  },
];
// ç»Ÿä¸€æŽ¥å£è¿”回的题目(模拟):固定五项选项 => è¡¨ä¸€ï¼Œå…¶ä½™ => è¡¨äºŒ
const RAW_QUESTIONS = [
  ...SATISFACTION_BASE.map(([title, counts]) => ({
    title,
    type: 'single',
    options: FIXED_OPTIONS.map((opt, i) => ({ option: opt, count: counts[i] })),
  })),
  ...OTHER_BASE.map((q) => ({
    title: q.title,
    type: q.type,
    total: q.total,
    options: q.options.map(([option, count]) => ({ option, count })),
  })),
];
// ç¡®å®šæ€§ä¼ªéšæœºï¼ˆä¾¿äºŽç­›é€‰æ¡ä»¶å›ºå®šæ—¶ç»“果稳定)
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: "DataOverview",
  name: 'DataOverview',
  data() {
    return {
      serviceSatisfaction: [
        { category: "诊疗过程中,医务人员是否注意保护您的隐私?", veryGoodCount: 3219, veryGoodRate: "67.84%", goodCount: 1351, goodRate: "28.47%", normalCount: 142, normalRate: "2.99%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "87.82" },
        { category: "您对医院服务设施的总体印象如何?", veryGoodCount: 3110, veryGoodRate: "65.54%", goodCount: 1369, goodRate: "28.85%", normalCount: 217, normalRate: "4.57%", badCount: 49, badRate: "1.03%", naCount: 0, naRate: "0.00%", score: "86.30" },
        { category: "医院的厕所是否清洁无异味?", veryGoodCount: 2973, veryGoodRate: "62.66%", goodCount: 1504, goodRate: "31.70%", normalCount: 235, normalRate: "4.95%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "85.44" },
        { category: "医院内的路标和指示是否明确?", veryGoodCount: 3234, veryGoodRate: "68.16%", goodCount: 1289, goodRate: "27.17%", normalCount: 196, normalRate: "4.13%", badCount: 26, badRate: "0.55%", naCount: 0, naRate: "0.00%", score: "87.64" },
        { category: "医院的空间布局是否便利?", veryGoodCount: 3037, veryGoodRate: "64.00%", goodCount: 1460, goodRate: "30.77%", normalCount: 209, normalRate: "4.40%", badCount: 39, badRate: "0.82%", naCount: 0, naRate: "0.00%", score: "85.99" },
        { category: "看病期间,您的抱怨和不满能否及时得到回应?", veryGoodCount: 3019, veryGoodRate: "63.62%", goodCount: 1317, goodRate: "27.76%", normalCount: 294, normalRate: "6.20%", badCount: 115, badRate: "2.42%", naCount: 0, naRate: "0.00%", score: "84.19" },
        { category: "医生是否用您听得懂的方式解释问题?", veryGoodCount: 3402, veryGoodRate: "71.70%", goodCount: 967, goodRate: "20.38%", normalCount: 254, normalRate: "5.35%", badCount: 122, badRate: "2.57%", naCount: 0, naRate: "0.00%", score: "87.07" },
        { category: "医生对您是否尊重?", veryGoodCount: 3495, veryGoodRate: "73.66%", goodCount: 897, goodRate: "18.90%", normalCount: 249, normalRate: "5.25%", badCount: 104, badRate: "2.19%", naCount: 0, naRate: "0.00%", score: "88.01" },
        { category: "医生是否仔细倾听您讲话?", veryGoodCount: 3422, veryGoodRate: "72.12%", goodCount: 937, goodRate: "19.75%", normalCount: 251, normalRate: "5.29%", badCount: 135, badRate: "2.85%", naCount: 0, naRate: "0.00%", score: "87.05" },
        { category: "对挂号服务人员服务态度是否满意", veryGoodCount: 3458, veryGoodRate: "72.88%", goodCount: 1205, goodRate: "25.40%", normalCount: 52, normalRate: "1.10%", badCount: 30, badRate: "0.63%", naCount: 0, naRate: "0.00%", score: "90.17" },
        { category: "您对预约挂号是否满意?", veryGoodCount: 3527, veryGoodRate: "74.33%", goodCount: 1088, goodRate: "22.93%", normalCount: 72, normalRate: "1.52%", badCount: 58, badRate: "1.22%", naCount: 0, naRate: "0.00%", score: "90.12" },
        { category: "护士对您是否尊重?", veryGoodCount: 3420, veryGoodRate: "72.08%", goodCount: 1184, goodRate: "24.95%", normalCount: 113, normalRate: "2.38%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.50" },
        { category: "护士是否用您听得懂的方式解释问题?", veryGoodCount: 3346, veryGoodRate: "70.52%", goodCount: 1250, goodRate: "26.34%", normalCount: 122, normalRate: "2.57%", badCount: 27, badRate: "0.57%", naCount: 0, naRate: "0.00%", score: "88.94" },
        { category: "护士是否仔细倾听您讲话?", veryGoodCount: 3378, veryGoodRate: "71.19%", goodCount: 1205, goodRate: "25.40%", normalCount: 134, normalRate: "2.82%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.06" },
        { category: "总计", veryGoodCount: 54863, veryGoodRate: "69.02%", goodCount: 20800, goodRate: "26.17%", normalCount: 2920, normalRate: "3.67%", badCount: 908, badRate: "1.14%", naCount: 0, naRate: "0.00%", score: "87.66" },
      ],
      questionStats: [
        {
          title: "第1题:您对我院工作人员的服务质量满意不满意?【分值单选题】- ç»¼åˆå¾—分",
          options: [
            { option: "非常满意", count: 1084, rate: "75.66%" },
            { option: "满意", count: 1959, rate: "20.92%" },
            { option: "一般", count: 167, rate: "1.78%" },
            { option: "不满意", count: 153, rate: "1.63%" },
          ],
          total: 3363
        },
        {
          title: "第2题:您对哪部分服务不满意?【多选题】",
          options: [
            { option: "医生诊断和诊疗", count: 131, rate: "32.11%" },
            { option: "护理服务", count: 47, rate: "11.52%" },
            { option: "其他医务人员技术水平", count: 30, rate: "7.33%" },
            { option: "工人", count: 72, rate: "17.65%" },
          ],
          total: 408
        },
        {
          title: "第3题:医生诊断和治疗中哪些服务质量不满意?【多选题】",
          options: [
            { option: "医生诊断错误", count: 23, rate: "15.44%" },
            { option: "对治疗效果不满意", count: 72, rate: "48.32%" },
            { option: "其他", count: 54, rate: "36.24%" },
          ],
          total: 131
        },
      ],
      deptComparison: [
        { dept: "内分泌科", q1VeryGood: 103, q1Good: 34, q1Normal: 0, q1Bad: 0, q1Score: "377.00", q2VeryGood: 101, q2Good: 35, q2Normal: 1, q2Bad: 0, q2Score: "374.00", totalScore: "75.25" },
        { dept: "皮肤科", q1VeryGood: 112, q1Good: 25, q1Normal: 2, q1Bad: 3, q1Score: "388.00", q2VeryGood: 111, q2Good: 27, q2Normal: 3, q2Bad: 1, q2Score: "390.00", totalScore: "76.12" },
        { dept: "肿瘤内科", q1VeryGood: 24, q1Good: 12, q1Normal: 2, q1Bad: 0, q1Score: "98.00", q2VeryGood: 25, q2Good: 12, q2Normal: 1, q2Bad: 0, q2Score: "100.00", totalScore: "70.22" },
        { dept: "消化内科", q1VeryGood: 180, q1Good: 52, q1Normal: 1, q1Bad: 0, q1Score: "645.00", q2VeryGood: 168, q2Good: 62, q2Normal: 3, q2Bad: 0, q2Score: "631.00", totalScore: "73.56" },
        { dept: "肾内科", q1VeryGood: 21, q1Good: 4, q1Normal: 0, q1Bad: 0, q1Score: "71.00", q2VeryGood: 19, q2Good: 6, q2Normal: 0, q2Bad: 0, q2Score: "65.00", totalScore: "68.50" },
      ],
      // é¡¶éƒ¨å…¨å±€ç­›é€‰
      queryParams: {
        dateRange: [],
        deptWard: '',
      },
      deptOptions: DEPTS,
      wardOptions: WARDS,
      // è¡¨æ ¼ä¸‰ï¼šç»Ÿè®¡ç»´åº¦ + ç‹¬ç«‹ç§‘室/病区条件('inherit' = æ²¿ç”¨é¡¶éƒ¨ï¼‰
      statDimension: 'dept',
      statDeptWard: 'inherit',
      // ç»Ÿä¸€æŽ¥å£è¿”回的数据(模拟)
      questionList: [],
      deptComparison: [],
      wardComparison: [],
    };
  },
  computed: {
    // å›ºå®šäº”项选项的题目 => è¡¨ä¸€
    satisfactionQuestions() {
      return this.questionList.filter((q) => this.isSatisfactionQuestion(q));
    },
    // å…¶ä½™é¢˜ç›® => è¡¨äºŒ
    otherQuestions() {
      return this.questionList.filter((q) => !this.isSatisfactionQuestion(q));
    },
    // è¡¨ä¸€ï¼šæ»¡æ„åº¦ç»Ÿè®¡ï¼ˆå«æ€»è®¡è¡Œï¼‰
    serviceSatisfaction() {
      const rows = this.satisfactionQuestions.map((q) => {
        const m = {};
        q.options.forEach((o) => (m[o.option] = o.count));
        const veryGood = m['非常满意'] || 0;
        const good = m['满意'] || 0;
        const normal = m['一般'] || 0;
        const bad = m['不满意'] || 0;
        const na = m['不适用'] || 0;
        const total = veryGood + good + normal + bad + na;
        return {
          category: q.title,
          veryGoodCount: veryGood,
          veryGoodRate: this.rateOf(veryGood, total),
          goodCount: good,
          goodRate: this.rateOf(good, total),
          normalCount: normal,
          normalRate: this.rateOf(normal, total),
          badCount: bad,
          badRate: this.rateOf(bad, total),
          naCount: na,
          naRate: this.rateOf(na, total),
          score: this.scoreOf([veryGood, good, normal, bad, na]),
        };
      });
      // æ€»è®¡è¡Œ
      const sum = { veryGood: 0, good: 0, normal: 0, bad: 0, na: 0 };
      rows.forEach((r) => {
        sum.veryGood += r.veryGoodCount;
        sum.good += r.goodCount;
        sum.normal += r.normalCount;
        sum.bad += r.badCount;
        sum.na += r.naCount;
      });
      const total = sum.veryGood + sum.good + sum.normal + sum.bad + sum.na;
      rows.push({
        category: '总计',
        veryGoodCount: sum.veryGood,
        veryGoodRate: this.rateOf(sum.veryGood, total),
        goodCount: sum.good,
        goodRate: this.rateOf(sum.good, total),
        normalCount: sum.normal,
        normalRate: this.rateOf(sum.normal, total),
        badCount: sum.bad,
        badRate: this.rateOf(sum.bad, total),
        naCount: sum.na,
        naRate: this.rateOf(sum.na, total),
        score: this.scoreOf([sum.veryGood, sum.good, sum.normal, sum.bad, sum.na]),
      });
      return rows;
    },
    // è¡¨äºŒï¼šå•选/多选统计
    questionStats() {
      return this.otherQuestions.map((q) => {
        const total = this.questionTotal(q);
        return {
          title: q.title,
          total,
          options: q.options.map((o) => ({
            option: o.option,
            count: o.count,
            rate: this.rateOf(o.count, total),
          })),
        };
      });
    },
    // è¡¨ä¸‰ï¼šæŒ‰ç»´åº¦ + ç‹¬ç«‹æ¡ä»¶è¿‡æ»¤åŽçš„行
    table3Rows() {
      const list = this.statDimension === 'dept' ? this.deptComparison : this.wardComparison;
      const f = this.statFilter();
      if (!f) return list;
      const [type, name] = f.split(':');
      if (type !== this.statDimension) return list;
      return list.filter((r) => r.name === name);
    },
  },
  mounted() {
    if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
      this.queryParams.dateRange = this.defaultRange();
    }
    this.fetchData();
  },
  methods: {
    // è¡¨ä¸‰ç‹¬ç«‹æ¡ä»¶ï¼š'inherit' æ—¶æ²¿ç”¨é¡¶éƒ¨æ¡ä»¶
    statFilter() {
      const v = this.statDeptWard === 'inherit' ? this.queryParams.deptWard : this.statDeptWard;
      return v || '';
    },
    onStatDimensionChange() {
      // åˆ‡æ¢ç»´åº¦åŽï¼Œè‹¥å½“前已选条件不属于新维度,则恢复“沿用顶部”
      if (this.statDeptWard !== 'inherit' && !this.statDeptWard.startsWith(this.statDimension + ':')) {
        this.statDeptWard = 'inherit';
      }
    },
    handleReset() {
      this.queryParams.dateRange = this.defaultRange();
      this.queryParams.deptWard = '';
      this.fetchData();
    },
    // åˆ¤æ–­é¢˜ç›®é€‰é¡¹æ˜¯å¦ä¸ºå›ºå®šæ»¡æ„åº¦äº”项
    isSatisfactionQuestion(q) {
      if (q.options.length !== FIXED_OPTIONS.length) return false;
      const set = q.options.map((o) => o.option);
      return FIXED_OPTIONS.every((o) => set.includes(o));
    },
    questionTotal(q) {
      if (q.total != null) return q.total;
      return q.options.reduce((s, o) => s + o.count, 0);
    },
    rateOf(count, total) {
      if (!total) return '0.00%';
      return ((count / total) * 100).toFixed(2) + '%';
    },
    // ç»¼åˆå¾—分:非常满意5分、满意4分、一般3分、不满意2分,换算百分制(不适用不计入)——占位公式,接口就绪后可按后端口径替换
    scoreOf(counts) {
      const [a, b, c, d] = counts;
      const total = a + b + c + d;
      if (!total) return '0.00';
      return (((a * 5 + b * 4 + c * 3 + d * 2) / total) * 20).toFixed(2);
    },
    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)];
    },
    dateRangeDays(range) {
      if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
        const ms = new Date(range[1]).getTime() - new Date(range[0]).getTime();
        return Math.max(1, Math.round(ms / 86400000));
      }
      return 30;
    },
    // æ‹‰å–数据(当前为模拟数据,接口就绪后替换为真实请求)
    fetchData() {
      const data = this.mockFetch(this.queryParams);
      this.questionList = data.questionList;
      this.deptComparison = data.deptComparison;
      this.wardComparison = data.wardComparison;
    },
    mockFetch(params) {
      const seed = hashString(JSON.stringify(params));
      const rnd = mulberry32(seed);
      const days = this.dateRangeDays(params.dateRange);
      const timeFactor = Math.max(0.05, days / 30);
      const scopeFactor = params.deptWard ? 0.18 : 1;
      const jitter = () => 0.9 + rnd() * 0.2;
      const questionList = RAW_QUESTIONS.map((q) => {
        const options = q.options.map((o) => ({
          option: o.option,
          count: Math.round(o.count * timeFactor * scopeFactor * jitter()),
        }));
        const item = { title: q.title, type: q.type, options };
        if (q.type === 'multiple' && q.total != null) {
          item.total = Math.round(q.total * timeFactor * scopeFactor);
        }
        return item;
      });
      const deptComparison = DEPTS.map((d) => this.genCompareRow(d, rnd));
      const wardComparison = WARDS.map((w) => this.genCompareRow(w, rnd));
      return { questionList, deptComparison, wardComparison };
    },
    genCompareRow(name, rnd) {
      const r = (lo, hi) => Math.round(lo + rnd() * (hi - lo));
      const q1VeryGood = r(20, 180);
      const q1Good = r(5, 55);
      const q1Normal = r(0, 4);
      const q1Bad = r(0, 3);
      const q2VeryGood = r(18, 175);
      const q2Good = r(5, 55);
      const q2Normal = r(0, 4);
      const q2Bad = r(0, 3);
      return {
        name,
        q1VeryGood, q1Good, q1Normal, q1Bad,
        q1Score: this.scoreOf([q1VeryGood, q1Good, q1Normal, q1Bad]),
        q2VeryGood, q2Good, q2Normal, q2Bad,
        q2Score: this.scoreOf([q2VeryGood, q2Good, q2Normal, q2Bad]),
        totalScore: this.scoreOf([
          q1VeryGood + q2VeryGood,
          q1Good + q2Good,
          q1Normal + q2Normal,
          q1Bad + q2Bad,
        ]),
      };
    },
    exportTable(tableId) {
      const tableNames = { table1: '各服务项目满意度统计', table2: '单选题多选题统计', table3: '各科室满意度评分对比' };
      const name = tableNames[tableId] || '统计数据';
@@ -152,9 +461,9 @@
        // ä»Žè¡¨æ ¼æå–数据构建带样式的 Excel
        const wb = XLSX.utils.book_new();
        const data = [];
        table.querySelectorAll('tr').forEach(tr => {
        table.querySelectorAll('tr').forEach((tr) => {
          const row = [];
          tr.querySelectorAll('th,td').forEach(td => {
          tr.querySelectorAll('th,td').forEach((td) => {
            row.push((td.innerText || '').trim());
          });
          if (row.length) data.push(row);
@@ -170,14 +479,16 @@
      } catch (e) {
        // fallback: CSV
        let csv = '';
        table.querySelectorAll('tr').forEach(tr => {
        table.querySelectorAll('tr').forEach((tr) => {
          const row = [];
          tr.querySelectorAll('th,td').forEach(td => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
          tr.querySelectorAll('th,td').forEach((td) => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
          csv += row.join(',') + '\n';
        });
        const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob); link.download = name + '.csv'; link.click();
        link.href = URL.createObjectURL(blob);
        link.download = name + '.csv';
        link.click();
      }
    },
  },
@@ -186,8 +497,10 @@
<style lang="scss" scoped>
.data-overview { .overview-card { margin-bottom: 20px; } }
.filter-card { .filter-form { margin-bottom: -10px; } .filter-select { width: 200px; } }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.card-title { font-size: 16px; font-weight: 600; color: #303133; }
.header-right { display: flex; align-items: center; .stat-select { width: 180px; margin: 0 12px; } }
.question-block { margin-bottom: 24px;
  .q-title { margin: 0 0 12px 0; font-size: 14px; color: #606266; }
  .q-footer { margin-top: 8px; font-size: 13px; color: #909399; text-align: right; }
src/views/followvisit/discharge/index.vue
@@ -243,6 +243,17 @@
            </div>
          </div>
        </el-col>
        <div class="color-legend">
          <span class="legend-item" v-if="orgname == '省立同德翠苑院区'">
            <span class="legend-block legend-blue"></span>未到随访时间
          </span>
          <span class="legend-item">
            <span class="legend-block legend-red"></span>填报内容存在异常
          </span>
          <span class="legend-item">
            <span class="legend-block legend-yellow"></span>填报内容存在警告
          </span>
        </div>
      </el-row>
      <div class="selected-info">
        å·²é€‰ä¸­
@@ -2309,14 +2320,41 @@
        this.getList();
      });
    },
    // å¼‚常列渲染
    // è¡Œé¢œè‰²æ¸²æŸ“:蓝色(未到随访时间) / çº¢è‰²(异常) / é»„色(警告)
    tableRowClassName({ row, rowIndex }) {
      // å½“前时间小于应随访时间且任务状态为待随访 â†’ æ•´è¡Œè“è‰²ï¼ˆå—华大学附属第一医院不生效)
      if (
        this.orgname == "南华大学附属第一医院" &&
        row.sendstate == 2 &&
        this.isBeforeVisitTime(row.visitTime)
      ) {
        return "blue-row";
      }
      if (row.excep == 1) {
        return "warning-row";
      } else if (row.excep == 2) {
        return "remind-row";
      }
      return "";
    },
    // åˆ¤æ–­å½“前时间是否早于应随访时间(按“天”比较)
    isBeforeVisitTime(visitTime) {
      if (!visitTime) return false;
      let date;
      if (typeof visitTime === "number") {
        date = new Date(visitTime);
      } else {
        date = new Date(String(visitTime).replace(/-/g, "/"));
      }
      if (isNaN(date.getTime())) return false;
      const now = new Date();
      const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const visitDay = new Date(
        date.getFullYear(),
        date.getMonth(),
        date.getDate()
      );
      return today.getTime() < visitDay.getTime();
    },
    restoreSelection() {
      if (!this.$refs.userform) {
@@ -2549,12 +2587,46 @@
::v-deep.el-table .remind-row {
  background: #fcf5aa;
}
::v-deep.el-table .blue-row {
  background: #c6e2ff;
}
.documentf {
  display: flex;
  justify-content: flex-end;
}
.color-legend {
  float: right;
  display: flex;
  align-items: center;
  height: 40px;
  gap: 16px;
  margin-right: 5px;
}
.legend-item {
  display: inline-flex;
  align-items: center;
  font-size: 16px;
  color: #666;
}
.legend-block {
  display: inline-block;
  width: 14px;
  height: 14px;
  border-radius: 3px;
  margin-right: 6px;
}
.legend-blue {
  background: #9cc9fa;
}
.legend-red {
  background: #f1aaaa;
}
.legend-yellow {
  background: #faf18d;
}
.download {
  text-align: center;
src/views/outsideChainnew.vue
@@ -22,9 +22,15 @@
          v-for="(item, index) in questionList"
          :key="item.aaa"
        >
          <div class="scriptTopic-dev" :key="index" v-if="item.scriptType == 1">
          <div
            class="scriptTopic-dev"
            :key="index"
            v-if="item.scriptType == 1"
            :class="{ 'required-error': item.requiredError }"
          >
            <div class="dev-text">
              {{ index + 1 }}、<span style="line-height: 1.5"
                ><span v-if="isRequired(item)" class="required-star">*</span
                >{{ item.scriptContent }}
                <span style="color: #3ba2f7">[单选]</span></span
              >
@@ -54,9 +60,15 @@
            </div>
          </div>
          <!-- å¤šé€‰ -->
          <div class="scriptTopic-dev" :key="index" v-if="item.scriptType == 2">
          <div
            class="scriptTopic-dev"
            :key="index"
            v-if="item.scriptType == 2"
            :class="{ 'required-error': item.requiredError }"
          >
            <div class="dev-text">
              {{ index + 1 }}、<span style="line-height: 1.5"
                ><span v-if="isRequired(item)" class="required-star">*</span
                >{{ item.scriptContent }}
                <span style="color: #3ba2f7">[多选]</span></span
              >
@@ -88,9 +100,11 @@
            class="scriptTopic-dev"
            :key="index"
            v-if="item.scriptType == 4 || item.scriptType == 3"
            :class="{ 'required-error': item.requiredError }"
          >
            <div class="dev-text">
              {{ index + 1 }}、<span style="line-height: 1.5"
                ><span v-if="isRequired(item)" class="required-star">*</span
                >{{ item.scriptContent
                }}<span style="color: #3ba2f7">[问答]</span></span
              >
@@ -421,6 +435,10 @@
    // ç¼“å­˜
    cache(subm) {
      console.log("进入缓存");
      // æäº¤åœºæ™¯ï¼šå…ˆåšå¿…填项校验
      if (subm === true && !this.validateRequired()) {
        return;
      }
      let form = {
        param1: this.taskid,
        param2: this.patid,
@@ -459,6 +477,45 @@
          }
        }
      });
    },
    // æ˜¯å¦å¿…填(1必填 2不必填)
    isRequired(item) {
      return item.ismandatory == 1 || item.ismandatory == "1";
    },
    // åˆ¤æ–­é¢˜ç›®æ˜¯å¦æœªå¡«å†™
    isQuestionEmpty(item) {
      const val = item.questionResult;
      if (item.scriptType == 2) {
        return !val || (Array.isArray(val) && val.length === 0);
      }
      return val === undefined || val === null || val === "";
    },
    // ä¿å­˜å‰å¿…填项校验
    validateRequired() {
      const list = this.questionList || [];
      list.forEach((item) => {
        if (item.requiredError) this.$set(item, "requiredError", false);
      });
      const unfilled = [];
      list.forEach((item) => {
        if (!this.isRequired(item)) return;
        if (this.isQuestionEmpty(item)) unfilled.push(item);
      });
      if (unfilled.length > 0) {
        unfilled.forEach((item) => this.$set(item, "requiredError", true));
        this.$modal.msgWarning(
          `还有 ${unfilled.length} é“必填题未填写,请补充后再提交`
        );
        this.$nextTick(() => {
          const idx = this.questionList.indexOf(unfilled[0]);
          const els = this.$el.querySelectorAll(".scriptTopic-dev");
          if (els[idx]) {
            els[idx].scrollIntoView({ behavior: "smooth", block: "center" });
          }
        });
        return false;
      }
      return true;
    },
    // å¤„理单选选项
    handleOptionChange(selectedvalue, index, arr) {
@@ -610,6 +667,18 @@
}
.toptitle {
}
.required-star {
  color: #f56c6c;
  margin-right: 2px;
  font-weight: 600;
}
.scriptTopic-dev.required-error {
  border: 1px solid #f56c6c;
  border-left: 4px solid #f56c6c;
  background: #fef0f0;
  border-radius: 4px;
  padding: 8px;
}
.bottom-fixed {
  position: fixed;
  bottom: 0;
src/views/outsideChainwtnew.vue
@@ -64,6 +64,7 @@
                      item.prompt &&
                      item.scriptResult &&
                      (item.scriptType !== 2 || item.scriptResult.length > 0),
                    'required-error': item.requiredError,
                  }"
                >
                  <!-- é¢˜ç›®é¢˜å¹² -->
@@ -71,7 +72,11 @@
                    <span class="question-number"
                      >{{ getQuestionNumber(group, index) }}.</span
                    >
                    <span class="question-text">{{ item.scriptContent }}</span>
                    <span class="question-text"
                      ><span v-if="isRequired(item)" class="required-star"
                        >*</span
                      >{{ item.scriptContent }}</span
                    >
                    <span class="question-type-tag">
                      {{
                        item.scriptType == 1
@@ -201,6 +206,7 @@
                  item.prompt &&
                  item.scriptResult &&
                  (item.scriptType !== 2 || item.scriptResult.length > 0),
                'required-error': item.requiredError,
              }"
            >
              <!-- é¢˜ç›®é¢˜å¹² -->
@@ -208,7 +214,11 @@
                <span class="question-number"
                  >{{ getVisibleQuestionIndex(index) }}.</span
                >
                <span class="question-text">{{ item.scriptContent }}</span>
                <span class="question-text"
                  ><span v-if="isRequired(item)" class="required-star"
                    >*</span
                  >{{ item.scriptContent }}</span
                >
                <span class="question-type-tag">
                  {{
                    item.scriptType == 1
@@ -695,6 +705,10 @@
    // ç¼“å­˜
    cache(subm) {
      console.log("进入缓存");
      // æäº¤åœºæ™¯ï¼šå…ˆåšå¿…填项校验
      if (subm === true && !this.validateRequired()) {
        return;
      }
      // æäº¤åœºæ™¯ï¼šå¼€å¯åŠ è½½çŠ¶æ€ï¼Œé˜²æ­¢é‡å¤ç‚¹å‡»
      if (subm === true) {
        this.submitting = true;
@@ -764,6 +778,46 @@
            this.$modal.msgError("提交失败,请重试");
          }
        });
    },
    // æ˜¯å¦å¿…填(1必填 2不必填)
    isRequired(item) {
      return item.ismandatory == 1 || item.ismandatory == "1";
    },
    // åˆ¤æ–­é¢˜ç›®æ˜¯å¦æœªå¡«å†™
    isQuestionEmpty(item) {
      const val = item.scriptResult;
      if (item.scriptType == 2) {
        return !val || (Array.isArray(val) && val.length === 0);
      }
      return val === undefined || val === null || val === "";
    },
    // ä¿å­˜å‰å¿…填项校验
    validateRequired() {
      const list = this.questionList || [];
      list.forEach((item) => {
        if (item.requiredError) this.$set(item, "requiredError", false);
      });
      const unfilled = [];
      list.forEach((item) => {
        if (item.ishide) return;
        if (!this.isRequired(item)) return;
        if (this.isQuestionEmpty(item)) unfilled.push(item);
      });
      if (unfilled.length > 0) {
        unfilled.forEach((item) => this.$set(item, "requiredError", true));
        this.$modal.msgWarning(
          `还有 ${unfilled.length} é“必填题未填写,请补充后再提交`
        );
        this.$nextTick(() => {
          const idx = this.visibleQuestions.indexOf(unfilled[0]);
          const els = this.$el.querySelectorAll(".question-item");
          if (els[idx]) {
            els[idx].scrollIntoView({ behavior: "smooth", block: "center" });
          }
        });
        return false;
      }
      return true;
    },
    // èŽ·å–å¯è§é¢˜ç›®çš„æ­£ç¡®åºå·ï¼ˆè§£å†³è·³é¢˜åŽåºå·ä¸è¿žç»­çš„é—®é¢˜ï¼‰
    getVisibleQuestionIndex(index) {
@@ -1041,6 +1095,12 @@
    border-left: 4px solid #e6a23c;
  }
  &.required-error {
    border-color: #f56c6c;
    border-left: 4px solid #f56c6c;
    background: #fef0f0;
  }
  &:last-child {
    margin-bottom: 0;
  }
@@ -1074,6 +1134,12 @@
  min-width: 16px;
}
.required-star {
  color: #f56c6c;
  margin-right: 2px;
  font-weight: 600;
}
// .question-text {
//   line-height: 1.5;
//   color: #2c3e50;
src/views/patient/dedicated/heart/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,388 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ  -->
    <el-form :inline="true" :model="queryParams" size="small" class="query-form">
      <el-form-item label="患者姓名">
        <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="主治医生">
        <el-input v-model="queryParams.drname" placeholder="请输入医生姓名" clearable @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="科室/病区">
        <el-select v-model="queryParams.deptWard" placeholder="全部" clearable filterable>
          <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>
      </el-form-item>
      <el-form-item label="心功能分级">
        <el-select v-model="queryParams.nyhaClass" placeholder="全部" clearable>
          <el-option v-for="g in NYHA_CLASSES" :key="g" :label="g" :value="g" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ“ä½œæ  -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button type="primary" icon="el-icon-plus" size="mini" @click="openAddDialog">添加患者</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
      </el-col>
    </el-row>
    <!-- åˆ—表 -->
    <el-table :data="currentPageData" v-loading="loading" border @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="50" align="center" />
      <el-table-column label="姓名" prop="name" min-width="90" align="center" fixed="left" />
      <el-table-column label="性别" prop="sex" width="60" align="center" />
      <el-table-column label="年龄" prop="age" width="70" align="center" />
      <el-table-column label="联系电话" prop="telcode" width="120" align="center" />
      <el-table-column label="科室" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
      <el-table-column label="病区" prop="wardname" min-width="90" align="center" />
      <el-table-column label="主治医生" prop="drname" width="90" align="center" />
      <el-table-column label="心脏诊断" prop="heartDiagnosis" min-width="120" align="center" show-overflow-tooltip />
      <el-table-column label="心功能分级" prop="nyhaClass" width="100" align="center">
        <template slot-scope="scope">
          <el-tag :type="nyhaTagType(scope.row.nyhaClass)" size="small">{{ scope.row.nyhaClass }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="心率(次/分)" prop="heartRate" width="100" align="center" />
      <el-table-column label="血压(mmHg)" prop="bloodPressure" width="120" align="center" />
      <el-table-column label="随访计划" prop="followPlan" width="100" align="center" />
      <el-table-column label="来源" prop="source" width="90" align="center" />
      <el-table-column label="加入时间" prop="addTime" width="150" align="center" />
      <el-table-column label="操作" width="120" align="center" fixed="right">
        <template slot-scope="scope">
          <el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">详情</el-button>
          <el-button type="text" size="mini" icon="el-icon-close" class="danger-text" @click="removeRow(scope.row)">移除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <el-pagination
      class="pagination-wrap"
      background
      layout="total, sizes, prev, pager, next, jumper"
      :total="filteredList.length"
      :current-page.sync="pageNum"
      :page-size.sync="pageSize"
      :page-sizes="[10, 20, 50]"
    />
    <!-- æ·»åŠ æ‚£è€…å¼¹çª— -->
    <el-dialog title="添加专病患者" :visible.sync="addVisible" width="80%" top="6vh">
      <el-tabs v-model="activeSource" @tab-click="onSourceTabChange">
        <el-tab-pane label="患者档案" name="archive" />
        <el-tab-pane label="出院记录" name="hospital" />
        <el-tab-pane label="门诊记录" name="outpatient" />
        <el-tab-pane label="在院记录" name="inpatient" />
      </el-tabs>
      <el-form :inline="true" size="small">
        <el-form-item label="姓名/诊断">
          <el-input v-model="poolKeyword" placeholder="输入姓名或诊断关键字" clearable style="width: 220px" @input="handlePoolSearch" />
        </el-form-item>
        <span class="pool-tip">共 {{ filteredPool.length }} æ¡ï¼Œå·²é€‰ {{ addSelected.length }} æ¡</span>
      </el-form>
      <el-table ref="addTable" :data="filteredPool" border size="small" max-height="420" @selection-change="onAddSelectionChange">
        <el-table-column type="selection" width="50" align="center" />
        <el-table-column label="姓名" prop="name" width="90" align="center" />
        <el-table-column label="性别" prop="sex" width="60" align="center" />
        <el-table-column label="年龄" prop="age" width="70" align="center" />
        <el-table-column label="联系电话" prop="telcode" width="120" align="center" />
        <el-table-column label="科室" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
        <el-table-column label="病区" prop="wardname" min-width="90" align="center" />
        <el-table-column label="诊断" prop="diagname" min-width="130" align="center" show-overflow-tooltip />
        <el-table-column label="医生" prop="drname" width="90" align="center" />
        <el-table-column label="日期" prop="sourceDate" width="110" align="center" />
      </el-table>
      <div slot="footer" class="dialog-footer">
        <el-button @click="addVisible = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="confirmAdd">确定添加</el-button>
      </div>
    </el-dialog>
    <!-- è¯¦æƒ…弹窗 -->
    <el-dialog title="专病患者详情" :visible.sync="detailVisible" width="720px">
      <el-descriptions :column="2" border size="medium" v-if="detailRow">
        <el-descriptions-item label="姓名">{{ detailRow.name }}</el-descriptions-item>
        <el-descriptions-item label="性别">{{ detailRow.sex }}</el-descriptions-item>
        <el-descriptions-item label="年龄">{{ detailRow.age }}</el-descriptions-item>
        <el-descriptions-item label="联系电话">{{ detailRow.telcode }}</el-descriptions-item>
        <el-descriptions-item label="科室">{{ detailRow.deptname }}</el-descriptions-item>
        <el-descriptions-item label="病区">{{ detailRow.wardname || '—' }}</el-descriptions-item>
        <el-descriptions-item label="主治医生">{{ detailRow.drname }}</el-descriptions-item>
        <el-descriptions-item label="心脏诊断">{{ detailRow.heartDiagnosis }}</el-descriptions-item>
        <el-descriptions-item label="心功能分级">{{ detailRow.nyhaClass }}</el-descriptions-item>
        <el-descriptions-item label="心率">{{ detailRow.heartRate }} æ¬¡/分</el-descriptions-item>
        <el-descriptions-item label="血压">{{ detailRow.bloodPressure }} mmHg</el-descriptions-item>
        <el-descriptions-item label="随访计划">{{ detailRow.followPlan }}</el-descriptions-item>
        <el-descriptions-item label="患者来源">{{ detailRow.source }}</el-descriptions-item>
        <el-descriptions-item label="加入时间">{{ detailRow.addTime }}</el-descriptions-item>
      </el-descriptions>
    </el-dialog>
  </div>
</template>
<script>
// å¿ƒè„ä¸“病患者 â€”— æ¨¡æ‹Ÿæ•°æ®ï¼ŒæŽ¥å£å°±ç»ªåŽæ›¿æ¢ getList / confirmAdd ä¸­çš„æœ¬åœ°æ•°æ®é€»è¾‘
const PERSONS = [
  { name: '张伟', sex: '男', age: 68 },
  { name: '李娜', sex: '女', age: 55 },
  { name: '王强', sex: '男', age: 72 },
  { name: '刘敏', sex: '女', age: 61 },
  { name: '陈静', sex: '女', age: 66 },
  { name: '杨军', sex: '男', age: 59 },
  { name: '赵丽', sex: '女', age: 70 },
  { name: '黄勇', sex: '男', age: 64 },
  { name: '周艳', sex: '女', age: 58 },
  { name: '吴刚', sex: '男', age: 75 },
];
const HEART_DIAGNOSES = ['冠心病', '心力衰竭', '高血压', '心房颤动', '心肌梗死', '心律失常', '心脏瓣膜病', '扩张型心肌病', '肥厚型心肌病', '急性冠脉综合征'];
const DEPTS = ['心血管内科', '老年科', '重症医学科', '康复医学科', '心外科', '内分泌科', '神经内科', '呼吸内科'];
const WARDS = ['一病区', '二病区', '三病区', '四病区', '五病区'];
const DOCTORS = ['王主任', '李医生', '张医生', '刘医生', '陈医生'];
const NYHA_CLASSES = ['Ⅰ级', 'Ⅱ级', 'Ⅲ级', 'Ⅳ级'];
const FOLLOW_PLANS = ['每周', '每两周', '每月', '每季度'];
const SOURCE_LABEL = { archive: '患者档案', hospital: '出院记录', outpatient: '门诊记录', inpatient: '在院记录' };
export default {
  name: 'HeartDedicated',
  data() {
    return {
      loading: false,
      // ä¸“病患者列表(已加入)
      patientList: [],
      ids: [],
      single: true,
      multiple: true,
      // æŸ¥è¯¢æ¡ä»¶
      queryParams: { name: '', drname: '', deptWard: '', nyhaClass: '' },
      pageNum: 1,
      pageSize: 10,
      deptOptions: DEPTS,
      wardOptions: WARDS,
      NYHA_CLASSES,
      // æ·»åŠ å¼¹çª—
      addVisible: false,
      activeSource: 'archive',
      poolKeyword: '',
      addSelected: [],
      mockPool: { archive: [], hospital: [], outpatient: [], inpatient: [] },
      // è¯¦æƒ…弹窗
      detailVisible: false,
      detailRow: null,
    };
  },
  computed: {
    filteredList() {
      const { name, drname, deptWard, nyhaClass } = this.queryParams;
      return this.patientList.filter((r) => {
        if (name && !r.name.includes(name)) return false;
        if (drname && !r.drname.includes(drname)) return false;
        if (deptWard) {
          const label = deptWard.split(':')[1];
          if (label && r.deptname !== label && r.wardname !== label) return false;
        }
        if (nyhaClass && r.nyhaClass !== nyhaClass) return false;
        return true;
      });
    },
    currentPageData() {
      const start = (this.pageNum - 1) * this.pageSize;
      return this.filteredList.slice(start, start + this.pageSize);
    },
    filteredPool() {
      const kw = this.poolKeyword.trim();
      const list = this.mockPool[this.activeSource] || [];
      if (!kw) return list;
      return list.filter((r) => r.name.includes(kw) || r.diagname.includes(kw));
    },
  },
  created() {
    this.buildPool();
    this.buildInitialList();
  },
  methods: {
    // ç”Ÿæˆå„来源的模拟患者池
    buildPool() {
      const mk = (idx, source, noPrefix) => {
        const p = PERSONS[idx % PERSONS.length];
        const isWard = source === 'hospital' || source === 'inpatient';
        return {
          id: noPrefix + (idx + 1),
          name: p.name,
          sex: p.sex,
          age: p.age,
          telcode: '139' + (10000000 + idx * 1111111),
          deptname: DEPTS[idx % DEPTS.length],
          wardname: isWard ? WARDS[idx % WARDS.length] : '',
          drname: DOCTORS[idx % DOCTORS.length],
          diagname: HEART_DIAGNOSES[idx % HEART_DIAGNOSES.length],
          sourceDate: this.recentDate(idx * 3 + (source === 'outpatient' ? 2 : 5)),
        };
      };
      this.mockPool = {
        archive: PERSONS.map((_, i) => mk(i, 'archive', 'da')),
        hospital: PERSONS.map((_, i) => mk(i, 'hospital', 'zy')),
        outpatient: PERSONS.map((_, i) => mk(i, 'outpatient', 'mz')),
        inpatient: PERSONS.map((_, i) => mk(i, 'inpatient', 'zy')),
      };
    },
    // é¢„置若干已加入的患者,保证首屏有数据
    buildInitialList() {
      const take = (source, n) =>
        this.mockPool[source].slice(0, n).map((row) => this.toDedicated(row, source));
      this.patientList = [
        ...take('hospital', 3),
        ...take('inpatient', 2),
        ...take('archive', 2),
        ...take('outpatient', 1),
      ];
    },
    // æ± æ‚£è€… => ä¸“病患者(补充心脏相关字段)
    toDedicated(row, source) {
      const n = Number(String(row.id).replace(/\D/g, '')) || 0;
      const nyhaClass = NYHA_CLASSES[n % NYHA_CLASSES.length];
      const heartRate = 58 + (n % 5) * 8;
      const high = 118 + (n % 4) * 8;
      const low = 72 + (n % 3) * 4;
      return {
        id: row.id,
        name: row.name,
        sex: row.sex,
        age: row.age,
        telcode: row.telcode,
        deptname: row.deptname,
        wardname: row.wardname,
        drname: row.drname,
        heartDiagnosis: row.diagname,
        nyhaClass,
        heartRate,
        bloodPressure: `${high}/${low}`,
        followPlan: FOLLOW_PLANS[n % FOLLOW_PLANS.length],
        source: SOURCE_LABEL[source] || source,
        addTime: this.now(),
      };
    },
    nyhaTagType(grade) {
      const map = { 'Ⅰ级': 'success', 'Ⅱ级': 'primary', 'Ⅲ级': 'warning', 'Ⅳ级': 'danger' };
      return map[grade] || 'info';
    },
    handleQuery() {
      this.pageNum = 1;
    },
    resetQuery() {
      this.queryParams = { name: '', drname: '', deptWard: '', nyhaClass: '' };
      this.pageNum = 1;
    },
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    openAddDialog() {
      this.addSelected = [];
      this.poolKeyword = '';
      this.activeSource = 'archive';
      this.addVisible = true;
      this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
    },
    onSourceTabChange() {
      this.addSelected = [];
      this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
    },
    handlePoolSearch() {
      // æœ¬åœ°è¿‡æ»¤ï¼Œç”± filteredPool è®¡ç®—属性完成
    },
    onAddSelectionChange(selection) {
      this.addSelected = selection;
    },
    confirmAdd() {
      if (!this.addSelected.length) {
        this.$modal.msgWarning('请先选择要添加的患者');
        return;
      }
      const existing = new Set(this.patientList.map((r) => r.id));
      const toAdd = this.addSelected
        .filter((row) => !existing.has(row.id))
        .map((row) => this.toDedicated(row, this.activeSource));
      this.patientList = [...toAdd, ...this.patientList];
      this.addVisible = false;
      this.$modal.msgSuccess(`成功添加 ${toAdd.length} ä½æ‚£è€…`);
    },
    removeRow(row) {
      this.$modal
        .confirm(`是否确认将患者"${row.name}"移出心脏专病列表?`)
        .then(() => {
          this.patientList = this.patientList.filter((r) => r.id !== row.id);
          this.$modal.msgSuccess('移除成功');
        })
        .catch(() => {});
    },
    handleDelete() {
      this.$modal
        .confirm(`是否确认删除选中的 ${this.ids.length} ä½æ‚£è€…?`)
        .then(() => {
          this.patientList = this.patientList.filter((r) => !this.ids.includes(r.id));
          this.$modal.msgSuccess('删除成功');
        })
        .catch(() => {});
    },
    openDetail(row) {
      this.detailRow = row;
      this.detailVisible = true;
    },
    handleExport() {
      const rows = this.filteredList;
      if (!rows.length) {
        this.$modal.msgWarning('暂无数据可导出');
        return;
      }
      const head = ['姓名', '性别', '年龄', '联系电话', '科室', '病区', '主治医生', '心脏诊断', '心功能分级', '心率(次/分)', '血压(mmHg)', '随访计划', '来源', '加入时间'];
      const keys = ['name', 'sex', 'age', 'telcode', 'deptname', 'wardname', 'drname', 'heartDiagnosis', 'nyhaClass', 'heartRate', 'bloodPressure', 'followPlan', 'source', 'addTime'];
      const csv = [head.join(',')]
        .concat(rows.map((r) => keys.map((k) => `"${(r[k] ?? '').toString().replace(/"/g, '""')}"`).join(',')))
        .join('\n');
      const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
      const link = document.createElement('a');
      link.href = URL.createObjectURL(blob);
      link.download = `心脏专病患者_${Date.now()}.csv`;
      link.click();
      URL.revokeObjectURL(link.href);
    },
    recentDate(daysAgo) {
      const d = new Date();
      d.setDate(d.getDate() - daysAgo);
      const mm = String(d.getMonth() + 1).padStart(2, '0');
      const dd = String(d.getDate()).padStart(2, '0');
      return `${d.getFullYear()}-${mm}-${dd}`;
    },
    now() {
      const d = new Date();
      const pad = (x) => String(x).padStart(2, '0');
      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
    },
  },
};
</script>
<style lang="scss" scoped>
.query-form { margin-bottom: 4px; }
.mb8 { margin-bottom: 8px; }
.pagination-wrap { margin-top: 16px; text-align: right; }
.pool-tip { font-size: 12px; color: #909399; margin-left: 8px; }
.danger-text { color: #f56c6c; }
</style>
src/views/patient/dedicated/tubeFeed/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,391 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ  -->
    <el-form :inline="true" :model="queryParams" size="small" class="query-form">
      <el-form-item label="患者姓名">
        <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="主治医生">
        <el-input v-model="queryParams.drname" placeholder="请输入医生姓名" clearable @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="科室/病区">
        <el-select v-model="queryParams.deptWard" placeholder="全部" clearable filterable>
          <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>
      </el-form-item>
      <el-form-item label="管饲方式">
        <el-select v-model="queryParams.feedType" placeholder="全部" clearable>
          <el-option v-for="t in TUBE_FEED_TYPES" :key="t" :label="t" :value="t" />
        </el-select>
      </el-form-item>
      <el-form-item label="管饲状态">
        <el-select v-model="queryParams.feedStatus" placeholder="全部" clearable>
          <el-option v-for="s in TUBE_STATUS" :key="s" :label="s" :value="s" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ“ä½œæ  -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button type="primary" icon="el-icon-plus" size="mini" @click="openAddDialog">添加患者</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
      </el-col>
    </el-row>
    <!-- åˆ—表 -->
    <el-table :data="currentPageData" v-loading="loading" border @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="50" align="center" />
      <el-table-column label="姓名" prop="name" min-width="90" align="center" fixed="left" />
      <el-table-column label="性别" prop="sex" width="60" align="center" />
      <el-table-column label="年龄" prop="age" width="70" align="center" />
      <el-table-column label="联系电话" prop="telcode" width="120" align="center" />
      <el-table-column label="科室" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
      <el-table-column label="病区" prop="wardname" min-width="90" align="center" />
      <el-table-column label="主治医生" prop="drname" width="90" align="center" />
      <el-table-column label="诊断" prop="diagname" min-width="120" align="center" show-overflow-tooltip />
      <el-table-column label="管饲方式" prop="feedType" width="100" align="center" />
      <el-table-column label="管饲开始日期" prop="feedStartDate" width="120" align="center" />
      <el-table-column label="营养方案" prop="nutrition" min-width="140" align="center" show-overflow-tooltip />
      <el-table-column label="每日能量(kcal)" prop="energy" width="110" align="center" />
      <el-table-column label="管饲状态" prop="feedStatus" width="90" align="center">
        <template slot-scope="scope">
          <el-tag :type="scope.row.feedStatus === '已停用' ? 'info' : 'success'" size="small">{{ scope.row.feedStatus }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="来源" prop="source" width="90" align="center" />
      <el-table-column label="加入时间" prop="addTime" width="150" align="center" />
      <el-table-column label="操作" width="120" align="center" fixed="right">
        <template slot-scope="scope">
          <el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">详情</el-button>
          <el-button type="text" size="mini" icon="el-icon-close" class="danger-text" @click="removeRow(scope.row)">移除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <el-pagination
      class="pagination-wrap"
      background
      layout="total, sizes, prev, pager, next, jumper"
      :total="filteredList.length"
      :current-page.sync="pageNum"
      :page-size.sync="pageSize"
      :page-sizes="[10, 20, 50]"
    />
    <!-- æ·»åŠ æ‚£è€…å¼¹çª— -->
    <el-dialog title="添加专病患者" :visible.sync="addVisible" width="80%" top="6vh">
      <el-tabs v-model="activeSource" @tab-click="onSourceTabChange">
        <el-tab-pane label="患者档案" name="archive" />
        <el-tab-pane label="出院记录" name="hospital" />
        <el-tab-pane label="门诊记录" name="outpatient" />
        <el-tab-pane label="在院记录" name="inpatient" />
      </el-tabs>
      <el-form :inline="true" size="small">
        <el-form-item label="姓名/诊断">
          <el-input v-model="poolKeyword" placeholder="输入姓名或诊断关键字" clearable style="width: 220px" @input="handlePoolSearch" />
        </el-form-item>
        <span class="pool-tip">共 {{ filteredPool.length }} æ¡ï¼Œå·²é€‰ {{ addSelected.length }} æ¡</span>
      </el-form>
      <el-table ref="addTable" :data="filteredPool" border size="small" max-height="420" @selection-change="onAddSelectionChange">
        <el-table-column type="selection" width="50" align="center" />
        <el-table-column label="姓名" prop="name" width="90" align="center" />
        <el-table-column label="性别" prop="sex" width="60" align="center" />
        <el-table-column label="年龄" prop="age" width="70" align="center" />
        <el-table-column label="联系电话" prop="telcode" width="120" align="center" />
        <el-table-column label="科室" prop="deptname" min-width="110" align="center" show-overflow-tooltip />
        <el-table-column label="病区" prop="wardname" min-width="90" align="center" />
        <el-table-column label="诊断" prop="diagname" min-width="130" align="center" show-overflow-tooltip />
        <el-table-column label="医生" prop="drname" width="90" align="center" />
        <el-table-column label="日期" prop="sourceDate" width="110" align="center" />
      </el-table>
      <div slot="footer" class="dialog-footer">
        <el-button @click="addVisible = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="confirmAdd">确定添加</el-button>
      </div>
    </el-dialog>
    <!-- è¯¦æƒ…弹窗 -->
    <el-dialog title="专病患者详情" :visible.sync="detailVisible" width="720px">
      <el-descriptions :column="2" border size="medium" v-if="detailRow">
        <el-descriptions-item label="姓名">{{ detailRow.name }}</el-descriptions-item>
        <el-descriptions-item label="性别">{{ detailRow.sex }}</el-descriptions-item>
        <el-descriptions-item label="年龄">{{ detailRow.age }}</el-descriptions-item>
        <el-descriptions-item label="联系电话">{{ detailRow.telcode }}</el-descriptions-item>
        <el-descriptions-item label="科室">{{ detailRow.deptname }}</el-descriptions-item>
        <el-descriptions-item label="病区">{{ detailRow.wardname || '—' }}</el-descriptions-item>
        <el-descriptions-item label="主治医生">{{ detailRow.drname }}</el-descriptions-item>
        <el-descriptions-item label="诊断">{{ detailRow.diagname }}</el-descriptions-item>
        <el-descriptions-item label="管饲方式">{{ detailRow.feedType }}</el-descriptions-item>
        <el-descriptions-item label="管饲开始日期">{{ detailRow.feedStartDate }}</el-descriptions-item>
        <el-descriptions-item label="营养方案">{{ detailRow.nutrition }}</el-descriptions-item>
        <el-descriptions-item label="每日能量">{{ detailRow.energy }} kcal</el-descriptions-item>
        <el-descriptions-item label="管饲状态">{{ detailRow.feedStatus }}</el-descriptions-item>
        <el-descriptions-item label="患者来源">{{ detailRow.source }}</el-descriptions-item>
        <el-descriptions-item label="加入时间">{{ detailRow.addTime }}</el-descriptions-item>
      </el-descriptions>
    </el-dialog>
  </div>
</template>
<script>
// ç®¡é¥²ä¸“病患者 â€”— æ¨¡æ‹Ÿæ•°æ®ï¼ŒæŽ¥å£å°±ç»ªåŽæ›¿æ¢ getList / confirmAdd ä¸­çš„æœ¬åœ°æ•°æ®é€»è¾‘
const PERSONS = [
  { name: '张伟', sex: '男', age: 68 },
  { name: '李娜', sex: '女', age: 55 },
  { name: '王强', sex: '男', age: 72 },
  { name: '刘敏', sex: '女', age: 61 },
  { name: '陈静', sex: '女', age: 66 },
  { name: '杨军', sex: '男', age: 59 },
  { name: '赵丽', sex: '女', age: 70 },
  { name: '黄勇', sex: '男', age: 64 },
  { name: '周艳', sex: '女', age: 58 },
  { name: '吴刚', sex: '男', age: 75 },
];
const TUBE_DIAGNOSES = ['脑卒中', '吞咽困难', '食管癌术后', '昏迷', '重症肺炎', '运动神经元病', '颅脑损伤', '帕金森病', '脑出血恢复期', '肌萎缩侧索硬化'];
const DEPTS = ['神经内科', '康复医学科', '老年科', '呼吸内科', '普外科', '重症医学科', '内分泌科', '消化内科'];
const WARDS = ['一病区', '二病区', '三病区', '四病区', '五病区'];
const DOCTORS = ['王主任', '李医生', '张医生', '刘医生', '陈医生'];
const TUBE_FEED_TYPES = ['鼻胃管', '胃造瘘管', '空肠造瘘管', '鼻肠管'];
const TUBE_STATUS = ['管饲中', '间歇管饲', '已停用'];
const NUTRITIONS = ['肠内营养乳剂(TPF)', '肠内营养混悬液(TPF-D)', '自制匀浆膳', '短肽型营养液'];
const SOURCE_LABEL = { archive: '患者档案', hospital: '出院记录', outpatient: '门诊记录', inpatient: '在院记录' };
export default {
  name: 'TubeFeedDedicated',
  data() {
    return {
      loading: false,
      // ä¸“病患者列表(已加入)
      patientList: [],
      ids: [],
      single: true,
      multiple: true,
      // æŸ¥è¯¢æ¡ä»¶
      queryParams: { name: '', drname: '', deptWard: '', feedType: '', feedStatus: '' },
      pageNum: 1,
      pageSize: 10,
      deptOptions: DEPTS,
      wardOptions: WARDS,
      TUBE_FEED_TYPES,
      TUBE_STATUS,
      // æ·»åŠ å¼¹çª—
      addVisible: false,
      activeSource: 'archive',
      poolKeyword: '',
      addSelected: [],
      mockPool: { archive: [], hospital: [], outpatient: [], inpatient: [] },
      // è¯¦æƒ…弹窗
      detailVisible: false,
      detailRow: null,
    };
  },
  computed: {
    filteredList() {
      const { name, drname, deptWard, feedType, feedStatus } = this.queryParams;
      return this.patientList.filter((r) => {
        if (name && !r.name.includes(name)) return false;
        if (drname && !r.drname.includes(drname)) return false;
        if (deptWard) {
          const label = deptWard.split(':')[1];
          if (label && r.deptname !== label && r.wardname !== label) return false;
        }
        if (feedType && r.feedType !== feedType) return false;
        if (feedStatus && r.feedStatus !== feedStatus) return false;
        return true;
      });
    },
    currentPageData() {
      const start = (this.pageNum - 1) * this.pageSize;
      return this.filteredList.slice(start, start + this.pageSize);
    },
    filteredPool() {
      const kw = this.poolKeyword.trim();
      const list = this.mockPool[this.activeSource] || [];
      if (!kw) return list;
      return list.filter((r) => r.name.includes(kw) || r.diagname.includes(kw));
    },
  },
  created() {
    this.buildPool();
    this.buildInitialList();
  },
  methods: {
    // ç”Ÿæˆå„来源的模拟患者池
    buildPool() {
      const mk = (idx, source, noPrefix) => {
        const p = PERSONS[idx % PERSONS.length];
        const isWard = source === 'hospital' || source === 'inpatient';
        return {
          id: noPrefix + (idx + 1),
          name: p.name,
          sex: p.sex,
          age: p.age,
          telcode: '138' + (10000000 + idx * 1111111),
          deptname: DEPTS[idx % DEPTS.length],
          wardname: isWard ? WARDS[idx % WARDS.length] : '',
          drname: DOCTORS[idx % DOCTORS.length],
          diagname: TUBE_DIAGNOSES[idx % TUBE_DIAGNOSES.length],
          sourceDate: this.recentDate(idx * 3 + (source === 'outpatient' ? 2 : 5)),
        };
      };
      this.mockPool = {
        archive: PERSONS.map((_, i) => mk(i, 'archive', 'da')),
        hospital: PERSONS.map((_, i) => mk(i, 'hospital', 'zy')),
        outpatient: PERSONS.map((_, i) => mk(i, 'outpatient', 'mz')),
        inpatient: PERSONS.map((_, i) => mk(i, 'inpatient', 'zy')),
      };
    },
    // é¢„置若干已加入的患者,保证首屏有数据
    buildInitialList() {
      const take = (source, n) =>
        this.mockPool[source].slice(0, n).map((row) => this.toDedicated(row, source));
      this.patientList = [
        ...take('hospital', 3),
        ...take('inpatient', 2),
        ...take('archive', 2),
        ...take('outpatient', 1),
      ];
    },
    // æ± æ‚£è€… => ä¸“病患者(补充管饲相关字段)
    toDedicated(row, source) {
      const n = Number(String(row.id).replace(/\D/g, '')) || 0;
      return {
        id: row.id,
        name: row.name,
        sex: row.sex,
        age: row.age,
        telcode: row.telcode,
        deptname: row.deptname,
        wardname: row.wardname,
        drname: row.drname,
        diagname: row.diagname,
        feedType: TUBE_FEED_TYPES[n % TUBE_FEED_TYPES.length],
        feedStartDate: this.recentDate(n % 10),
        nutrition: NUTRITIONS[n % NUTRITIONS.length],
        energy: 1500 + (n % 5) * 100,
        feedStatus: TUBE_STATUS[n % 2],
        source: SOURCE_LABEL[source] || source,
        addTime: this.now(),
      };
    },
    handleQuery() {
      this.pageNum = 1;
    },
    resetQuery() {
      this.queryParams = { name: '', drname: '', deptWard: '', feedType: '', feedStatus: '' };
      this.pageNum = 1;
    },
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    openAddDialog() {
      this.addSelected = [];
      this.poolKeyword = '';
      this.activeSource = 'archive';
      this.addVisible = true;
      this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
    },
    onSourceTabChange() {
      this.addSelected = [];
      this.$nextTick(() => this.$refs.addTable && this.$refs.addTable.clearSelection());
    },
    handlePoolSearch() {
      // æœ¬åœ°è¿‡æ»¤ï¼Œç”± filteredPool è®¡ç®—属性完成
    },
    onAddSelectionChange(selection) {
      this.addSelected = selection;
    },
    confirmAdd() {
      if (!this.addSelected.length) {
        this.$modal.msgWarning('请先选择要添加的患者');
        return;
      }
      const existing = new Set(this.patientList.map((r) => r.id));
      const toAdd = this.addSelected
        .filter((row) => !existing.has(row.id))
        .map((row) => this.toDedicated(row, this.activeSource));
      this.patientList = [...toAdd, ...this.patientList];
      this.addVisible = false;
      this.$modal.msgSuccess(`成功添加 ${toAdd.length} ä½æ‚£è€…`);
    },
    removeRow(row) {
      this.$modal
        .confirm(`是否确认将患者"${row.name}"移出管饲专病列表?`)
        .then(() => {
          this.patientList = this.patientList.filter((r) => r.id !== row.id);
          this.$modal.msgSuccess('移除成功');
        })
        .catch(() => {});
    },
    handleDelete() {
      this.$modal
        .confirm(`是否确认删除选中的 ${this.ids.length} ä½æ‚£è€…?`)
        .then(() => {
          this.patientList = this.patientList.filter((r) => !this.ids.includes(r.id));
          this.$modal.msgSuccess('删除成功');
        })
        .catch(() => {});
    },
    openDetail(row) {
      this.detailRow = row;
      this.detailVisible = true;
    },
    handleExport() {
      const rows = this.filteredList;
      if (!rows.length) {
        this.$modal.msgWarning('暂无数据可导出');
        return;
      }
      const head = ['姓名', '性别', '年龄', '联系电话', '科室', '病区', '主治医生', '诊断', '管饲方式', '管饲开始日期', '营养方案', '每日能量(kcal)', '管饲状态', '来源', '加入时间'];
      const keys = ['name', 'sex', 'age', 'telcode', 'deptname', 'wardname', 'drname', 'diagname', 'feedType', 'feedStartDate', 'nutrition', 'energy', 'feedStatus', 'source', 'addTime'];
      const csv = [head.join(',')]
        .concat(rows.map((r) => keys.map((k) => `"${(r[k] ?? '').toString().replace(/"/g, '""')}"`).join(',')))
        .join('\n');
      const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
      const link = document.createElement('a');
      link.href = URL.createObjectURL(blob);
      link.download = `管饲专病患者_${Date.now()}.csv`;
      link.click();
      URL.revokeObjectURL(link.href);
    },
    recentDate(daysAgo) {
      const d = new Date();
      d.setDate(d.getDate() - daysAgo);
      const mm = String(d.getMonth() + 1).padStart(2, '0');
      const dd = String(d.getDate()).padStart(2, '0');
      return `${d.getFullYear()}-${mm}-${dd}`;
    },
    now() {
      const d = new Date();
      const pad = (x) => String(x).padStart(2, '0');
      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
    },
  },
};
</script>
<style lang="scss" scoped>
.query-form { margin-bottom: 4px; }
.mb8 { margin-bottom: 8px; }
.pagination-wrap { margin-top: 16px; text-align: right; }
.pool-tip { font-size: 12px; color: #909399; margin-left: 8px; }
.danger-text { color: #f56c6c; }
</style>
src/views/repositoryai/templateku/configurat/index.vue
@@ -533,7 +533,7 @@
                  <el-col :span="12"
                    ><el-form-item label="是否必填">
                      <el-select
                        v-model="topicobj.isMust"
                        v-model="topicobj.ismandatory"
                        placeholder="默认必填"
                      >
                        <el-option
@@ -1086,7 +1086,7 @@
                    <el-col :span="12"
                      ><el-form-item label="是否必填">
                        <el-select
                          v-model="indexform.isMust"
                          v-model="indexform.ismandatory"
                          placeholder="默认必填"
                        >
                          <el-option
Ëæ·ÃÍâÁ´Í¨ÓÃ9.12.zip
Binary files differ
Ëæ·ÃͨÓà 9.14.zip
Binary files differ